mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
TIFF: invert WhiteIsZero gray once, not twice
A WhiteIsZero (PhotometricInterpretation 0) gray TIFF opened as a negative at every bit depth. The `tiff` crate (0.11) already inverts those samples as it reads them, for every layout it accepts, and refuses the WhiteIsZero layouts it can't invert; the decoder here then inverted them a second time. WhiteIsZero is common in scanner and fax output, and CCITT fax images use it by definition. The second inversion is removed. The new test builds 8- and 16-bit WhiteIsZero and BlackIsZero strips in code and fails without the fix (the lowest sample came out black instead of white). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b8df5f472c
commit
cc425d0cea
@@ -25,7 +25,6 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result<Decoded> {
|
||||
let icc = tag_bytes(&mut d, Tag::IccProfile);
|
||||
let xmp = tag_bytes(&mut d, Tag::from_u16_exhaustive(700)).map(|v| String::from_utf8_lossy(&v).trim_end_matches('\0').to_string());
|
||||
let orientation = d.get_tag_u32(Tag::Orientation).ok().map(|v| v as u16);
|
||||
let photometric = d.get_tag_u32(Tag::PhotometricInterpretation).ok();
|
||||
let planar = d.get_tag_u32(Tag::PlanarConfiguration).ok() == Some(2);
|
||||
let extra = d.get_tag_u16_vec(Tag::ExtraSamples).ok().unwrap_or_default();
|
||||
let colormap = d.get_tag_u16_vec(Tag::ColorMap).ok();
|
||||
@@ -94,14 +93,8 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result<Decoded> {
|
||||
return finish(F, raw, meta, (w, h), opts);
|
||||
}
|
||||
|
||||
// WhiteIsZero → invert.
|
||||
if photometric == Some(0) {
|
||||
match &mut buf {
|
||||
Buf::U8(v) => v.iter_mut().for_each(|x| *x = 255 - *x),
|
||||
Buf::U16(v) => v.iter_mut().for_each(|x| *x = 65535 - *x),
|
||||
Buf::F32(v) => v.iter_mut().for_each(|x| *x = 1.0 - *x),
|
||||
}
|
||||
}
|
||||
// WhiteIsZero needs nothing here: the `tiff` crate inverts those samples as it reads them (and
|
||||
// refuses WhiteIsZero layouts it can't invert), so inverting again would show a negative.
|
||||
|
||||
// Drop extra samples beyond colour + one alpha.
|
||||
let keep = n_color + has_alpha as usize;
|
||||
|
||||
@@ -245,6 +245,44 @@ fn tiff_cmyk_and_orientation() {
|
||||
assert!(red[0] > 0.99 && red[1] < 0.01 && red[2] < 0.01, "{red:?}");
|
||||
}
|
||||
|
||||
/// A one-strip, uncompressed, single-sample TIFF (little-endian) whose samples are `bits` wide and
|
||||
/// already packed into `strip`, one padded row after another.
|
||||
fn packed_tiff(width: u16, height: u16, bits: u16, photometric: u16, strip: &[u8]) -> Vec<u8> {
|
||||
const STRIP_AT: u32 = 8 + 2 + 9 * 12 + 4;
|
||||
let short = |tag: u16, v: u16| [&tag.to_le_bytes()[..], &3u16.to_le_bytes(), &1u32.to_le_bytes(), &v.to_le_bytes(), &[0, 0]].concat();
|
||||
let long = |tag: u16, v: u32| [&tag.to_le_bytes()[..], &4u16.to_le_bytes(), &1u32.to_le_bytes(), &v.to_le_bytes()].concat();
|
||||
let mut b = b"II*\0".to_vec();
|
||||
b.extend(8u32.to_le_bytes());
|
||||
b.extend(9u16.to_le_bytes());
|
||||
b.extend(short(256, width));
|
||||
b.extend(short(257, height));
|
||||
b.extend(short(258, bits));
|
||||
b.extend(short(259, 1));
|
||||
b.extend(short(262, photometric));
|
||||
b.extend(long(273, STRIP_AT));
|
||||
b.extend(short(277, 1));
|
||||
b.extend(short(278, height));
|
||||
b.extend(long(279, strip.len() as u32));
|
||||
b.extend(0u32.to_le_bytes());
|
||||
assert_eq!(b.len(), STRIP_AT as usize);
|
||||
b.extend_from_slice(strip);
|
||||
b
|
||||
}
|
||||
|
||||
/// WhiteIsZero (photometric 0) gray: the lowest sample is white. The `tiff` crate already inverts these
|
||||
/// samples, and a second inversion here used to show every WhiteIsZero scan as a negative.
|
||||
#[test]
|
||||
fn tiff_white_is_zero_is_inverted_once() {
|
||||
// Two pixels: the lowest sample, then the highest, at 8 and 16 bits.
|
||||
for (bits, strip) in [(8, vec![0u8, 255]), (16, [0u16.to_le_bytes(), 65535u16.to_le_bytes()].concat())] {
|
||||
for (photometric, first) in [(0, 1.0), (1, 0.0)] {
|
||||
let d = decode(&packed_tiff(2, 1, bits, photometric, &strip), DecodeOptions::default()).unwrap();
|
||||
let (a, b) = (d.image.get(0, 0)[1], d.image.get(1, 0)[1]);
|
||||
assert!((a - first).abs() < 1e-6 && (b - (1.0 - first)).abs() < 1e-6, "{bits}-bit, photometric {photometric}: {a} {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webp_lossless_exact() {
|
||||
let img = gradient(31, 19);
|
||||
|
||||
Reference in New Issue
Block a user