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.
This commit is contained in:
radialmonster 2026-10-09 04:02:20 -04:00
parent c7719ddcbf
commit 1399c445ef
2 changed files with 58 additions and 38 deletions

View File

@ -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];

View File

@ -255,12 +255,11 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
};
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<u8> {
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
let px: Vec<u16> = (0..16 * 8).map(|i| ((i * 37) % 900 + 100) as u16).collect();
let strip: Vec<u8> = 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<u16> = (0..w * h).map(|i| ((i * 37) % 900 + 100) as u16).collect();
let (tb, codes) = test_table();
let strip: Vec<u8> = 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")));
}
}