From 29e43d23144c84dfa0f0bb1c01ca7dc5daace059 Mon Sep 17 00:00:00 2001 From: Lilith River Date: Mon, 9 Mar 2026 22:41:20 -0600 Subject: [PATCH] fix: handle WhiteIsZero for extended bit depths, add comprehensive tests Fix invert_colors to support extended bit depths (9-15 promoted to u16, 17-31 promoted to u32). Byte-wise NOT doesn't work for promoted samples because it flips unused upper bits; use (max - value) instead. Add regression tests covering the extended depth matrix: - Gray 10/14/24-bit (contiguous, BlackIsZero) - RGB 10/14/24-bit (contiguous) - RGB 10/12/14/24-bit (planar) - Gray 12-bit with WhiteIsZero (synthetic 4x2 image) - Gray 12-bit with horizontal predictor (synthetic 4x2 image) --- src/decoder/mod.rs | 17 ++++ tests/decode_images.rs | 150 ++++++++++++++++++++++++++++ tests/images/hpredict-1c-12b.tiff | Bin 0 -> 158 bytes tests/images/miniswhite-1c-12b.tiff | Bin 0 -> 146 bytes 4 files changed, 167 insertions(+) create mode 100644 tests/images/hpredict-1c-12b.tiff create mode 100644 tests/images/miniswhite-1c-12b.tiff diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c4ecf93..8427a26 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -740,6 +740,23 @@ fn invert_colors( *x = !*x; } } + (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). + let max = (1u16 << n) - 1; + for chunk in buf.as_chunks_mut::<2>().0 { + let v = u16::from_ne_bytes(*chunk); + *chunk = (max - v).to_ne_bytes(); + } + } + (ColorType::Gray(n @ 17..=31), SampleFormat::Uint) => { + // Extended depths promoted to u32: invert within the valid bit range. + let max = (1u32 << n) - 1; + for chunk in buf.as_chunks_mut::<4>().0 { + let v = u32::from_ne_bytes(*chunk); + *chunk = (max - v).to_ne_bytes(); + } + } (ColorType::Gray(32), SampleFormat::IEEEFP) => { for x in buf.as_chunks_mut::<4>().0 { let v = f32::from_ne_bytes(*x); diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 031bd34..4ba0e3e 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -8,6 +8,7 @@ use std::io::{Read, Seek}; use std::path::PathBuf; const TEST_IMAGE_DIR: &str = "./tests/images/"; +const LIBTIFFPIC_DEPTH_DIR: &str = "./tests/libtiffpic/depth/"; macro_rules! test_image_sum { ($name:ident, $buffer:ident, $sum_ty:ty) => { @@ -40,6 +41,32 @@ test_image_sum!(test_image_sum_u64, U64, u64); test_image_sum!(test_image_sum_f32, F32, f32); test_image_sum!(test_image_sum_f64, F64, f64); +/// Like test_image_sum but reads from an arbitrary directory. +macro_rules! test_image_sum_in { + ($name:ident, $buffer:ident, $sum_ty:ty) => { + fn $name(dir: &str, file: &str, expected_type: ColorType, expected_sum: $sum_ty) { + let path = PathBuf::from(dir).join(file); + let img_file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::open(img_file).expect("Cannot create decoder"); + + decoder.next_image().expect("Cannot read image IFD"); + assert_eq!(decoder.colortype().unwrap(), expected_type); + let img_res = decoder.read_image().unwrap(); + + match img_res { + DecodingResult::$buffer(res) => { + let sum: $sum_ty = res.into_iter().map(<$sum_ty>::from).sum(); + assert_eq!(sum, expected_sum); + } + _ => panic!("Wrong bit depth"), + } + } + }; +} + +test_image_sum_in!(test_depth_sum_u16, U16, u64); +test_image_sum_in!(test_depth_sum_u32, U32, u64); + /// Tests that a decoder can be constructed for an image and the color type /// read from the IFD and is of the appropriate type, but the type is /// unsupported. @@ -810,3 +837,126 @@ fn test_decode_huge_g4() { panic!("Unexpected decoding result type"); } } + +// ---- Extended bit depth tests (9-31 bits per sample) ---- +// +// These test the unpacking of packed N-bit samples to standard-width output. +// All flower-* images are 73x43 pixels from the libtiffpic depth suite. + +// Gray extended depths (contiguous, BlackIsZero) +#[test] +fn test_gray_u10() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-minisblack-10.tif", + ColorType::Gray(10), + 1175406, + ); +} + +#[test] +fn test_gray_u14() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-minisblack-14.tif", + ColorType::Gray(14), + 18847564, + ); +} + +#[test] +fn test_gray_u24() { + test_depth_sum_u32( + LIBTIFFPIC_DEPTH_DIR, + "flower-minisblack-24.tif", + ColorType::Gray(24), + 19201687050, + ); +} + +// RGB contiguous extended depths +#[test] +fn test_rgb_contig_u10() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-contig-10.tif", + ColorType::RGB(10), + 3302314, + ); +} + +#[test] +fn test_rgb_contig_u14() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-contig-14.tif", + ColorType::RGB(14), + 52955900, + ); +} + +#[test] +fn test_rgb_contig_u24() { + test_depth_sum_u32( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-contig-24.tif", + ColorType::RGB(24), + 53926969278, + ); +} + +// RGB planar extended depths (read_image returns first plane only) +#[test] +fn test_rgb_planar_u10() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-planar-10.tif", + ColorType::RGB(10), + 1214176, + ); +} + +#[test] +fn test_rgb_planar_u12() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-planar-12.tif", + ColorType::RGB(12), + 4865028, + ); +} + +#[test] +fn test_rgb_planar_u14() { + test_depth_sum_u16( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-planar-14.tif", + ColorType::RGB(14), + 19468331, + ); +} + +#[test] +fn test_rgb_planar_u24() { + test_depth_sum_u32( + LIBTIFFPIC_DEPTH_DIR, + "flower-rgb-planar-24.tif", + ColorType::RGB(24), + 19835471019, + ); +} + +// WhiteIsZero + extended bit depth (12-bit gray, synthetic 4x2 image) +#[test] +fn test_gray_u12_white_is_zero() { + // Values: [0, 1000, 2000, 4095, 100, 500, 3000, 3500] + // After WhiteIsZero inversion: [4095, 3095, 2095, 0, 3995, 3595, 1095, 595] + test_image_sum_u16("miniswhite-1c-12b.tiff", ColorType::Gray(12), 18565); +} + +// Horizontal predictor + extended bit depth (12-bit gray, synthetic 4x2 image) +#[test] +fn test_gray_u12_hpredict() { + // Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000] + test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000); +} diff --git a/tests/images/hpredict-1c-12b.tiff b/tests/images/hpredict-1c-12b.tiff new file mode 100644 index 0000000000000000000000000000000000000000..de2fcc84891b6e87583a532037d30887329cf6b9 GIT binary patch literal 158 zcmebD)MDUZU|`^8U|?isU<9&QfS3`=W&*OApllu>n;FUmsbfQ83o^2R^-cnciz11O gLD?X4#Gz_H=GY>M2P3HmN-?lGqyUj!^9vvX01%!BRsaA1 literal 0 HcmV?d00001 diff --git a/tests/images/miniswhite-1c-12b.tiff b/tests/images/miniswhite-1c-12b.tiff new file mode 100644 index 0000000000000000000000000000000000000000..a85842e0b58f9c20675e262f4ce815e7d8bb57e8 GIT binary patch literal 146 zcmebD)MDUZU|`^4U|?isU<9&QfS3`=W&*OApllu>n;FUmsbhn(flLNLMi#K%HXvIR iNsSnk4Kha@ss?0EFp?S|7ijQ{TK@lRj$d~7t^ok?CI|xn literal 0 HcmV?d00001