From dee0c2fb11a60858c69faf0ee3d5ba2860561b9f Mon Sep 17 00:00:00 2001 From: "jacovdbergh@gmail.com" Date: Wed, 8 Jul 2026 08:10:41 +0200 Subject: [PATCH] fix: clip border tile rows when discarding extra samples Tiled images with a width that is not a multiple of the tile width wrote the border tiles' horizontal padding into the next output row. Fixes #400. --- CHANGES.md | 7 ++++ src/decoder/image.rs | 4 +++ tests/decode_images.rs | 31 ++++++++++++++++++ .../images/tiled-extra-samples-border-u8.tif | Bin 0 -> 16590 bytes 4 files changed, 42 insertions(+) create mode 100644 tests/images/tiled-extra-samples-border-u8.tif diff --git a/CHANGES.md b/CHANGES.md index a4989f7..ffbab4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Version 0.12.0 (unreleased) +Fixes: +- Decoding a tiled image whose width is not a multiple of the tile width no + longer corrupts the columns below the right border tiles when extra samples + are discarded (e.g. RGB with an unspecified extra sample). The horizontal + tile padding was written past the end of the output row, spilling into the + leftmost columns of the following row. + Additions: - Added the `TiffHeader` type. It can be used to create a `Decoder` in a state before the first IFD and allows opening files at an arbitrary first IFD diff --git a/src/decoder/image.rs b/src/decoder/image.rs index b90e25c..4a33b9b 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -1308,6 +1308,10 @@ impl Image { let photo_range = photometric_bit_end / 8..bits_per_pixel / 8; let mut encoded = vec![0u8; chunk_row_bytes]; for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { + // Clip to the chunk's data width: border chunks are stored at the full chunk + // width, and writing their horizontal padding would spill into the following + // output row (and predict/invert would run past the chunk's own columns). + let row = &mut row[..data_row_bytes]; reader.read_exact(&mut encoded)?; Self::compact_photometric_bytes(&mut encoded, row, &photo_range); diff --git a/tests/decode_images.rs b/tests/decode_images.rs index d2cafd3..9e705c3 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -475,6 +475,37 @@ fn test_tiled_rect_rgb_u8() { test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032); } +#[test] +fn test_tiled_extra_samples_border_u8() { + // Regression test for https://github.com/image-rs/image-tiff/issues/400 + // + // A tiled image whose dimensions are not multiples of the tile size, with + // an extra sample that is discarded on decode. The horizontal padding of + // the right border tiles used to be written past the end of the output + // row, spilling into the leftmost columns of the following row. The + // fixture's tile padding is filled with 0xEE so any spill is visible. + let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-extra-samples-border-u8.tif"); + 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.dimensions().unwrap(), (40, 40)); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); + + let DecodingSampleBuffer::U8(data) = decoder.read_image().unwrap() else { + panic!("Wrong bit depth") + }; + + let mut expected = Vec::with_capacity(40 * 40 * 3); + for y in 0..40u32 { + for x in 0..40u32 { + expected.push(((x * 3) % 251) as u8); + expected.push(((y * 7) % 251) as u8); + expected.push(((x + y) % 251) as u8); + } + } + assert_eq!(data, expected); +} + #[test] fn test_inner_access() { let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif"); diff --git a/tests/images/tiled-extra-samples-border-u8.tif b/tests/images/tiled-extra-samples-border-u8.tif new file mode 100644 index 0000000000000000000000000000000000000000..119cdaaef44340d20444364545320ca9f78bc57a GIT binary patch literal 16590 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