# Conflicts:
#	docs/gaps.md
This commit is contained in:
Brandon Thomas
2026-10-14 21:57:58 -04:00
11 changed files with 255 additions and 12 deletions
+16 -2
View File
@@ -843,7 +843,7 @@ impl ExportOptions {
"jxlEffort" => one_of(k, v, &["fast", "normal"])?,
"dngCompression" => {
serde_json::from_value::<DngCompression>(v.clone())
.map_err(|_| bad(format!("`{k}` must be one of lossless|deflate|uncompressed")))?;
.map_err(|_| bad(format!("`{k}` must be one of lossless|deflate|uncompressed|lossy")))?;
}
"metadata" => one_of(k, v, &["all", "allExceptCamera", "copyright", "none"])?,
"colorSpace" => {
@@ -1506,6 +1506,9 @@ enum Work {
read: Option<crate::merge::ByteReader>,
packet: String,
dng: Option<DngCompression>,
/// Lossy DNG: JPEG quality and the long edge to scale to (`None`: full size).
quality: u8,
lossy_edge: Option<u32>,
/// The original must not carry a location ([`removes_location`]).
remove_location: bool,
label: String,
@@ -1575,6 +1578,11 @@ fn prepare_guarded(
read: session.media.file_bytes.clone(),
packet: crate::sidecar::sidecar_packet_with(p, &session.catalog, removes_location(p, &session.catalog, o)),
dng,
quality: o.quality,
lossy_edge: o.resize.is_some().then(|| {
let (w, h) = output_size(p, o);
u32::try_from(w.max(h)).unwrap_or(u32::MAX)
}),
remove_location: removes_location(p, &session.catalog, o),
label: p.file_name.clone(),
size: (p.width as usize, p.height as usize),
@@ -1634,7 +1642,7 @@ impl PreparedExport {
};
Ok(Exported { file_name, bytes, width: r.image.width, height: r.image.height, sidecars: Vec::new(), gpu_fallback })
}
Work::File { path, read, packet, dng, label, size, remove_location } => {
Work::File { path, read, packet, dng, quality, lossy_edge, label, size, remove_location } => {
let bytes = match &read {
Some(r) => r(&path)?,
None => std::fs::read(&path).map_err(|e| format!("{path}: {e}"))?,
@@ -1664,6 +1672,9 @@ impl PreparedExport {
DngCompression::Lossless => lightcraft_raw::DngCompression::Lj92 { tile: 256 },
DngCompression::Deflate => lightcraft_raw::DngCompression::Deflate { tile: 256, half: false },
DngCompression::Uncompressed => lightcraft_raw::DngCompression::Uncompressed,
DngCompression::Lossy => {
lightcraft_raw::DngCompression::LossyJpeg { tile: 256, quality: quality.clamp(1, 100), long_edge: lossy_edge }
}
};
let dng = lightcraft_raw::write_dng(&raw, &lightcraft_raw::DngWriteOptions { xmp: Some(packet), compression, ..Default::default() })
.map_err(|e| e.to_string())?;
@@ -1683,6 +1694,9 @@ pub enum DngCompression {
/// Deflate (zip) tiles with prediction.
Deflate,
Uncompressed,
/// Lossy DNG: demosaiced linear data as JPEG tiles at the export quality, scaled to the resize
/// setting when there is one (Lightroom's "Use Lossy Compression").
Lossy,
}
/// The destination of a batch: a folder (with `ExportOptions::subfolder` and the conflict policy
+29 -1
View File
@@ -935,7 +935,7 @@ fn dng_export_compression_choices() {
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"] {
for c in ["lossless", "deflate", "uncompressed", "lossy"] {
let o = crate::export::ExportOptions::from_json(&json!({"format": "dng", "dngCompression": c}));
let mut out = Vec::new();
let mut write = |_: &str, b: &[u8]| {
@@ -948,6 +948,7 @@ fn dng_export_compression_choices() {
sizes.push((c, out.len()));
}
assert!(sizes[2].1 > sizes[0].1 && sizes[2].1 > sizes[1].1, "uncompressed is the biggest: {sizes:?}");
assert!(sizes[3].1 < sizes[0].1 / 2, "lossy is much smaller than lossless: {sizes:?}");
}
fn write_jpeg(path: &Path, capture: Option<&str>) {
@@ -1209,3 +1210,30 @@ fn files_sharing_a_stem_get_their_own_sidecars() {
let _ = std::fs::remove_dir_all(&src);
let _ = std::fs::remove_dir_all(&lib);
}
/// Lossy DNG export: demosaiced JPEG tiles at the resize long edge, smaller than lossless, colour tags kept.
#[test]
fn lossy_dng_export_scales_and_shrinks() {
let src = temp_dir("lossydng");
std::fs::write(src.join("synth.dng"), synthetic_dng_with(None, Default::default())).unwrap();
let mut s = Session::new().with_fs();
s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
let id = s.catalog.photos().next().unwrap().id;
let mut export = |p: serde_json::Value| {
let o = crate::export::ExportOptions::from_json(&p);
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();
out
};
let lossless = export(json!({"format": "dng"}));
let lossy = export(json!({"format": "dng", "dngCompression": "lossy", "quality": 80, "longEdge": 32}));
let raw = lightcraft_raw::decode(&lossy).unwrap();
assert_eq!((raw.cpp, raw.cfa.is_none(), raw.width.max(raw.height)), (3, true, 32));
assert_eq!(raw.color.as_shot_neutral, Some([0.5, 1.0, 0.7]));
assert!(lossy.len() < lossless.len(), "{} vs {}", lossy.len(), lossless.len());
let _ = std::fs::remove_dir_all(&src);
}