From fb1afc81e99561ce6a4fdf9ff9ae62d3e6bb8943 Mon Sep 17 00:00:00 2001 From: Lilith River Date: Mon, 9 Mar 2026 22:48:47 -0600 Subject: [PATCH] fix: handle WhiteIsZero for non-standard sub-byte depths (3/5/6/7-bit) For sub-byte depths where samples cross byte boundaries (3, 5, 6, 7-bit), byte-wise NOT corrupts samples. Added per-sample inversion that extracts each N-bit sample from the packed stream, computes (max - value), and writes it back. Tests cover: - 3/5/7-bit BlackIsZero (verify basic decoding) - 3-bit and 6-bit WhiteIsZero (verify per-sample inversion) --- src/decoder/mod.rs | 35 +++++++++++++++++++++++++ tests/decode_images.rs | 40 ++++++++++++++++++++++++++++- tests/images/minisblack-1c-3b.tiff | Bin 0 -> 140 bytes tests/images/minisblack-1c-5b.tiff | Bin 0 -> 144 bytes tests/images/minisblack-1c-7b.tiff | Bin 0 -> 148 bytes tests/images/miniswhite-1c-3b.tiff | Bin 0 -> 140 bytes tests/images/miniswhite-1c-6b.tiff | Bin 0 -> 146 bytes 7 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/images/minisblack-1c-3b.tiff create mode 100644 tests/images/minisblack-1c-5b.tiff create mode 100644 tests/images/minisblack-1c-7b.tiff create mode 100644 tests/images/miniswhite-1c-3b.tiff create mode 100644 tests/images/miniswhite-1c-6b.tiff diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8427a26..81a227c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -740,6 +740,41 @@ fn invert_colors( *x = !*x; } } + (ColorType::Gray(n @ (3 | 5 | 6 | 7)), SampleFormat::Uint) => { + // Non-standard sub-byte depths where samples may cross byte boundaries. + // Byte-wise NOT would corrupt samples spanning two bytes, so we invert + // each N-bit sample individually within the packed stream. + let max = (1u8 << n) - 1; + let total_bits = buf.len() * 8; + let mut bit_offset = 0; + while bit_offset + n as usize <= total_bits { + let byte_idx = bit_offset / 8; + let bit_in_byte = bit_offset % 8; + + if bit_in_byte + n as usize <= 8 { + // Sample fits within one byte + let shift = 8 - bit_in_byte - n as usize; + let val = (buf[byte_idx] >> shift) & max; + let mask = max << shift; + buf[byte_idx] = (buf[byte_idx] & !mask) | ((max - val) << shift); + } else { + // Sample spans two bytes + let combined = ((buf[byte_idx] as u16) << 8) + | buf.get(byte_idx + 1).copied().unwrap_or(0) as u16; + let shift = 16 - bit_in_byte - n as usize; + let max16 = max as u16; + let val = ((combined >> shift) & max16) as u8; + let new_combined = + (combined & !(max16 << shift)) | ((max - val) as u16) << shift; + buf[byte_idx] = (new_combined >> 8) as u8; + if byte_idx + 1 < buf.len() { + buf[byte_idx + 1] = new_combined as u8; + } + } + + bit_offset += n as usize; + } + } (ColorType::Gray(n @ 9..=15), SampleFormat::Uint) => { // Extended depths promoted to u16: invert within the valid bit range. // Byte-wise NOT would flip the upper padding bits, so we use (max - v). diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 4ba0e3e..c2ff3dd 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -946,6 +946,8 @@ fn test_rgb_planar_u24() { ); } +// ---- WhiteIsZero tests across bit depths ---- + // WhiteIsZero + extended bit depth (12-bit gray, synthetic 4x2 image) #[test] fn test_gray_u12_white_is_zero() { @@ -954,7 +956,43 @@ fn test_gray_u12_white_is_zero() { test_image_sum_u16("miniswhite-1c-12b.tiff", ColorType::Gray(12), 18565); } -// Horizontal predictor + extended bit depth (12-bit gray, synthetic 4x2 image) +// WhiteIsZero + non-standard sub-byte depths (3-bit and 6-bit, synthetic 8x2 images) +// These verify per-sample inversion for packed depths where samples cross byte boundaries. +#[test] +fn test_gray_u3_white_is_zero() { + test_image_sum_u8("miniswhite-1c-3b.tiff", ColorType::Gray(3), 765); +} + +#[test] +fn test_gray_u6_white_is_zero() { + // Values: [0, 10, 20, 30, 40, 50, 60, 63] × 2 rows + // After inversion: [63, 53, 43, 33, 23, 13, 3, 0] × 2 rows + test_image_sum_u8("miniswhite-1c-6b.tiff", ColorType::Gray(6), 2124); +} + +// ---- Non-standard sub-byte depths (BlackIsZero) ---- +// Verify that odd bit depths (3, 5, 7) decode without error. + +#[test] +fn test_gray_u3() { + // 8x2 image, values [0..7, 7..0], packed to 6 bytes + test_image_sum_u8("minisblack-1c-3b.tiff", ColorType::Gray(3), 765); +} + +#[test] +fn test_gray_u5() { + // 8x2 image, packed to 10 bytes + test_image_sum_u8("minisblack-1c-5b.tiff", ColorType::Gray(5), 1275); +} + +#[test] +fn test_gray_u7() { + // 8x2 image, packed to 14 bytes + test_image_sum_u8("minisblack-1c-7b.tiff", ColorType::Gray(7), 1711); +} + +// ---- Horizontal predictor + extended bit depth ---- + #[test] fn test_gray_u12_hpredict() { // Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000] diff --git a/tests/images/minisblack-1c-3b.tiff b/tests/images/minisblack-1c-3b.tiff new file mode 100644 index 0000000000000000000000000000000000000000..1ea0bfb0871a6460273ba404870d64b27cb09abd GIT binary patch literal 140 zcmebD)MDUZU|`^4U|?isU<9&2JVq#+3CL!GvYCNwW+)q^jtz+|$jAcL+XfUDMG_Z- ZvO(sEL)EYW*}+I^fLsPv%kp2xIsnS}1yKM1 literal 0 HcmV?d00001 diff --git a/tests/images/minisblack-1c-5b.tiff b/tests/images/minisblack-1c-5b.tiff new file mode 100644 index 0000000000000000000000000000000000000000..1bf67027a380ce8c763144990ce4237219e110de GIT binary patch literal 144 zcmebD)MDUZU|`^4U|?isU<9&2JVq#+3CL!GvRQ#_W+)q^jtz+|$jAcL+XfUDMG_Z- evO(sEL)8FnVhBc31LQI=3j9jH|L;BP3