@@ -142,6 +142,8 @@ def _read_strips(data: bytes, ifd: IFD, header: TIFFHeader,
142142 if offsets is None or byte_counts is None :
143143 raise ValueError ("Missing strip offsets or byte counts" )
144144
145+ planar = ifd .planar_config # 1=chunky (interleaved), 2=planar (separate)
146+
145147 # Determine output region
146148 if window is not None :
147149 r0 , c0 , r1 , c1 = window
@@ -154,47 +156,85 @@ def _read_strips(data: bytes, ifd: IFD, header: TIFFHeader,
154156
155157 out_h = r1 - r0
156158 out_w = c1 - c0
157- row_bytes = width * samples * bytes_per_sample
158159
159160 if samples > 1 :
160161 result = np .empty ((out_h , out_w , samples ), dtype = dtype )
161162 else :
162163 result = np .empty ((out_h , out_w ), dtype = dtype )
163164
164- # Only decompress strips that overlap the requested row range
165- first_strip = r0 // rps
166- last_strip = min ((r1 - 1 ) // rps , len (offsets ) - 1 )
167-
168- for strip_idx in range (first_strip , last_strip + 1 ):
169- strip_row = strip_idx * rps
170- strip_rows = min (rps , height - strip_row )
171- if strip_rows <= 0 :
172- continue
173-
174- strip_data = data [offsets [strip_idx ]:offsets [strip_idx ] + byte_counts [strip_idx ]]
175- expected = strip_rows * width * samples * bytes_per_sample
176- chunk = decompress (strip_data , compression , expected ,
177- width = width , height = strip_rows , samples = samples )
178-
179- if pred in (2 , 3 ):
180- if not chunk .flags .writeable :
181- chunk = chunk .copy ()
182- chunk = _apply_predictor (chunk , pred , width , strip_rows , bytes_per_sample * samples )
183-
184- # Reshape the decompressed strip to (strip_rows, width[, samples])
185- if samples > 1 :
186- strip_pixels = chunk .view (dtype ).reshape (strip_rows , width , samples )
187- else :
188- strip_pixels = chunk .view (dtype ).reshape (strip_rows , width )
165+ if planar == 2 and samples > 1 :
166+ # Planar configuration: each band stored as separate strips.
167+ # Strip offsets are laid out as [band0_strip0, band0_strip1, ...,
168+ # band1_strip0, band1_strip1, ..., band2_strip0, ...].
169+ strips_per_band = math .ceil (height / rps )
170+ first_strip = r0 // rps
171+ last_strip = min ((r1 - 1 ) // rps , strips_per_band - 1 )
172+
173+ for band_idx in range (samples ):
174+ band_offset = band_idx * strips_per_band
175+
176+ for strip_idx in range (first_strip , last_strip + 1 ):
177+ global_idx = band_offset + strip_idx
178+ if global_idx >= len (offsets ):
179+ continue
180+
181+ strip_row = strip_idx * rps
182+ strip_rows = min (rps , height - strip_row )
183+ if strip_rows <= 0 :
184+ continue
185+
186+ strip_data = data [offsets [global_idx ]:offsets [global_idx ] + byte_counts [global_idx ]]
187+ expected = strip_rows * width * bytes_per_sample
188+ chunk = decompress (strip_data , compression , expected ,
189+ width = width , height = strip_rows , samples = 1 )
190+
191+ if pred in (2 , 3 ):
192+ if not chunk .flags .writeable :
193+ chunk = chunk .copy ()
194+ chunk = _apply_predictor (chunk , pred , width , strip_rows , bytes_per_sample )
195+
196+ strip_pixels = chunk .view (dtype ).reshape (strip_rows , width )
197+
198+ src_r0 = max (r0 - strip_row , 0 )
199+ src_r1 = min (r1 - strip_row , strip_rows )
200+ dst_r0 = max (strip_row - r0 , 0 )
201+ dst_r1 = dst_r0 + (src_r1 - src_r0 )
202+
203+ if dst_r1 > dst_r0 :
204+ result [dst_r0 :dst_r1 , :, band_idx ] = strip_pixels [src_r0 :src_r1 , c0 :c1 ]
205+ else :
206+ # Chunky (interleaved) -- default path
207+ first_strip = r0 // rps
208+ last_strip = min ((r1 - 1 ) // rps , len (offsets ) - 1 )
209+
210+ for strip_idx in range (first_strip , last_strip + 1 ):
211+ strip_row = strip_idx * rps
212+ strip_rows = min (rps , height - strip_row )
213+ if strip_rows <= 0 :
214+ continue
215+
216+ strip_data = data [offsets [strip_idx ]:offsets [strip_idx ] + byte_counts [strip_idx ]]
217+ expected = strip_rows * width * samples * bytes_per_sample
218+ chunk = decompress (strip_data , compression , expected ,
219+ width = width , height = strip_rows , samples = samples )
220+
221+ if pred in (2 , 3 ):
222+ if not chunk .flags .writeable :
223+ chunk = chunk .copy ()
224+ chunk = _apply_predictor (chunk , pred , width , strip_rows , bytes_per_sample * samples )
189225
190- # Compute the overlap between this strip and the output window
191- src_r0 = max (r0 - strip_row , 0 )
192- src_r1 = min (r1 - strip_row , strip_rows )
193- dst_r0 = max (strip_row - r0 , 0 )
194- dst_r1 = dst_r0 + (src_r1 - src_r0 )
226+ if samples > 1 :
227+ strip_pixels = chunk .view (dtype ).reshape (strip_rows , width , samples )
228+ else :
229+ strip_pixels = chunk .view (dtype ).reshape (strip_rows , width )
195230
196- if dst_r1 > dst_r0 :
197- result [dst_r0 :dst_r1 ] = strip_pixels [src_r0 :src_r1 , c0 :c1 ]
231+ src_r0 = max (r0 - strip_row , 0 )
232+ src_r1 = min (r1 - strip_row , strip_rows )
233+ dst_r0 = max (strip_row - r0 , 0 )
234+ dst_r1 = dst_r0 + (src_r1 - src_r0 )
235+
236+ if dst_r1 > dst_r0 :
237+ result [dst_r0 :dst_r1 ] = strip_pixels [src_r0 :src_r1 , c0 :c1 ]
198238
199239 return result
200240
@@ -241,6 +281,7 @@ def _read_tiles(data: bytes, ifd: IFD, header: TIFFHeader,
241281 if offsets is None or byte_counts is None :
242282 raise ValueError ("Missing tile offsets or byte counts" )
243283
284+ planar = ifd .planar_config
244285 tiles_across = math .ceil (width / tw )
245286 tiles_down = math .ceil (height / th )
246287
@@ -257,70 +298,73 @@ def _read_tiles(data: bytes, ifd: IFD, header: TIFFHeader,
257298 out_h = r1 - r0
258299 out_w = c1 - c0
259300
260- # Use np.empty for full-image reads (every pixel written by tile placement),
261- # np.zeros for windowed reads (edge regions may not be covered).
262301 _alloc = np .zeros if window is not None else np .empty
263302 if samples > 1 :
264303 result = _alloc ((out_h , out_w , samples ), dtype = dtype )
265304 else :
266305 result = _alloc ((out_h , out_w ), dtype = dtype )
267306
268- # Which tiles overlap the window
269307 tile_row_start = r0 // th
270308 tile_row_end = min (math .ceil (r1 / th ), tiles_down )
271309 tile_col_start = c0 // tw
272310 tile_col_end = min (math .ceil (c1 / tw ), tiles_across )
273311
274- for tr in range (tile_row_start , tile_row_end ):
275- for tc in range (tile_col_start , tile_col_end ):
276- tile_idx = tr * tiles_across + tc
277- if tile_idx >= len (offsets ):
278- continue
279-
280- tile_data = data [offsets [tile_idx ]:offsets [tile_idx ] + byte_counts [tile_idx ]]
281- expected = tw * th * samples * bytes_per_sample
282- chunk = decompress (tile_data , compression , expected ,
283- width = tw , height = th , samples = samples )
284-
285- if pred in (2 , 3 ):
286- if not chunk .flags .writeable :
287- chunk = chunk .copy ()
288- chunk = _apply_predictor (chunk , pred , tw , th , bytes_per_sample * samples )
289-
290- # Reshape tile
291- if samples > 1 :
292- tile_pixels = chunk .view (dtype ).reshape (th , tw , samples )
293- else :
294- tile_pixels = chunk .view (dtype ).reshape (th , tw )
295-
296- # Compute overlap between tile and window
297- tile_r0 = tr * th
298- tile_c0 = tc * tw
299- tile_r1 = tile_r0 + th
300- tile_c1 = tile_c0 + tw
301-
302- # Source region within the tile
303- src_r0 = max (r0 - tile_r0 , 0 )
304- src_c0 = max (c0 - tile_c0 , 0 )
305- src_r1 = min (r1 - tile_r0 , th )
306- src_c1 = min (c1 - tile_c0 , tw )
307-
308- # Dest region within the output
309- dst_r0 = max (tile_r0 - r0 , 0 )
310- dst_c0 = max (tile_c0 - c0 , 0 )
311- dst_r1 = dst_r0 + (src_r1 - src_r0 )
312- dst_c1 = dst_c0 + (src_c1 - src_c0 )
313-
314- # Clip to actual image bounds within tile
315- actual_tile_h = min (th , height - tile_r0 )
316- actual_tile_w = min (tw , width - tile_c0 )
317- src_r1 = min (src_r1 , actual_tile_h )
318- src_c1 = min (src_c1 , actual_tile_w )
319- dst_r1 = dst_r0 + (src_r1 - src_r0 )
320- dst_c1 = dst_c0 + (src_c1 - src_c0 )
321-
322- if dst_r1 > dst_r0 and dst_c1 > dst_c0 :
323- result [dst_r0 :dst_r1 , dst_c0 :dst_c1 ] = tile_pixels [src_r0 :src_r1 , src_c0 :src_c1 ]
312+ # Number of bands to iterate (1 for chunky, samples for planar)
313+ band_count = samples if (planar == 2 and samples > 1 ) else 1
314+ tiles_per_band = tiles_across * tiles_down
315+
316+ for band_idx in range (band_count ):
317+ band_tile_offset = band_idx * tiles_per_band if band_count > 1 else 0
318+ # For planar, each tile has 1 sample; for chunky, samples per tile
319+ tile_samples = 1 if band_count > 1 else samples
320+
321+ for tr in range (tile_row_start , tile_row_end ):
322+ for tc in range (tile_col_start , tile_col_end ):
323+ tile_idx = band_tile_offset + tr * tiles_across + tc
324+ if tile_idx >= len (offsets ):
325+ continue
326+
327+ tile_data = data [offsets [tile_idx ]:offsets [tile_idx ] + byte_counts [tile_idx ]]
328+ expected = tw * th * tile_samples * bytes_per_sample
329+ chunk = decompress (tile_data , compression , expected ,
330+ width = tw , height = th , samples = tile_samples )
331+
332+ if pred in (2 , 3 ):
333+ if not chunk .flags .writeable :
334+ chunk = chunk .copy ()
335+ chunk = _apply_predictor (chunk , pred , tw , th ,
336+ bytes_per_sample * tile_samples )
337+
338+ if tile_samples > 1 :
339+ tile_pixels = chunk .view (dtype ).reshape (th , tw , tile_samples )
340+ else :
341+ tile_pixels = chunk .view (dtype ).reshape (th , tw )
342+
343+ tile_r0 = tr * th
344+ tile_c0 = tc * tw
345+
346+ src_r0 = max (r0 - tile_r0 , 0 )
347+ src_c0 = max (c0 - tile_c0 , 0 )
348+ src_r1 = min (r1 - tile_r0 , th )
349+ src_c1 = min (c1 - tile_c0 , tw )
350+
351+ dst_r0 = max (tile_r0 - r0 , 0 )
352+ dst_c0 = max (tile_c0 - c0 , 0 )
353+
354+ actual_tile_h = min (th , height - tile_r0 )
355+ actual_tile_w = min (tw , width - tile_c0 )
356+ src_r1 = min (src_r1 , actual_tile_h )
357+ src_c1 = min (src_c1 , actual_tile_w )
358+ dst_r1 = dst_r0 + (src_r1 - src_r0 )
359+ dst_c1 = dst_c0 + (src_c1 - src_c0 )
360+
361+ if dst_r1 > dst_r0 and dst_c1 > dst_c0 :
362+ src_slice = tile_pixels [src_r0 :src_r1 , src_c0 :src_c1 ]
363+ if band_count > 1 :
364+ # Planar: place single-band tile into the band slice
365+ result [dst_r0 :dst_r1 , dst_c0 :dst_c1 , band_idx ] = src_slice
366+ else :
367+ result [dst_r0 :dst_r1 , dst_c0 :dst_c1 ] = src_slice
324368
325369 return result
326370
0 commit comments