Skip to main content

oxigdal_core/buffer/
raster_window.rs

1//! Sub-region window into a raster dataset.
2
3use crate::error::{OxiGdalError, Result};
4use core::fmt;
5
6/// A sub-region window into a raster dataset.
7/// Defines a rectangular area for reading/writing a portion of raster data.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct RasterWindow {
10    /// Column offset from the left edge (0-based)
11    pub col_off: u32,
12    /// Row offset from the top edge (0-based)
13    pub row_off: u32,
14    /// Width of the window in pixels
15    pub width: u32,
16    /// Height of the window in pixels
17    pub height: u32,
18}
19
20impl RasterWindow {
21    /// Create a new window. Validates that width and height are > 0.
22    pub fn new(col_off: u32, row_off: u32, width: u32, height: u32) -> Result<Self> {
23        if width == 0 {
24            return Err(OxiGdalError::invalid_parameter(
25                "width",
26                "window width must be greater than 0",
27            ));
28        }
29        if height == 0 {
30            return Err(OxiGdalError::invalid_parameter(
31                "height",
32                "window height must be greater than 0",
33            ));
34        }
35        Ok(Self {
36            col_off,
37            row_off,
38            width,
39            height,
40        })
41    }
42
43    /// Create a window covering the full extent of a raster.
44    pub fn full(raster_width: u32, raster_height: u32) -> Result<Self> {
45        Self::new(0, 0, raster_width, raster_height)
46    }
47
48    /// Check if this window fits within a raster of the given dimensions.
49    pub fn fits_within(&self, raster_width: u32, raster_height: u32) -> bool {
50        let col_end = u64::from(self.col_off) + u64::from(self.width);
51        let row_end = u64::from(self.row_off) + u64::from(self.height);
52        col_end <= u64::from(raster_width) && row_end <= u64::from(raster_height)
53    }
54
55    /// Validate that this window fits within the given raster dimensions.
56    /// Returns an error if any part is out of bounds.
57    pub fn validate_bounds(&self, raster_width: u32, raster_height: u32) -> Result<()> {
58        if !self.fits_within(raster_width, raster_height) {
59            return Err(OxiGdalError::OutOfBounds {
60                message: format!(
61                    "Window (col_off={}, row_off={}, width={}, height={}) exceeds raster bounds ({}x{})",
62                    self.col_off,
63                    self.row_off,
64                    self.width,
65                    self.height,
66                    raster_width,
67                    raster_height
68                ),
69            });
70        }
71        Ok(())
72    }
73
74    /// Total number of pixels in this window.
75    pub fn pixel_count(&self) -> u64 {
76        u64::from(self.width) * u64::from(self.height)
77    }
78
79    /// Intersect this window with another, returning the overlapping region.
80    /// Returns None if they don't overlap.
81    pub fn intersection(&self, other: &RasterWindow) -> Option<RasterWindow> {
82        let left = self.col_off.max(other.col_off);
83        let top = self.row_off.max(other.row_off);
84
85        let self_right = self.col_off.checked_add(self.width)?;
86        let other_right = other.col_off.checked_add(other.width)?;
87        let right = self_right.min(other_right);
88
89        let self_bottom = self.row_off.checked_add(self.height)?;
90        let other_bottom = other.row_off.checked_add(other.height)?;
91        let bottom = self_bottom.min(other_bottom);
92
93        if left >= right || top >= bottom {
94            return None;
95        }
96
97        // Safety: width/height are > 0 since left < right and top < bottom
98        Some(RasterWindow {
99            col_off: left,
100            row_off: top,
101            width: right - left,
102            height: bottom - top,
103        })
104    }
105
106    /// Compute the union bounding box of this window and another.
107    pub fn union_bounds(&self, other: &RasterWindow) -> RasterWindow {
108        let left = self.col_off.min(other.col_off);
109        let top = self.row_off.min(other.row_off);
110
111        let self_right = u64::from(self.col_off) + u64::from(self.width);
112        let other_right = u64::from(other.col_off) + u64::from(other.width);
113        let right = self_right.max(other_right);
114
115        let self_bottom = u64::from(self.row_off) + u64::from(self.height);
116        let other_bottom = u64::from(other.row_off) + u64::from(other.height);
117        let bottom = self_bottom.max(other_bottom);
118
119        // Widths/heights saturate to u32::MAX if they overflow
120        let width = u32::try_from(right - u64::from(left)).unwrap_or(u32::MAX);
121        let height = u32::try_from(bottom - u64::from(top)).unwrap_or(u32::MAX);
122
123        RasterWindow {
124            col_off: left,
125            row_off: top,
126            width,
127            height,
128        }
129    }
130
131    /// Check if this window contains the given pixel coordinates.
132    pub fn contains_pixel(&self, col: u32, row: u32) -> bool {
133        col >= self.col_off
134            && row >= self.row_off
135            && u64::from(col) < u64::from(self.col_off) + u64::from(self.width)
136            && u64::from(row) < u64::from(self.row_off) + u64::from(self.height)
137    }
138
139    /// Subdivide this window into tiles of the given size.
140    /// The last tiles in each row/column may be smaller.
141    pub fn subdivide(&self, tile_width: u32, tile_height: u32) -> Result<Vec<RasterWindow>> {
142        if tile_width == 0 {
143            return Err(OxiGdalError::invalid_parameter(
144                "tile_width",
145                "tile width must be greater than 0",
146            ));
147        }
148        if tile_height == 0 {
149            return Err(OxiGdalError::invalid_parameter(
150                "tile_height",
151                "tile height must be greater than 0",
152            ));
153        }
154
155        let cols = self.width.div_ceil(tile_width);
156        let rows = self.height.div_ceil(tile_height);
157        let capacity = u64::from(cols) * u64::from(rows);
158
159        let mut tiles = Vec::with_capacity(usize::try_from(capacity).map_err(|_| {
160            OxiGdalError::invalid_parameter("tile_size", "too many tiles would be generated")
161        })?);
162
163        for row_idx in 0..rows {
164            for col_idx in 0..cols {
165                let c = self.col_off + col_idx * tile_width;
166                let r = self.row_off + row_idx * tile_height;
167                let w = tile_width.min(self.col_off + self.width - c);
168                let h = tile_height.min(self.row_off + self.height - r);
169                tiles.push(RasterWindow {
170                    col_off: c,
171                    row_off: r,
172                    width: w,
173                    height: h,
174                });
175            }
176        }
177
178        Ok(tiles)
179    }
180
181    /// Convert a pixel coordinate from window-local to global raster coordinates.
182    pub fn to_global(&self, local_col: u32, local_row: u32) -> Result<(u32, u32)> {
183        if local_col >= self.width || local_row >= self.height {
184            return Err(OxiGdalError::OutOfBounds {
185                message: format!(
186                    "Local coordinate ({}, {}) is outside window dimensions ({}x{})",
187                    local_col, local_row, self.width, self.height
188                ),
189            });
190        }
191        let global_col =
192            self.col_off
193                .checked_add(local_col)
194                .ok_or_else(|| OxiGdalError::OutOfBounds {
195                    message: "Global column coordinate overflow".to_string(),
196                })?;
197        let global_row =
198            self.row_off
199                .checked_add(local_row)
200                .ok_or_else(|| OxiGdalError::OutOfBounds {
201                    message: "Global row coordinate overflow".to_string(),
202                })?;
203        Ok((global_col, global_row))
204    }
205
206    /// Convert a pixel coordinate from global raster to window-local coordinates.
207    /// Returns None if the global coordinate is outside this window.
208    pub fn to_local(&self, global_col: u32, global_row: u32) -> Option<(u32, u32)> {
209        if !self.contains_pixel(global_col, global_row) {
210            return None;
211        }
212        Some((global_col - self.col_off, global_row - self.row_off))
213    }
214
215    /// Extract the bytes for this window from a full-raster band buffer.
216    /// Assumes row-major layout with `bytes_per_pixel` bytes per pixel and
217    /// `raster_width` pixels per row in the source buffer.
218    pub fn extract_from_buffer(
219        &self,
220        source: &[u8],
221        raster_width: u32,
222        bytes_per_pixel: u32,
223    ) -> Result<Vec<u8>> {
224        if bytes_per_pixel == 0 {
225            return Err(OxiGdalError::invalid_parameter(
226                "bytes_per_pixel",
227                "must be greater than 0",
228            ));
229        }
230
231        let rw = u64::from(raster_width);
232        let bpp = u64::from(bytes_per_pixel);
233        let row_stride = rw.checked_mul(bpp).ok_or_else(|| {
234            OxiGdalError::invalid_parameter("raster_width", "row stride overflow")
235        })?;
236
237        // Validate source buffer size: we need at least (row_off + height) rows
238        let last_row = u64::from(self.row_off) + u64::from(self.height);
239        let required_len = last_row.checked_mul(row_stride).ok_or_else(|| {
240            OxiGdalError::invalid_parameter("raster_width", "buffer size calculation overflow")
241        })?;
242        if u64::try_from(source.len()).unwrap_or(0) < required_len {
243            return Err(OxiGdalError::OutOfBounds {
244                message: format!(
245                    "Source buffer too small: need {} bytes, got {}",
246                    required_len,
247                    source.len()
248                ),
249            });
250        }
251
252        // Validate column bounds
253        let col_end = u64::from(self.col_off) + u64::from(self.width);
254        if col_end > rw {
255            return Err(OxiGdalError::OutOfBounds {
256                message: format!(
257                    "Window column extent {} exceeds raster width {}",
258                    col_end, raster_width
259                ),
260            });
261        }
262
263        let win_row_bytes = u64::from(self.width) * bpp;
264        let total_bytes = win_row_bytes * u64::from(self.height);
265        let total_usize = usize::try_from(total_bytes).map_err(|_| {
266            OxiGdalError::invalid_parameter("window", "window data size exceeds addressable memory")
267        })?;
268
269        let mut result = Vec::with_capacity(total_usize);
270
271        for row in 0..self.height {
272            let src_row = u64::from(self.row_off + row);
273            let src_offset = src_row * row_stride + u64::from(self.col_off) * bpp;
274            let start = src_offset as usize;
275            let end = start + win_row_bytes as usize;
276            result.extend_from_slice(&source[start..end]);
277        }
278
279        Ok(result)
280    }
281
282    /// Write window data back into a full-raster band buffer.
283    pub fn write_to_buffer(
284        &self,
285        window_data: &[u8],
286        dest: &mut [u8],
287        raster_width: u32,
288        bytes_per_pixel: u32,
289    ) -> Result<()> {
290        if bytes_per_pixel == 0 {
291            return Err(OxiGdalError::invalid_parameter(
292                "bytes_per_pixel",
293                "must be greater than 0",
294            ));
295        }
296
297        let rw = u64::from(raster_width);
298        let bpp = u64::from(bytes_per_pixel);
299        let row_stride = rw.checked_mul(bpp).ok_or_else(|| {
300            OxiGdalError::invalid_parameter("raster_width", "row stride overflow")
301        })?;
302
303        let win_row_bytes = u64::from(self.width) * bpp;
304        let expected_data_len = win_row_bytes * u64::from(self.height);
305        if u64::try_from(window_data.len()).unwrap_or(0) != expected_data_len {
306            return Err(OxiGdalError::invalid_parameter(
307                "window_data",
308                format!(
309                    "expected {} bytes, got {}",
310                    expected_data_len,
311                    window_data.len()
312                ),
313            ));
314        }
315
316        // Validate dest buffer size
317        let last_row = u64::from(self.row_off) + u64::from(self.height);
318        let required_len = last_row.checked_mul(row_stride).ok_or_else(|| {
319            OxiGdalError::invalid_parameter("raster_width", "buffer size calculation overflow")
320        })?;
321        if u64::try_from(dest.len()).unwrap_or(0) < required_len {
322            return Err(OxiGdalError::OutOfBounds {
323                message: format!(
324                    "Destination buffer too small: need {} bytes, got {}",
325                    required_len,
326                    dest.len()
327                ),
328            });
329        }
330
331        // Validate column bounds
332        let col_end = u64::from(self.col_off) + u64::from(self.width);
333        if col_end > rw {
334            return Err(OxiGdalError::OutOfBounds {
335                message: format!(
336                    "Window column extent {} exceeds raster width {}",
337                    col_end, raster_width
338                ),
339            });
340        }
341
342        for row in 0..self.height {
343            let dst_row = u64::from(self.row_off + row);
344            let dst_offset = dst_row * row_stride + u64::from(self.col_off) * bpp;
345            let dst_start = dst_offset as usize;
346            let dst_end = dst_start + win_row_bytes as usize;
347
348            let src_offset = u64::from(row) * win_row_bytes;
349            let src_start = src_offset as usize;
350            let src_end = src_start + win_row_bytes as usize;
351
352            dest[dst_start..dst_end].copy_from_slice(&window_data[src_start..src_end]);
353        }
354
355        Ok(())
356    }
357}
358
359impl fmt::Display for RasterWindow {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        write!(
362            f,
363            "Window(col_off={}, row_off={}, width={}, height={})",
364            self.col_off, self.row_off, self.width, self.height
365        )
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_new_valid() {
375        let w = RasterWindow::new(0, 0, 100, 100).expect("should create valid window");
376        assert_eq!(w.col_off, 0);
377        assert_eq!(w.row_off, 0);
378        assert_eq!(w.width, 100);
379        assert_eq!(w.height, 100);
380    }
381
382    #[test]
383    fn test_new_zero_width_error() {
384        let result = RasterWindow::new(0, 0, 0, 100);
385        assert!(result.is_err());
386    }
387
388    #[test]
389    fn test_new_zero_height_error() {
390        let result = RasterWindow::new(0, 0, 100, 0);
391        assert!(result.is_err());
392    }
393
394    #[test]
395    fn test_full() {
396        let w = RasterWindow::full(256, 256).expect("should create full window");
397        assert_eq!(w.col_off, 0);
398        assert_eq!(w.row_off, 0);
399        assert_eq!(w.width, 256);
400        assert_eq!(w.height, 256);
401    }
402
403    #[test]
404    fn test_fits_within() {
405        let w = RasterWindow::new(10, 10, 50, 50).expect("valid window");
406        assert!(w.fits_within(100, 100));
407        assert!(!w.fits_within(30, 30));
408    }
409
410    #[test]
411    fn test_validate_bounds_ok() {
412        let w = RasterWindow::new(0, 0, 100, 100).expect("valid window");
413        assert!(w.validate_bounds(100, 100).is_ok());
414    }
415
416    #[test]
417    fn test_validate_bounds_overflow() {
418        let w = RasterWindow::new(50, 50, 100, 100).expect("valid window");
419        assert!(w.validate_bounds(100, 100).is_err());
420    }
421
422    #[test]
423    fn test_pixel_count() {
424        let w = RasterWindow::new(0, 0, 50, 50).expect("valid window");
425        assert_eq!(w.pixel_count(), 2500);
426    }
427
428    #[test]
429    fn test_intersection_overlap() {
430        let a = RasterWindow::new(0, 0, 100, 100).expect("valid window");
431        let b = RasterWindow::new(50, 50, 100, 100).expect("valid window");
432        let isect = a.intersection(&b).expect("should overlap");
433        assert_eq!(isect.col_off, 50);
434        assert_eq!(isect.row_off, 50);
435        assert_eq!(isect.width, 50);
436        assert_eq!(isect.height, 50);
437    }
438
439    #[test]
440    fn test_intersection_no_overlap() {
441        let a = RasterWindow::new(0, 0, 50, 50).expect("valid window");
442        let b = RasterWindow::new(100, 100, 50, 50).expect("valid window");
443        assert!(a.intersection(&b).is_none());
444    }
445
446    #[test]
447    fn test_union_bounds() {
448        let a = RasterWindow::new(10, 10, 40, 40).expect("valid window");
449        let b = RasterWindow::new(30, 30, 60, 60).expect("valid window");
450        let u = a.union_bounds(&b);
451        assert_eq!(u.col_off, 10);
452        assert_eq!(u.row_off, 10);
453        assert_eq!(u.width, 80);
454        assert_eq!(u.height, 80);
455    }
456
457    #[test]
458    fn test_contains_pixel() {
459        let w = RasterWindow::new(10, 10, 50, 50).expect("valid window");
460        assert!(w.contains_pixel(10, 10));
461        assert!(w.contains_pixel(59, 59));
462        assert!(!w.contains_pixel(60, 60));
463        assert!(!w.contains_pixel(9, 9));
464    }
465
466    #[test]
467    fn test_subdivide() {
468        let w = RasterWindow::new(0, 0, 100, 100).expect("valid window");
469        let tiles = w.subdivide(30, 30).expect("should subdivide");
470        // 4 cols (30, 30, 30, 10) * 4 rows (30, 30, 30, 10) = 16 tiles
471        assert_eq!(tiles.len(), 16);
472
473        // First tile
474        assert_eq!(tiles[0].col_off, 0);
475        assert_eq!(tiles[0].row_off, 0);
476        assert_eq!(tiles[0].width, 30);
477        assert_eq!(tiles[0].height, 30);
478
479        // Last tile in first row (partial width)
480        assert_eq!(tiles[3].col_off, 90);
481        assert_eq!(tiles[3].row_off, 0);
482        assert_eq!(tiles[3].width, 10);
483        assert_eq!(tiles[3].height, 30);
484
485        // Last tile (bottom-right, partial both)
486        assert_eq!(tiles[15].col_off, 90);
487        assert_eq!(tiles[15].row_off, 90);
488        assert_eq!(tiles[15].width, 10);
489        assert_eq!(tiles[15].height, 10);
490    }
491
492    #[test]
493    fn test_to_global_and_local() {
494        let w = RasterWindow::new(10, 20, 50, 50).expect("valid window");
495
496        // Local (5, 3) -> global (15, 23)
497        let (gc, gr) = w.to_global(5, 3).expect("should convert");
498        assert_eq!((gc, gr), (15, 23));
499
500        // Global (15, 23) -> local (5, 3)
501        let local = w.to_local(15, 23).expect("should be inside");
502        assert_eq!(local, (5, 3));
503
504        // Outside global coord
505        assert!(w.to_local(0, 0).is_none());
506
507        // Out-of-bounds local coord
508        assert!(w.to_global(50, 50).is_err());
509    }
510
511    #[test]
512    fn test_extract_from_buffer() {
513        // 4x4 raster, 1 byte per pixel, values 0..16
514        let source: Vec<u8> = (0u8..16).collect();
515        let w = RasterWindow::new(1, 1, 2, 2).expect("valid window");
516        let extracted = w
517            .extract_from_buffer(&source, 4, 1)
518            .expect("should extract");
519
520        // Row 1: pixels at col 1,2 → values 5, 6
521        // Row 2: pixels at col 1,2 → values 9, 10
522        assert_eq!(extracted, vec![5, 6, 9, 10]);
523    }
524
525    #[test]
526    fn test_write_to_buffer() {
527        // 4x4 raster, 1 byte per pixel, all zeros
528        let mut dest = vec![0u8; 16];
529        let w = RasterWindow::new(1, 1, 2, 2).expect("valid window");
530        let window_data = vec![0xAA, 0xBB, 0xCC, 0xDD];
531        w.write_to_buffer(&window_data, &mut dest, 4, 1)
532            .expect("should write");
533
534        // Check the 4x4 buffer
535        #[rustfmt::skip]
536        let expected: Vec<u8> = vec![
537            0x00, 0x00, 0x00, 0x00,
538            0x00, 0xAA, 0xBB, 0x00,
539            0x00, 0xCC, 0xDD, 0x00,
540            0x00, 0x00, 0x00, 0x00,
541        ];
542        assert_eq!(dest, expected);
543    }
544
545    #[test]
546    fn test_display() {
547        let w = RasterWindow::new(5, 10, 100, 200).expect("valid window");
548        assert_eq!(
549            w.to_string(),
550            "Window(col_off=5, row_off=10, width=100, height=200)"
551        );
552    }
553}