Skip to main content

oxigeo_core/buffer/
mod.rs

1// Allow unsafe blocks for low-level buffer operations that require
2// direct memory access for performance-critical typed slice conversions
3#![allow(unsafe_code)]
4
5//! Buffer types for raster and vector data
6//!
7//! This module provides efficient buffer types for storing and manipulating
8//! geospatial data. When the `arrow` feature is enabled, buffers are backed
9//! by Apache Arrow arrays for zero-copy interoperability.
10//!
11//! # Overview
12//!
13//! The [`RasterBuffer`] type is the core buffer abstraction in `OxiGeo`, providing
14//! type-safe storage for raster pixel data with automatic memory management.
15//!
16//! # Examples
17//!
18//! ## Creating buffers
19//!
20//! ```
21//! use oxigeo_core::buffer::RasterBuffer;
22//! use oxigeo_core::types::{RasterDataType, NoDataValue};
23//!
24//! // Create a zero-filled buffer
25//! let buffer = RasterBuffer::zeros(1000, 1000, RasterDataType::Float32);
26//!
27//! // Create a buffer with nodata value
28//! let nodata = NoDataValue::Float(-9999.0);
29//! let buffer = RasterBuffer::nodata_filled(1000, 1000, RasterDataType::Float32, nodata);
30//! ```
31//!
32//! ## Working with pixel data
33//!
34//! ```
35//! use oxigeo_core::buffer::RasterBuffer;
36//! use oxigeo_core::types::RasterDataType;
37//!
38//! let mut buffer = RasterBuffer::zeros(100, 100, RasterDataType::UInt8);
39//!
40//! // Set pixel value
41//! buffer.set_pixel(50, 50, 255.0)?;
42//!
43//! // Get pixel value
44//! let value = buffer.get_pixel(50, 50)?;
45//! assert_eq!(value, 255.0);
46//! # Ok::<(), oxigeo_core::error::OxiGeoError>(())
47//! ```
48//!
49//! ## Computing statistics
50//!
51//! ```
52//! use oxigeo_core::buffer::RasterBuffer;
53//! use oxigeo_core::types::RasterDataType;
54//!
55//! let buffer = RasterBuffer::zeros(1000, 1000, RasterDataType::Float32);
56//! let stats = buffer.compute_statistics()?;
57//!
58//! println!("Min: {}, Max: {}", stats.min, stats.max);
59//! println!("Mean: {}, StdDev: {}", stats.mean, stats.std_dev);
60//! println!("Valid pixels: {}", stats.valid_count);
61//! # Ok::<(), oxigeo_core::error::OxiGeoError>(())
62//! ```
63//!
64//! # See Also
65//!
66//! - [`RasterDataType`] - Supported pixel data types
67//! - [`NoDataValue`] - Representation of missing data
68//! - [`RasterStatistics`] - Pixel statistics
69//!
70//! [`RasterDataType`]: crate::types::RasterDataType
71//! [`NoDataValue`]: crate::types::NoDataValue
72//! [`RasterStatistics`]: crate::types::RasterStatistics
73
74use core::fmt;
75
76#[cfg(not(feature = "std"))]
77use crate::compat::*;
78use crate::error::{OxiGeoError, Result};
79#[cfg(not(feature = "std"))]
80use crate::math::FloatExt;
81use crate::types::{NoDataValue, RasterDataType};
82
83mod band_iterator;
84pub use band_iterator::{BandIterator, BandRef, MultiBandBuffer};
85
86mod raster_window;
87pub use raster_window::RasterWindow;
88
89pub mod mask;
90pub use mask::Mask;
91
92#[cfg(feature = "std")]
93pub use crate::simd_buffer::{ArenaTile, TileIteratorArena};
94
95#[cfg(feature = "arrow")]
96pub mod arrow_convert;
97
98/// A typed buffer for raster data
99#[derive(Clone)]
100pub struct RasterBuffer {
101    /// The underlying bytes
102    data: Vec<u8>,
103    /// Width in pixels
104    width: u64,
105    /// Height in pixels
106    height: u64,
107    /// Data type
108    data_type: RasterDataType,
109    /// `NoData` value
110    nodata: NoDataValue,
111}
112
113impl RasterBuffer {
114    /// Creates a new raster buffer
115    ///
116    /// # Errors
117    /// Returns an error if the data size doesn't match the dimensions and type
118    pub fn new(
119        data: Vec<u8>,
120        width: u64,
121        height: u64,
122        data_type: RasterDataType,
123        nodata: NoDataValue,
124    ) -> Result<Self> {
125        let expected_size = width * height * data_type.size_bytes() as u64;
126        if data.len() as u64 != expected_size {
127            return Err(OxiGeoError::InvalidParameter {
128                parameter: "data",
129                message: format!(
130                    "Data size mismatch: expected {} bytes for {}x{} {:?}, got {}",
131                    expected_size,
132                    width,
133                    height,
134                    data_type,
135                    data.len()
136                ),
137            });
138        }
139
140        Ok(Self {
141            data,
142            width,
143            height,
144            data_type,
145            nodata,
146        })
147    }
148
149    /// Creates a zero-filled buffer
150    #[must_use]
151    pub fn zeros(width: u64, height: u64, data_type: RasterDataType) -> Self {
152        let size = (width * height * data_type.size_bytes() as u64) as usize;
153        Self {
154            data: vec![0u8; size],
155            width,
156            height,
157            data_type,
158            nodata: NoDataValue::None,
159        }
160    }
161
162    /// Creates a buffer filled with the nodata value
163    #[must_use]
164    pub fn nodata_filled(
165        width: u64,
166        height: u64,
167        data_type: RasterDataType,
168        nodata: NoDataValue,
169    ) -> Self {
170        let mut buffer = Self::zeros(width, height, data_type);
171        buffer.nodata = nodata;
172
173        // Fill with nodata value if defined
174        if let Some(value) = nodata.as_f64() {
175            buffer.fill_value(value);
176        }
177
178        buffer
179    }
180
181    /// Fills the buffer with a constant value
182    pub fn fill_value(&mut self, value: f64) {
183        match self.data_type {
184            RasterDataType::UInt8 => {
185                let v = value as u8;
186                self.data.fill(v);
187            }
188            RasterDataType::Int8 => {
189                let v = value as i8;
190                self.data.fill(v as u8);
191            }
192            RasterDataType::UInt16 => {
193                let v = (value as u16).to_ne_bytes();
194                for chunk in self.data.chunks_exact_mut(2) {
195                    chunk.copy_from_slice(&v);
196                }
197            }
198            RasterDataType::Int16 => {
199                let v = (value as i16).to_ne_bytes();
200                for chunk in self.data.chunks_exact_mut(2) {
201                    chunk.copy_from_slice(&v);
202                }
203            }
204            RasterDataType::UInt32 => {
205                let v = (value as u32).to_ne_bytes();
206                for chunk in self.data.chunks_exact_mut(4) {
207                    chunk.copy_from_slice(&v);
208                }
209            }
210            RasterDataType::Int32 => {
211                let v = (value as i32).to_ne_bytes();
212                for chunk in self.data.chunks_exact_mut(4) {
213                    chunk.copy_from_slice(&v);
214                }
215            }
216            RasterDataType::Float32 => {
217                let v = (value as f32).to_ne_bytes();
218                for chunk in self.data.chunks_exact_mut(4) {
219                    chunk.copy_from_slice(&v);
220                }
221            }
222            RasterDataType::Float64 => {
223                let v = value.to_ne_bytes();
224                for chunk in self.data.chunks_exact_mut(8) {
225                    chunk.copy_from_slice(&v);
226                }
227            }
228            RasterDataType::UInt64 => {
229                let v = (value as u64).to_ne_bytes();
230                for chunk in self.data.chunks_exact_mut(8) {
231                    chunk.copy_from_slice(&v);
232                }
233            }
234            RasterDataType::Int64 => {
235                let v = (value as i64).to_ne_bytes();
236                for chunk in self.data.chunks_exact_mut(8) {
237                    chunk.copy_from_slice(&v);
238                }
239            }
240            RasterDataType::CFloat32 => {
241                // Complex pixel: 8 bytes = [real_f32 (4 bytes), imag_f32 (4 bytes)]
242                // Fill real = value, imag = 0.0
243                let real_bytes = (value as f32).to_ne_bytes();
244                let imag_bytes = 0f32.to_ne_bytes();
245                for chunk in self.data.chunks_exact_mut(8) {
246                    chunk[..4].copy_from_slice(&real_bytes);
247                    chunk[4..].copy_from_slice(&imag_bytes);
248                }
249            }
250            RasterDataType::CFloat64 => {
251                // Complex pixel: 16 bytes = [real_f64 (8 bytes), imag_f64 (8 bytes)]
252                // Fill real = value, imag = 0.0
253                let real_bytes = value.to_ne_bytes();
254                let imag_bytes = 0f64.to_ne_bytes();
255                for chunk in self.data.chunks_exact_mut(16) {
256                    chunk[..8].copy_from_slice(&real_bytes);
257                    chunk[8..].copy_from_slice(&imag_bytes);
258                }
259            }
260        }
261    }
262
263    /// Returns the width in pixels
264    #[must_use]
265    pub const fn width(&self) -> u64 {
266        self.width
267    }
268
269    /// Returns the height in pixels
270    #[must_use]
271    pub const fn height(&self) -> u64 {
272        self.height
273    }
274
275    /// Returns the data type
276    #[must_use]
277    pub const fn data_type(&self) -> RasterDataType {
278        self.data_type
279    }
280
281    /// Returns the nodata value
282    #[must_use]
283    pub const fn nodata(&self) -> NoDataValue {
284        self.nodata
285    }
286
287    /// Returns the total number of pixels
288    #[must_use]
289    pub const fn pixel_count(&self) -> u64 {
290        self.width * self.height
291    }
292
293    /// Returns the raw bytes
294    #[must_use]
295    pub fn as_bytes(&self) -> &[u8] {
296        &self.data
297    }
298
299    /// Returns mutable raw bytes
300    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
301        &mut self.data
302    }
303
304    /// Consumes the buffer and returns the raw bytes
305    #[must_use]
306    pub fn into_bytes(self) -> Vec<u8> {
307        self.data
308    }
309
310    /// Creates a buffer from typed vector data
311    ///
312    /// # Arguments
313    /// * `width` - Width in pixels
314    /// * `height` - Height in pixels
315    /// * `data` - Typed data (e.g., `Vec<f32>`, `Vec<u8>`)
316    /// * `data_type` - The raster data type
317    ///
318    /// # Errors
319    /// Returns an error if the data size doesn't match dimensions and type
320    pub fn from_typed_vec<T: Copy + 'static>(
321        width: usize,
322        height: usize,
323        data: Vec<T>,
324        data_type: RasterDataType,
325    ) -> Result<Self> {
326        let expected_pixels = width * height;
327        if data.len() != expected_pixels {
328            return Err(OxiGeoError::InvalidParameter {
329                parameter: "data",
330                message: format!(
331                    "Data length mismatch: expected {} pixels for {}x{}, got {}",
332                    expected_pixels,
333                    width,
334                    height,
335                    data.len()
336                ),
337            });
338        }
339
340        // Convert typed data to bytes
341        let type_size = core::mem::size_of::<T>();
342        let expected_type_size = data_type.size_bytes();
343        if type_size != expected_type_size {
344            return Err(OxiGeoError::InvalidParameter {
345                parameter: "data_type",
346                message: format!(
347                    "Type size mismatch: provided type has {} bytes, {:?} expects {} bytes",
348                    type_size, data_type, expected_type_size
349                ),
350            });
351        }
352
353        let byte_data: Vec<u8> = data
354            .iter()
355            .flat_map(|v| {
356                // SAFETY: We're reading the bytes of a Copy type
357                let ptr = v as *const T as *const u8;
358                unsafe { core::slice::from_raw_parts(ptr, type_size) }.to_vec()
359            })
360            .collect();
361
362        Self::new(
363            byte_data,
364            width as u64,
365            height as u64,
366            data_type,
367            NoDataValue::None,
368        )
369    }
370
371    /// Returns the buffer data as a typed slice
372    ///
373    /// # Type Parameters
374    /// * `T` - The target type (must match the buffer's data type size)
375    ///
376    /// # Errors
377    /// Returns an error if the type size doesn't match the data type
378    pub fn as_slice<T: Copy + 'static>(&self) -> Result<&[T]> {
379        let type_size = core::mem::size_of::<T>();
380        let expected_size = self.data_type.size_bytes();
381
382        if type_size != expected_size {
383            return Err(OxiGeoError::InvalidParameter {
384                parameter: "T",
385                message: format!(
386                    "Type size mismatch: requested type has {} bytes, buffer contains {:?} ({} bytes)",
387                    type_size, self.data_type, expected_size
388                ),
389            });
390        }
391
392        let pixel_count = (self.width * self.height) as usize;
393        // SAFETY: We've verified the type size matches, and the data is properly aligned
394        // for the original type it was created with
395        let slice =
396            unsafe { core::slice::from_raw_parts(self.data.as_ptr() as *const T, pixel_count) };
397        Ok(slice)
398    }
399
400    /// Returns the buffer data as a mutable typed slice
401    ///
402    /// # Type Parameters
403    /// * `T` - The target type (must match the buffer's data type size)
404    ///
405    /// # Errors
406    /// Returns an error if the type size doesn't match the data type
407    pub fn as_slice_mut<T: Copy + 'static>(&mut self) -> Result<&mut [T]> {
408        let type_size = core::mem::size_of::<T>();
409        let expected_size = self.data_type.size_bytes();
410
411        if type_size != expected_size {
412            return Err(OxiGeoError::InvalidParameter {
413                parameter: "T",
414                message: format!(
415                    "Type size mismatch: requested type has {} bytes, buffer contains {:?} ({} bytes)",
416                    type_size, self.data_type, expected_size
417                ),
418            });
419        }
420
421        let pixel_count = (self.width * self.height) as usize;
422        // SAFETY: We've verified the type size matches, and the data is properly aligned
423        // for the original type it was created with
424        let slice = unsafe {
425            core::slice::from_raw_parts_mut(self.data.as_mut_ptr() as *mut T, pixel_count)
426        };
427        Ok(slice)
428    }
429
430    /// Gets a pixel value as f64
431    ///
432    /// # Errors
433    /// Returns an error if coordinates are out of bounds
434    pub fn get_pixel(&self, x: u64, y: u64) -> Result<f64> {
435        if x >= self.width || y >= self.height {
436            return Err(OxiGeoError::OutOfBounds {
437                message: format!(
438                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
439                    x, y, self.width, self.height
440                ),
441            });
442        }
443
444        let pixel_size = self.data_type.size_bytes();
445        let offset = (y * self.width + x) as usize * pixel_size;
446
447        let value = match self.data_type {
448            RasterDataType::UInt8 => f64::from(self.data[offset]),
449            RasterDataType::Int8 => f64::from(self.data[offset] as i8),
450            RasterDataType::UInt16 => {
451                let bytes: [u8; 2] = self.data[offset..offset + 2].try_into().map_err(|_| {
452                    OxiGeoError::Internal {
453                        message: "Invalid slice length".to_string(),
454                    }
455                })?;
456                f64::from(u16::from_ne_bytes(bytes))
457            }
458            RasterDataType::Int16 => {
459                let bytes: [u8; 2] = self.data[offset..offset + 2].try_into().map_err(|_| {
460                    OxiGeoError::Internal {
461                        message: "Invalid slice length".to_string(),
462                    }
463                })?;
464                f64::from(i16::from_ne_bytes(bytes))
465            }
466            RasterDataType::UInt32 => {
467                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
468                    OxiGeoError::Internal {
469                        message: "Invalid slice length".to_string(),
470                    }
471                })?;
472                f64::from(u32::from_ne_bytes(bytes))
473            }
474            RasterDataType::Int32 => {
475                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
476                    OxiGeoError::Internal {
477                        message: "Invalid slice length".to_string(),
478                    }
479                })?;
480                f64::from(i32::from_ne_bytes(bytes))
481            }
482            RasterDataType::Float32 => {
483                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
484                    OxiGeoError::Internal {
485                        message: "Invalid slice length".to_string(),
486                    }
487                })?;
488                f64::from(f32::from_ne_bytes(bytes))
489            }
490            RasterDataType::Float64 => {
491                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
492                    OxiGeoError::Internal {
493                        message: "Invalid slice length".to_string(),
494                    }
495                })?;
496                f64::from_ne_bytes(bytes)
497            }
498            RasterDataType::UInt64 => {
499                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
500                    OxiGeoError::Internal {
501                        message: "Invalid slice length".to_string(),
502                    }
503                })?;
504                u64::from_ne_bytes(bytes) as f64
505            }
506            RasterDataType::Int64 => {
507                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
508                    OxiGeoError::Internal {
509                        message: "Invalid slice length".to_string(),
510                    }
511                })?;
512                i64::from_ne_bytes(bytes) as f64
513            }
514            RasterDataType::CFloat32 => {
515                // Return only the real part; CFloat32 pixel = [real_f32 | imag_f32]
516                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
517                    OxiGeoError::Internal {
518                        message: "Invalid slice length".to_string(),
519                    }
520                })?;
521                f64::from(f32::from_ne_bytes(bytes))
522            }
523            RasterDataType::CFloat64 => {
524                // Return only the real part; CFloat64 pixel = [real_f64 | imag_f64]
525                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
526                    OxiGeoError::Internal {
527                        message: "Invalid slice length".to_string(),
528                    }
529                })?;
530                f64::from_ne_bytes(bytes)
531            }
532        };
533
534        Ok(value)
535    }
536
537    /// Sets a pixel value
538    ///
539    /// # Errors
540    /// Returns an error if coordinates are out of bounds
541    pub fn set_pixel(&mut self, x: u64, y: u64, value: f64) -> Result<()> {
542        if x >= self.width || y >= self.height {
543            return Err(OxiGeoError::OutOfBounds {
544                message: format!(
545                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
546                    x, y, self.width, self.height
547                ),
548            });
549        }
550
551        let pixel_size = self.data_type.size_bytes();
552        let offset = (y * self.width + x) as usize * pixel_size;
553
554        match self.data_type {
555            RasterDataType::UInt8 => {
556                self.data[offset] = value as u8;
557            }
558            RasterDataType::Int8 => {
559                self.data[offset] = (value as i8) as u8;
560            }
561            RasterDataType::UInt16 => {
562                let bytes = (value as u16).to_ne_bytes();
563                self.data[offset..offset + 2].copy_from_slice(&bytes);
564            }
565            RasterDataType::Int16 => {
566                let bytes = (value as i16).to_ne_bytes();
567                self.data[offset..offset + 2].copy_from_slice(&bytes);
568            }
569            RasterDataType::UInt32 => {
570                let bytes = (value as u32).to_ne_bytes();
571                self.data[offset..offset + 4].copy_from_slice(&bytes);
572            }
573            RasterDataType::Int32 => {
574                let bytes = (value as i32).to_ne_bytes();
575                self.data[offset..offset + 4].copy_from_slice(&bytes);
576            }
577            RasterDataType::Float32 => {
578                let bytes = (value as f32).to_ne_bytes();
579                self.data[offset..offset + 4].copy_from_slice(&bytes);
580            }
581            RasterDataType::Float64 => {
582                let bytes = value.to_ne_bytes();
583                self.data[offset..offset + 8].copy_from_slice(&bytes);
584            }
585            RasterDataType::UInt64 => {
586                let bytes = (value as u64).to_ne_bytes();
587                self.data[offset..offset + 8].copy_from_slice(&bytes);
588            }
589            RasterDataType::Int64 => {
590                let bytes = (value as i64).to_ne_bytes();
591                self.data[offset..offset + 8].copy_from_slice(&bytes);
592            }
593            RasterDataType::CFloat32 => {
594                // Set only the real part
595                let bytes = (value as f32).to_ne_bytes();
596                self.data[offset..offset + 4].copy_from_slice(&bytes);
597            }
598            RasterDataType::CFloat64 => {
599                // Set only the real part
600                let bytes = value.to_ne_bytes();
601                self.data[offset..offset + 8].copy_from_slice(&bytes);
602            }
603        }
604
605        Ok(())
606    }
607
608    // ─── Typed Pixel Accessors ────────────────────────────────────────────
609
610    /// Gets a pixel value as `u8`.
611    ///
612    /// # Errors
613    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt8`.
614    pub fn get_u8(&self, x: u64, y: u64) -> Result<u8> {
615        self.check_bounds(x, y)?;
616        self.check_type(RasterDataType::UInt8)?;
617        let offset = (y * self.width + x) as usize;
618        Ok(self.data[offset])
619    }
620
621    /// Gets a pixel value as `i8`.
622    ///
623    /// # Errors
624    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int8`.
625    pub fn get_i8(&self, x: u64, y: u64) -> Result<i8> {
626        self.check_bounds(x, y)?;
627        self.check_type(RasterDataType::Int8)?;
628        let offset = (y * self.width + x) as usize;
629        Ok(self.data[offset] as i8)
630    }
631
632    /// Gets a pixel value as `u16`.
633    ///
634    /// # Errors
635    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt16`.
636    pub fn get_u16(&self, x: u64, y: u64) -> Result<u16> {
637        self.check_bounds(x, y)?;
638        self.check_type(RasterDataType::UInt16)?;
639        let offset = (y * self.width + x) as usize * 2;
640        let bytes: [u8; 2] =
641            self.data[offset..offset + 2]
642                .try_into()
643                .map_err(|_| OxiGeoError::Internal {
644                    message: "Invalid slice length".to_string(),
645                })?;
646        Ok(u16::from_ne_bytes(bytes))
647    }
648
649    /// Gets a pixel value as `i16`.
650    ///
651    /// # Errors
652    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int16`.
653    pub fn get_i16(&self, x: u64, y: u64) -> Result<i16> {
654        self.check_bounds(x, y)?;
655        self.check_type(RasterDataType::Int16)?;
656        let offset = (y * self.width + x) as usize * 2;
657        let bytes: [u8; 2] =
658            self.data[offset..offset + 2]
659                .try_into()
660                .map_err(|_| OxiGeoError::Internal {
661                    message: "Invalid slice length".to_string(),
662                })?;
663        Ok(i16::from_ne_bytes(bytes))
664    }
665
666    /// Gets a pixel value as `u32`.
667    ///
668    /// # Errors
669    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt32`.
670    pub fn get_u32(&self, x: u64, y: u64) -> Result<u32> {
671        self.check_bounds(x, y)?;
672        self.check_type(RasterDataType::UInt32)?;
673        let offset = (y * self.width + x) as usize * 4;
674        let bytes: [u8; 4] =
675            self.data[offset..offset + 4]
676                .try_into()
677                .map_err(|_| OxiGeoError::Internal {
678                    message: "Invalid slice length".to_string(),
679                })?;
680        Ok(u32::from_ne_bytes(bytes))
681    }
682
683    /// Gets a pixel value as `i32`.
684    ///
685    /// # Errors
686    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int32`.
687    pub fn get_i32(&self, x: u64, y: u64) -> Result<i32> {
688        self.check_bounds(x, y)?;
689        self.check_type(RasterDataType::Int32)?;
690        let offset = (y * self.width + x) as usize * 4;
691        let bytes: [u8; 4] =
692            self.data[offset..offset + 4]
693                .try_into()
694                .map_err(|_| OxiGeoError::Internal {
695                    message: "Invalid slice length".to_string(),
696                })?;
697        Ok(i32::from_ne_bytes(bytes))
698    }
699
700    /// Gets a pixel value as `u64`.
701    ///
702    /// # Errors
703    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt64`.
704    pub fn get_u64(&self, x: u64, y: u64) -> Result<u64> {
705        self.check_bounds(x, y)?;
706        self.check_type(RasterDataType::UInt64)?;
707        let offset = (y * self.width + x) as usize * 8;
708        let bytes: [u8; 8] =
709            self.data[offset..offset + 8]
710                .try_into()
711                .map_err(|_| OxiGeoError::Internal {
712                    message: "Invalid slice length".to_string(),
713                })?;
714        Ok(u64::from_ne_bytes(bytes))
715    }
716
717    /// Gets a pixel value as `i64`.
718    ///
719    /// # Errors
720    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int64`.
721    pub fn get_i64(&self, x: u64, y: u64) -> Result<i64> {
722        self.check_bounds(x, y)?;
723        self.check_type(RasterDataType::Int64)?;
724        let offset = (y * self.width + x) as usize * 8;
725        let bytes: [u8; 8] =
726            self.data[offset..offset + 8]
727                .try_into()
728                .map_err(|_| OxiGeoError::Internal {
729                    message: "Invalid slice length".to_string(),
730                })?;
731        Ok(i64::from_ne_bytes(bytes))
732    }
733
734    /// Gets a pixel value as `f32`.
735    ///
736    /// # Errors
737    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float32`.
738    pub fn get_f32(&self, x: u64, y: u64) -> Result<f32> {
739        self.check_bounds(x, y)?;
740        self.check_type(RasterDataType::Float32)?;
741        let offset = (y * self.width + x) as usize * 4;
742        let bytes: [u8; 4] =
743            self.data[offset..offset + 4]
744                .try_into()
745                .map_err(|_| OxiGeoError::Internal {
746                    message: "Invalid slice length".to_string(),
747                })?;
748        Ok(f32::from_ne_bytes(bytes))
749    }
750
751    /// Gets a pixel value as `f64`.
752    ///
753    /// # Errors
754    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float64`.
755    pub fn get_f64(&self, x: u64, y: u64) -> Result<f64> {
756        self.check_bounds(x, y)?;
757        self.check_type(RasterDataType::Float64)?;
758        let offset = (y * self.width + x) as usize * 8;
759        let bytes: [u8; 8] =
760            self.data[offset..offset + 8]
761                .try_into()
762                .map_err(|_| OxiGeoError::Internal {
763                    message: "Invalid slice length".to_string(),
764                })?;
765        Ok(f64::from_ne_bytes(bytes))
766    }
767
768    // ─── Typed Pixel Setters ─────────────────────────────────────────────
769
770    /// Sets a pixel value from `u8`.
771    ///
772    /// # Errors
773    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt8`.
774    pub fn set_u8(&mut self, x: u64, y: u64, value: u8) -> Result<()> {
775        self.check_bounds(x, y)?;
776        self.check_type(RasterDataType::UInt8)?;
777        let offset = (y * self.width + x) as usize;
778        self.data[offset] = value;
779        Ok(())
780    }
781
782    /// Sets a pixel value from `f32`.
783    ///
784    /// # Errors
785    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float32`.
786    pub fn set_f32(&mut self, x: u64, y: u64, value: f32) -> Result<()> {
787        self.check_bounds(x, y)?;
788        self.check_type(RasterDataType::Float32)?;
789        let offset = (y * self.width + x) as usize * 4;
790        self.data[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
791        Ok(())
792    }
793
794    /// Sets a pixel value from `f64`.
795    ///
796    /// # Errors
797    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float64`.
798    pub fn set_f64(&mut self, x: u64, y: u64, value: f64) -> Result<()> {
799        self.check_bounds(x, y)?;
800        self.check_type(RasterDataType::Float64)?;
801        let offset = (y * self.width + x) as usize * 8;
802        self.data[offset..offset + 8].copy_from_slice(&value.to_ne_bytes());
803        Ok(())
804    }
805
806    // ─── Row & Window Access ─────────────────────────────────────────────
807
808    /// Returns a row of pixel data as a typed slice.
809    ///
810    /// # Errors
811    /// Returns an error if `y` is out of bounds or type size mismatches.
812    pub fn row_slice<T: Copy + 'static>(&self, y: u64) -> Result<&[T]> {
813        if y >= self.height {
814            return Err(OxiGeoError::OutOfBounds {
815                message: format!("Row {} out of bounds for height {}", y, self.height),
816            });
817        }
818        let type_size = core::mem::size_of::<T>();
819        let expected_size = self.data_type.size_bytes();
820        if type_size != expected_size {
821            return Err(OxiGeoError::InvalidParameter {
822                parameter: "T",
823                message: format!(
824                    "Type size {} doesn't match {:?} size {}",
825                    type_size, self.data_type, expected_size
826                ),
827            });
828        }
829        let row_start = (y * self.width) as usize * expected_size;
830        let row_end = row_start + self.width as usize * expected_size;
831        // SAFETY: type size verified above, row bounds verified
832        let slice = unsafe {
833            core::slice::from_raw_parts(
834                self.data[row_start..row_end].as_ptr() as *const T,
835                self.width as usize,
836            )
837        };
838        Ok(slice)
839    }
840
841    /// Returns a rectangular window of pixel data as a new `RasterBuffer`.
842    ///
843    /// # Errors
844    /// Returns an error if the window extends outside buffer bounds.
845    pub fn window(&self, x: u64, y: u64, width: u64, height: u64) -> Result<Self> {
846        if x + width > self.width || y + height > self.height {
847            return Err(OxiGeoError::OutOfBounds {
848                message: format!(
849                    "Window ({},{}) {}x{} exceeds buffer {}x{}",
850                    x, y, width, height, self.width, self.height
851                ),
852            });
853        }
854        let pixel_size = self.data_type.size_bytes();
855        let row_bytes = width as usize * pixel_size;
856        let mut data = Vec::with_capacity(height as usize * row_bytes);
857        for row in y..y + height {
858            let src_start = (row * self.width + x) as usize * pixel_size;
859            data.extend_from_slice(&self.data[src_start..src_start + row_bytes]);
860        }
861        Self::new(data, width, height, self.data_type, self.nodata)
862    }
863
864    // ─── Private Helpers ─────────────────────────────────────────────────
865
866    fn check_bounds(&self, x: u64, y: u64) -> Result<()> {
867        if x >= self.width || y >= self.height {
868            return Err(OxiGeoError::OutOfBounds {
869                message: format!(
870                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
871                    x, y, self.width, self.height
872                ),
873            });
874        }
875        Ok(())
876    }
877
878    fn check_type(&self, expected: RasterDataType) -> Result<()> {
879        if self.data_type != expected {
880            return Err(OxiGeoError::InvalidParameter {
881                parameter: "data_type",
882                message: format!(
883                    "Buffer contains {:?} data, requested {:?}",
884                    self.data_type, expected
885                ),
886            });
887        }
888        Ok(())
889    }
890
891    /// Returns true if the given value equals the nodata value
892    #[must_use]
893    pub fn is_nodata(&self, value: f64) -> bool {
894        match self.nodata.as_f64() {
895            Some(nd) => {
896                if nd.is_nan() && value.is_nan() {
897                    true
898                } else {
899                    (nd - value).abs() < f64::EPSILON
900                }
901            }
902            None => false,
903        }
904    }
905
906    /// Converts the buffer to a different data type
907    ///
908    /// # Errors
909    /// Returns an error if conversion fails
910    pub fn convert_to(&self, target_type: RasterDataType) -> Result<Self> {
911        if target_type == self.data_type {
912            return Ok(self.clone());
913        }
914
915        let mut result = Self::zeros(self.width, self.height, target_type);
916        result.nodata = self.nodata;
917
918        for y in 0..self.height {
919            for x in 0..self.width {
920                let value = self.get_pixel(x, y)?;
921                result.set_pixel(x, y, value)?;
922            }
923        }
924
925        Ok(result)
926    }
927
928    /// Computes basic statistics
929    pub fn compute_statistics(&self) -> Result<BufferStatistics> {
930        let mut min = f64::MAX;
931        let mut max = f64::MIN;
932        let mut sum = 0.0;
933        let mut sum_sq = 0.0;
934        let mut valid_count = 0u64;
935
936        for y in 0..self.height {
937            for x in 0..self.width {
938                let value = self.get_pixel(x, y)?;
939                if !self.is_nodata(value) && value.is_finite() {
940                    min = min.min(value);
941                    max = max.max(value);
942                    sum += value;
943                    sum_sq += value * value;
944                    valid_count += 1;
945                }
946            }
947        }
948
949        if valid_count == 0 {
950            return Ok(BufferStatistics {
951                min: f64::NAN,
952                max: f64::NAN,
953                mean: f64::NAN,
954                std_dev: f64::NAN,
955                valid_count: 0,
956                histogram: None,
957            });
958        }
959
960        let mean = sum / valid_count as f64;
961        let variance = (sum_sq / valid_count as f64) - (mean * mean);
962        let std_dev = variance.sqrt();
963
964        Ok(BufferStatistics {
965            min,
966            max,
967            mean,
968            std_dev,
969            valid_count,
970            histogram: None,
971        })
972    }
973
974    /// Computes statistics and an optional histogram in one pass.
975    ///
976    /// The returned [`BufferStatistics`] contains a `histogram` of `bin_count` bins
977    /// with uniform spacing covering the range `[min, max]`. Each bin holds the count
978    /// of valid pixels whose value falls within that bin's interval.
979    ///
980    /// # Arguments
981    ///
982    /// * `bin_count` — Number of histogram bins. Must be ≥ 1.
983    ///
984    /// # Errors
985    ///
986    /// Returns [`crate::error::OxiGeoError::InvalidParameter`] if `bin_count` is 0.
987    /// Returns errors from pixel access on corrupt buffers.
988    ///
989    /// # Notes
990    ///
991    /// - NaN and infinite values are excluded (same as [`RasterBuffer::compute_statistics`]).
992    /// - When all valid values are identical (`min == max`), all counts go into bin 0.
993    /// - When no valid pixels exist, `histogram` is `Some(vec![0; bin_count])`.
994    pub fn compute_statistics_with_histogram(&self, bin_count: usize) -> Result<BufferStatistics> {
995        if bin_count == 0 {
996            return Err(OxiGeoError::InvalidParameter {
997                parameter: "bin_count",
998                message: "bin_count must be at least 1".to_string(),
999            });
1000        }
1001
1002        // First pass: collect min/max/sum/sum_sq and all valid values for histogram.
1003        let mut min = f64::MAX;
1004        let mut max = f64::MIN;
1005        let mut sum = 0.0f64;
1006        let mut sum_sq = 0.0f64;
1007        let mut valid_count = 0u64;
1008        // Collect valid values so histogram binning can reuse them without a re-read.
1009        let total_pixels = (self.width * self.height) as usize;
1010        let mut valid_values: Vec<f64> = Vec::with_capacity(total_pixels);
1011
1012        for y in 0..self.height {
1013            for x in 0..self.width {
1014                let value = self.get_pixel(x, y)?;
1015                if !self.is_nodata(value) && value.is_finite() {
1016                    min = min.min(value);
1017                    max = max.max(value);
1018                    sum += value;
1019                    sum_sq += value * value;
1020                    valid_count += 1;
1021                    valid_values.push(value);
1022                }
1023            }
1024        }
1025
1026        // Build histogram bins (all-zero when there are no valid pixels).
1027        let mut bins = vec![0u64; bin_count];
1028
1029        if valid_count > 0 {
1030            let range = max - min;
1031            if range == 0.0 {
1032                // All valid values are identical: everything goes into bin 0.
1033                bins[0] = valid_count;
1034            } else {
1035                for v in &valid_values {
1036                    let bin_idx = (((v - min) / range) * bin_count as f64).floor() as usize;
1037                    // Clamp to [0, bin_count-1] (handles the max edge case exactly).
1038                    let bin_idx = bin_idx.min(bin_count - 1);
1039                    bins[bin_idx] += 1;
1040                }
1041            }
1042        }
1043
1044        if valid_count == 0 {
1045            return Ok(BufferStatistics {
1046                min: f64::NAN,
1047                max: f64::NAN,
1048                mean: f64::NAN,
1049                std_dev: f64::NAN,
1050                valid_count: 0,
1051                histogram: Some(bins),
1052            });
1053        }
1054
1055        let mean = sum / valid_count as f64;
1056        let variance = (sum_sq / valid_count as f64) - (mean * mean);
1057        let std_dev = variance.max(0.0).sqrt();
1058
1059        Ok(BufferStatistics {
1060            min,
1061            max,
1062            mean,
1063            std_dev,
1064            valid_count,
1065            histogram: Some(bins),
1066        })
1067    }
1068}
1069
1070impl fmt::Debug for RasterBuffer {
1071    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1072        f.debug_struct("RasterBuffer")
1073            .field("width", &self.width)
1074            .field("height", &self.height)
1075            .field("data_type", &self.data_type)
1076            .field("nodata", &self.nodata)
1077            .field("bytes", &self.data.len())
1078            .finish()
1079    }
1080}
1081
1082/// Statistics computed from a buffer
1083///
1084/// Note: `Copy` is intentionally not derived because the optional histogram
1085/// contains a `Vec`, making bitwise copy semantics inappropriate.
1086#[derive(Debug, Clone, PartialEq)]
1087pub struct BufferStatistics {
1088    /// Minimum value
1089    pub min: f64,
1090    /// Maximum value
1091    pub max: f64,
1092    /// Mean value
1093    pub mean: f64,
1094    /// Standard deviation
1095    pub std_dev: f64,
1096    /// Number of valid (non-nodata) pixels
1097    pub valid_count: u64,
1098    /// Optional histogram bin counts (uniform spacing from `min` to `max`)
1099    ///
1100    /// `None` when computed via [`RasterBuffer::compute_statistics`].
1101    /// `Some(bins)` when computed via [`RasterBuffer::compute_statistics_with_histogram`].
1102    pub histogram: Option<Vec<u64>>,
1103}
1104
1105#[cfg(feature = "arrow")]
1106mod arrow_support {
1107    //! Arrow integration for zero-copy interoperability
1108
1109    use arrow_array::{Array, Float64Array};
1110
1111    use super::{OxiGeoError, RasterBuffer, Result};
1112
1113    impl RasterBuffer {
1114        /// Creates a `RasterBuffer` from an Arrow array
1115        ///
1116        /// # Errors
1117        /// Returns an error if the array type doesn't match
1118        pub fn from_arrow_array<A: Array>(_array: &A, _width: u64, _height: u64) -> Result<Self> {
1119            // This is a simplified implementation
1120            // A full implementation would handle all Arrow types
1121            Err(OxiGeoError::NotSupported {
1122                operation: "Arrow array conversion".to_string(),
1123            })
1124        }
1125
1126        /// Converts to an Arrow `Float64Array`
1127        pub fn to_float64_array(&self) -> Result<Float64Array> {
1128            let mut values = Vec::with_capacity(self.pixel_count() as usize);
1129            for y in 0..self.height {
1130                for x in 0..self.width {
1131                    values.push(self.get_pixel(x, y)?);
1132                }
1133            }
1134            Ok(Float64Array::from(values))
1135        }
1136    }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    #![allow(clippy::expect_used)]
1142
1143    use super::*;
1144
1145    #[test]
1146    fn test_buffer_creation() {
1147        let buffer = RasterBuffer::zeros(100, 100, RasterDataType::UInt8);
1148        assert_eq!(buffer.width(), 100);
1149        assert_eq!(buffer.height(), 100);
1150        assert_eq!(buffer.pixel_count(), 10_000);
1151        assert_eq!(buffer.as_bytes().len(), 10_000);
1152    }
1153
1154    #[test]
1155    fn test_pixel_access() {
1156        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1157
1158        buffer.set_pixel(5, 5, 42.0).expect("set should work");
1159        let value = buffer.get_pixel(5, 5).expect("get should work");
1160        assert!((value - 42.0).abs() < f64::EPSILON);
1161
1162        // Out of bounds
1163        assert!(buffer.get_pixel(100, 0).is_err());
1164        assert!(buffer.set_pixel(0, 100, 0.0).is_err());
1165    }
1166
1167    #[test]
1168    fn test_nodata() {
1169        let buffer = RasterBuffer::nodata_filled(
1170            10,
1171            10,
1172            RasterDataType::Float32,
1173            NoDataValue::Float(-9999.0),
1174        );
1175
1176        assert!(buffer.is_nodata(-9999.0));
1177        assert!(!buffer.is_nodata(0.0));
1178    }
1179
1180    #[test]
1181    fn test_statistics() {
1182        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1183
1184        // Fill with values 0-99
1185        for y in 0..10 {
1186            for x in 0..10 {
1187                let value = (y * 10 + x) as f64;
1188                buffer.set_pixel(x, y, value).expect("set should work");
1189            }
1190        }
1191
1192        let stats = buffer.compute_statistics().expect("stats should work");
1193        assert!((stats.min - 0.0).abs() < f64::EPSILON);
1194        assert!((stats.max - 99.0).abs() < f64::EPSILON);
1195        assert!((stats.mean - 49.5).abs() < 0.01);
1196        assert_eq!(stats.valid_count, 100);
1197    }
1198
1199    #[test]
1200    fn test_data_validation() {
1201        // Wrong size should fail
1202        let result = RasterBuffer::new(
1203            vec![0u8; 100],
1204            10,
1205            10,
1206            RasterDataType::UInt16, // Needs 200 bytes
1207            NoDataValue::None,
1208        );
1209        assert!(result.is_err());
1210
1211        // Correct size should succeed
1212        let result = RasterBuffer::new(
1213            vec![0u8; 200],
1214            10,
1215            10,
1216            RasterDataType::UInt16,
1217            NoDataValue::None,
1218        );
1219        assert!(result.is_ok());
1220    }
1221
1222    #[test]
1223    fn test_typed_get_u8() {
1224        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1225        buffer.set_pixel(3, 4, 200.0).expect("set should work");
1226        assert_eq!(buffer.get_u8(3, 4).expect("get_u8"), 200);
1227        // Wrong type should error
1228        assert!(buffer.get_f32(3, 4).is_err());
1229    }
1230
1231    #[test]
1232    fn test_typed_get_i8() {
1233        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int8);
1234        buffer.set_pixel(0, 0, -42.0).expect("set should work");
1235        assert_eq!(buffer.get_i8(0, 0).expect("get_i8"), -42);
1236    }
1237
1238    #[test]
1239    fn test_typed_get_u16() {
1240        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt16);
1241        buffer.set_pixel(5, 5, 60000.0).expect("set should work");
1242        assert_eq!(buffer.get_u16(5, 5).expect("get_u16"), 60000);
1243    }
1244
1245    #[test]
1246    fn test_typed_get_i16() {
1247        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int16);
1248        buffer.set_pixel(2, 3, -1234.0).expect("set should work");
1249        assert_eq!(buffer.get_i16(2, 3).expect("get_i16"), -1234);
1250    }
1251
1252    #[test]
1253    fn test_typed_get_u32() {
1254        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt32);
1255        buffer.set_pixel(0, 0, 100_000.0).expect("set should work");
1256        assert_eq!(buffer.get_u32(0, 0).expect("get_u32"), 100_000);
1257    }
1258
1259    #[test]
1260    fn test_typed_get_i32() {
1261        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int32);
1262        buffer.set_pixel(1, 1, -50_000.0).expect("set should work");
1263        assert_eq!(buffer.get_i32(1, 1).expect("get_i32"), -50_000);
1264    }
1265
1266    #[test]
1267    fn test_typed_get_u64() {
1268        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt64);
1269        buffer
1270            .set_pixel(0, 0, 1_000_000.0)
1271            .expect("set should work");
1272        assert_eq!(buffer.get_u64(0, 0).expect("get_u64"), 1_000_000);
1273    }
1274
1275    #[test]
1276    fn test_typed_get_i64() {
1277        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int64);
1278        buffer.set_pixel(0, 0, -999_999.0).expect("set should work");
1279        assert_eq!(buffer.get_i64(0, 0).expect("get_i64"), -999_999);
1280    }
1281
1282    #[test]
1283    fn test_typed_get_f32() {
1284        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1285        buffer
1286            .set_f32(7, 8, core::f32::consts::PI)
1287            .expect("set_f32 should work");
1288        let val = buffer.get_f32(7, 8).expect("get_f32");
1289        assert!((val - core::f32::consts::PI).abs() < 1e-5);
1290    }
1291
1292    #[test]
1293    fn test_typed_get_f64() {
1294        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float64);
1295        buffer
1296            .set_f64(9, 9, core::f64::consts::E)
1297            .expect("set_f64 should work");
1298        let val = buffer.get_f64(9, 9).expect("get_f64");
1299        assert!((val - core::f64::consts::E).abs() < 1e-9);
1300    }
1301
1302    #[test]
1303    fn test_typed_set_u8() {
1304        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1305        buffer.set_u8(0, 0, 255).expect("set_u8 should work");
1306        assert_eq!(buffer.get_u8(0, 0).expect("get_u8"), 255);
1307    }
1308
1309    #[test]
1310    fn test_typed_out_of_bounds() {
1311        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1312        assert!(buffer.get_f32(10, 0).is_err());
1313        assert!(buffer.get_f32(0, 10).is_err());
1314    }
1315
1316    #[test]
1317    fn test_typed_wrong_type() {
1318        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1319        assert!(buffer.get_f32(0, 0).is_err());
1320        assert!(buffer.get_u16(0, 0).is_err());
1321        assert!(buffer.get_i32(0, 0).is_err());
1322        assert!(buffer.get_f64(0, 0).is_err());
1323    }
1324
1325    #[test]
1326    fn test_row_slice() {
1327        let mut buffer = RasterBuffer::zeros(5, 3, RasterDataType::Float32);
1328        for x in 0..5 {
1329            buffer
1330                .set_pixel(x, 1, (x + 10) as f64)
1331                .expect("set should work");
1332        }
1333        let row: &[f32] = buffer.row_slice(1).expect("row_slice should work");
1334        assert_eq!(row.len(), 5);
1335        assert!((row[0] - 10.0).abs() < 1e-5);
1336        assert!((row[4] - 14.0).abs() < 1e-5);
1337    }
1338
1339    #[test]
1340    fn test_row_slice_out_of_bounds() {
1341        let buffer = RasterBuffer::zeros(5, 3, RasterDataType::Float32);
1342        assert!(buffer.row_slice::<f32>(3).is_err());
1343    }
1344
1345    #[test]
1346    fn test_window() {
1347        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1348        buffer.set_pixel(5, 5, 42.0).expect("set should work");
1349        buffer.set_pixel(6, 6, 99.0).expect("set should work");
1350
1351        let win = buffer.window(4, 4, 4, 4).expect("window should work");
1352        assert_eq!(win.width(), 4);
1353        assert_eq!(win.height(), 4);
1354        // (5,5) in original -> (1,1) in window
1355        let val = win.get_pixel(1, 1).expect("get should work");
1356        assert!((val - 42.0).abs() < f64::EPSILON);
1357        // (6,6) in original -> (2,2) in window
1358        let val = win.get_pixel(2, 2).expect("get should work");
1359        assert!((val - 99.0).abs() < f64::EPSILON);
1360    }
1361
1362    #[test]
1363    fn test_window_out_of_bounds() {
1364        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1365        assert!(buffer.window(8, 8, 4, 4).is_err());
1366    }
1367
1368    #[test]
1369    fn test_fill_value_cfloat32_writes_real_zero_imag() {
1370        // Use a value that is not a well-known constant to avoid clippy::approx_constant
1371        let fill: f64 = 1.23456789;
1372        let mut buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat32);
1373        buf.fill_value(fill);
1374        // get_pixel returns only the real part for complex types
1375        let v = buf.get_pixel(0, 0).expect("pixel access");
1376        assert!(
1377            (v - fill).abs() < 1e-5,
1378            "real part should be {fill}, got {v}"
1379        );
1380        // Confirm imag bytes are zero (bytes 4-8 of pixel 0)
1381        let raw = buf.as_bytes();
1382        let imag_bytes: [u8; 4] = raw[4..8].try_into().expect("slice");
1383        let imag = f32::from_ne_bytes(imag_bytes);
1384        assert_eq!(imag, 0.0f32, "imaginary part should be 0.0");
1385    }
1386
1387    #[test]
1388    fn test_fill_value_cfloat64_writes_real_zero_imag() {
1389        // Use an arbitrary value that is not a well-known constant (avoids clippy::approx_constant)
1390        let fill: f64 = 9.876_543_21_f64;
1391        let mut buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat64);
1392        buf.fill_value(fill);
1393        let v = buf.get_pixel(0, 0).expect("pixel access");
1394        assert!((v - fill).abs() < 1e-9, "real part mismatch, got {v}");
1395        // Confirm imag bytes are zero (bytes 8-16 of pixel 0)
1396        let raw = buf.as_bytes();
1397        let imag_bytes: [u8; 8] = raw[8..16].try_into().expect("slice");
1398        let imag = f64::from_ne_bytes(imag_bytes);
1399        assert_eq!(imag, 0.0f64, "imaginary part should be 0.0");
1400    }
1401}