Skip to main content

async_geotiff/io/
window.rs

1//! Bbox -> pixel window math + tile stitching.
2
3use 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/// Marker for IFD dimensions that have not been checked by [`IfdInfo::validate`].
98#[derive(Clone, Copy, Debug)]
99pub enum UncheckedIfdInfo {}
100
101/// Marker produced after checking that tile and image dimensions are nonzero.
102#[derive(Clone, Copy, Debug)]
103pub enum ValidatedIfdInfo {}
104
105/// Owned IFD layout needed for stitching without borrowing TIFF metadata.
106///
107/// [`Self::validate`] checks tile and image dimensions. It does not validate
108/// decoded array shapes, band counts or subsequent edits to these public fields.
109#[derive(Clone, Copy, Debug)]
110pub struct IfdInfo<State = UncheckedIfdInfo> {
111    /// Width of one internal tile in pixels.
112    pub tile_width: u32,
113    /// Height of one internal tile in pixels.
114    pub tile_height: u32,
115    /// Number of native sample bands, including extra samples.
116    pub bands: usize,
117    /// Whether decoded samples are band-planar or pixel-interleaved.
118    pub planar: PlanarConfiguration,
119    /// Width of the full image at this IFD's resolution, in pixels.
120    pub image_width: u32,
121    /// Height of the full image at this IFD's resolution, in pixels.
122    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    /// Store the supplied tile dimensions, band layout and image dimensions
142    /// without validating or clamping them. Call [`Self::validate`] before stitching.
143    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    /// Check nonzero tile and image dimensions and return the checked marker.
163    /// Band counts and decoded array shapes are not checked here.
164    ///
165    /// # Errors
166    /// Returns [`AgError::Tiff`] if any tile or image dimension is zero.
167    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
186/// Convert pixel window to inclusive tile coordinate range
187/// (tx_min..=tx_max, ty_min..=ty_max). Returns None if the IFD lacks tile dims.
188pub 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
207/// Stitch the fetched tiles (in (tx, ty) iteration order) into a planar
208/// (bands, h, w) buffer cropped to the requested window. The output
209/// `PixelData` variant matches the source dtype — no upcast to f64.
210///
211/// This matters because the downstream warp uses `fast_image_resize` which
212/// has native U8/U16/F32 SIMD paths; an upfront cast to f64 would burn
213/// allocations + a typed loop and then need a second cast back to f32.
214pub 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
234/// Like [`stitch`] but takes a plain-data [`IfdInfo`]; used inside
235/// `spawn_blocking` where holding a `&ImageFileDirectory` would tie the
236/// closure to `self`. The two functions share all the per-tile work.
237pub 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    // Determine output dtype from the first decoded array. All tiles in an
258    // IFD share dtype, so this is safe.
259    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                // Zero-copy view over the decoded tile's raw slice; `permuted_axes`
281                // is also a view (no alloc). The single allocation per tile is
282                // gone — only the final `assign` does a memcpy into `out`.
283                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        // Less common dtypes upcast to f32 to keep PixelData enum small.
318        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
327/// Owned `Array3<U>` from a typed slice via an element-wise mapping —
328/// used by the upcast-to-f32 fallback where allocation is unavoidable.
329fn 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
347/// Zero-copy view of a typed decoded-tile slice in planar `(bands, H, W)`
348/// layout. Planar inputs view directly; chunky inputs use `permuted_axes`,
349/// which is still a view (axis re-ordering only) — no allocation.
350fn 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
366/// Fallback for Int8/Int64/UInt64/Bool — upcast to f32 (avoids growing
367/// `PixelData` enum + lets the warp's f32 SIMD path stay native).
368fn 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
421/// Stitch the first sample band of each mask tile into a `(height, width)`
422/// validity array, treating nonzero values as valid. `coords` supplies `(tx, ty)`
423/// for each matching array. The requested window uses IFD pixel coordinates;
424/// cells not covered by a supplied tile retain `true`.
425///
426/// # Errors
427/// Returns an error for zero or unrepresentable window dimensions, overflowing
428/// window endpoints, zero IFD dimensions or unsupported sample layout.
429pub 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
449/// Plain-data version used inside `spawn_blocking`.
450pub 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
510/// Fill only the requested part of one decoded mask tile into the final mask.
511///
512/// Valid decoded arrays take the checked-row fast path: one bounds check per
513/// source row followed by a linear destination write. The fallback preserves
514/// the old behavior for malformed, truncated arrays, where missing cells were
515/// materialized as `false` before the crop was copied.
516fn 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, // row 0
626            0, 1, 0, 1, 0, // row 1
627            1, 1, 0, 0, 1, // row 2
628            0, 0, 1, 1, 0, // row 3
629        ];
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, // row 0
659            1, 9, 9, 0, 9, 9, 1, 9, 9, // row 1
660        ];
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}