From 855d3a7959ca002227670c37ddea16dda71edf61 Mon Sep 17 00:00:00 2001 From: Gremiger Date: Wed, 7 Oct 2026 21:43:23 +0000 Subject: [PATCH] Issue #212: Raw: keep vendor white balance when writing DNG NEF (and other vendor raws without a colour matrix) carry their as-shot white balance as vendor multipliers, not as a DNG AsShotNeutral. The DNG writer only copied AsShotNeutral/AsShotWhiteXY, so Convert to DNG and Copy as DNG produced files with no white balance at all, which develop dark and green. Write the reciprocal of the vendor multipliers as AsShotNeutral when the source has no DNG white of its own. --- crates/raw/src/dngwrite.rs | 3 +++ crates/raw/tests/dng.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/crates/raw/src/dngwrite.rs b/crates/raw/src/dngwrite.rs index 3df8d706d..bd46a2c5b 100644 --- a/crates/raw/src/dngwrite.rs +++ b/crates/raw/src/dngwrite.rs @@ -177,6 +177,9 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { ifd.set(t::AS_SHOT_NEUTRAL, rat_vec(&v)); } else if let Some(xy) = c.as_shot_white_xy { ifd.set(t::AS_SHOT_WHITE_XY, rat_vec(&[xy.x, xy.y])); + } else if let Some(m) = raw.wb_multipliers.filter(|m| m.iter().all(|v| *v > 0.0 && v.is_finite())) { + // vendor raws (NEF, ARW…) carry white balance as multipliers: the DNG neutral is their reciprocal + ifd.set(t::AS_SHOT_NEUTRAL, rat_vec(&m.map(|v| 1.0 / v as f64))); } ifd.set(t::BASELINE_EXPOSURE, Value::SRational(vec![srational(c.baseline_exposure)])); // the source's own profile look travels with its data (conversions, smart previews, merges) diff --git a/crates/raw/tests/dng.rs b/crates/raw/tests/dng.rs index 5f0aad287..0d38ebb40 100644 --- a/crates/raw/tests/dng.rs +++ b/crates/raw/tests/dng.rs @@ -453,6 +453,22 @@ fn colour_transform_from_decoded_dng() { assert!(o.iter().all(|v| (v - 1.0).abs() < 1e-9)); } +/// Issue #212: a NEF converted to DNG came out dark and green. NEF white balance is vendor multipliers +/// (no colour matrix, no `AsShotNeutral`); the DNG must carry them as its as-shot neutral. +#[test] +fn vendor_white_balance_survives_dng_conversion() { + let mut raw = synthetic(16, 16, Some(Cfa::bayer("RGGB").unwrap()), 1); + raw.color = ColorData::default(); + raw.wb_multipliers = Some([2.25, 1.0, 1.7421875]); + let before = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw)); + let back = decode(&write_dng(&raw, &DngWriteOptions::default()).unwrap()).unwrap(); + let after = lightcraft_raw::color::camera_transform(&back, lightcraft_raw::color::as_shot_white_xy(&back)); + for c in 0..3 { + assert!((before.wb[c] - after.wb[c]).abs() < 1e-3, "{:?} vs {:?}", before.wb, after.wb); + } + assert!((after.wb[0] - 2.25).abs() < 1e-3 && (after.wb[2] - 1.7421875).abs() < 1e-3, "{:?}", after.wb); +} + #[test] fn rejects_unsupported_and_broken() { let (w, h) = (8, 8);