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//! ## Reading pixels into a buffer you own (no extra allocation)
50//!
51//! [`RasterBuffer::copy_to_slice`] converts and writes straight into a
52//! caller-supplied slice, so nothing is allocated or copied twice — the
53//! equivalent of GDAL's `RasterBand::read_into_slice`:
54//!
55//! ```
56//! use oxigeo_core::buffer::RasterBuffer;
57//! use oxigeo_core::types::RasterDataType;
58//!
59//! let mut buffer = RasterBuffer::zeros(4, 2, RasterDataType::UInt16);
60//! buffer.set_pixel(0, 0, 7.0)?;
61//!
62//! // The only allocation is the destination, and it belongs to the caller.
63//! let mut pixels = vec![0.0f64; 8];
64//! buffer.copy_to_slice(&mut pixels)?;
65//! assert_eq!(pixels[0], 7.0);
66//! # Ok::<(), oxigeo_core::error::OxiGeoError>(())
67//! ```
68//!
69//! With `ndarray` the destination is simply the array's backing slice:
70//!
71//! ```text
72//! let mut array = ndarray::Array2::<f64>::zeros((height, width));
73//! if let Some(dst) = array.as_slice_mut() {
74//!     buffer.copy_to_slice(dst)?;
75//! }
76//! ```
77//!
78//! ## Computing statistics
79//!
80//! ```
81//! use oxigeo_core::buffer::RasterBuffer;
82//! use oxigeo_core::types::RasterDataType;
83//!
84//! let buffer = RasterBuffer::zeros(1000, 1000, RasterDataType::Float32);
85//! let stats = buffer.compute_statistics()?;
86//!
87//! println!("Min: {}, Max: {}", stats.min, stats.max);
88//! println!("Mean: {}, StdDev: {}", stats.mean, stats.std_dev);
89//! println!("Valid pixels: {}", stats.valid_count);
90//! # Ok::<(), oxigeo_core::error::OxiGeoError>(())
91//! ```
92//!
93//! # See Also
94//!
95//! - [`RasterDataType`] - Supported pixel data types
96//! - [`NoDataValue`] - Representation of missing data
97//! - [`RasterStatistics`] - Pixel statistics
98//!
99//! [`RasterDataType`]: crate::types::RasterDataType
100//! [`NoDataValue`]: crate::types::NoDataValue
101//! [`RasterStatistics`]: crate::types::RasterStatistics
102
103use core::fmt;
104
105#[cfg(not(feature = "std"))]
106use crate::compat::*;
107use crate::error::{OxiGeoError, Result};
108#[cfg(not(feature = "std"))]
109use crate::math::FloatExt;
110use crate::types::{NoDataValue, RasterDataType};
111
112pub mod element;
113pub use element::{
114    FloatToIntRounding, RasterElement, RasterElementKind, convert_raw_bytes, convert_raw_into,
115    convert_raw_into_with, elements_as_bytes,
116};
117
118mod band_iterator;
119pub use band_iterator::{BandIterator, BandRef, MultiBandBuffer};
120
121mod raster_window;
122pub use raster_window::RasterWindow;
123
124pub mod mask;
125pub use mask::Mask;
126
127#[cfg(feature = "std")]
128pub use crate::simd_buffer::{ArenaTile, TileIteratorArena};
129
130#[cfg(feature = "arrow")]
131pub mod arrow_convert;
132
133/// A typed buffer for raster data
134#[derive(Clone)]
135pub struct RasterBuffer {
136    /// The underlying bytes
137    data: Vec<u8>,
138    /// Width in pixels
139    width: u64,
140    /// Height in pixels
141    height: u64,
142    /// Data type
143    data_type: RasterDataType,
144    /// `NoData` value
145    nodata: NoDataValue,
146}
147
148impl RasterBuffer {
149    /// Creates a new raster buffer
150    ///
151    /// # Errors
152    /// Returns an error if the data size doesn't match the dimensions and type
153    pub fn new(
154        data: Vec<u8>,
155        width: u64,
156        height: u64,
157        data_type: RasterDataType,
158        nodata: NoDataValue,
159    ) -> Result<Self> {
160        let expected_size = width * height * data_type.size_bytes() as u64;
161        if data.len() as u64 != expected_size {
162            return Err(OxiGeoError::InvalidParameter {
163                parameter: "data",
164                message: format!(
165                    "Data size mismatch: expected {} bytes for {}x{} {:?}, got {}",
166                    expected_size,
167                    width,
168                    height,
169                    data_type,
170                    data.len()
171                ),
172            });
173        }
174
175        Ok(Self {
176            data,
177            width,
178            height,
179            data_type,
180            nodata,
181        })
182    }
183
184    /// Creates a zero-filled buffer
185    #[must_use]
186    pub fn zeros(width: u64, height: u64, data_type: RasterDataType) -> Self {
187        let size = (width * height * data_type.size_bytes() as u64) as usize;
188        Self {
189            data: vec![0u8; size],
190            width,
191            height,
192            data_type,
193            nodata: NoDataValue::None,
194        }
195    }
196
197    /// Creates a buffer filled with the nodata value
198    #[must_use]
199    pub fn nodata_filled(
200        width: u64,
201        height: u64,
202        data_type: RasterDataType,
203        nodata: NoDataValue,
204    ) -> Self {
205        let mut buffer = Self::zeros(width, height, data_type);
206        buffer.nodata = nodata;
207
208        // Fill with nodata value if defined
209        if let Some(value) = nodata.as_f64() {
210            buffer.fill_value(value);
211        }
212
213        buffer
214    }
215
216    /// Fills the buffer with a constant value
217    pub fn fill_value(&mut self, value: f64) {
218        match self.data_type {
219            RasterDataType::UInt8 => {
220                let v = value as u8;
221                self.data.fill(v);
222            }
223            RasterDataType::Int8 => {
224                let v = value as i8;
225                self.data.fill(v as u8);
226            }
227            RasterDataType::UInt16 => {
228                let v = (value as u16).to_ne_bytes();
229                for chunk in self.data.chunks_exact_mut(2) {
230                    chunk.copy_from_slice(&v);
231                }
232            }
233            RasterDataType::Int16 => {
234                let v = (value as i16).to_ne_bytes();
235                for chunk in self.data.chunks_exact_mut(2) {
236                    chunk.copy_from_slice(&v);
237                }
238            }
239            RasterDataType::UInt32 => {
240                let v = (value as u32).to_ne_bytes();
241                for chunk in self.data.chunks_exact_mut(4) {
242                    chunk.copy_from_slice(&v);
243                }
244            }
245            RasterDataType::Int32 => {
246                let v = (value as i32).to_ne_bytes();
247                for chunk in self.data.chunks_exact_mut(4) {
248                    chunk.copy_from_slice(&v);
249                }
250            }
251            RasterDataType::Float32 => {
252                let v = (value as f32).to_ne_bytes();
253                for chunk in self.data.chunks_exact_mut(4) {
254                    chunk.copy_from_slice(&v);
255                }
256            }
257            RasterDataType::Float64 => {
258                let v = value.to_ne_bytes();
259                for chunk in self.data.chunks_exact_mut(8) {
260                    chunk.copy_from_slice(&v);
261                }
262            }
263            RasterDataType::UInt64 => {
264                let v = (value as u64).to_ne_bytes();
265                for chunk in self.data.chunks_exact_mut(8) {
266                    chunk.copy_from_slice(&v);
267                }
268            }
269            RasterDataType::Int64 => {
270                let v = (value as i64).to_ne_bytes();
271                for chunk in self.data.chunks_exact_mut(8) {
272                    chunk.copy_from_slice(&v);
273                }
274            }
275            RasterDataType::CFloat32 => {
276                // Complex pixel: 8 bytes = [real_f32 (4 bytes), imag_f32 (4 bytes)]
277                // Fill real = value, imag = 0.0
278                let real_bytes = (value as f32).to_ne_bytes();
279                let imag_bytes = 0f32.to_ne_bytes();
280                for chunk in self.data.chunks_exact_mut(8) {
281                    chunk[..4].copy_from_slice(&real_bytes);
282                    chunk[4..].copy_from_slice(&imag_bytes);
283                }
284            }
285            RasterDataType::CFloat64 => {
286                // Complex pixel: 16 bytes = [real_f64 (8 bytes), imag_f64 (8 bytes)]
287                // Fill real = value, imag = 0.0
288                let real_bytes = value.to_ne_bytes();
289                let imag_bytes = 0f64.to_ne_bytes();
290                for chunk in self.data.chunks_exact_mut(16) {
291                    chunk[..8].copy_from_slice(&real_bytes);
292                    chunk[8..].copy_from_slice(&imag_bytes);
293                }
294            }
295        }
296    }
297
298    /// Returns the width in pixels
299    #[must_use]
300    pub const fn width(&self) -> u64 {
301        self.width
302    }
303
304    /// Returns the height in pixels
305    #[must_use]
306    pub const fn height(&self) -> u64 {
307        self.height
308    }
309
310    /// Returns the data type
311    #[must_use]
312    pub const fn data_type(&self) -> RasterDataType {
313        self.data_type
314    }
315
316    /// Returns the nodata value
317    #[must_use]
318    pub const fn nodata(&self) -> NoDataValue {
319        self.nodata
320    }
321
322    /// Returns the total number of pixels
323    #[must_use]
324    pub const fn pixel_count(&self) -> u64 {
325        self.width * self.height
326    }
327
328    /// Returns the raw bytes
329    #[must_use]
330    pub fn as_bytes(&self) -> &[u8] {
331        &self.data
332    }
333
334    /// Returns mutable raw bytes
335    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
336        &mut self.data
337    }
338
339    /// Consumes the buffer and returns the raw bytes
340    #[must_use]
341    pub fn into_bytes(self) -> Vec<u8> {
342        self.data
343    }
344
345    /// Creates a buffer from typed vector data
346    ///
347    /// The samples are copied in one bulk `memcpy` and keep their native
348    /// endianness.
349    ///
350    /// # Arguments
351    /// * `width` - Width in pixels
352    /// * `height` - Height in pixels
353    /// * `data` - Typed data (e.g., `Vec<f32>`, `Vec<u8>`)
354    /// * `data_type` - The raster data type
355    ///
356    /// # Type Parameters
357    /// * `T` - A plain-old-data numeric type whose size matches `data_type`.
358    ///   Types with padding bytes (e.g. `#[repr(C)]` structs) must not be used;
359    ///   prefer [`RasterBuffer::from_element_slice`], which is restricted to the
360    ///   ten sealed [`RasterElement`] types and cannot be misused.
361    ///
362    /// # Errors
363    /// Returns an error if the data size doesn't match dimensions and type
364    pub fn from_typed_vec<T: Copy + 'static>(
365        width: usize,
366        height: usize,
367        data: Vec<T>,
368        data_type: RasterDataType,
369    ) -> Result<Self> {
370        let expected_pixels = width * height;
371        if data.len() != expected_pixels {
372            return Err(OxiGeoError::InvalidParameter {
373                parameter: "data",
374                message: format!(
375                    "Data length mismatch: expected {} pixels for {}x{}, got {}",
376                    expected_pixels,
377                    width,
378                    height,
379                    data.len()
380                ),
381            });
382        }
383
384        // Convert typed data to bytes
385        let type_size = core::mem::size_of::<T>();
386        let expected_type_size = data_type.size_bytes();
387        if type_size != expected_type_size {
388            return Err(OxiGeoError::InvalidParameter {
389                parameter: "data_type",
390                message: format!(
391                    "Type size mismatch: provided type has {} bytes, {:?} expects {} bytes",
392                    type_size, data_type, expected_type_size
393                ),
394            });
395        }
396
397        // Bulk copy: one `memcpy` for the whole vector instead of a per-element
398        // `Vec` allocation inside `flat_map`.
399        //
400        // SAFETY: `T: Copy` has no drop glue and the source slice is live and
401        // initialised for `data.len() * size_of::<T>()` bytes. `u8` has an
402        // alignment of 1, so the reinterpretation cannot violate alignment, and
403        // the borrow ends before `data` is dropped. (As documented above, `T`
404        // must be padding-free; that requirement predates this optimisation and
405        // is enforced by using `from_element_slice` instead.)
406        let byte_data: Vec<u8> = unsafe {
407            core::slice::from_raw_parts(data.as_ptr().cast::<u8>(), data.len() * type_size)
408        }
409        .to_vec();
410
411        Self::new(
412            byte_data,
413            width as u64,
414            height as u64,
415            data_type,
416            NoDataValue::None,
417        )
418    }
419
420    /// Creates a buffer from a slice of typed samples.
421    ///
422    /// The type-safe counterpart of [`RasterBuffer::from_typed_vec`]: the data
423    /// type is derived from `T`, the samples are copied in one bulk `memcpy`,
424    /// and only the ten [`RasterElement`] types are accepted.
425    ///
426    /// # Errors
427    /// Returns an error if `data.len()` differs from `width * height`.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// use oxigeo_core::buffer::RasterBuffer;
433    /// use oxigeo_core::types::RasterDataType;
434    ///
435    /// let buffer = RasterBuffer::from_element_slice(2, 2, &[1.0f32, 2.0, 3.0, 4.0])?;
436    /// assert_eq!(buffer.data_type(), RasterDataType::Float32);
437    /// assert_eq!(buffer.get_pixel(1, 1)?, 4.0);
438    /// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
439    /// ```
440    pub fn from_element_slice<T: RasterElement>(
441        width: u64,
442        height: u64,
443        data: &[T],
444    ) -> Result<Self> {
445        let expected_pixels = width * height;
446        if data.len() as u64 != expected_pixels {
447            return Err(OxiGeoError::InvalidParameter {
448                parameter: "data",
449                message: format!(
450                    "Data length mismatch: expected {} pixels for {}x{}, got {}",
451                    expected_pixels,
452                    width,
453                    height,
454                    data.len()
455                ),
456            });
457        }
458
459        Self::new(
460            elements_as_bytes(data).to_vec(),
461            width,
462            height,
463            T::DATA_TYPE,
464            NoDataValue::None,
465        )
466    }
467
468    /// Returns the buffer data as a typed slice (zero-copy)
469    ///
470    /// # Type Parameters
471    /// * `T` - The target type (must match the buffer's data type size)
472    ///
473    /// # Errors
474    /// Returns an error if the type size doesn't match the data type, or if the
475    /// buffer's storage is not aligned for `T`. Use
476    /// [`RasterBuffer::to_typed_vec`] or [`RasterBuffer::copy_to_slice`] when an
477    /// alignment-independent typed view is needed.
478    pub fn as_slice<T: Copy + 'static>(&self) -> Result<&[T]> {
479        Self::check_type_size::<T>(self.data_type)?;
480
481        let pixel_count = (self.width * self.height) as usize;
482        if pixel_count == 0 {
483            // An empty `Vec<u8>` has a dangling, 1-byte-aligned pointer, which
484            // `from_raw_parts` rejects even for a zero length.
485            return Ok(&[]);
486        }
487
488        let ptr = self.data.as_ptr().cast::<T>();
489        Self::check_alignment(ptr)?;
490        // SAFETY: The type size matches the pixel size (checked above), so
491        // `pixel_count * size_of::<T>()` equals the buffer length; the pointer is
492        // aligned for `T` (checked above) and non-null (`pixel_count > 0`); the
493        // returned slice borrows `self`, and `T: Copy` primitives have no
494        // invalid bit patterns.
495        let slice = unsafe { core::slice::from_raw_parts(ptr, pixel_count) };
496        Ok(slice)
497    }
498
499    /// Returns the buffer data as a mutable typed slice (zero-copy)
500    ///
501    /// # Type Parameters
502    /// * `T` - The target type (must match the buffer's data type size)
503    ///
504    /// # Errors
505    /// Returns an error if the type size doesn't match the data type, or if the
506    /// buffer's storage is not aligned for `T`.
507    pub fn as_slice_mut<T: Copy + 'static>(&mut self) -> Result<&mut [T]> {
508        Self::check_type_size::<T>(self.data_type)?;
509
510        let pixel_count = (self.width * self.height) as usize;
511        if pixel_count == 0 {
512            return Ok(&mut []);
513        }
514
515        let ptr = self.data.as_mut_ptr().cast::<T>();
516        Self::check_alignment(ptr.cast_const())?;
517        // SAFETY: Same reasoning as `as_slice`, plus the unique borrow of `self`
518        // guaranteeing exclusive access for the lifetime of the returned slice.
519        let slice = unsafe { core::slice::from_raw_parts_mut(ptr, pixel_count) };
520        Ok(slice)
521    }
522
523    // ─── Bulk Typed Access ───────────────────────────────────────────────
524
525    /// Converts every pixel into a caller-supplied slice, in one pass and
526    /// without allocating.
527    ///
528    /// This is the `OxiGeo` equivalent of GDAL's
529    /// `RasterBand::read_into_slice`: the destination belongs to the caller
530    /// (a `Vec<f64>`, an `ndarray::Array2<f64>`'s backing slice, a
531    /// memory-mapped region…), so no temporary buffer and no second pass are
532    /// needed. Unlike [`RasterBuffer::as_slice`] it works for *any* destination
533    /// type and needs no alignment or data-type match.
534    ///
535    /// When `T` already matches the buffer's data type the copy is a plain
536    /// `memcpy`. See [`element`] for the exact conversion semantics; floats are
537    /// rounded with [`FloatToIntRounding::Nearest`].
538    ///
539    /// # Errors
540    /// Returns an error if `dst.len()` differs from [`RasterBuffer::pixel_count`].
541    ///
542    /// # Examples
543    ///
544    /// ```
545    /// use oxigeo_core::buffer::RasterBuffer;
546    /// use oxigeo_core::types::RasterDataType;
547    ///
548    /// let mut buffer = RasterBuffer::zeros(3, 2, RasterDataType::UInt16);
549    /// buffer.set_pixel(2, 1, 40_000.0)?;
550    ///
551    /// // Allocate once, reuse across tiles/bands: nothing else is allocated.
552    /// let mut pixels = vec![0.0f64; buffer.pixel_count() as usize];
553    /// buffer.copy_to_slice(&mut pixels)?;
554    ///
555    /// assert_eq!(pixels.last().copied(), Some(40_000.0));
556    /// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
557    /// ```
558    pub fn copy_to_slice<T: RasterElement>(&self, dst: &mut [T]) -> Result<()> {
559        convert_raw_into(&self.data, self.data_type, dst)
560    }
561
562    /// [`RasterBuffer::copy_to_slice`] with an explicit float→integer rounding
563    /// mode.
564    ///
565    /// # Errors
566    /// Returns an error if `dst.len()` differs from [`RasterBuffer::pixel_count`].
567    pub fn copy_to_slice_with<T: RasterElement>(
568        &self,
569        dst: &mut [T],
570        rounding: FloatToIntRounding,
571    ) -> Result<()> {
572        convert_raw_into_with(&self.data, self.data_type, dst, rounding)
573    }
574
575    /// Converts every pixel into a freshly allocated `Vec<T>`.
576    ///
577    /// Exactly one allocation, then a single conversion pass. Prefer
578    /// [`RasterBuffer::copy_to_slice`] when a destination already exists.
579    ///
580    /// # Errors
581    /// Returns an error only if the buffer's declared size and its storage
582    /// disagree, which the constructors make impossible.
583    ///
584    /// # Examples
585    ///
586    /// ```
587    /// use oxigeo_core::buffer::RasterBuffer;
588    /// use oxigeo_core::types::RasterDataType;
589    ///
590    /// let buffer = RasterBuffer::from_element_slice(2, 1, &[-3i16, 300])?;
591    /// assert_eq!(buffer.to_typed_vec::<f64>()?, vec![-3.0, 300.0]);
592    /// // Integer destinations saturate instead of wrapping.
593    /// assert_eq!(buffer.to_typed_vec::<u8>()?, vec![0, 255]);
594    /// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
595    /// ```
596    pub fn to_typed_vec<T: RasterElement>(&self) -> Result<Vec<T>> {
597        self.to_typed_vec_with(FloatToIntRounding::Nearest)
598    }
599
600    /// [`RasterBuffer::to_typed_vec`] with an explicit float→integer rounding
601    /// mode.
602    ///
603    /// # Errors
604    /// Returns an error only if the buffer's declared size and its storage
605    /// disagree, which the constructors make impossible.
606    pub fn to_typed_vec_with<T: RasterElement>(
607        &self,
608        rounding: FloatToIntRounding,
609    ) -> Result<Vec<T>> {
610        let mut out = vec![T::default(); self.pixel_count() as usize];
611        self.copy_to_slice_with(&mut out, rounding)?;
612        Ok(out)
613    }
614
615    /// Gets a pixel value as f64
616    ///
617    /// # Errors
618    /// Returns an error if coordinates are out of bounds
619    pub fn get_pixel(&self, x: u64, y: u64) -> Result<f64> {
620        if x >= self.width || y >= self.height {
621            return Err(OxiGeoError::OutOfBounds {
622                message: format!(
623                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
624                    x, y, self.width, self.height
625                ),
626            });
627        }
628
629        let pixel_size = self.data_type.size_bytes();
630        let offset = (y * self.width + x) as usize * pixel_size;
631
632        let value = match self.data_type {
633            RasterDataType::UInt8 => f64::from(self.data[offset]),
634            RasterDataType::Int8 => f64::from(self.data[offset] as i8),
635            RasterDataType::UInt16 => {
636                let bytes: [u8; 2] = self.data[offset..offset + 2].try_into().map_err(|_| {
637                    OxiGeoError::Internal {
638                        message: "Invalid slice length".to_string(),
639                    }
640                })?;
641                f64::from(u16::from_ne_bytes(bytes))
642            }
643            RasterDataType::Int16 => {
644                let bytes: [u8; 2] = self.data[offset..offset + 2].try_into().map_err(|_| {
645                    OxiGeoError::Internal {
646                        message: "Invalid slice length".to_string(),
647                    }
648                })?;
649                f64::from(i16::from_ne_bytes(bytes))
650            }
651            RasterDataType::UInt32 => {
652                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
653                    OxiGeoError::Internal {
654                        message: "Invalid slice length".to_string(),
655                    }
656                })?;
657                f64::from(u32::from_ne_bytes(bytes))
658            }
659            RasterDataType::Int32 => {
660                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
661                    OxiGeoError::Internal {
662                        message: "Invalid slice length".to_string(),
663                    }
664                })?;
665                f64::from(i32::from_ne_bytes(bytes))
666            }
667            RasterDataType::Float32 => {
668                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
669                    OxiGeoError::Internal {
670                        message: "Invalid slice length".to_string(),
671                    }
672                })?;
673                f64::from(f32::from_ne_bytes(bytes))
674            }
675            RasterDataType::Float64 => {
676                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
677                    OxiGeoError::Internal {
678                        message: "Invalid slice length".to_string(),
679                    }
680                })?;
681                f64::from_ne_bytes(bytes)
682            }
683            RasterDataType::UInt64 => {
684                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
685                    OxiGeoError::Internal {
686                        message: "Invalid slice length".to_string(),
687                    }
688                })?;
689                u64::from_ne_bytes(bytes) as f64
690            }
691            RasterDataType::Int64 => {
692                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
693                    OxiGeoError::Internal {
694                        message: "Invalid slice length".to_string(),
695                    }
696                })?;
697                i64::from_ne_bytes(bytes) as f64
698            }
699            RasterDataType::CFloat32 => {
700                // Return only the real part; CFloat32 pixel = [real_f32 | imag_f32]
701                let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().map_err(|_| {
702                    OxiGeoError::Internal {
703                        message: "Invalid slice length".to_string(),
704                    }
705                })?;
706                f64::from(f32::from_ne_bytes(bytes))
707            }
708            RasterDataType::CFloat64 => {
709                // Return only the real part; CFloat64 pixel = [real_f64 | imag_f64]
710                let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().map_err(|_| {
711                    OxiGeoError::Internal {
712                        message: "Invalid slice length".to_string(),
713                    }
714                })?;
715                f64::from_ne_bytes(bytes)
716            }
717        };
718
719        Ok(value)
720    }
721
722    /// Sets a pixel value
723    ///
724    /// # Errors
725    /// Returns an error if coordinates are out of bounds
726    pub fn set_pixel(&mut self, x: u64, y: u64, value: f64) -> Result<()> {
727        if x >= self.width || y >= self.height {
728            return Err(OxiGeoError::OutOfBounds {
729                message: format!(
730                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
731                    x, y, self.width, self.height
732                ),
733            });
734        }
735
736        let pixel_size = self.data_type.size_bytes();
737        let offset = (y * self.width + x) as usize * pixel_size;
738
739        match self.data_type {
740            RasterDataType::UInt8 => {
741                self.data[offset] = value as u8;
742            }
743            RasterDataType::Int8 => {
744                self.data[offset] = (value as i8) as u8;
745            }
746            RasterDataType::UInt16 => {
747                let bytes = (value as u16).to_ne_bytes();
748                self.data[offset..offset + 2].copy_from_slice(&bytes);
749            }
750            RasterDataType::Int16 => {
751                let bytes = (value as i16).to_ne_bytes();
752                self.data[offset..offset + 2].copy_from_slice(&bytes);
753            }
754            RasterDataType::UInt32 => {
755                let bytes = (value as u32).to_ne_bytes();
756                self.data[offset..offset + 4].copy_from_slice(&bytes);
757            }
758            RasterDataType::Int32 => {
759                let bytes = (value as i32).to_ne_bytes();
760                self.data[offset..offset + 4].copy_from_slice(&bytes);
761            }
762            RasterDataType::Float32 => {
763                let bytes = (value as f32).to_ne_bytes();
764                self.data[offset..offset + 4].copy_from_slice(&bytes);
765            }
766            RasterDataType::Float64 => {
767                let bytes = value.to_ne_bytes();
768                self.data[offset..offset + 8].copy_from_slice(&bytes);
769            }
770            RasterDataType::UInt64 => {
771                let bytes = (value as u64).to_ne_bytes();
772                self.data[offset..offset + 8].copy_from_slice(&bytes);
773            }
774            RasterDataType::Int64 => {
775                let bytes = (value as i64).to_ne_bytes();
776                self.data[offset..offset + 8].copy_from_slice(&bytes);
777            }
778            RasterDataType::CFloat32 => {
779                // Set only the real part
780                let bytes = (value as f32).to_ne_bytes();
781                self.data[offset..offset + 4].copy_from_slice(&bytes);
782            }
783            RasterDataType::CFloat64 => {
784                // Set only the real part
785                let bytes = value.to_ne_bytes();
786                self.data[offset..offset + 8].copy_from_slice(&bytes);
787            }
788        }
789
790        Ok(())
791    }
792
793    // ─── Typed Pixel Accessors ────────────────────────────────────────────
794
795    /// Gets a pixel value as `u8`.
796    ///
797    /// # Errors
798    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt8`.
799    pub fn get_u8(&self, x: u64, y: u64) -> Result<u8> {
800        self.check_bounds(x, y)?;
801        self.check_type(RasterDataType::UInt8)?;
802        let offset = (y * self.width + x) as usize;
803        Ok(self.data[offset])
804    }
805
806    /// Gets a pixel value as `i8`.
807    ///
808    /// # Errors
809    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int8`.
810    pub fn get_i8(&self, x: u64, y: u64) -> Result<i8> {
811        self.check_bounds(x, y)?;
812        self.check_type(RasterDataType::Int8)?;
813        let offset = (y * self.width + x) as usize;
814        Ok(self.data[offset] as i8)
815    }
816
817    /// Gets a pixel value as `u16`.
818    ///
819    /// # Errors
820    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt16`.
821    pub fn get_u16(&self, x: u64, y: u64) -> Result<u16> {
822        self.check_bounds(x, y)?;
823        self.check_type(RasterDataType::UInt16)?;
824        let offset = (y * self.width + x) as usize * 2;
825        let bytes: [u8; 2] =
826            self.data[offset..offset + 2]
827                .try_into()
828                .map_err(|_| OxiGeoError::Internal {
829                    message: "Invalid slice length".to_string(),
830                })?;
831        Ok(u16::from_ne_bytes(bytes))
832    }
833
834    /// Gets a pixel value as `i16`.
835    ///
836    /// # Errors
837    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int16`.
838    pub fn get_i16(&self, x: u64, y: u64) -> Result<i16> {
839        self.check_bounds(x, y)?;
840        self.check_type(RasterDataType::Int16)?;
841        let offset = (y * self.width + x) as usize * 2;
842        let bytes: [u8; 2] =
843            self.data[offset..offset + 2]
844                .try_into()
845                .map_err(|_| OxiGeoError::Internal {
846                    message: "Invalid slice length".to_string(),
847                })?;
848        Ok(i16::from_ne_bytes(bytes))
849    }
850
851    /// Gets a pixel value as `u32`.
852    ///
853    /// # Errors
854    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt32`.
855    pub fn get_u32(&self, x: u64, y: u64) -> Result<u32> {
856        self.check_bounds(x, y)?;
857        self.check_type(RasterDataType::UInt32)?;
858        let offset = (y * self.width + x) as usize * 4;
859        let bytes: [u8; 4] =
860            self.data[offset..offset + 4]
861                .try_into()
862                .map_err(|_| OxiGeoError::Internal {
863                    message: "Invalid slice length".to_string(),
864                })?;
865        Ok(u32::from_ne_bytes(bytes))
866    }
867
868    /// Gets a pixel value as `i32`.
869    ///
870    /// # Errors
871    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int32`.
872    pub fn get_i32(&self, x: u64, y: u64) -> Result<i32> {
873        self.check_bounds(x, y)?;
874        self.check_type(RasterDataType::Int32)?;
875        let offset = (y * self.width + x) as usize * 4;
876        let bytes: [u8; 4] =
877            self.data[offset..offset + 4]
878                .try_into()
879                .map_err(|_| OxiGeoError::Internal {
880                    message: "Invalid slice length".to_string(),
881                })?;
882        Ok(i32::from_ne_bytes(bytes))
883    }
884
885    /// Gets a pixel value as `u64`.
886    ///
887    /// # Errors
888    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt64`.
889    pub fn get_u64(&self, x: u64, y: u64) -> Result<u64> {
890        self.check_bounds(x, y)?;
891        self.check_type(RasterDataType::UInt64)?;
892        let offset = (y * self.width + x) as usize * 8;
893        let bytes: [u8; 8] =
894            self.data[offset..offset + 8]
895                .try_into()
896                .map_err(|_| OxiGeoError::Internal {
897                    message: "Invalid slice length".to_string(),
898                })?;
899        Ok(u64::from_ne_bytes(bytes))
900    }
901
902    /// Gets a pixel value as `i64`.
903    ///
904    /// # Errors
905    /// Returns an error if coordinates are out of bounds or the buffer type is not `Int64`.
906    pub fn get_i64(&self, x: u64, y: u64) -> Result<i64> {
907        self.check_bounds(x, y)?;
908        self.check_type(RasterDataType::Int64)?;
909        let offset = (y * self.width + x) as usize * 8;
910        let bytes: [u8; 8] =
911            self.data[offset..offset + 8]
912                .try_into()
913                .map_err(|_| OxiGeoError::Internal {
914                    message: "Invalid slice length".to_string(),
915                })?;
916        Ok(i64::from_ne_bytes(bytes))
917    }
918
919    /// Gets a pixel value as `f32`.
920    ///
921    /// # Errors
922    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float32`.
923    pub fn get_f32(&self, x: u64, y: u64) -> Result<f32> {
924        self.check_bounds(x, y)?;
925        self.check_type(RasterDataType::Float32)?;
926        let offset = (y * self.width + x) as usize * 4;
927        let bytes: [u8; 4] =
928            self.data[offset..offset + 4]
929                .try_into()
930                .map_err(|_| OxiGeoError::Internal {
931                    message: "Invalid slice length".to_string(),
932                })?;
933        Ok(f32::from_ne_bytes(bytes))
934    }
935
936    /// Gets a pixel value as `f64`.
937    ///
938    /// # Errors
939    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float64`.
940    pub fn get_f64(&self, x: u64, y: u64) -> Result<f64> {
941        self.check_bounds(x, y)?;
942        self.check_type(RasterDataType::Float64)?;
943        let offset = (y * self.width + x) as usize * 8;
944        let bytes: [u8; 8] =
945            self.data[offset..offset + 8]
946                .try_into()
947                .map_err(|_| OxiGeoError::Internal {
948                    message: "Invalid slice length".to_string(),
949                })?;
950        Ok(f64::from_ne_bytes(bytes))
951    }
952
953    // ─── Typed Pixel Setters ─────────────────────────────────────────────
954
955    /// Sets a pixel value from `u8`.
956    ///
957    /// # Errors
958    /// Returns an error if coordinates are out of bounds or the buffer type is not `UInt8`.
959    pub fn set_u8(&mut self, x: u64, y: u64, value: u8) -> Result<()> {
960        self.check_bounds(x, y)?;
961        self.check_type(RasterDataType::UInt8)?;
962        let offset = (y * self.width + x) as usize;
963        self.data[offset] = value;
964        Ok(())
965    }
966
967    /// Sets a pixel value from `f32`.
968    ///
969    /// # Errors
970    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float32`.
971    pub fn set_f32(&mut self, x: u64, y: u64, value: f32) -> Result<()> {
972        self.check_bounds(x, y)?;
973        self.check_type(RasterDataType::Float32)?;
974        let offset = (y * self.width + x) as usize * 4;
975        self.data[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
976        Ok(())
977    }
978
979    /// Sets a pixel value from `f64`.
980    ///
981    /// # Errors
982    /// Returns an error if coordinates are out of bounds or the buffer type is not `Float64`.
983    pub fn set_f64(&mut self, x: u64, y: u64, value: f64) -> Result<()> {
984        self.check_bounds(x, y)?;
985        self.check_type(RasterDataType::Float64)?;
986        let offset = (y * self.width + x) as usize * 8;
987        self.data[offset..offset + 8].copy_from_slice(&value.to_ne_bytes());
988        Ok(())
989    }
990
991    // ─── Row & Window Access ─────────────────────────────────────────────
992
993    /// Returns a row of pixel data as a typed slice (zero-copy).
994    ///
995    /// # Errors
996    /// Returns an error if `y` is out of bounds, the type size mismatches, or
997    /// the buffer's storage is not aligned for `T`.
998    pub fn row_slice<T: Copy + 'static>(&self, y: u64) -> Result<&[T]> {
999        if y >= self.height {
1000            return Err(OxiGeoError::OutOfBounds {
1001                message: format!("Row {} out of bounds for height {}", y, self.height),
1002            });
1003        }
1004        let expected_size = self.data_type.size_bytes();
1005        Self::check_type_size::<T>(self.data_type)?;
1006
1007        if self.width == 0 {
1008            return Ok(&[]);
1009        }
1010
1011        let row_start = (y * self.width) as usize * expected_size;
1012        let row_end = row_start + self.width as usize * expected_size;
1013        let ptr = self.data[row_start..row_end].as_ptr().cast::<T>();
1014        Self::check_alignment(ptr)?;
1015        // SAFETY: The row bounds are inside the buffer (checked above) and cover
1016        // exactly `width * size_of::<T>()` bytes; `row_start` is a multiple of
1017        // the sample size, and the resulting pointer is aligned for `T`
1018        // (checked above) and non-null (`width > 0`).
1019        let slice = unsafe { core::slice::from_raw_parts(ptr, self.width as usize) };
1020        Ok(slice)
1021    }
1022
1023    /// Returns a rectangular window of pixel data as a new `RasterBuffer`.
1024    ///
1025    /// # Errors
1026    /// Returns an error if the window extends outside buffer bounds.
1027    pub fn window(&self, x: u64, y: u64, width: u64, height: u64) -> Result<Self> {
1028        if x + width > self.width || y + height > self.height {
1029            return Err(OxiGeoError::OutOfBounds {
1030                message: format!(
1031                    "Window ({},{}) {}x{} exceeds buffer {}x{}",
1032                    x, y, width, height, self.width, self.height
1033                ),
1034            });
1035        }
1036        let pixel_size = self.data_type.size_bytes();
1037        let row_bytes = width as usize * pixel_size;
1038        let mut data = Vec::with_capacity(height as usize * row_bytes);
1039        for row in y..y + height {
1040            let src_start = (row * self.width + x) as usize * pixel_size;
1041            data.extend_from_slice(&self.data[src_start..src_start + row_bytes]);
1042        }
1043        Self::new(data, width, height, self.data_type, self.nodata)
1044    }
1045
1046    // ─── Private Helpers ─────────────────────────────────────────────────
1047
1048    fn check_bounds(&self, x: u64, y: u64) -> Result<()> {
1049        if x >= self.width || y >= self.height {
1050            return Err(OxiGeoError::OutOfBounds {
1051                message: format!(
1052                    "Pixel ({}, {}) out of bounds for {}x{} buffer",
1053                    x, y, self.width, self.height
1054                ),
1055            });
1056        }
1057        Ok(())
1058    }
1059
1060    /// Verifies that `T` has the same size as one sample of `data_type`.
1061    fn check_type_size<T>(data_type: RasterDataType) -> Result<()> {
1062        let type_size = core::mem::size_of::<T>();
1063        let expected_size = data_type.size_bytes();
1064        if type_size != expected_size {
1065            return Err(OxiGeoError::InvalidParameter {
1066                parameter: "T",
1067                message: format!(
1068                    "Type size mismatch: requested type has {} bytes, buffer contains {:?} ({} bytes)",
1069                    type_size, data_type, expected_size
1070                ),
1071            });
1072        }
1073        Ok(())
1074    }
1075
1076    /// Verifies that `ptr` satisfies `T`'s alignment.
1077    ///
1078    /// The buffer's storage is a `Vec<u8>`, which is only guaranteed to be
1079    /// 1-byte aligned; reinterpreting it as `&[T]` without this check would be
1080    /// undefined behaviour whenever the allocator (or a custom global allocator)
1081    /// hands back an under-aligned block.
1082    fn check_alignment<T>(ptr: *const T) -> Result<()> {
1083        if ptr.is_aligned() {
1084            return Ok(());
1085        }
1086        Err(OxiGeoError::InvalidParameter {
1087            parameter: "T",
1088            message: format!(
1089                "Buffer storage is misaligned for the requested type: it needs {}-byte alignment but starts {} byte(s) past one; use `to_typed_vec`/`copy_to_slice` for an alignment-independent typed copy",
1090                core::mem::align_of::<T>(),
1091                ptr.addr() % core::mem::align_of::<T>()
1092            ),
1093        })
1094    }
1095
1096    fn check_type(&self, expected: RasterDataType) -> Result<()> {
1097        if self.data_type != expected {
1098            return Err(OxiGeoError::InvalidParameter {
1099                parameter: "data_type",
1100                message: format!(
1101                    "Buffer contains {:?} data, requested {:?}",
1102                    self.data_type, expected
1103                ),
1104            });
1105        }
1106        Ok(())
1107    }
1108
1109    /// Returns true if the given value equals the nodata value
1110    #[must_use]
1111    pub fn is_nodata(&self, value: f64) -> bool {
1112        match self.nodata.as_f64() {
1113            Some(nd) => {
1114                if nd.is_nan() && value.is_nan() {
1115                    true
1116                } else {
1117                    (nd - value).abs() < f64::EPSILON
1118                }
1119            }
1120            None => false,
1121        }
1122    }
1123
1124    /// Converts the buffer to a different data type
1125    ///
1126    /// The `nodata` value is carried over unchanged (it is *not* re-encoded into
1127    /// the target type), exactly as before.
1128    ///
1129    /// Values that do not fit the target type saturate at its bounds and
1130    /// floating-point samples are truncated towards zero
1131    /// ([`FloatToIntRounding::Truncate`]), matching the historical per-pixel
1132    /// implementation bit-for-bit. Use [`RasterBuffer::copy_to_slice_with`] or
1133    /// [`convert_raw_bytes`] to pick [`FloatToIntRounding::Nearest`] instead.
1134    ///
1135    /// Integer→integer conversions are exact: they no longer bridge through
1136    /// `f64`, so `u64`/`i64` values beyond 2<sup>53</sup> keep every bit.
1137    ///
1138    /// # Errors
1139    /// Returns an error if conversion fails
1140    pub fn convert_to(&self, target_type: RasterDataType) -> Result<Self> {
1141        if target_type == self.data_type {
1142            return Ok(self.clone());
1143        }
1144
1145        let mut result = Self::zeros(self.width, self.height, target_type);
1146        result.nodata = self.nodata;
1147
1148        convert_raw_bytes(
1149            &self.data,
1150            self.data_type,
1151            &mut result.data,
1152            target_type,
1153            FloatToIntRounding::Truncate,
1154        )?;
1155
1156        Ok(result)
1157    }
1158
1159    /// Computes basic statistics
1160    pub fn compute_statistics(&self) -> Result<BufferStatistics> {
1161        let mut min = f64::MAX;
1162        let mut max = f64::MIN;
1163        let mut sum = 0.0;
1164        let mut sum_sq = 0.0;
1165        let mut valid_count = 0u64;
1166
1167        for y in 0..self.height {
1168            for x in 0..self.width {
1169                let value = self.get_pixel(x, y)?;
1170                if !self.is_nodata(value) && value.is_finite() {
1171                    min = min.min(value);
1172                    max = max.max(value);
1173                    sum += value;
1174                    sum_sq += value * value;
1175                    valid_count += 1;
1176                }
1177            }
1178        }
1179
1180        if valid_count == 0 {
1181            return Ok(BufferStatistics {
1182                min: f64::NAN,
1183                max: f64::NAN,
1184                mean: f64::NAN,
1185                std_dev: f64::NAN,
1186                valid_count: 0,
1187                histogram: None,
1188            });
1189        }
1190
1191        let mean = sum / valid_count as f64;
1192        let variance = (sum_sq / valid_count as f64) - (mean * mean);
1193        let std_dev = variance.sqrt();
1194
1195        Ok(BufferStatistics {
1196            min,
1197            max,
1198            mean,
1199            std_dev,
1200            valid_count,
1201            histogram: None,
1202        })
1203    }
1204
1205    /// Computes statistics and an optional histogram in one pass.
1206    ///
1207    /// The returned [`BufferStatistics`] contains a `histogram` of `bin_count` bins
1208    /// with uniform spacing covering the range `[min, max]`. Each bin holds the count
1209    /// of valid pixels whose value falls within that bin's interval.
1210    ///
1211    /// # Arguments
1212    ///
1213    /// * `bin_count` — Number of histogram bins. Must be ≥ 1.
1214    ///
1215    /// # Errors
1216    ///
1217    /// Returns [`crate::error::OxiGeoError::InvalidParameter`] if `bin_count` is 0.
1218    /// Returns errors from pixel access on corrupt buffers.
1219    ///
1220    /// # Notes
1221    ///
1222    /// - NaN and infinite values are excluded (same as [`RasterBuffer::compute_statistics`]).
1223    /// - When all valid values are identical (`min == max`), all counts go into bin 0.
1224    /// - When no valid pixels exist, `histogram` is `Some(vec![0; bin_count])`.
1225    pub fn compute_statistics_with_histogram(&self, bin_count: usize) -> Result<BufferStatistics> {
1226        if bin_count == 0 {
1227            return Err(OxiGeoError::InvalidParameter {
1228                parameter: "bin_count",
1229                message: "bin_count must be at least 1".to_string(),
1230            });
1231        }
1232
1233        // First pass: collect min/max/sum/sum_sq and all valid values for histogram.
1234        let mut min = f64::MAX;
1235        let mut max = f64::MIN;
1236        let mut sum = 0.0f64;
1237        let mut sum_sq = 0.0f64;
1238        let mut valid_count = 0u64;
1239        // Collect valid values so histogram binning can reuse them without a re-read.
1240        let total_pixels = (self.width * self.height) as usize;
1241        let mut valid_values: Vec<f64> = Vec::with_capacity(total_pixels);
1242
1243        for y in 0..self.height {
1244            for x in 0..self.width {
1245                let value = self.get_pixel(x, y)?;
1246                if !self.is_nodata(value) && value.is_finite() {
1247                    min = min.min(value);
1248                    max = max.max(value);
1249                    sum += value;
1250                    sum_sq += value * value;
1251                    valid_count += 1;
1252                    valid_values.push(value);
1253                }
1254            }
1255        }
1256
1257        // Build histogram bins (all-zero when there are no valid pixels).
1258        let mut bins = vec![0u64; bin_count];
1259
1260        if valid_count > 0 {
1261            let range = max - min;
1262            if range == 0.0 {
1263                // All valid values are identical: everything goes into bin 0.
1264                bins[0] = valid_count;
1265            } else {
1266                for v in &valid_values {
1267                    let bin_idx = (((v - min) / range) * bin_count as f64).floor() as usize;
1268                    // Clamp to [0, bin_count-1] (handles the max edge case exactly).
1269                    let bin_idx = bin_idx.min(bin_count - 1);
1270                    bins[bin_idx] += 1;
1271                }
1272            }
1273        }
1274
1275        if valid_count == 0 {
1276            return Ok(BufferStatistics {
1277                min: f64::NAN,
1278                max: f64::NAN,
1279                mean: f64::NAN,
1280                std_dev: f64::NAN,
1281                valid_count: 0,
1282                histogram: Some(bins),
1283            });
1284        }
1285
1286        let mean = sum / valid_count as f64;
1287        let variance = (sum_sq / valid_count as f64) - (mean * mean);
1288        let std_dev = variance.max(0.0).sqrt();
1289
1290        Ok(BufferStatistics {
1291            min,
1292            max,
1293            mean,
1294            std_dev,
1295            valid_count,
1296            histogram: Some(bins),
1297        })
1298    }
1299}
1300
1301impl fmt::Debug for RasterBuffer {
1302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1303        f.debug_struct("RasterBuffer")
1304            .field("width", &self.width)
1305            .field("height", &self.height)
1306            .field("data_type", &self.data_type)
1307            .field("nodata", &self.nodata)
1308            .field("bytes", &self.data.len())
1309            .finish()
1310    }
1311}
1312
1313/// Statistics computed from a buffer
1314///
1315/// Note: `Copy` is intentionally not derived because the optional histogram
1316/// contains a `Vec`, making bitwise copy semantics inappropriate.
1317#[derive(Debug, Clone, PartialEq)]
1318pub struct BufferStatistics {
1319    /// Minimum value
1320    pub min: f64,
1321    /// Maximum value
1322    pub max: f64,
1323    /// Mean value
1324    pub mean: f64,
1325    /// Standard deviation
1326    pub std_dev: f64,
1327    /// Number of valid (non-nodata) pixels
1328    pub valid_count: u64,
1329    /// Optional histogram bin counts (uniform spacing from `min` to `max`)
1330    ///
1331    /// `None` when computed via [`RasterBuffer::compute_statistics`].
1332    /// `Some(bins)` when computed via [`RasterBuffer::compute_statistics_with_histogram`].
1333    pub histogram: Option<Vec<u64>>,
1334}
1335
1336#[cfg(feature = "arrow")]
1337mod arrow_support {
1338    //! Arrow integration for zero-copy interoperability
1339
1340    use arrow_array::{Array, Float64Array};
1341
1342    use super::{OxiGeoError, RasterBuffer, Result};
1343
1344    impl RasterBuffer {
1345        /// Creates a `RasterBuffer` from an Arrow array
1346        ///
1347        /// # Errors
1348        /// Returns an error if the array type doesn't match
1349        pub fn from_arrow_array<A: Array>(_array: &A, _width: u64, _height: u64) -> Result<Self> {
1350            // This is a simplified implementation
1351            // A full implementation would handle all Arrow types
1352            Err(OxiGeoError::NotSupported {
1353                operation: "Arrow array conversion".to_string(),
1354            })
1355        }
1356
1357        /// Converts to an Arrow `Float64Array`
1358        pub fn to_float64_array(&self) -> Result<Float64Array> {
1359            let mut values = Vec::with_capacity(self.pixel_count() as usize);
1360            for y in 0..self.height {
1361                for x in 0..self.width {
1362                    values.push(self.get_pixel(x, y)?);
1363                }
1364            }
1365            Ok(Float64Array::from(values))
1366        }
1367    }
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372    #![allow(clippy::expect_used)]
1373
1374    use super::*;
1375
1376    #[test]
1377    fn test_buffer_creation() {
1378        let buffer = RasterBuffer::zeros(100, 100, RasterDataType::UInt8);
1379        assert_eq!(buffer.width(), 100);
1380        assert_eq!(buffer.height(), 100);
1381        assert_eq!(buffer.pixel_count(), 10_000);
1382        assert_eq!(buffer.as_bytes().len(), 10_000);
1383    }
1384
1385    #[test]
1386    fn test_pixel_access() {
1387        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1388
1389        buffer.set_pixel(5, 5, 42.0).expect("set should work");
1390        let value = buffer.get_pixel(5, 5).expect("get should work");
1391        assert!((value - 42.0).abs() < f64::EPSILON);
1392
1393        // Out of bounds
1394        assert!(buffer.get_pixel(100, 0).is_err());
1395        assert!(buffer.set_pixel(0, 100, 0.0).is_err());
1396    }
1397
1398    #[test]
1399    fn test_nodata() {
1400        let buffer = RasterBuffer::nodata_filled(
1401            10,
1402            10,
1403            RasterDataType::Float32,
1404            NoDataValue::Float(-9999.0),
1405        );
1406
1407        assert!(buffer.is_nodata(-9999.0));
1408        assert!(!buffer.is_nodata(0.0));
1409    }
1410
1411    #[test]
1412    fn test_statistics() {
1413        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1414
1415        // Fill with values 0-99
1416        for y in 0..10 {
1417            for x in 0..10 {
1418                let value = (y * 10 + x) as f64;
1419                buffer.set_pixel(x, y, value).expect("set should work");
1420            }
1421        }
1422
1423        let stats = buffer.compute_statistics().expect("stats should work");
1424        assert!((stats.min - 0.0).abs() < f64::EPSILON);
1425        assert!((stats.max - 99.0).abs() < f64::EPSILON);
1426        assert!((stats.mean - 49.5).abs() < 0.01);
1427        assert_eq!(stats.valid_count, 100);
1428    }
1429
1430    #[test]
1431    fn test_data_validation() {
1432        // Wrong size should fail
1433        let result = RasterBuffer::new(
1434            vec![0u8; 100],
1435            10,
1436            10,
1437            RasterDataType::UInt16, // Needs 200 bytes
1438            NoDataValue::None,
1439        );
1440        assert!(result.is_err());
1441
1442        // Correct size should succeed
1443        let result = RasterBuffer::new(
1444            vec![0u8; 200],
1445            10,
1446            10,
1447            RasterDataType::UInt16,
1448            NoDataValue::None,
1449        );
1450        assert!(result.is_ok());
1451    }
1452
1453    #[test]
1454    fn test_typed_get_u8() {
1455        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1456        buffer.set_pixel(3, 4, 200.0).expect("set should work");
1457        assert_eq!(buffer.get_u8(3, 4).expect("get_u8"), 200);
1458        // Wrong type should error
1459        assert!(buffer.get_f32(3, 4).is_err());
1460    }
1461
1462    #[test]
1463    fn test_typed_get_i8() {
1464        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int8);
1465        buffer.set_pixel(0, 0, -42.0).expect("set should work");
1466        assert_eq!(buffer.get_i8(0, 0).expect("get_i8"), -42);
1467    }
1468
1469    #[test]
1470    fn test_typed_get_u16() {
1471        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt16);
1472        buffer.set_pixel(5, 5, 60000.0).expect("set should work");
1473        assert_eq!(buffer.get_u16(5, 5).expect("get_u16"), 60000);
1474    }
1475
1476    #[test]
1477    fn test_typed_get_i16() {
1478        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int16);
1479        buffer.set_pixel(2, 3, -1234.0).expect("set should work");
1480        assert_eq!(buffer.get_i16(2, 3).expect("get_i16"), -1234);
1481    }
1482
1483    #[test]
1484    fn test_typed_get_u32() {
1485        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt32);
1486        buffer.set_pixel(0, 0, 100_000.0).expect("set should work");
1487        assert_eq!(buffer.get_u32(0, 0).expect("get_u32"), 100_000);
1488    }
1489
1490    #[test]
1491    fn test_typed_get_i32() {
1492        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int32);
1493        buffer.set_pixel(1, 1, -50_000.0).expect("set should work");
1494        assert_eq!(buffer.get_i32(1, 1).expect("get_i32"), -50_000);
1495    }
1496
1497    #[test]
1498    fn test_typed_get_u64() {
1499        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt64);
1500        buffer
1501            .set_pixel(0, 0, 1_000_000.0)
1502            .expect("set should work");
1503        assert_eq!(buffer.get_u64(0, 0).expect("get_u64"), 1_000_000);
1504    }
1505
1506    #[test]
1507    fn test_typed_get_i64() {
1508        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Int64);
1509        buffer.set_pixel(0, 0, -999_999.0).expect("set should work");
1510        assert_eq!(buffer.get_i64(0, 0).expect("get_i64"), -999_999);
1511    }
1512
1513    #[test]
1514    fn test_typed_get_f32() {
1515        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1516        buffer
1517            .set_f32(7, 8, core::f32::consts::PI)
1518            .expect("set_f32 should work");
1519        let val = buffer.get_f32(7, 8).expect("get_f32");
1520        assert!((val - core::f32::consts::PI).abs() < 1e-5);
1521    }
1522
1523    #[test]
1524    fn test_typed_get_f64() {
1525        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float64);
1526        buffer
1527            .set_f64(9, 9, core::f64::consts::E)
1528            .expect("set_f64 should work");
1529        let val = buffer.get_f64(9, 9).expect("get_f64");
1530        assert!((val - core::f64::consts::E).abs() < 1e-9);
1531    }
1532
1533    #[test]
1534    fn test_typed_set_u8() {
1535        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1536        buffer.set_u8(0, 0, 255).expect("set_u8 should work");
1537        assert_eq!(buffer.get_u8(0, 0).expect("get_u8"), 255);
1538    }
1539
1540    #[test]
1541    fn test_typed_out_of_bounds() {
1542        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1543        assert!(buffer.get_f32(10, 0).is_err());
1544        assert!(buffer.get_f32(0, 10).is_err());
1545    }
1546
1547    #[test]
1548    fn test_typed_wrong_type() {
1549        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1550        assert!(buffer.get_f32(0, 0).is_err());
1551        assert!(buffer.get_u16(0, 0).is_err());
1552        assert!(buffer.get_i32(0, 0).is_err());
1553        assert!(buffer.get_f64(0, 0).is_err());
1554    }
1555
1556    #[test]
1557    fn test_row_slice() {
1558        let mut buffer = RasterBuffer::zeros(5, 3, RasterDataType::Float32);
1559        for x in 0..5 {
1560            buffer
1561                .set_pixel(x, 1, (x + 10) as f64)
1562                .expect("set should work");
1563        }
1564
1565        // The zero-copy view requires the storage to be aligned for `f32`.
1566        // Production allocators over-align `Vec<u8>`, but an allocator that
1567        // honours the requested 1-byte alignment exactly (Miri's, or a custom
1568        // global allocator) makes `row_slice` report the documented alignment
1569        // error instead of producing undefined behaviour.
1570        match buffer.row_slice::<f32>(1) {
1571            Ok(row) => {
1572                assert_eq!(row.len(), 5);
1573                assert!((row[0] - 10.0).abs() < 1e-5);
1574                assert!((row[4] - 14.0).abs() < 1e-5);
1575            }
1576            Err(err) => {
1577                let text = format!("{err:?}");
1578                assert!(text.contains("misaligned"), "unexpected error: {text}");
1579            }
1580        }
1581
1582        // The alignment-free path always works.
1583        let all: Vec<f32> = buffer.to_typed_vec().expect("to_typed_vec should work");
1584        assert!((all[5] - 10.0).abs() < 1e-5);
1585        assert!((all[9] - 14.0).abs() < 1e-5);
1586    }
1587
1588    #[test]
1589    fn test_row_slice_out_of_bounds() {
1590        let buffer = RasterBuffer::zeros(5, 3, RasterDataType::Float32);
1591        assert!(buffer.row_slice::<f32>(3).is_err());
1592    }
1593
1594    #[test]
1595    fn test_window() {
1596        let mut buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1597        buffer.set_pixel(5, 5, 42.0).expect("set should work");
1598        buffer.set_pixel(6, 6, 99.0).expect("set should work");
1599
1600        let win = buffer.window(4, 4, 4, 4).expect("window should work");
1601        assert_eq!(win.width(), 4);
1602        assert_eq!(win.height(), 4);
1603        // (5,5) in original -> (1,1) in window
1604        let val = win.get_pixel(1, 1).expect("get should work");
1605        assert!((val - 42.0).abs() < f64::EPSILON);
1606        // (6,6) in original -> (2,2) in window
1607        let val = win.get_pixel(2, 2).expect("get should work");
1608        assert!((val - 99.0).abs() < f64::EPSILON);
1609    }
1610
1611    #[test]
1612    fn test_window_out_of_bounds() {
1613        let buffer = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
1614        assert!(buffer.window(8, 8, 4, 4).is_err());
1615    }
1616
1617    #[test]
1618    fn test_fill_value_cfloat32_writes_real_zero_imag() {
1619        // Use a value that is not a well-known constant to avoid clippy::approx_constant
1620        let fill: f64 = 1.23456789;
1621        let mut buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat32);
1622        buf.fill_value(fill);
1623        // get_pixel returns only the real part for complex types
1624        let v = buf.get_pixel(0, 0).expect("pixel access");
1625        assert!(
1626            (v - fill).abs() < 1e-5,
1627            "real part should be {fill}, got {v}"
1628        );
1629        // Confirm imag bytes are zero (bytes 4-8 of pixel 0)
1630        let raw = buf.as_bytes();
1631        let imag_bytes: [u8; 4] = raw[4..8].try_into().expect("slice");
1632        let imag = f32::from_ne_bytes(imag_bytes);
1633        assert_eq!(imag, 0.0f32, "imaginary part should be 0.0");
1634    }
1635
1636    #[test]
1637    fn test_fill_value_cfloat64_writes_real_zero_imag() {
1638        // Use an arbitrary value that is not a well-known constant (avoids clippy::approx_constant)
1639        let fill: f64 = 9.876_543_21_f64;
1640        let mut buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat64);
1641        buf.fill_value(fill);
1642        let v = buf.get_pixel(0, 0).expect("pixel access");
1643        assert!((v - fill).abs() < 1e-9, "real part mismatch, got {v}");
1644        // Confirm imag bytes are zero (bytes 8-16 of pixel 0)
1645        let raw = buf.as_bytes();
1646        let imag_bytes: [u8; 8] = raw[8..16].try_into().expect("slice");
1647        let imag = f64::from_ne_bytes(imag_bytes);
1648        assert_eq!(imag, 0.0f64, "imaginary part should be 0.0");
1649    }
1650}