mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 11:09:23 -06:00
gpu (its render already runs under catch_unwind and falls back to the CPU): - a failed readback (device lost, out of memory) logs, marks the GPU broken and returns zeros, so crate::render discards the result and renders on the CPU, instead of panicking; - an unknown kernel name does the same instead of panic!; - finish-parameter fields are looked up as Option (debug_assert on an unknown name) instead of panic!; - the warp sampler matches (mode, warp) like the CPU pipeline; - Buf::raw keeps one documented expect (Some from construction to Drop). merge: - pyramid building (push_pull, Blender::finish), camera graph growth, bundle adjustment, focal median and alignment use let-else / filter_map instead of expect; - synth::bracket_dngs / pano_views return Result (a missing demo scene or a DNG write error is an error); lightcraft-cli synth-merge and the engine merge tests handle it. lightcraft-gpu, lightcraft-merge and lightcraft-cli now #![deny] unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in the steering PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
4.8 KiB
Rust
95 lines
4.8 KiB
Rust
//! Photo Merge through the command layer: synthetic brackets / panorama views on disk → merged
|
||
//! DNG next to the sources → imported, selected, Auto Settings / auto crop applied.
|
||
|
||
use serde_json::json;
|
||
|
||
use crate::Session;
|
||
|
||
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||
let d = std::env::temp_dir().join(format!("lc-merge-{tag}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&d);
|
||
std::fs::create_dir_all(&d).unwrap();
|
||
d
|
||
}
|
||
|
||
fn import(s: &mut Session, paths: Vec<String>) -> Vec<u64> {
|
||
let r = s.execute("library.import", &json!({"paths": paths})).unwrap();
|
||
r["imported"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
|
||
}
|
||
|
||
#[test]
|
||
fn hdr_merge_command_creates_and_imports_a_dng() {
|
||
let dir = temp_dir("hdr");
|
||
let mut paths = Vec::new();
|
||
for (i, b) in lightcraft_merge::synth::bracket_dngs(480, 320, &[-2.0, 0.0, 2.0]).unwrap().into_iter().enumerate() {
|
||
let p = dir.join(format!("IMG_{i}.dng"));
|
||
std::fs::write(&p, b).unwrap();
|
||
paths.push(p.to_string_lossy().to_string());
|
||
}
|
||
let mut s = Session::new().with_fs();
|
||
let ids = import(&mut s, paths);
|
||
assert_eq!(ids.len(), 3);
|
||
// preview: rendered PNG, nothing added
|
||
let png = dir.join("preview.png");
|
||
let pv = s.execute("merge.hdr", &json!({"ids": ids, "deghost": "medium", "preview": true, "showOverlay": true, "previewPath": png})).unwrap();
|
||
assert!(png.exists() && pv["previewSize"][0].as_u64().unwrap() > 100, "{pv}");
|
||
assert_eq!(s.catalog.photos().count(), 3);
|
||
let ev: Vec<f64> = pv["ev"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||
assert!((ev[0] + 2.0).abs() < 0.1 && (ev[2] - 2.0).abs() < 0.1, "{ev:?}");
|
||
// full merge
|
||
let r = s.execute("merge.hdr", &json!({"ids": ids, "autoSettings": true, "stack": true})).unwrap();
|
||
let path = r["path"].as_str().unwrap();
|
||
assert!(path.ends_with("IMG_0-HDR.dng"), "{path}");
|
||
let id = lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap());
|
||
let p = s.catalog.photo(id).unwrap().clone();
|
||
assert_eq!((p.width, p.height), (480, 320));
|
||
assert_eq!(p.kind, lightcraft_catalog::MediaKind::Raw);
|
||
assert_eq!(s.selection.active, Some(id));
|
||
let d = s.develop_of(id).unwrap();
|
||
assert!(d.light.exposure != 0.0 || d.light.highlights != 0.0, "auto settings applied");
|
||
let img = s.render_now(id, 256, 256).unwrap();
|
||
assert_eq!(img.image.width, 256);
|
||
// Create Stack: the result tops a collapsed stack of its sources, so the grid shows the result
|
||
let st = s.catalog.stack_of(id).expect("merge result stacked").clone();
|
||
assert_eq!(st.top(), id);
|
||
assert_eq!(st.photos[1..].iter().map(|p| p.0).collect::<Vec<_>>(), ids);
|
||
assert!(st.collapsed);
|
||
assert_eq!(s.visible_cloned(), vec![id]);
|
||
// the second merge doesn't overwrite the first
|
||
let r2 = s.execute("merge.hdr", &json!({"ids": ids, "autoSettings": false})).unwrap();
|
||
assert!(r2["path"].as_str().unwrap().ends_with("IMG_0-HDR-2.dng"));
|
||
let id2 = lightcraft_catalog::PhotoId(r2["id"].as_u64().unwrap());
|
||
assert!(s.catalog.stack_of(id2).is_none(), "no stack without the option");
|
||
// the type filter finds merge results (and only them)
|
||
s.execute("library.filter", &json!({"merged": "hdr"})).unwrap();
|
||
let found: std::collections::HashSet<_> = s.catalog.query(&s.filter, &Default::default()).into_iter().collect();
|
||
assert_eq!(found, [id, id2].into_iter().collect());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn panorama_command_with_auto_crop() {
|
||
let dir = temp_dir("pano");
|
||
let mut paths = Vec::new();
|
||
for (i, v) in lightcraft_merge::synth::pano_views(480, 360, 420.0, &[-25.0, 0.0, 25.0]).unwrap().into_iter().enumerate() {
|
||
let img = v.to_srgb8();
|
||
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||
let p = dir.join(format!("P_{i}.png"));
|
||
std::fs::write(&p, png).unwrap();
|
||
paths.push(p.to_string_lossy().to_string());
|
||
}
|
||
let mut s = Session::new().with_fs();
|
||
let ids = import(&mut s, paths);
|
||
let r = s.execute("merge.panorama", &json!({"ids": ids, "projection": "cylindrical", "autoCrop": true, "autoSettings": false})).unwrap();
|
||
assert_eq!(r["projection"], "cylindrical");
|
||
assert_eq!(r["used"].as_array().unwrap().len(), 3);
|
||
let id = lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap());
|
||
let p = s.catalog.photo(id).unwrap().clone();
|
||
assert!(p.width > 800 && p.height > 300, "{}×{}", p.width, p.height);
|
||
let crop = s.develop_of(id).unwrap().crop.geometry.rect;
|
||
assert!(crop.width() < 1.0 && crop.width() * crop.height() > 0.5, "{crop:?}");
|
||
// too few photos
|
||
assert!(s.execute("merge.panorama", &json!({"ids": [ids[0]]})).is_err());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|