mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #1750
This commit is contained in:
@@ -125,6 +125,23 @@ pub(crate) fn starting_colour_tags(raw: &mut RawImage, bytes: &[u8]) -> Option<C
|
||||
dng_colour(&raw.color, &t, look.as_ref())
|
||||
}
|
||||
|
||||
/// Give a raw about to be written as a DNG of its own (Export as DNG, Convert / Copy as DNG) the colour it opens
|
||||
/// with, when the file has no colour matrices (issue #1745: a Z 9 NEF's DNG had no `ColorMatrix1`, and other raw
|
||||
/// converters refused it). The lens corrections stay in the DNG; only the fit sees the raw without them, as when
|
||||
/// the photo opens.
|
||||
pub(crate) fn with_starting_colour(raw: &mut RawImage, bytes: &[u8]) {
|
||||
if lightcraft_raw::color::has_matrix(&raw.color) {
|
||||
return;
|
||||
}
|
||||
let list3 = raw.opcodes.list3.clone();
|
||||
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||
let tags = starting_colour_tags(raw, bytes);
|
||||
raw.opcodes.list3 = list3;
|
||||
if let Some(tags) = tags {
|
||||
raw.color = tags;
|
||||
}
|
||||
}
|
||||
|
||||
/// DNG tags that render white-balanced camera RGB at its as-shot white as `t` with `look` on top does: one forward
|
||||
/// matrix (with a colour matrix and as-shot neutral that agree with it at that white), the look's hue/saturation
|
||||
/// table as `ProfileHueSatMap` and its tone curve as `ProfileToneCurve`. Not quite the same render: a DNG table
|
||||
|
||||
@@ -35,7 +35,8 @@ pub(crate) fn write_dng_with(file_bytes: Option<&crate::merge::ByteReader>, path
|
||||
Some(r) => r(path)?,
|
||||
None => std::fs::read(path).map_err(|e| format!("{path}: {e}"))?,
|
||||
};
|
||||
let raw = lightcraft_raw::decode(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut raw = lightcraft_raw::decode(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
crate::camera_preview::with_starting_colour(&mut raw, &bytes);
|
||||
drop(bytes);
|
||||
let dng = lightcraft_raw::write_dng(&raw, &lightcraft_raw::DngWriteOptions { xmp: (!packet.is_empty()).then_some(packet), ..Default::default() })
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -1710,7 +1710,8 @@ impl PreparedExport {
|
||||
if lightcraft_raw::probe(&bytes).is_none() {
|
||||
return Err(format!("{label}: DNG export needs a raw photo"));
|
||||
}
|
||||
let raw = lightcraft_raw::decode(&bytes).map_err(|e| format!("{label}: {e}"))?;
|
||||
let mut raw = lightcraft_raw::decode(&bytes).map_err(|e| format!("{label}: {e}"))?;
|
||||
crate::camera_preview::with_starting_colour(&mut raw, &bytes);
|
||||
drop(bytes);
|
||||
let compression = match dng {
|
||||
DngCompression::Lossless => lightcraft_raw::DngCompression::Lj92 { tile: 256 },
|
||||
|
||||
@@ -951,6 +951,56 @@ fn dng_export_compression_choices() {
|
||||
assert!(sizes[3].1 < sizes[0].1 / 2, "lossy is much smaller than lossless: {sizes:?}");
|
||||
}
|
||||
|
||||
/// Issue #1745: the DNG of a raw without colour matrices of its own (a Nikon Z 9 NEF) had no `ColorMatrix1`, and
|
||||
/// other raw converters refused it. Export as DNG and Convert / Copy as DNG now write the colour the raw opens with:
|
||||
/// the measured matrices of a camera that has them (Z f), the neutral fallback of one that doesn't (Z 9, whose
|
||||
/// look is fitted to its JPEG when there is one).
|
||||
#[test]
|
||||
fn dngs_of_raws_without_colour_matrices_carry_the_colour_they_open_with() {
|
||||
use lightcraft_raw::color::{as_shot_white_xy, camera_transform, has_matrix};
|
||||
let dir = temp_dir("dng-colour");
|
||||
let samples = [[0.2, 0.3, 0.1], [0.05, 0.02, 0.4], [0.6, 0.6, 0.6], [0.01, 0.5, 0.03]];
|
||||
let render = |t: &lightcraft_raw::color::CameraTransform, c: [f64; 3]| {
|
||||
t.matrix.apply(std::array::from_fn(|i| c[i] * t.wb[i] as f64)).map(|v| v * t.baseline_exposure.exp2())
|
||||
};
|
||||
for (model, measured) in [("NIKON Z f", true), ("NIKON Z 9", false)] {
|
||||
let metadata = lightcraft_meta::Metadata { make: Some("NIKON CORPORATION".into()), model: Some(model.into()), ..Default::default() };
|
||||
let mut raw = lightcraft_raw::decode(&synthetic_dng_with(None, metadata)).unwrap();
|
||||
// a vendor raw's colour: a white balance, no matrices
|
||||
raw.color = lightcraft_raw::ColorData { as_shot_neutral: Some([0.5, 1.0, 0.7]), ..Default::default() };
|
||||
let bytes = lightcraft_raw::write_dng(&raw, &Default::default()).unwrap();
|
||||
let path = dir.join(format!("{model}.dng"));
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut opened = lightcraft_raw::decode(&bytes).unwrap();
|
||||
assert!(!has_matrix(&opened.color));
|
||||
let (_, want, look) = crate::camera_preview::starting_colour(&mut opened, &bytes);
|
||||
assert_eq!((want.matrix_is_fallback, look.is_none()), (!measured, true), "{model}");
|
||||
|
||||
let mut s = Session::new().with_fs();
|
||||
s.execute("library.import", &json!({"paths": [path.to_string_lossy()]})).unwrap();
|
||||
let id = s.active().unwrap();
|
||||
let o = crate::export::ExportOptions::from_json(&json!({"format": "dng"}));
|
||||
let mut exported = Vec::new();
|
||||
let mut write = |_: &str, b: &[u8]| {
|
||||
exported = 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 converted = crate::cmd::convert::write_dng_with(None, &path.to_string_lossy(), String::new()).unwrap();
|
||||
for (how, dng) in [("export", exported), ("convert", std::fs::read(&converted).unwrap())] {
|
||||
let back = lightcraft_raw::decode(&dng).unwrap();
|
||||
assert!(back.color.color_matrix[0].is_some() && back.color.illuminant[0] != 0, "{model} {how}: {:?}", back.color);
|
||||
let got = camera_transform(&back, as_shot_white_xy(&back));
|
||||
for c in samples {
|
||||
let (a, b) = (render(&want, c), render(&got, c));
|
||||
assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-3), "{model} {how} {c:?}: {a:?} vs {b:?}");
|
||||
}
|
||||
}
|
||||
std::fs::remove_file(&converted).unwrap();
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
fn write_jpeg(path: &Path, capture: Option<&str>) {
|
||||
let (w, h) = (32usize, 24usize);
|
||||
let data: Vec<[u8; 4]> = (0..w * h).map(|i| [(i % w * 7) as u8, (i / w * 9) as u8, 90, 255]).collect();
|
||||
|
||||
@@ -391,7 +391,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-EXP-DIALOG | Export dialog | P0 | ✅ | `cmd:dialog.export`, `cmd:app.export` (`preset`), `cmd:export.presets`, `cmd:export.savePreset`, `cmd:export.deletePreset`, `crates/ui-egui/src/panels/dialogs.rs`, `crates/engine/src/export.rs` | batch export; built-in presets (JPEG Small 2048 px / Large full size, Original + Settings, DNG) and saved user presets (library prefs) load into the dialog; deleting a user preset is command-only; on a short window the options scroll and Cancel / OK stay visible (LR-BEHAV-DIALOGS, issue #781) |
|
||||
| LR-EXP-TYPE | File types | P0 | ✅ | `cmd:app.export` (`format`), `crates/engine/src/export.rs` (`ExportFormat`), `crates/engine/src/tests_export.rs` | JPEG, PNG, TIFF, PSD (flat, 8/16-bit, no layers: `crates/codecs/src/psd.rs`), WebP, AVIF, JPEG XL (issue #247: `jixel`, native only; lossy with quality or lossless, 8/16-bit, effort fast/normal (jixel's slowest search left out: jxl-oxide misdecodes its files), progressive, EXIF/XMP boxes; lossy files in Adobe RGB / ProPhoto are written as Rec. 2020, lossless keep any space via ICC; `crates/codecs/src/jxl_encode.rs`), DNG (raw photos: lossless re-encode with the edits in the embedded XMP), Original (+ XMP sidecar). Non-raw → DNG not supported; no JPEG XL export in the web build |
|
||||
| LR-EXP-TYPE | File types | P0 | ✅ | `cmd:app.export` (`format`), `crates/engine/src/export.rs` (`ExportFormat`), `crates/engine/src/tests_export.rs` | JPEG, PNG, TIFF, PSD (flat, 8/16-bit, no layers: `crates/codecs/src/psd.rs`), WebP, AVIF, JPEG XL (issue #247: `jixel`, native only; lossy with quality or lossless, 8/16-bit, effort fast/normal (jixel's slowest search left out: jxl-oxide misdecodes its files), progressive, EXIF/XMP boxes; lossy files in Adobe RGB / ProPhoto are written as Rec. 2020, lossless keep any space via ICC; `crates/codecs/src/jxl_encode.rs`), DNG (raw photos: lossless re-encode with the edits in the embedded XMP; a raw without colour matrices of its own, as a NEF, gets the colour it opens with as `ColorMatrix1` / `ForwardMatrix1` / `AsShotNeutral`, also in Convert / Copy as DNG, so other raw converters open it, issue #1745), Original (+ XMP sidecar). Non-raw → DNG not supported; no JPEG XL export in the web build |
|
||||
| LR-EXP-DIM | Output size | P0 | ✅ | `cmd:app.export` (`longEdge`, `shortEdge`, `width`, `height`, `megapixels`, `percent`, `dontEnlarge`, `ppi`), `crates/engine/src/export.rs` (`Resize`), `crates/ui-egui/src/panels/dialogs.rs` | full size = the cropped native size (no longer upscaled); W × H fits either orientation; ppi written to JFIF / pHYs / TIFF tags; at most 65,535 px an edge, and an export enlarged past the photo's size stops at 512 megapixels (Lightroom's ceiling too) and is refused, with the size it would need, when it needs more memory than the computer has (it aborted the process before) |
|
||||
| LR-EXP-QUALITY | JPEG quality | P0 | ✅ | `cmd:app.export` (`quality`, `limitKb`) | |
|
||||
| LR-EXP-BITDEPTH | Bit depth | P1 | ✅ | `cmd:app.export` (`bitDepth`), `crates/pipeline/src/output.rs` (`OutputDepth`), `crates/engine/src/export.rs` | 16-bit PNG/TIFF/JPEG XL rendered at 16 bits (TIFF defaults to 16), 32-bit float linear TIFF with a linear profile, 10-bit AVIF; high-bit-depth renders run on the CPU |
|
||||
|
||||
Reference in New Issue
Block a user