1use std::borrow::Borrow;
4use std::marker::PhantomData;
5
6use crate::error::{AgError, AgResult};
7use crate::pixel::PixelData;
8use async_tiff::tags::PlanarConfiguration;
9use async_tiff::{Array, ImageFileDirectory, TypedArray};
10use ndarray::{s, Array2, Array3};
11
12fn checked_window_end(offset: i64, len: u64, axis: &'static str) -> AgResult<i64> {
13 let len = i64::try_from(len)
14 .map_err(|_| AgError::Tiff(format!("{axis} window length exceeds i64")))?;
15 offset
16 .checked_add(len)
17 .ok_or_else(|| AgError::Tiff(format!("{axis} window end overflows i64")))
18}
19
20#[derive(Clone, Copy)]
21struct TileCopyWindow {
22 src_r0: usize,
23 src_r1: usize,
24 src_c0: usize,
25 src_c1: usize,
26 dst_r0: usize,
27 dst_r1: usize,
28 dst_c0: usize,
29 dst_c1: usize,
30}
31
32#[derive(Clone, Copy)]
33struct TileFootprint {
34 col: i64,
35 row: i64,
36 width: i64,
37 height: i64,
38}
39
40#[derive(Clone, Copy)]
41struct RequestedWindow {
42 col: i64,
43 row: i64,
44 col_end: i64,
45 row_end: i64,
46}
47
48fn tile_copy_window(tile: TileFootprint, requested: RequestedWindow) -> Option<TileCopyWindow> {
49 let world_r0 = tile.row.max(requested.row);
50 let world_r1 = tile.row.checked_add(tile.height)?.min(requested.row_end);
51 let world_c0 = tile.col.max(requested.col);
52 let world_c1 = tile.col.checked_add(tile.width)?.min(requested.col_end);
53 if world_r1 <= world_r0 || world_c1 <= world_c0 {
54 return None;
55 }
56 Some(TileCopyWindow {
57 src_r0: (world_r0 - tile.row) as usize,
58 src_r1: (world_r1 - tile.row) as usize,
59 src_c0: (world_c0 - tile.col) as usize,
60 src_c1: (world_c1 - tile.col) as usize,
61 dst_r0: (world_r0 - requested.row) as usize,
62 dst_r1: (world_r1 - requested.row) as usize,
63 dst_c0: (world_c0 - requested.col) as usize,
64 dst_c1: (world_c1 - requested.col) as usize,
65 })
66}
67
68fn tile_footprint(tile_col: i64, tile_row: i64, tile_w: i64, tile_h: i64) -> TileFootprint {
69 TileFootprint {
70 col: tile_col,
71 row: tile_row,
72 width: tile_w,
73 height: tile_h,
74 }
75}
76
77fn requested_window(col_off: i64, row_off: i64, col_end: i64, row_end: i64) -> RequestedWindow {
78 RequestedWindow {
79 col: col_off,
80 row: row_off,
81 col_end,
82 row_end,
83 }
84}
85
86pub(crate) fn validate_window_shape(width: u64, height: u64) -> AgResult<()> {
87 if width == 0 || height == 0 {
88 return Err(AgError::Tiff("window dimensions must be non-zero".into()));
89 }
90 i64::try_from(width).map_err(|_| AgError::Tiff("window width exceeds i64".into()))?;
91 i64::try_from(height).map_err(|_| AgError::Tiff("window height exceeds i64".into()))?;
92 usize::try_from(width).map_err(|_| AgError::Tiff("window width exceeds usize".into()))?;
93 usize::try_from(height).map_err(|_| AgError::Tiff("window height exceeds usize".into()))?;
94 Ok(())
95}
96
97#[derive(Clone, Copy, Debug)]
99pub enum UncheckedIfdInfo {}
100
101#[derive(Clone, Copy, Debug)]
103pub enum ValidatedIfdInfo {}
104
105#[derive(Clone, Copy, Debug)]
110pub struct IfdInfo<State = UncheckedIfdInfo> {
111 pub tile_width: u32,
113 pub tile_height: u32,
115 pub bands: usize,
117 pub planar: PlanarConfiguration,
119 pub image_width: u32,
121 pub image_height: u32,
123 _state: PhantomData<State>,
124}
125
126impl From<&ImageFileDirectory> for IfdInfo<UncheckedIfdInfo> {
127 fn from(ifd: &ImageFileDirectory) -> Self {
128 Self {
129 tile_width: ifd.tile_width().unwrap_or_else(|| ifd.image_width()),
130 tile_height: ifd.tile_height().unwrap_or_else(|| ifd.image_height()),
131 bands: ifd.samples_per_pixel() as usize,
132 planar: ifd.planar_configuration(),
133 image_width: ifd.image_width(),
134 image_height: ifd.image_height(),
135 _state: PhantomData,
136 }
137 }
138}
139
140impl IfdInfo<UncheckedIfdInfo> {
141 pub fn from_parts(
144 tile_width: u32,
145 tile_height: u32,
146 bands: usize,
147 planar: PlanarConfiguration,
148 image_width: u32,
149 image_height: u32,
150 ) -> Self {
151 Self {
152 tile_width,
153 tile_height,
154 bands,
155 planar,
156 image_width,
157 image_height,
158 _state: PhantomData,
159 }
160 }
161
162 pub fn validate(self) -> AgResult<IfdInfo<ValidatedIfdInfo>> {
168 if self.tile_width == 0 || self.tile_height == 0 {
169 return Err(AgError::Tiff("IFD has zero tile dimensions".into()));
170 }
171 if self.image_width == 0 || self.image_height == 0 {
172 return Err(AgError::Tiff("IFD has zero image dimensions".into()));
173 }
174 Ok(IfdInfo {
175 tile_width: self.tile_width,
176 tile_height: self.tile_height,
177 bands: self.bands,
178 planar: self.planar,
179 image_width: self.image_width,
180 image_height: self.image_height,
181 _state: PhantomData,
182 })
183 }
184}
185
186pub fn tile_range_for_window(
189 ifd: &ImageFileDirectory,
190 col_off: i64,
191 row_off: i64,
192 width: u64,
193 height: u64,
194) -> Option<((u32, u32), (u32, u32))> {
195 validate_window_shape(width, height).ok()?;
196 let tw = i64::from(tile_width(ifd)?);
197 let th = i64::from(tile_height(ifd)?);
198 let col_end = checked_window_end(col_off, width, "column").ok()?;
199 let row_end = checked_window_end(row_off, height, "row").ok()?;
200 let tx0 = (col_off.max(0) / tw) as u32;
201 let ty0 = (row_off.max(0) / th) as u32;
202 let tx1 = ((col_end - 1).max(0) / tw) as u32;
203 let ty1 = ((row_end - 1).max(0) / th) as u32;
204 Some(((tx0, tx1), (ty0, ty1)))
205}
206
207pub fn stitch<A: Borrow<Array>>(
215 arrays: &[A],
216 coords: &[(usize, usize)],
217 ifd: &ImageFileDirectory,
218 col_off: i64,
219 row_off: i64,
220 width: u64,
221 height: u64,
222) -> AgResult<PixelData> {
223 stitch_with_info(
224 arrays,
225 coords,
226 &IfdInfo::from(ifd).validate()?,
227 col_off,
228 row_off,
229 width,
230 height,
231 )
232}
233
234pub fn stitch_with_info<A: Borrow<Array>>(
238 arrays: &[A],
239 coords: &[(usize, usize)],
240 info: &IfdInfo<ValidatedIfdInfo>,
241 col_off: i64,
242 row_off: i64,
243 width: u64,
244 height: u64,
245) -> AgResult<PixelData> {
246 let planar = info.planar;
247 validate_window_shape(width, height)?;
248 let bands = info.bands;
249 let dst_h = height as usize;
250 let dst_w = width as usize;
251 let tw = info.tile_width as i64;
252 let th = info.tile_height as i64;
253 let row_end = checked_window_end(row_off, height, "row")?;
254 let col_end = checked_window_end(col_off, width, "column")?;
255 let requested = requested_window(col_off, row_off, col_end, row_end);
256
257 let first = arrays
260 .first()
261 .ok_or_else(|| AgError::Tiff("stitch: no tiles".into()))?
262 .borrow();
263
264 macro_rules! stitch_for {
265 ($variant:ident, $t:ty, $wrap:ident) => {{
266 let mut out = Array3::<$t>::from_elem((bands, dst_h, dst_w), <$t>::default());
267 for ((tx, ty), arr) in coords.iter().zip(arrays.iter()) {
268 let arr = arr.borrow();
269 let tile_col = (*tx as i64) * tw;
270 let tile_row = (*ty as i64) * th;
271 let typed: &[$t] = match arr.data() {
272 TypedArray::$variant(v) => v.as_slice(),
273 _ => {
274 return Err(AgError::Tiff(
275 "stitch: mixed dtypes across tiles in one IFD".into(),
276 ))
277 }
278 };
279 let shape = arr.shape();
280 let typed_view = arrange_planar_view::<$t>(typed, shape, planar)?;
284 let s = typed_view.shape();
285 let (a_h, a_w) = (s[1] as i64, s[2] as i64);
286
287 let Some(win) =
288 tile_copy_window(tile_footprint(tile_col, tile_row, a_w, a_h), requested)
289 else {
290 continue;
291 };
292 let band_take = bands.min(s[0]);
293 let src_sub = typed_view.slice(s![
294 ..band_take,
295 win.src_r0..win.src_r1,
296 win.src_c0..win.src_c1
297 ]);
298 out.slice_mut(s![
299 ..band_take,
300 win.dst_r0..win.dst_r1,
301 win.dst_c0..win.dst_c1
302 ])
303 .assign(&src_sub);
304 }
305 Ok(PixelData::$wrap(out))
306 }};
307 }
308
309 match first.data() {
310 TypedArray::UInt8(_) => stitch_for!(UInt8, u8, U8),
311 TypedArray::UInt16(_) => stitch_for!(UInt16, u16, U16),
312 TypedArray::Int16(_) => stitch_for!(Int16, i16, I16),
313 TypedArray::UInt32(_) => stitch_for!(UInt32, u32, U32),
314 TypedArray::Int32(_) => stitch_for!(Int32, i32, I32),
315 TypedArray::Float32(_) => stitch_for!(Float32, f32, F32),
316 TypedArray::Float64(_) => stitch_for!(Float64, f64, F64),
317 TypedArray::Int8(_)
319 | TypedArray::UInt64(_)
320 | TypedArray::Int64(_)
321 | TypedArray::Bool(_) => {
322 stitch_upcast_to_f32(arrays, coords, info, col_off, row_off, width, height)
323 }
324 }
325}
326
327fn upcast_owned<T, U: 'static + Copy>(
330 src: &[T],
331 shape: [usize; 3],
332 planar: PlanarConfiguration,
333 f: impl Fn(&T) -> U,
334) -> AgResult<Array3<U>> {
335 let mapped: Vec<U> = src.iter().map(f).collect();
336 let arr = Array3::from_shape_vec(shape, mapped)
337 .map_err(|e| AgError::Tiff(format!("upcast shape: {e}")))?;
338 match planar {
339 PlanarConfiguration::Planar => Ok(arr),
340 PlanarConfiguration::Chunky => Ok(arr.permuted_axes([2, 0, 1])),
341 _ => Err(AgError::UnsupportedPixel(
342 "unknown planar configuration".into(),
343 )),
344 }
345}
346
347fn arrange_planar_view<'a, T: 'static>(
351 src: &'a [T],
352 shape: [usize; 3],
353 planar: PlanarConfiguration,
354) -> AgResult<ndarray::ArrayView3<'a, T>> {
355 let view = ndarray::ArrayView3::<T>::from_shape(shape, src)
356 .map_err(|e| AgError::Tiff(format!("stitch shape: {e}")))?;
357 match planar {
358 PlanarConfiguration::Planar => Ok(view),
359 PlanarConfiguration::Chunky => Ok(view.permuted_axes([2, 0, 1])),
360 _ => Err(AgError::UnsupportedPixel(
361 "unknown planar configuration".into(),
362 )),
363 }
364}
365
366fn stitch_upcast_to_f32<A: Borrow<Array>>(
369 arrays: &[A],
370 coords: &[(usize, usize)],
371 info: &IfdInfo<ValidatedIfdInfo>,
372 col_off: i64,
373 row_off: i64,
374 width: u64,
375 height: u64,
376) -> AgResult<PixelData> {
377 validate_window_shape(width, height)?;
378 let tw = info.tile_width as i64;
379 let th = info.tile_height as i64;
380 let bands = info.bands;
381 let row_end = checked_window_end(row_off, height, "row")?;
382 let col_end = checked_window_end(col_off, width, "column")?;
383 let requested = requested_window(col_off, row_off, col_end, row_end);
384 let mut out = Array3::<f32>::zeros((bands, height as usize, width as usize));
385 let planar = info.planar;
386 for ((tx, ty), arr) in coords.iter().zip(arrays.iter()) {
387 let arr = arr.borrow();
388 let tile_col = (*tx as i64) * tw;
389 let tile_row = (*ty as i64) * th;
390 let shape = arr.shape();
391 let typed_f32: Array3<f32> = match arr.data() {
392 TypedArray::Int8(v) => upcast_owned(v, shape, planar, |&x| x as f32)?,
393 TypedArray::Int64(v) => upcast_owned(v, shape, planar, |&x| x as f32)?,
394 TypedArray::UInt64(v) => upcast_owned(v, shape, planar, |&x| x as f32)?,
395 TypedArray::Bool(v) => {
396 upcast_owned(v, shape, planar, |&x| if x { 1.0f32 } else { 0.0 })?
397 }
398 _ => unreachable!("stitch_upcast_to_f32 called with non-upcast variant"),
399 };
400 let s = typed_f32.shape();
401 let (a_h, a_w) = (s[1] as i64, s[2] as i64);
402 let Some(win) = tile_copy_window(tile_footprint(tile_col, tile_row, a_w, a_h), requested)
403 else {
404 continue;
405 };
406 let band_take = bands.min(s[0]);
407 out.slice_mut(s![
408 ..band_take,
409 win.dst_r0..win.dst_r1,
410 win.dst_c0..win.dst_c1
411 ])
412 .assign(&typed_f32.slice(s![
413 ..band_take,
414 win.src_r0..win.src_r1,
415 win.src_c0..win.src_c1
416 ]));
417 }
418 Ok(PixelData::F32(out))
419}
420
421pub fn stitch_mask<A: Borrow<Array>>(
430 arrays: &[A],
431 coords: &[(usize, usize)],
432 ifd: &ImageFileDirectory,
433 col_off: i64,
434 row_off: i64,
435 width: u64,
436 height: u64,
437) -> AgResult<Array2<bool>> {
438 stitch_mask_with_info(
439 arrays,
440 coords,
441 &IfdInfo::from(ifd).validate()?,
442 col_off,
443 row_off,
444 width,
445 height,
446 )
447}
448
449pub fn stitch_mask_with_info<A: Borrow<Array>>(
451 arrays: &[A],
452 coords: &[(usize, usize)],
453 info: &IfdInfo<ValidatedIfdInfo>,
454 col_off: i64,
455 row_off: i64,
456 width: u64,
457 height: u64,
458) -> AgResult<Array2<bool>> {
459 validate_window_shape(width, height)?;
460 let tw = info.tile_width as i64;
461 let th = info.tile_height as i64;
462 let row_end = checked_window_end(row_off, height, "row")?;
463 let col_end = checked_window_end(col_off, width, "column")?;
464 let requested = requested_window(col_off, row_off, col_end, row_end);
465 let mut out = Array2::<bool>::from_elem((height as usize, width as usize), true);
466 for ((tx, ty), arr) in coords.iter().zip(arrays.iter()) {
467 let arr = arr.borrow();
468 let tile_col = (*tx as i64) * tw;
469 let tile_row = (*ty as i64) * th;
470 let shape = arr.shape();
471 let (a_h, a_w, stride) = match info.planar {
472 PlanarConfiguration::Planar => (shape[1], shape[2], 1),
473 PlanarConfiguration::Chunky => (shape[0], shape[1], shape[2]),
474 _ => {
475 return Err(AgError::UnsupportedPixel(
476 "unknown planar configuration".into(),
477 ))
478 }
479 };
480
481 let Some(win) = tile_copy_window(
482 tile_footprint(tile_col, tile_row, a_w as i64, a_h as i64),
483 requested,
484 ) else {
485 continue;
486 };
487
488 macro_rules! fill {
489 ($values:expr, $predicate:expr) => {
490 fill_mask_window(&mut out, $values, a_w, stride, win, $predicate)
491 };
492 }
493 match arr.data() {
494 TypedArray::Bool(v) => fill!(v, |&x| x),
495 TypedArray::UInt8(v) => fill!(v, |&x| x != 0),
496 TypedArray::UInt16(v) => fill!(v, |&x| x != 0),
497 TypedArray::UInt32(v) => fill!(v, |&x| x != 0),
498 TypedArray::UInt64(v) => fill!(v, |&x| x != 0),
499 TypedArray::Int8(v) => fill!(v, |&x| x != 0),
500 TypedArray::Int16(v) => fill!(v, |&x| x != 0),
501 TypedArray::Int32(v) => fill!(v, |&x| x != 0),
502 TypedArray::Int64(v) => fill!(v, |&x| x != 0),
503 TypedArray::Float32(v) => fill!(v, |&x| x != 0.0),
504 TypedArray::Float64(v) => fill!(v, |&x| x != 0.0),
505 }
506 }
507 Ok(out)
508}
509
510fn fill_mask_window<T, F: Fn(&T) -> bool>(
517 out: &mut Array2<bool>,
518 src: &[T],
519 src_width: usize,
520 stride: usize,
521 win: TileCopyWindow,
522 pred: F,
523) {
524 let rows = win.src_r1 - win.src_r0;
525 let cols = win.src_c1 - win.src_c0;
526 debug_assert_eq!(rows, win.dst_r1 - win.dst_r0);
527 debug_assert_eq!(cols, win.dst_c1 - win.dst_c0);
528 if rows == 0 || cols == 0 {
529 return;
530 }
531
532 let dst_width = out.ncols();
533 let dst = out
534 .as_slice_mut()
535 .expect("stitch mask output is freshly allocated in standard layout");
536
537 for row in 0..rows {
538 let src_start = ((win.src_r0 + row) * src_width + win.src_c0) * stride;
539 let src_span = (cols - 1) * stride + 1;
540 let dst_start = (win.dst_r0 + row) * dst_width + win.dst_c0;
541 let dst_row = &mut dst[dst_start..dst_start + cols];
542
543 let src_row = if stride == 0 {
544 None
545 } else {
546 src.get(src_start..src_start + src_span)
547 };
548 if let Some(src_row) = src_row {
549 for (dst_cell, src_cell) in dst_row.iter_mut().zip(src_row.iter().step_by(stride)) {
550 *dst_cell = pred(src_cell);
551 }
552 } else {
553 for (col, dst_cell) in dst_row.iter_mut().enumerate() {
554 *dst_cell = src
555 .get(src_start + col * stride)
556 .map(&pred)
557 .unwrap_or(false);
558 }
559 }
560 }
561}
562
563fn tile_width(ifd: &ImageFileDirectory) -> Option<u32> {
564 let width = ifd.tile_width().unwrap_or_else(|| ifd.image_width());
565 (width > 0).then_some(width)
566}
567
568fn tile_height(ifd: &ImageFileDirectory) -> Option<u32> {
569 let height = ifd.tile_height().unwrap_or_else(|| ifd.image_height());
570 (height > 0).then_some(height)
571}
572
573#[cfg(test)]
574mod tests {
575 use ndarray::{arr2, Array2};
576
577 use super::{
578 fill_mask_window, requested_window, tile_copy_window, tile_footprint,
579 validate_window_shape, TileCopyWindow,
580 };
581
582 fn pure(tw: i64, th: i64, col: i64, row: i64, w: u64, h: u64) -> ((i64, i64), (i64, i64)) {
583 let ce = col + w as i64;
584 let re = row + h as i64;
585 (
586 (col.max(0) / tw, (ce - 1).max(0) / tw),
587 (row.max(0) / th, (re - 1).max(0) / th),
588 )
589 }
590
591 #[test]
592 fn one_tile_window_yields_one_tile() {
593 let r = pure(256, 256, 10, 10, 50, 50);
594 assert_eq!(r, ((0, 0), (0, 0)));
595 }
596
597 #[test]
598 fn straddling_window_spans_multiple_tiles() {
599 let r = pure(256, 256, 200, 200, 200, 200);
600 assert_eq!(r, ((0, 1), (0, 1)));
601 }
602
603 #[test]
604 fn rejects_windows_that_cannot_fit_index_types() {
605 assert!(validate_window_shape(i64::MAX as u64 + 1, 1).is_err());
606 assert!(validate_window_shape(1, 0).is_err());
607 }
608
609 #[test]
610 fn shared_tile_copy_window_clips_source_and_destination() {
611 let win = tile_copy_window(
612 tile_footprint(0, 0, 256, 256),
613 requested_window(200, 200, 400, 400),
614 )
615 .unwrap();
616 assert_eq!((win.src_c0, win.src_c1), (200, 256));
617 assert_eq!((win.src_r0, win.src_r1), (200, 256));
618 assert_eq!((win.dst_c0, win.dst_c1), (0, 56));
619 assert_eq!((win.dst_r0, win.dst_r1), (0, 56));
620 }
621
622 #[test]
623 fn fill_mask_window_crops_planar_interior_into_destination() {
624 let src = [
625 1u8, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, ];
630 let mut out = Array2::from_elem((4, 6), true);
631 let win = TileCopyWindow {
632 src_r0: 1,
633 src_r1: 3,
634 src_c0: 1,
635 src_c1: 4,
636 dst_r0: 1,
637 dst_r1: 3,
638 dst_c0: 2,
639 dst_c1: 5,
640 };
641
642 fill_mask_window(&mut out, &src, 5, 1, win, |&value| value != 0);
643
644 assert_eq!(
645 out,
646 arr2(&[
647 [true, true, true, true, true, true],
648 [true, true, true, false, true, true],
649 [true, true, true, false, false, true],
650 [true, true, true, true, true, true],
651 ])
652 );
653 }
654
655 #[test]
656 fn fill_mask_window_uses_first_chunky_band_only() {
657 let src = [
658 0u8, 9, 9, 1, 9, 9, 0, 9, 9, 1, 9, 9, 0, 9, 9, 1, 9, 9, ];
661 let mut out = Array2::from_elem((2, 3), true);
662 let win = TileCopyWindow {
663 src_r0: 0,
664 src_r1: 2,
665 src_c0: 0,
666 src_c1: 3,
667 dst_r0: 0,
668 dst_r1: 2,
669 dst_c0: 0,
670 dst_c1: 3,
671 };
672
673 fill_mask_window(&mut out, &src, 3, 3, win, |&value| value != 0);
674
675 assert_eq!(out, arr2(&[[false, true, false], [true, false, true]]));
676 }
677
678 #[test]
679 fn fill_mask_window_places_short_edge_tile_for_negative_request() {
680 let src = [1u8, 0, 0, 1];
681 let requested = requested_window(-1, -1, 3, 3);
682 let win = tile_copy_window(tile_footprint(0, 0, 2, 2), requested).unwrap();
683 let mut out = Array2::from_elem((4, 4), true);
684
685 fill_mask_window(&mut out, &src, 2, 1, win, |&value| value != 0);
686
687 assert_eq!(
688 out,
689 arr2(&[
690 [true, true, true, true],
691 [true, true, false, true],
692 [true, false, true, true],
693 [true, true, true, true],
694 ])
695 );
696 }
697
698 #[test]
699 fn fill_mask_window_treats_truncated_source_cells_as_false() {
700 let src = [1u8, 0, 1, 0];
701 let mut out = Array2::from_elem((2, 3), true);
702 let win = TileCopyWindow {
703 src_r0: 0,
704 src_r1: 2,
705 src_c0: 0,
706 src_c1: 3,
707 dst_r0: 0,
708 dst_r1: 2,
709 dst_c0: 0,
710 dst_c1: 3,
711 };
712
713 fill_mask_window(&mut out, &src, 3, 1, win, |&value| value != 0);
714
715 assert_eq!(out, arr2(&[[true, false, true], [false, false, false]]));
716 }
717}