mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user