XMP.434: reconcile current main & complete warning locales

Preserve Target-aware sidecar operations, add French/Ukrainian warnings, and ensure omitted diagnostic fields decode in the native UI.

Generated with Codex.
This commit is contained in:
Adrian D'souza
2026-10-11 02:11:00 +05:30
405 changed files with 79341 additions and 2857 deletions
+282
View File
@@ -503,6 +503,12 @@ fn damaged_smart_previews_are_rebuilt_and_failed_writes_leave_none() {
};
assert_eq!((r["done"].as_u64(), r["failed"].as_u64(), r["repaired"].as_u64()), (Some(1), Some(0), Some(1)), "{r}");
assert_eq!(std::fs::read(&file).unwrap(), full, "rebuilt whole in the background");
// its row in the activity stack (issue #345) goes with it
let t0 = std::time::Instant::now();
while !s.activity.list().is_empty() && t0.elapsed() < std::time::Duration::from_secs(10) {
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(s.activity.list().is_empty(), "{:?}", s.activity.list());
let _ = std::fs::remove_dir_all(&src);
let _ = std::fs::remove_dir_all(&lib);
}
@@ -980,8 +986,27 @@ fn emptying_trashed_photos_of_one_album_and_stack_leaves_nothing_dangling() {
fn deleting_several_photos_permanently_leaves_nothing_dangling() {
let (mut s, src, ids, al) = two_trashed_in_album_and_stack("trash-perm-refs");
s.execute("library.select", &json!({"ids": ids})).unwrap();
let files = [src.join("a.png"), src.join("b.png")].map(|path| {
let bytes = std::fs::read(&path).unwrap();
(path, bytes)
});
let snapshot = s.catalog.to_snapshot();
s.drain_log();
s.execute("photo.deletePermanently", &json!({"ids": ids})).unwrap();
assert!(s.catalog.album(lightcraft_catalog::AlbumId(al)).unwrap().photos.is_empty());
assert!(ids.iter().all(|i| s.catalog.photo(lightcraft_catalog::PhotoId(*i)).is_none()));
let removed = s.catalog.to_snapshot();
let log: String = s.drain_log().iter().map(lightcraft_catalog::Catalog::op_to_log_line).collect();
let mut reopened = lightcraft_catalog::Catalog::from_snapshot(&snapshot).unwrap();
reopened.replay(&log).unwrap();
assert_eq!(reopened.to_snapshot(), removed);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.catalog.to_snapshot(), snapshot, "one undo restores photos, album and stack");
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.catalog.to_snapshot(), removed);
for (path, bytes) in files {
assert_eq!(std::fs::read(path).unwrap(), bytes, "permanent deletion only removes catalog records");
}
let _ = std::fs::remove_dir_all(&src);
}
@@ -1111,3 +1136,260 @@ fn folder_import_picks_up_undecodable_raw_containers_as_preview_only() {
assert!(s.catalog.photos().all(|p| p.preview_only.is_some() && p.kind == lightcraft_catalog::MediaKind::Raw && (p.width, p.height) == (40, 30)));
let _ = std::fs::remove_dir_all(&src);
}
/// Rewrite the JSON header line of the smart preview at `file`.
fn edit_smart_header(file: &Path, edit: impl FnOnce(&mut Value)) {
let bytes = std::fs::read(file).unwrap();
let start = b"LCSP1\n".len();
let nl = start + bytes[start..].iter().position(|b| *b == b'\n').unwrap();
let mut head: Value = serde_json::from_slice(&bytes[start..nl]).unwrap();
edit(&mut head);
let mut out = bytes[..start].to_vec();
out.extend_from_slice(head.to_string().as_bytes());
out.extend_from_slice(&bytes[nl..]);
std::fs::write(file, out).unwrap();
}
fn smart_header(file: &Path) -> Value {
let bytes = std::fs::read(file).unwrap();
let start = b"LCSP1\n".len();
let nl = start + bytes[start..].iter().position(|b| *b == b'\n').unwrap();
serde_json::from_slice(&bytes[start..nl]).unwrap()
}
/// A camera tone curve with a recognisable shape (`k` changes it).
fn test_tone(k: f32) -> lightcraft_pipeline::tone::CameraTone {
lightcraft_pipeline::tone::CameraTone::new(std::array::from_fn(|i| {
let x = 0.004 * 1.17f32.powi(i as i32);
[x, 0.95 * (1.0 - (-k * x).exp())]
}))
.unwrap()
}
/// `plain`, but reporting `tone` as the camera look the (current) fit gives the file; counts its calls.
fn look_loader(
plain: crate::media::FileLoader,
tone: lightcraft_pipeline::tone::CameraTone,
calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
) -> crate::media::FileLoader {
std::sync::Arc::new(move |path, max_edge| {
calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let (img, mut info) = plain(path, max_edge)?;
info.camera_tone = Some(tone);
Ok((img, info))
})
}
/// The follow-up to the camera tone curve change (#499): a smart preview keeps the look the fit
/// gave it, so one written by an older fit is rebuilt when its original is online, kept while the
/// original is offline, and not rebuilt again once current. Only raws that take the per-file
/// camera look go stale (a PNG's proxy never does), and the opening scan runs once per fit version.
#[test]
fn smart_previews_from_an_older_look_fit_are_refreshed() {
use std::sync::atomic::{AtomicUsize, Ordering};
let src = temp_dir("smartlook-src");
let lib = temp_dir("smartlook-lib");
write_png(&src.join("a.png"), 3);
write_png(&src.join("b.png"), 9);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
s.execute("library.import", &json!({"paths": [src.join("a.png").to_string_lossy(), src.join("b.png").to_string_lossy()]})).unwrap();
let find = |s: &Session, name: &str| s.catalog.photos().find(|p| p.file_name == name).unwrap().id;
let (id, png) = (find(&s, "a.png"), find(&s, "b.png"));
// `a` stands for a raw that takes the per-file camera look (the test loader decodes its pixels)
let mut raw = (**s.catalog.photo(id).unwrap()).clone();
raw.format = "ARW".into();
raw.kind = lightcraft_catalog::MediaKind::Raw;
s.catalog.apply(lightcraft_catalog::Op::RemovePhoto { id }).unwrap();
s.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(raw) }).unwrap();
assert!(s.catalog.photo(id).unwrap().relative_wb() && !s.catalog.photo(png).unwrap().relative_wb());
s.execute("library.select", &json!({"ids": [id.0, png.0], "active": id.0})).unwrap();
let dir = s.media.smart_dir.clone().unwrap();
let file = dir.join(crate::smart::file_name(s.catalog.photo(id).unwrap()));
let png_file = dir.join(crate::smart::file_name(s.catalog.photo(png).unwrap()));
let (old, new) = (test_tone(1.5), test_tone(3.5));
let calls = std::sync::Arc::new(AtomicUsize::new(0));
let plain = s.media.file_loader.clone().unwrap();
s.media.file_loader = Some(look_loader(plain.clone(), old, calls.clone()));
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(2), Some(0)), "{r}");
assert_eq!(smart_header(&file)["look_version"], json!(crate::camera_preview::LOOK_VERSION), "the writer stamps the fit version");
assert!(!crate::smart::is_stale(&file));
// current header: kept as it is, the original is not even read
let before = std::fs::read(&file).unwrap();
calls.store(0, Ordering::SeqCst);
s.media.file_loader = Some(look_loader(plain.clone(), new, calls.clone()));
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(2), Some(0)), "{r}");
assert!(std::fs::read(&file).unwrap() == before, "a current proxy is not rebuilt");
assert_eq!(calls.load(Ordering::SeqCst), 0);
// old headers (no field), originals offline: kept unchanged, and still marked stale
for f in [&file, &png_file] {
edit_smart_header(f, |h| {
h.as_object_mut().unwrap().remove("look_version");
});
}
assert!(crate::smart::is_stale(&file), "no field = version 0");
let old_bytes = std::fs::read(&file).unwrap();
let png_bytes = std::fs::read(&png_file).unwrap();
std::fs::rename(&src, src.with_extension("offline")).unwrap();
s.media.forget(id);
s.media.forget(png);
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64(), r["staleKept"].as_u64()), (Some(2), Some(0), Some(1)), "{r}");
assert!(r["failed"].as_array().unwrap().is_empty(), "{r}");
assert!(std::fs::read(&file).unwrap() == old_bytes, "offline: the stored preview and curve are untouched");
assert!(crate::smart::is_stale(&file), "and it stays marked for the next time");
assert_eq!(crate::smart::decode(&old_bytes).unwrap().1, Some(old), "the old look is still there");
// ... and the opening refresh keeps it too, then marks the folder checked
assert!(!crate::smart::look_checked(&dir));
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
assert!(std::fs::read(&file).unwrap() == old_bytes);
assert!(crate::smart::look_checked(&dir));
// the original comes back: the scan has run at this version, so the next opening leaves it;
// Build Smart Previews rebuilds it with the current look
std::fs::rename(src.with_extension("offline"), &src).unwrap();
calls.store(0, Ordering::SeqCst);
assert!(crate::cmd::previews::refresh_stale_smart_previews(&mut s).is_none(), "checked once per version");
assert!(std::fs::read(&file).unwrap() == old_bytes);
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!((r["refreshed"].as_u64(), calls.load(Ordering::SeqCst)), (Some(1), 1), "{r}");
assert_eq!(smart_header(&file)["look_version"], json!(crate::camera_preview::LOOK_VERSION));
assert_eq!(crate::smart::decode(&std::fs::read(&file).unwrap()).unwrap().1, Some(new), "rebuilt with the current look");
assert!(!crate::smart::is_stale(&file));
assert!(std::fs::read(&png_file).unwrap() == png_bytes, "a PNG's proxy is never rebuilt for the look");
// a folder checked at an older version (or not at all) is scanned once at the next opening
edit_smart_header(&file, |h| h["look_version"] = json!(0));
std::fs::write(dir.join("look-version"), b"0\n").unwrap();
assert!(!crate::smart::look_checked(&dir));
s.media.forget(id);
calls.store(0, Ordering::SeqCst);
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
assert_eq!(calls.load(Ordering::SeqCst), 1, "only the raw's original is read");
assert!(!crate::smart::is_stale(&file));
assert!(std::fs::read(&png_file).unwrap() == png_bytes);
assert!(crate::smart::look_checked(&dir));
// once only
let rebuilt = std::fs::read(&file).unwrap();
assert!(crate::cmd::previews::refresh_stale_smart_previews(&mut s).is_none());
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!((r["refreshed"].as_u64(), calls.load(Ordering::SeqCst)), (Some(0), 1), "{r}");
assert!(std::fs::read(&file).unwrap() == rebuilt);
// Build Smart Previews refreshes a stale one the same way
edit_smart_header(&file, |h| h["look_version"] = json!(0));
s.media.file_loader = Some(plain);
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
assert_eq!(r["refreshed"], 1, "{r}");
assert_eq!(crate::smart::decode(&std::fs::read(&file).unwrap()).unwrap().1, None, "this fit gives no curve: none is kept");
let _ = std::fs::remove_dir_all(&src);
let _ = std::fs::remove_dir_all(&lib);
}
/// The look marker: missing, damaged or older than this build's fit version counts as unchecked.
#[test]
fn smart_preview_look_marker_is_read_safely() {
let dir = temp_dir("smartlook-marker");
assert!(!crate::smart::look_checked(&dir), "no marker");
let cur = crate::camera_preview::LOOK_VERSION;
for (text, checked) in [
(String::new(), false),
("banana".into(), false),
("-1".into(), false),
(format!("{}", cur.saturating_sub(1)), false),
("9".repeat(200), false),
(format!("{cur}\n"), true),
(format!(" {}\n", cur + 1), true),
] {
std::fs::write(dir.join("look-version"), &text).unwrap();
assert_eq!(crate::smart::look_checked(&dir), checked, "{text:?}");
}
std::fs::remove_file(dir.join("look-version")).unwrap();
crate::smart::mark_look_checked(&dir).unwrap();
assert!(crate::smart::look_checked(&dir));
assert!(crate::smart::mark_look_checked(&dir.join("missing")).is_err());
let _ = std::fs::remove_dir_all(&dir);
}
/// A hostile or damaged `look_version` never panics: anything that is not a non-negative integer
/// counts as 0 (rebuilt when the original is online); a version from a newer build is left alone.
#[test]
fn malformed_smart_preview_look_versions_are_handled() {
let dir = temp_dir("smartlook-bad");
let img = lightcraft_scenes::demo_library()[0].render(32, 20);
let file = dir.join("a.lcsp");
std::fs::write(&file, crate::smart::encode(&img, None).unwrap()).unwrap();
let cur = u64::from(crate::camera_preview::LOOK_VERSION);
for bad in [json!("1"), json!(-1), json!(1.5), json!(null), json!([]), json!({}), json!(true), json!(1e30), json!(-0.0)] {
let shown = bad.to_string();
edit_smart_header(&file, |h| h["look_version"] = bad);
assert!(crate::smart::is_valid(&file), "{shown}");
assert!(crate::smart::is_stale(&file), "{shown} counts as version 0");
assert!(crate::smart::decode(&std::fs::read(&file).unwrap()).is_ok(), "{shown}");
}
for fine in [json!(cur), json!(cur + 1), json!(u64::MAX)] {
let shown = fine.to_string();
edit_smart_header(&file, |h| h["look_version"] = fine);
assert!(!crate::smart::is_stale(&file), "{shown}");
}
// not a header at all, or missing
assert!(!crate::smart::is_stale(&dir.join("missing.lcsp")));
std::fs::write(&file, b"LCSP1\n{not json\n\xFF\xD8").unwrap();
assert!(!crate::smart::is_stale(&file));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_file_gone_since_the_import_review_is_not_imported() {
let src = temp_dir("gone-since-review");
write_png(&src.join("a.png"), 1);
write_png(&src.join("b.png"), 2);
let mut s = Session::new().with_fs();
s.execute("library.importPreview", &json!({"paths": [src.to_string_lossy()]})).unwrap();
std::fs::remove_file(src.join("b.png")).unwrap();
let r = s.execute("library.import", &json!({"paths": [src.join("a.png").to_string_lossy(), src.join("b.png").to_string_lossy()]})).unwrap();
assert_eq!((ids(&r, "imported"), ids(&r, "failed")), (1, 1), "{r}");
assert_eq!(s.catalog.len(), 1, "no photo for a file that isn't there");
let _ = std::fs::remove_dir_all(&src);
}
/// A HEIC laid out like an iPhone's (a grid of HEVC pictures, a Display P3 profile, a quarter turn
/// in the container, a thumbnail) imports as a normal photo that develops and exports upright like
/// a JPEG. In a build without the codecs' `heif` feature it is reported as failed, saying why.
#[test]
fn heic_imports_as_a_normal_photo_or_says_why_not() {
use lightcraft_heif::testdata::{Prop, Spec, build};
let src = temp_dir("heic");
let photo = |x: u32, y: u32| [((x * 7 + y * 13) % 200 + 30) as u16, 110, 150];
let p3 = lightcraft_codecs::icc::write_named(lightcraft_codecs::NamedSpace::DisplayP3);
let bytes = build(&Spec { props: vec![Prop::Icc(p3), Prop::Irot(1)], thumbnail: Some(&photo), ..Spec::new(96, 64, &photo) });
std::fs::write(src.join("IMG_0001.HEIC"), &bytes).unwrap();
let mut s = Session::new().with_fs();
let r = s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
if !lightcraft_codecs::Format::Heif.can_decode() {
assert_eq!((ids(&r, "imported"), ids(&r, "failed")), (0, 1), "{r}");
assert!(r["failed"].to_string().contains("isn't included in this build"), "{r}");
let _ = std::fs::remove_dir_all(&src);
return;
}
assert_eq!((ids(&r, "imported"), ids(&r, "failed")), (1, 0), "{r}");
let p = s.catalog.photos().next().unwrap();
let id = p.id;
// Upright (the container's turn applied) and not preview-only.
assert_eq!((p.kind, p.preview_only.clone(), p.width, p.height), (lightcraft_catalog::MediaKind::Image, None, 64, 96));
s.execute("library.select", &json!({"ids": [id.0], "active": id.0})).unwrap();
let plain = crate::export::export_photo(&mut s, id, &crate::export::ExportOptions::from_json(&json!({"format": "png"})), 1).unwrap();
assert_eq!((plain.width, plain.height), (64, 96));
s.execute("develop.set", &json!({"control": "light.exposure", "value": 1.0})).unwrap();
let brighter = crate::export::export_photo(&mut s, id, &crate::export::ExportOptions::from_json(&json!({"format": "png"})), 1).unwrap();
let mean = |png: &[u8]| {
let d = lightcraft_codecs::decode(png, Default::default()).unwrap();
d.image.data.iter().map(|p| p[1]).sum::<f32>() / d.image.data.len() as f32
};
assert!(mean(&brighter.bytes) > mean(&plain.bytes) * 1.3, "exposure +1 brightens the HEIC");
let _ = std::fs::remove_dir_all(&src);
}