From 9b452e93b708db6844a4402f4aac76311afbf44b Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Sat, 3 Oct 2026 23:07:34 +0200 Subject: [PATCH] - fix tiled image reading --- src/decoder/image.rs | 13 ++++++++- tests/decode_images.rs | 41 +++++++++++++++++++++++++++ tests/images/tiled-planar-2c-16b.tif | Bin 0 -> 4432 bytes 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/images/tiled-planar-2c-16b.tif diff --git a/src/decoder/image.rs b/src/decoder/image.rs index fd062d0..08060c9 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -36,6 +36,9 @@ impl TileAttributes { pub fn tiles_down(&self) -> usize { self.image_height.div_ceil(self.tile_length) } + pub fn tiles_per_plane(&self) -> usize { + self.tiles_across() * self.tiles_down() + } fn padding_right(&self) -> usize { (self.tile_width - self.image_width % self.tile_width) % self.tile_width } @@ -1016,7 +1019,15 @@ impl Image { } ChunkType::Tile => { let tile_attrs = self.tile_attributes.as_ref().unwrap(); - let (padding_right, padding_down) = tile_attrs.get_padding(chunk_index as usize); + // The chunk index counts the tiles of all planes, but the tile grid repeats per + // plane, so only the position within the plane decides the padding. + let tiles_per_plane = tile_attrs.tiles_per_plane(); + let tile = if tiles_per_plane == 0 { + 0 + } else { + chunk_index as usize % tiles_per_plane + }; + let (padding_right, padding_down) = tile_attrs.get_padding(tile); let tile_width = tile_attrs.tile_width - padding_right; let tile_length = tile_attrs.tile_length - padding_down; diff --git a/tests/decode_images.rs b/tests/decode_images.rs index d7db448..1046410 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -506,6 +506,47 @@ fn test_tiled_extra_samples_border_u8() { assert_eq!(data, expected); } +#[test] +fn test_tiled_planar_border_u16() { + // A planar (separate) tiled image whose dimensions are not multiples of the tile size. + // The tile grid repeats for every plane, but the chunk (tile) index counts all planes, so + // the border padding of the second plane used to be looked up with the wrong tile index. + // The bottom-right tile of plane 1 then claimed a full tile height, and the decoder ran + // past the end of the output buffer. + // + // Note that `read_image` only decodes the first plane, so this has to go through + // `read_image_to_buffer` to reach the tiles that were mis-sized. + let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-planar-2c-16b.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(), (30, 20)); + assert_eq!( + decoder.colortype().unwrap(), + ColorType::Multiband { + bit_depth: 16, + num_samples: 2 + } + ); + + let mut buffer = DecodingSampleBuffer::U16(Vec::new()); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + let DecodingSampleBuffer::U16(data) = buffer else { + panic!("Wrong bit depth") + }; + assert_eq!(data.len(), 2 * 20 * 30); + + let mut expected = Vec::with_capacity(2 * 20 * 30); + for plane in 0..2u16 { + for y in 0..20u16 { + for x in 0..30u16 { + expected.push(1000 * plane + 10 * y + x); + } + } + } + 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-planar-2c-16b.tif b/tests/images/tiled-planar-2c-16b.tif new file mode 100644 index 0000000000000000000000000000000000000000..771890b84697b73298f0069d71c323bf1a54c170 GIT binary patch literal 4432 zcmeH}^;cC(7>3`&ya*VG3Kpn{g@{PlVj-xQh%KU`f?#5c*xlXT-Q9V+JO6>(-92*_ z-(55J@;kq<_xkpJ&b%|TX3w+FIwv(X3jP2p0UpAmskSap5eIAb@hJG{dyqbrX4A)1 z8H)owKB%VKKGGFZ)NaPq^!R%FnoDRA`)dxf`b1ju#OZm;>v2K4Pg2nug7oJu7AI>q zD-P3rQuJ8EiJH@k=FuO$M`6VD+^qcUh_>Mw@zLS&vC-iPv6=HFmAfAJ?-LoGk(ZN` zlQ%ItCcgk_x*<*J^3~<1D?nE{T@`dy)SZ3)b9yKB=tc#?7iHk5R~LY?D5syQ0s>Ld z;UK;1l~D!32tif7f7MaL;hG3V7;0%Fbr6oah(JAu>!Sf0q7fp|7*S|~Xf$;=2F(zQ zIK(3X&Cvoa(aPc0XoI$BheWhT2XsUybapriU670vbVWCGM=E-tr^CI_8-36hY3PS^ z^v3`Uba)U3V+e*~7&0&%nHYhQ4v#_>Mq>=dA{#jvhg{@2JRTD;5tA?(Q;?6Tn1<;N z&%jK~!febz0p_9*^Dy7x1z3nhSd1lDie*@i6I&R=5Zs9g; zzJt5Chx>Sdhj@g?c!H-6KZ9rS9G=Guco8q*WxRq{y=y4Fl-v#!=TtnJRSDZp#iPlE zZKto{b-aN$O$_0;@HXDTyLb=p;{$xCe|bOl=B1aC-GNWkr|L8Hx%xtVslHNQt8dh| z>O1wl`a%7ueo{X>{EPZk{ic3bf2cpzU+QoDr@=#;eW*zKvJCy`&j6NXIfu)$0s~o* zL9E2etioW1I9!$0Se-Rklc5Y_E!Jiohr?Ny5v<4hY`}(W#7H)FIEqae&8CcDGsZHG z@l0^IIa{zLTd_6Uur1p$k?kGsz>e(1&P-w#CNqUy+0EhZOl1%DWH0t+ANFM$`#GG> z{v5!89K^vK!l4|-42Op^lOs5iqnO3f9K*59b~uOQn9Dql=LAmVBu?fOhx0j=(>R?o zIFqwDn{!y;@LU#h9_Mob7jh97a|xF^yo}4af-AX-tGR}2xsK~uoKx{=-b`WJsdzNG zu}N?NmIP zT-bIh9!)N6I~9*67q*>>N0STNPB(BPH*qsfF1&?XxsBVogFCs4ySayZy?Jrt?7lY3 zxP9Eu13bt>JWQL9@F