From 67dc806ca925ccee99dbecab6c50d62a1b8bc9a7 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Sun, 11 Oct 2026 17:59:44 -0400 Subject: [PATCH] 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 27e30002f..ba7dac532 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 44527cf9c..9feccdcbd 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"); + } +}