Files
lightcraft/crates/engine/src/tests_xmp.rs
T
Brandon ThomasandClaude Opus 5.5 1aaafb6218 LR-EXP-DNGOPT: DNG export compression (lossless / ZIP / none)
ExportOptions.dng_compression (`dngCompression`: lossless (default) | deflate | uncompressed),
in the export dialog when the format is DNG. Corpus-gated test: all three decode, uncompressed
is the biggest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:27:45 -04:00

448 lines
22 KiB
Rust

//! XMP sidecars end to end: save → delete library → re-import restores edits; auto-write; foreign
//! `crs:` sidecars; DNG-embedded XMP; read metadata from file (undoable).
use std::path::{Path, PathBuf};
use lightcraft_catalog::{ColorLabel, Flag, PhotoId};
use serde_json::json;
use crate::Session;
pub(crate) fn temp_dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("lc-xmp-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn write_png(path: &Path, seed: u8) {
let (w, h) = (40usize, 24usize);
let data: Vec<[u8; 4]> = (0..w * h).map(|i| [(i % w * 6) as u8, (i / w * 9) as u8, seed, 255]).collect();
let img = lightcraft_raster::Rgba8 { width: w, height: h, data };
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
std::fs::write(path, bytes).unwrap();
}
fn only(s: &Session) -> PhotoId {
s.catalog.photos().next().unwrap().id
}
#[test]
fn save_delete_library_reimport_restores_edits() {
let src = temp_dir("roundtrip-src");
let lib = temp_dir("roundtrip-lib");
write_png(&src.join("dune.png"), 1);
write_png(&src.join("plain.png"), 2);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
s.execute("library.import", &json!({"paths": [src.join("dune.png").to_string_lossy()]})).unwrap();
let id = only(&s);
s.execute("library.select", &json!({"ids": [id.0]})).unwrap();
s.execute("photo.rate", &json!({"rating": 4})).unwrap();
s.execute("photo.pick", &json!({})).unwrap();
s.execute("photo.label", &json!({"label": "blue"})).unwrap();
s.execute("photo.setMeta", &json!({"title": "Dune", "caption": "Evening light", "copyright": "© me", "keywords": ["sand", "dusk"]})).unwrap();
s.execute("develop.set", &json!({"control": "light.exposure", "value": 0.8})).unwrap();
s.execute("develop.set", &json!({"control": "effects.clarity", "value": 25})).unwrap();
s.execute("mask.add", &json!({"kind": "radial"})).unwrap();
let edited = s.catalog.photo(id).unwrap().clone();
let r = s.execute("photo.saveMetadataToFile", &json!({})).unwrap();
assert_eq!(r["written"][0], src.join("dune.xmp").to_string_lossy().as_ref(), "{r}");
assert!(src.join("dune.xmp").is_file());
assert!(!src.join("plain.xmp").exists());
drop(s);
// delete the library; a fresh library re-imports the folder
std::fs::remove_dir_all(&lib).unwrap();
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
let r = s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
assert_eq!(r["imported"].as_array().unwrap().len(), 2, "{r}");
assert_eq!(r["sidecars"], 1, "{r}");
let p = s.catalog.photos().find(|p| p.file_name == "dune.png").unwrap().clone();
assert_eq!(p.develop, edited.develop, "develop settings restored exactly");
assert_eq!(p.develop.masks.len(), 1);
assert_eq!((p.rating, p.flag, p.label), (4, Flag::Pick, Some(ColorLabel::Blue)));
assert_eq!(p.meta.title, "Dune");
assert_eq!(p.meta.caption, "Evening light");
assert_eq!(p.meta.copyright, "© me");
assert_eq!(p.meta.keywords, vec!["sand".to_string(), "dusk".to_string()]);
assert!(p.edited.is_some());
let plain = s.catalog.photos().find(|p| p.file_name == "plain.png").unwrap();
assert!(plain.develop.is_unedited() && plain.rating == 0);
let _ = std::fs::remove_dir_all(&src);
let _ = std::fs::remove_dir_all(&lib);
}
#[test]
fn auto_write_and_naming_preference() {
let src = temp_dir("auto");
let lib = temp_dir("auto-lib");
write_png(&src.join("a.png"), 3);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
s.execute("photo.rate", &json!({"rating": 2})).unwrap();
assert!(!src.join("a.xmp").exists(), "auto-write is off by default");
let v = s.execute("library.xmpPreferences", &json!({"autoWrite": true, "naming": "full"})).unwrap();
assert_eq!(v, json!({"autoWrite": true, "naming": "full"}));
s.execute("photo.rate", &json!({"rating": 5})).unwrap();
let sidecar = src.join("a.png.xmp");
assert!(sidecar.is_file());
assert!(std::fs::read_to_string(&sidecar).unwrap().contains("<xmp:Rating>5</xmp:Rating>"));
// a slider drag writes once, at the end
s.execute("develop.beginInteraction", &json!({"label": "Exposure"})).unwrap();
s.execute("develop.set", &json!({"control": "light.exposure", "value": 1.5})).unwrap();
let exposure = |p: &Path| match crate::sidecar::parse_sidecar(&std::fs::read_to_string(p).unwrap(), false).unwrap().develop {
Some(crate::sidecar::DevelopPatch::Full(d)) => d.light.exposure,
other => panic!("{other:?}"),
};
assert_eq!(exposure(&sidecar), 0.0, "not written mid-drag");
s.execute("develop.endInteraction", &json!({})).unwrap();
assert_eq!(exposure(&sidecar), 1.5);
// undo rewrites too
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(exposure(&sidecar), 0.0);
// the preference persists with the library
drop(s);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
assert!(s.xmp.auto_write);
assert_eq!(s.xmp.naming, crate::sidecar::SidecarNaming::Full);
let v = s.execute("library.toggleAutoWriteXmp", &json!({})).unwrap();
assert_eq!(v["autoWrite"], false);
let _ = std::fs::remove_dir_all(&src);
let _ = std::fs::remove_dir_all(&lib);
}
/// Hand-written sidecar in the style other raw developers use (attribute form, `crs:` fields).
const FOREIGN: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
xmp:Rating="3" xmp:Label="Red"
crs:ProcessVersion="11.0" crs:IncrementalTemperature="+20" crs:IncrementalTint="-5"
crs:Exposure2012="+1.10" crs:Shadows2012="+35" crs:Vibrance="+15" crs:Clarity2012="+10"
crs:SplitToningShadowHue="220" crs:SplitToningShadowSaturation="18"
crs:HasCrop="True" crs:CropLeft="0.1" crs:CropTop="0" crs:CropRight="0.9" crs:CropBottom="1" crs:CropAngle="0">
<dc:subject><rdf:Bag><rdf:li>harbour</rdf:li></rdf:Bag></dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
#[test]
fn foreign_crs_sidecar_on_import_and_read_from_file() {
let src = temp_dir("foreign");
write_png(&src.join("harbour.png"), 4);
std::fs::write(src.join("harbour.xmp"), FOREIGN).unwrap();
let mut s = Session::new().with_fs();
let r = s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
assert_eq!(r["sidecars"], 1);
let p = s.catalog.photos().next().unwrap().clone();
assert_eq!((p.rating, p.label), (3, Some(ColorLabel::Red)));
assert_eq!(p.meta.keywords, vec!["harbour".to_string()]);
let d = &p.develop;
assert_eq!((d.light.exposure, d.light.shadows, d.color.vibrance, d.effects.clarity), (1.1, 35.0, 15.0, 10.0));
assert!(d.wb.temp > 6500.0 && d.wb.tint == -5.0, "relative WB for a rendered file: {:?}", d.wb);
assert_eq!((d.grading.shadows.hue, d.grading.shadows.sat), (220.0, 18.0));
assert_eq!((d.crop.geometry.rect.x0, d.crop.geometry.rect.x1), (0.1, 0.9));
assert!(s.render_now(p.id, 32, 32).is_ok());
// change things, then read the sidecar again: one undo step restores the edits
s.execute("library.select", &json!({"ids": [p.id.0]})).unwrap();
s.execute("develop.reset", &json!({})).unwrap();
s.execute("photo.rate", &json!({"rating": 1})).unwrap();
let r = s.execute("photo.readMetadataFromFile", &json!({})).unwrap();
assert_eq!(r["read"].as_array().unwrap().len(), 1, "{r}");
let q = s.catalog.photo(p.id).unwrap();
assert_eq!(q.rating, 3);
assert_eq!(q.develop.light.exposure, 1.1);
s.execute("edit.undo", &json!({})).unwrap();
let q = s.catalog.photo(p.id).unwrap();
assert_eq!((q.rating, q.develop.light.exposure), (1, 0.0));
// no sidecar → reported, not an error
std::fs::remove_file(src.join("harbour.xmp")).unwrap();
let r = s.execute("photo.readMetadataFromFile", &json!({})).unwrap();
assert_eq!(r["failed"].as_array().unwrap().len(), 1);
let _ = std::fs::remove_dir_all(&src);
}
fn synthetic_dng(xmp: &str) -> Vec<u8> {
synthetic_dng_with(Some(xmp), lightcraft_meta::Metadata::default())
}
/// A tiny Bayer DNG written by our own DNG writer (optional embedded XMP, camera metadata).
pub(crate) fn synthetic_dng_with(xmp: Option<&str>, metadata: lightcraft_meta::Metadata) -> Vec<u8> {
use lightcraft_raw::*;
let (w, h) = (32usize, 24usize);
let cfa = Cfa::bayer("RGGB").unwrap();
let data: Vec<u16> = (0..w * h).map(|i| 256 + ((i % w) * 300 + (i / w) * 200) as u16).collect();
let raw = RawImage {
format: RawFormat::Dng,
width: w,
height: h,
cpp: 1,
data: RawData::U16(data),
cfa: Some(cfa),
bits: 14,
black: BlackLevel::uniform(256.0),
white: vec![16000.0],
active_area: Rect::new(0, 0, w, h),
crop: Rect::new(0, 0, w, h),
orientation: Orientation::Normal,
color: ColorData {
illuminant: [17, 21],
color_matrix: [
Some(lightcraft_color::Mat3([[0.9, 0.2, -0.15], [-0.3, 1.25, 0.08], [0.02, -0.12, 0.85]])),
Some(lightcraft_color::Mat3([[0.7, 0.3, -0.1], [-0.35, 1.3, 0.1], [0.05, -0.2, 1.0]])),
],
as_shot_neutral: Some([0.5, 1.0, 0.7]),
..Default::default()
},
wb_multipliers: None,
linearized: false,
opcodes: OpcodeLists::default(),
metadata,
};
write_dng(&raw, &DngWriteOptions { xmp: xmp.map(str::to_string), ..Default::default() }).unwrap()
}
#[test]
fn dng_embedded_crs_settings_are_read_on_import() {
let src = temp_dir("dng");
let x = r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
xmp:Rating="2" crs:WhiteBalance="Custom" crs:Temperature="4300" crs:Tint="+7" crs:Contrast2012="+30" crs:Dehaze="+12"/>
</rdf:RDF></x:xmpmeta>"#;
std::fs::write(src.join("synth.dng"), synthetic_dng(x)).unwrap();
let mut s = Session::new().with_fs();
let r = s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
assert_eq!((r["imported"].as_array().unwrap().len(), r["sidecars"].as_u64()), (1, Some(1)), "{r}");
let p = s.catalog.photos().next().unwrap();
assert_eq!(p.kind, lightcraft_catalog::MediaKind::Raw);
assert_eq!((p.develop.wb.temp, p.develop.wb.tint), (4300.0, 7.0));
assert_eq!((p.develop.light.contrast, p.develop.effects.dehaze), (30.0, 12.0));
assert_eq!(p.develop.detail.sharpen_amount, 40.0, "raw defaults kept for fields the XMP doesn't set");
assert_eq!(p.rating, 2);
// a sidecar next to the DNG takes precedence over the embedded XMP
let id = p.id;
s.execute("library.select", &json!({"ids": [id.0]})).unwrap();
s.execute("develop.set", &json!({"control": "light.contrast", "value": -10})).unwrap();
s.execute("photo.saveMetadataToFile", &json!({})).unwrap();
let mut s2 = Session::new().with_fs();
s2.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
assert_eq!(s2.catalog.photos().next().unwrap().develop.light.contrast, -10.0);
let _ = std::fs::remove_dir_all(&src);
}
#[test]
fn demo_photos_have_no_sidecar() {
let mut s = Session::with_demo();
let r = s.execute("photo.saveMetadataToFile", &json!({})).unwrap();
assert_eq!(r["written"].as_array().unwrap().len(), 0);
assert_eq!(r["failed"].as_array().unwrap().len(), 1);
}
/// All Metadata: EXIF rows from the file and its XMP, for a photo on disk.
#[test]
fn all_metadata_lists_exif_and_xmp() {
let dir = temp_dir("allmeta");
let path = dir.join("tagged.jpg");
let img = lightcraft_raster::Rgba8 { width: 16, height: 8, data: vec![[90, 120, 200, 255]; 128] };
let exif = lightcraft_meta::write_exif(&lightcraft_meta::Metadata { make: Some("Maker".into()), iso: Some(800), ..Default::default() });
let meta = lightcraft_codecs::EncodeMeta { exif: Some(&exif), ..Default::default() };
let jpg = lightcraft_codecs::encode_jpeg(&lightcraft_codecs::EncodeImage::rgba8(&img), 90, Default::default(), &meta).unwrap();
std::fs::write(&path, jpg).unwrap();
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [path.to_string_lossy()]})).unwrap();
s.execute("photo.setMeta", &json!({"title": "A title"})).unwrap();
s.execute("photo.saveMetadataToFile", &json!({})).unwrap();
let r = s.execute("photo.allMetadata", &json!({})).unwrap();
let exif = r["exif"].as_array().unwrap();
assert!(exif.iter().any(|e| e["name"] == "Make" && e["value"] == "Maker"), "{r}");
assert!(exif.iter().any(|e| e["name"] == "ISO Speed" && e["value"] == "800"));
assert!(r["xmp"].as_array().unwrap().iter().any(|x| x["name"] == "dc:title" && x["value"] == "A title"), "the sidecar's fields: {r}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Convert to DNG: a corpus raw (skipped without the corpus) becomes a DNG next to it with the
/// photo relinked and its edits kept; undo goes back to the original; non-raws are skipped.
#[test]
fn convert_raw_to_dng() {
let corpus = std::env::var_os("LIGHTCRAFT_CORPUS")
.map(PathBuf::from)
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
.join("raw/nef-nikon-d5100-uncompressed.nef");
let mut s = Session::new().with_fs();
// a non-raw is skipped
let dir = temp_dir("todng");
let png = dir.join("a.png");
write_png(&png, 3);
s.execute("library.import", &json!({"paths": [png.to_string_lossy()]})).unwrap();
let r = s.execute("photo.convertToDng", &json!({})).unwrap();
assert_eq!((r["converted"].as_array().unwrap().len(), r["skipped"].as_array().unwrap().len()), (0, 1));
if !corpus.exists() {
eprintln!("skip: {} absent", corpus.display());
return;
}
let nef = dir.join("shot.nef");
std::fs::copy(&corpus, &nef).unwrap();
s.execute("library.import", &json!({"paths": [nef.to_string_lossy()]})).unwrap();
let id = s.active().unwrap();
s.execute("develop.set", &json!({"control": "light.exposure", "value": 0.6})).unwrap();
let r = s.execute("photo.convertToDng", &json!({})).unwrap();
assert_eq!(r["converted"].as_array().unwrap().len(), 1, "{r}");
let p = s.catalog.photo(id).unwrap().clone();
assert_eq!((p.file_name.as_str(), p.format.as_str()), ("shot.dng", "DNG"));
assert!(dir.join("shot.dng").exists() && nef.exists(), "the original is kept");
assert_eq!(p.develop.light.exposure, 0.6, "edits kept");
let bytes = std::fs::read(dir.join("shot.dng")).unwrap();
assert_eq!(lightcraft_raw::probe(&bytes), Some(lightcraft_raw::RawFormat::Dng));
assert!(s.render_now(id, 64, 64).is_ok(), "the DNG renders");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.catalog.photo(id).unwrap().file_name, "shot.nef");
// Copy as DNG at import: only the DNG lands in the destination
let card = dir.join("card");
std::fs::create_dir_all(&card).unwrap();
std::fs::copy(&corpus, card.join("DSC_1.NEF")).unwrap();
let dest = dir.join("dest");
let mut s = Session::new().with_fs();
let r = s
.execute(
"library.import",
&json!({"paths": [card.to_string_lossy()], "mode": "copy", "destination": dest.to_string_lossy(), "organize": "flat", "dng": true}),
)
.unwrap();
assert_eq!(r["imported"].as_array().unwrap().len(), 1, "{r}");
let names: Vec<String> = std::fs::read_dir(&dest).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect();
assert_eq!(names, ["DSC_1.dng"]);
assert!(card.join("DSC_1.NEF").exists(), "the card is untouched");
let id2 = lightcraft_catalog::PhotoId(r["imported"][0].as_u64().unwrap());
assert_eq!(s.catalog.photo(id2).unwrap().format, "DNG");
let _ = std::fs::remove_dir_all(&dir);
}
/// Edit in External Editor (engine half): a 16-bit TIFF `-Edit` copy with the edits, next to
/// the original, added and stacked on top of it; a second one doesn't overwrite the first.
#[test]
fn external_edit_copy_is_stacked() {
let dir = temp_dir("extedit");
let src = dir.join("beach.png");
write_png(&src, 9);
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
let orig = s.active().unwrap();
s.execute("develop.set", &json!({"control": "light.exposure", "value": 1.0})).unwrap();
let r = s.execute("photo.editExternal", &json!({})).unwrap();
let path = r["path"].as_str().unwrap().to_string();
assert!(path.ends_with("beach-Edit.tif"), "{path}");
let bytes = std::fs::read(&path).unwrap();
let d = lightcraft_codecs::decode(&bytes, Default::default()).unwrap();
assert_eq!(d.bit_depth, 16);
let new = lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap());
assert_eq!(s.active(), Some(new));
let st = s.catalog.stack_of(new).expect("stacked");
assert_eq!((st.top(), st.photos.contains(&orig)), (new, true));
let r2 = s.execute("photo.editExternal", &json!({"colorSpace": "proPhoto"})).unwrap();
assert!(r2["path"].as_str().unwrap().ends_with("beach-Edit-Edit.tif") || r2["path"].as_str().unwrap().contains("-2"), "{r2}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Reload: a file changed on disk (an external editor saved it) gets its new size / hash, the
/// cached source is dropped and renders change; unchanged files are left alone.
#[test]
fn reload_picks_up_changed_files() {
let dir = temp_dir("reload");
let f = dir.join("x.png");
write_png(&f, 10);
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [f.to_string_lossy()]})).unwrap();
let id = s.active().unwrap();
let before = s.render_now(id, 32, 32).unwrap().image.data;
let key0 = s.thumb_job(id, 128).unwrap().key;
assert_eq!(s.execute("photo.reload", &json!({})).unwrap()["reloaded"], json!([]), "unchanged");
write_png(&f, 200);
let r = s.execute("photo.reload", &json!({})).unwrap();
assert_eq!(r["reloaded"], json!([id.0]));
assert_ne!(s.thumb_job(id, 128).unwrap().key, key0, "the UI sees a new render key");
assert_ne!(s.render_now(id, 32, 32).unwrap().image.data, before, "renders the new pixels");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn duplicate_copies_the_file_and_the_edits() {
let dir = temp_dir("dup");
let f = dir.join("pic.png");
write_png(&f, 5);
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [f.to_string_lossy()]})).unwrap();
let orig = s.active().unwrap();
s.execute("develop.set", &json!({"control": "light.exposure", "value": 0.8})).unwrap();
s.execute("photo.rate", &json!({"rating": 4})).unwrap();
let alb = s.execute("album.create", &json!({"name": "Keep", "addSelected": true})).unwrap()["id"].as_u64().unwrap();
let r = s.execute("photo.duplicate", &json!({})).unwrap();
let dup = lightcraft_catalog::PhotoId(r["ids"][0].as_u64().unwrap());
let d = s.catalog.photo(dup).unwrap().clone();
assert_eq!(d.file_name, "pic-copy.png");
assert!(dir.join("pic-copy.png").exists());
assert_eq!((d.develop.light.exposure, d.rating), (0.8, 4));
assert_eq!(s.catalog.album_count(lightcraft_catalog::AlbumId(alb)), 2);
assert_ne!(dup, orig);
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.catalog.photo(dup).is_none(), "one undo step");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn gps_typed_in_is_saved_to_xmp() {
let mut s = Session::with_demo();
let id = s.active().unwrap_or_else(|| s.catalog.photos().next().unwrap().id);
s.execute("library.select", &json!({"ids": [id.0]})).unwrap();
s.execute("photo.setMeta", &json!({"gps": "48°51'30\"N 2°17'40\"E"})).unwrap();
let (la, lo) = s.catalog.photo(id).unwrap().meta.gps.unwrap();
assert!((la - 48.8583).abs() < 1e-3 && (lo - 2.2944).abs() < 1e-3, "{la} {lo}");
let x = crate::sidecar::sidecar_packet(s.catalog.photo(id).unwrap(), &s.catalog);
let back = lightcraft_meta::parse_xmp(&x).unwrap().metadata.gps.unwrap();
assert!((back.latitude - la).abs() < 1e-5 && (back.longitude - lo).abs() < 1e-5);
assert!(s.execute("photo.setMeta", &json!({"gps": "north pole-ish"})).is_err());
s.execute("photo.setMeta", &json!({"gps": null})).unwrap();
assert!(s.catalog.photo(id).unwrap().meta.gps.is_none());
}
/// DNG export compression: lossless, zip and none all decode; none is the biggest.
#[test]
fn dng_export_compression_choices() {
let corpus = std::env::var_os("LIGHTCRAFT_CORPUS")
.map(PathBuf::from)
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
.join("raw/nef-nikon-d5100-uncompressed.nef");
if !corpus.exists() {
eprintln!("skip: {} absent", corpus.display());
return;
}
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [corpus.to_string_lossy()]})).unwrap();
let id = s.active().unwrap();
let mut sizes = Vec::new();
for c in ["lossless", "deflate", "uncompressed"] {
let o = crate::export::ExportOptions::from_json(&json!({"format": "dng", "dngCompression": c}));
let mut out = Vec::new();
let mut write = |_: &str, b: &[u8]| {
out = b.to_vec();
Ok(())
};
crate::export::export_batch(&mut s, &[id], &o, &crate::export::Destination { dir: "x".into(), exact: None }, &mut write, &|_| false).unwrap();
let raw = lightcraft_raw::decode(&out).unwrap_or_else(|e| panic!("{c}: {e}"));
assert!(raw.width > 1000, "{c}");
sizes.push((c, out.len()));
}
assert!(sizes[2].1 > sizes[0].1 && sizes[2].1 > sizes[1].1, "uncompressed is the biggest: {sizes:?}");
}