From 1399c445ef8f4ba11e26e1fd9c9bf1272c8ea757 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Fri, 9 Oct 2026 04:02:20 -0400 Subject: [PATCH] LR-IMP-FORMATS: read the PEF colour-filter layout from the Exif CFAPattern for Huffman-compressed files too The Huffman-compressed Pentax files (compression 65535) assumed BGGR from the array origin. The K10D, K200D, K2000 and every body from the K-3 on are RGGB, so red and blue were exchanged before demosaicing. They now use the Exif CFAPattern from the first sample of the stored array, like the other compressions; BGGR stays the fallback for files without the tag. Extends the CFA test to compression 65535. RENDER_CACHE_VERSION 17 to 18. --- crates/engine/src/media.rs | 2 +- crates/raw/src/vendor/pef.rs | 94 ++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 5b892250..ed16f8ae 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 = 17; +pub const RENDER_CACHE_VERSION: u64 = 18; /// 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]; diff --git a/crates/raw/src/vendor/pef.rs b/crates/raw/src/vendor/pef.rs index 5fa7c229..d065fc4f 100644 --- a/crates/raw/src/vendor/pef.rs +++ b/crates/raw/src/vendor/pef.rs @@ -255,12 +255,11 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { }; let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.bytes(t::CFA_PATTERN_EP)) { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, - // The uncompressed bodies state their pattern in the Exif `CFAPattern`, read from the first sample of the - // stored array, not of the image area: the *ist D (area origin 19, 11) says RGGB and measures RGGB there. A - // pattern shifted by an odd origin would exchange red and blue. (The Huffman-compressed bodies keep the - // assumed layout here.) - _ if matches!(info.compression, 1 | 32773) => cfa_from_exif(&tiff).unwrap_or_else(|| Cfa::bayer_static("BGGR")), - _ => Cfa::bayer_static("BGGR"), + // Every body states its layout in the Exif `CFAPattern`, read from the first sample of the stored array, not + // of the image area: the *ist D (origin 19, 11) and the 645D (80, 59) only fit unshifted. The 12-bit 10 MP + // bodies and everything from the K-3 on say RGGB; the K-7 / K-5 / 645D generation says BGGR. Files without the + // tag keep BGGR. + _ => cfa_from_exif(&tiff).unwrap_or_else(|| Cfa::bayer_static("BGGR")), }; let black = match mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_POINT)).as_deref() { Some(v @ [_, _, _, _]) => { @@ -423,49 +422,70 @@ mod tests { } } - /// A 16 x 8 big-endian 12-bit PEF-style file with a 12 x 6 image area at the odd origin (3, 1) and the Exif - /// `CFAPattern` `pattern`; the samples are plain 16-bit words (compression 1) or packed 12-bit (32773). + /// A 16 x 8 big-endian 12-bit PEF-style file with a 12 x 6 image area at the odd origin (3, 1), the Exif + /// `CFAPattern` `pattern` and an AOC maker note with the area tags (and, for 65535, a Huffman table). The samples + /// are plain 16-bit words (1), packed 12-bit (32773) or Huffman coded (65535). fn pef_with_pattern(compression: u16, pattern: [u8; 4]) -> Vec { use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value}; - let px: Vec = (0..16 * 8).map(|i| ((i * 37) % 900 + 100) as u16).collect(); - let strip: Vec = if compression == 1 { - px.iter().flat_map(|v| v.to_be_bytes()).collect() - } else { - px.chunks(2).flat_map(|p| [(p[0] >> 4) as u8, ((p[0] & 15) << 4 | p[1] >> 8) as u8, p[1] as u8]).collect() + let (w, h) = (16usize, 8usize); + let px: Vec = (0..w * h).map(|i| ((i * 37) % 900 + 100) as u16).collect(); + let (tb, codes) = test_table(); + let strip: Vec = match compression { + 1 => px.iter().flat_map(|v| v.to_be_bytes()).collect(), + 32773 => px.chunks(2).flat_map(|p| [(p[0] >> 4) as u8, ((p[0] & 15) << 4 | p[1] >> 8) as u8, p[1] as u8]).collect(), + _ => encode(&px, w, &codes), }; - let mut ifd = IfdBuilder::new(); - ifd.set(0x0100, Value::Long(vec![16])); - ifd.set(0x0101, Value::Long(vec![8])); - ifd.set(0x0102, Value::Short(vec![12])); - ifd.set(0x0103, Value::Short(vec![compression])); - ifd.set(0x0106, Value::Short(vec![32803])); - ifd.set(0x010f, Value::Ascii("PENTAX Corporation".into())); - ifd.set_image(ImageData::Strips { rows_per_strip: 8, strips: vec![strip] }); - // AOC + "MM", then one IFD: area origin and size (inline), next-IFD 0 - let mut note = b"AOCMM".to_vec(); - note.extend_from_slice(&2u16.to_be_bytes()); - for (tag, val) in [(0x0038u16, [0u8, 3, 0, 1]), (0x0039, [0, 12, 0, 6])] { - note.extend_from_slice(&tag.to_be_bytes()); - note.extend_from_slice(&3u16.to_be_bytes()); - note.extend_from_slice(&2u32.to_be_bytes()); - note.extend_from_slice(&val); + let build = |table_at: u32| { + let mut ifd = IfdBuilder::new(); + ifd.set(0x0100, Value::Long(vec![w as u32])); + ifd.set(0x0101, Value::Long(vec![h as u32])); + ifd.set(0x0102, Value::Short(vec![12])); + ifd.set(0x0103, Value::Short(vec![compression])); + ifd.set(0x0106, Value::Short(vec![32803])); + ifd.set(0x010f, Value::Ascii("PENTAX Corporation".into())); + ifd.set_image(ImageData::Strips { rows_per_strip: h as u32, strips: vec![strip.clone()] }); + // AOC + "MM", then the IFD (area origin, area size and, for Huffman data, the table), next-IFD 0, the table + let mut note = b"AOCMM".to_vec(); + let mut entries = vec![(0x0038u16, 3u16, 2u32, [0u8, 3, 0, 1]), (0x0039, 3, 2, [0, 12, 0, 6])]; + if compression == 65535 { + entries.push((0x0220, 7, tb.len() as u32, table_at.to_be_bytes())); + } + note.extend_from_slice(&(entries.len() as u16).to_be_bytes()); + for (tag, ty, cnt, val) in entries { + note.extend_from_slice(&tag.to_be_bytes()); + note.extend_from_slice(&ty.to_be_bytes()); + note.extend_from_slice(&cnt.to_be_bytes()); + note.extend_from_slice(&val); + } + note.extend_from_slice(&0u32.to_be_bytes()); + if compression == 65535 { + note.extend_from_slice(&tb); + } + let mut exif = IfdBuilder::new(); + exif.set(0xa302, Value::Undefined([&[0, 2, 0, 2][..], &pattern[..]].concat())); + exif.set(0x927c, Value::Undefined(note)); + ifd.set_child(0x8769, exif); + TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap() + }; + let first = build(0); + if compression != 65535 { + return first; } - note.extend_from_slice(&0u32.to_be_bytes()); - let mut exif = IfdBuilder::new(); - exif.set(0xa302, Value::Undefined([&[0, 2, 0, 2][..], &pattern[..]].concat())); - exif.set(0x927c, Value::Undefined(note)); - ifd.set_child(0x8769, exif); - TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap() + let at = Tiff::parse(&first).unwrap().exif().unwrap().get(0x927c).unwrap().offset; + build((at + 6 + 2 + 3 * 12 + 4) as u32) } #[test] - fn uncompressed_files_use_the_exif_pattern_from_the_array_origin_not_the_area_origin() { - for compression in [1u16, 32773] { + fn the_pattern_is_the_exif_cfapattern_from_the_array_origin_for_every_compression() { + for compression in [1u16, 32773, 65535] { for (p, name) in [([0u8, 1, 1, 2], "RGGB"), ([2, 1, 1, 0], "BGGR"), ([1, 0, 2, 1], "GRBG"), ([1, 2, 0, 1], "GBRG")] { let img = decode(&pef_with_pattern(compression, p), Mode::Full).unwrap(); assert_eq!(img.active_area, Rect::new(3, 1, 12, 6), "compression {compression}"); assert_eq!(img.cfa, Some(Cfa::bayer_static(name)), "compression {compression}, Exif {name}"); } } + // no tag: the assumed layout stays + let img = decode(&tiny_pef(4, 2, 1, 16, &[0; 16]), Mode::Full).unwrap(); + assert_eq!(img.cfa, Some(Cfa::bayer_static("BGGR"))); } }