From 67dc806ca925ccee99dbecab6c50d62a1b8bc9a7 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Sun, 11 Oct 2026 17:59:44 -0400 Subject: [PATCH 1/2] LR-IMP-FORMATS: DNG CFAPattern is anchored at the ActiveArea, not the sensor (odd active-area offsets rendered magenta, #1086) The DNG CFAPattern describes the top-left of the ActiveArea, but RawImage::cfa is sensor-anchored (the crop to the active area shifts it by the area's offset). The DNG reader now moves the tag's pattern out to the sensor origin, and the DNG writer states the pattern back at the ActiveArea. A DNG whose area starts on an odd row or column (the Canon EOS 60D DNG of #1086, top 51) no longer reads RGGB as GBRG. --- crates/raw/src/dng.rs | 4 +++- crates/raw/src/dngwrite.rs | 4 +++- crates/raw/tests/dng.rs | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/raw/src/dng.rs b/crates/raw/src/dng.rs index 27e30002f6..ba7dac532a 100644 --- a/crates/raw/src/dng.rs +++ b/crates/raw/src/dng.rs @@ -290,7 +290,9 @@ pub(crate) fn decode_as(bytes: &[u8], mode: Mode, format: RawFormat) -> Result Result> { if let Some(cfa) = &raw.cfa { ifd.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![cfa.height as u16, cfa.width as u16])); - ifd.set(t::CFA_PATTERN_EP, Value::Byte(cfa.pattern.clone())); + // the tag is anchored at the ActiveArea's top-left, `raw.cfa` at the sensor origin + let a = raw.active_area; + ifd.set(t::CFA_PATTERN_EP, Value::Byte(cfa.shifted(a.x, a.y).pattern)); ifd.set(t::CFA_PLANE_COLOR, Value::Byte(vec![0, 1, 2])); ifd.set(t::CFA_LAYOUT, Value::Short(vec![1])); } diff --git a/crates/raw/tests/dng.rs b/crates/raw/tests/dng.rs index 44527cf9c1..9feccdcbd9 100644 --- a/crates/raw/tests/dng.rs +++ b/crates/raw/tests/dng.rs @@ -989,3 +989,24 @@ fn baseline_sharpness_is_read_when_plausible() { assert_eq!(decode(&bytes).unwrap().color.baseline_sharpness, None); assert_eq!(decode(&profile_dng(ByteOrder::Big, |_| {})).unwrap().color.baseline_sharpness, None); } + +/// The DNG CFAPattern is anchored at the top-left of the ActiveArea: a DNG whose area starts on an odd row or +/// column still demosaics with the pattern the tag states (#1086), and the writer states it back the same way. +#[test] +fn cfa_pattern_is_relative_to_the_active_area() { + let (w, h) = (20, 12); + for (top, left, sensor_name) in [(0u32, 0u32, "GRBG"), (1, 0, "BGGR"), (0, 1, "RGGB"), (1, 1, "GBRG"), (3, 5, "GBRG"), (2, 4, "GRBG")] { + let mut raw = base_ifd(w, h, 16, 1, true); + raw.set_image(ImageData::Strips { rows_per_strip: h as u32, strips: vec![u16_bytes(&pattern(w, h, 1, 16), ByteOrder::Little)] }); + raw.set(t::WHITE_LEVEL, Value::Short(vec![65535])); + raw.set(t::ACTIVE_AREA, Value::Long(vec![top, left, h as u32 - 1, w as u32 - 1])); + let img = decode(&dng(raw, ByteOrder::Little)).unwrap(); + assert_eq!(img.cfa.as_ref().unwrap().name(), sensor_name, "sensor anchor for area at ({left}, {top})"); + assert_eq!(img.normalized().unwrap().cfa.unwrap().name(), "GRBG", "tag pattern for area at ({left}, {top})"); + let back = + decode(&write_dng(&img, &DngWriteOptions { compression: DngCompression::Uncompressed, order: ByteOrder::Little, xmp: None }).unwrap()) + .unwrap(); + assert_eq!(back.cfa.as_ref().unwrap().name(), sensor_name); + assert_eq!(back.normalized().unwrap().cfa.unwrap().name(), "GRBG"); + } +} From 81733ffd8158853a4d3f0f9f335cd80f41d88ab3 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Sun, 11 Oct 2026 22:59:24 -0400 Subject: [PATCH 2/2] LR-IMP-FORMATS: bump RENDER_CACHE_VERSION for the DNG CFA phase fix --- crates/engine/src/media.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 6f6f0359c4..58b9712ace 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -62,7 +62,7 @@ impl SettingsHashes { } /// Bump when the pipeline's output changes, to invalidate cached thumbnails. -pub const RENDER_CACHE_VERSION: u64 = 61; +pub const RENDER_CACHE_VERSION: u64 = 62; /// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache). pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];