Skip to main content

oxigdal_core/buffer/
raster_window.rs

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