Skip to main content

zenpixels/
buffer.rs

1//! Opaque pixel buffer abstraction.
2//!
3//! Provides format-aware pixel storage that carries its own metadata:
4//! [`PixelBuffer`] (owned), [`PixelSlice`] (borrowed, immutable), and
5//! [`PixelSliceMut`] (borrowed, mutable).
6//!
7//! These types track pixel format via [`PixelDescriptor`] and color context
8//! via [`ColorContext`].
9//!
10//! Typed variants (e.g., `PixelBuffer<Rgb<u8>>`) enforce format correctness
11//! at compile time through the [`Pixel`] trait. Use `erase()` to convert
12//! to a type-erased form, or `try_typed()` to go back.
13//!
14//! For format conversions (e.g., `convert_to`, `to_rgb8`), see the
15//! `zenpixels-convert` crate which provides `PixelBufferConvertExt`.
16
17use alloc::sync::Arc;
18use alloc::vec;
19use alloc::vec::Vec;
20use core::fmt;
21use core::marker::PhantomData;
22
23use whereat::At;
24#[cfg(feature = "rgb")]
25use whereat::ResultAtExt;
26
27#[cfg(feature = "imgref")]
28use imgref::ImgRef;
29#[cfg(feature = "imgref")]
30use imgref::ImgVec;
31#[cfg(feature = "rgb")]
32use rgb::alt::BGRA;
33#[cfg(feature = "rgb")]
34use rgb::{Gray, Rgb, Rgba};
35
36use crate::color::ColorContext;
37use crate::descriptor::{
38    AlphaMode, ColorPrimaries, PixelDescriptor, SignalRange, TransferFunction,
39};
40#[cfg(feature = "rgb")]
41use crate::pixel_types::{GrayAlpha8, GrayAlpha16, GrayAlphaF32};
42
43// ---------------------------------------------------------------------------
44// Padded pixel types (32-bit SIMD-friendly)
45// ---------------------------------------------------------------------------
46
47/// 32-bit RGB pixel with padding byte (RGBx).
48///
49/// Same memory layout as `Rgba<u8>` but the 4th byte is padding,
50/// not alpha. Use this for SIMD-friendly 32-bit RGB processing
51/// without alpha semantics.
52#[derive(bytemuck::Zeroable, bytemuck::Pod, Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
53#[repr(C)]
54pub struct Rgbx {
55    /// Red channel.
56    pub r: u8,
57    /// Green channel.
58    pub g: u8,
59    /// Blue channel.
60    pub b: u8,
61    /// Padding byte. Value is unspecified and should be ignored.
62    pub x: u8,
63}
64
65/// 32-bit BGR pixel with padding byte (BGRx).
66///
67/// Same memory layout as `BGRA<u8>` but the 4th byte is padding.
68#[derive(bytemuck::Zeroable, bytemuck::Pod, Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69#[repr(C)]
70pub struct Bgrx {
71    /// Blue channel.
72    pub b: u8,
73    /// Green channel.
74    pub g: u8,
75    /// Red channel.
76    pub r: u8,
77    /// Padding byte. Value is unspecified and should be ignored.
78    pub x: u8,
79}
80
81// ---------------------------------------------------------------------------
82// Pixel trait
83// ---------------------------------------------------------------------------
84
85/// Compile-time pixel format descriptor.
86///
87/// Implemented for pixel types to associate them with their
88/// [`PixelDescriptor`]. This enables typed [`PixelSlice`] construction
89/// where the type system enforces format correctness.
90///
91/// The trait is open (not sealed) — custom pixel types can implement it.
92/// The `new_typed()` constructors include a compile-time assertion that
93/// `size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()` to catch bad impls.
94pub trait Pixel: bytemuck::Pod {
95    /// The pixel format descriptor for this type.
96    const DESCRIPTOR: PixelDescriptor;
97}
98
99impl Pixel for Rgbx {
100    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGBX8;
101}
102
103impl Pixel for Bgrx {
104    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::BGRX8;
105}
106
107#[cfg(feature = "rgb")]
108impl Pixel for Rgb<u8> {
109    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGB8;
110}
111
112#[cfg(feature = "rgb")]
113impl Pixel for Rgba<u8> {
114    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGBA8;
115}
116
117#[cfg(feature = "rgb")]
118impl Pixel for Gray<u8> {
119    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAY8;
120}
121
122#[cfg(feature = "rgb")]
123impl Pixel for Rgb<u16> {
124    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGB16;
125}
126
127#[cfg(feature = "rgb")]
128impl Pixel for Rgba<u16> {
129    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGBA16;
130}
131
132#[cfg(feature = "rgb")]
133impl Pixel for Gray<u16> {
134    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAY16;
135}
136
137#[cfg(feature = "rgb")]
138impl Pixel for Rgb<f32> {
139    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGBF32;
140}
141
142#[cfg(feature = "rgb")]
143impl Pixel for Rgba<f32> {
144    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::RGBAF32;
145}
146
147#[cfg(feature = "rgb")]
148impl Pixel for Gray<f32> {
149    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAYF32;
150}
151
152#[cfg(feature = "rgb")]
153impl Pixel for BGRA<u8> {
154    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::BGRA8;
155}
156
157#[cfg(feature = "rgb")]
158impl Pixel for GrayAlpha8 {
159    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAYA8;
160}
161
162#[cfg(feature = "rgb")]
163impl Pixel for GrayAlpha16 {
164    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAYA16;
165}
166
167#[cfg(feature = "rgb")]
168impl Pixel for GrayAlphaF32 {
169    const DESCRIPTOR: PixelDescriptor = PixelDescriptor::GRAYAF32;
170}
171
172// ---------------------------------------------------------------------------
173// BufferError
174// ---------------------------------------------------------------------------
175
176/// Errors from pixel buffer operations.
177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178#[non_exhaustive]
179pub enum BufferError {
180    /// Data pointer is not aligned for the channel type.
181    AlignmentViolation,
182    /// Data slice is too small for the given dimensions and stride.
183    InsufficientData,
184    /// Stride is smaller than `width * bytes_per_pixel`.
185    StrideTooSmall,
186    /// Stride is not a multiple of `bytes_per_pixel`.
187    ///
188    /// Every row must start on a pixel boundary. If stride is not a
189    /// multiple of bpp, rows after the first will be misaligned.
190    StrideNotPixelAligned,
191    /// Width or height is zero or causes overflow.
192    InvalidDimensions,
193    /// Descriptor bytes_per_pixel mismatch in `reinterpret()`.
194    ///
195    /// The new descriptor has a different `bytes_per_pixel()` than the
196    /// current one, so reinterpreting the buffer would be invalid.
197    IncompatibleDescriptor,
198    /// Buffer allocation failed (out of memory or overflow).
199    AllocationFailed,
200}
201
202impl fmt::Display for BufferError {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Self::AlignmentViolation => write!(f, "data is not aligned for the channel type"),
206            Self::InsufficientData => {
207                write!(f, "data slice is too small for the given dimensions")
208            }
209            Self::StrideTooSmall => write!(f, "stride is smaller than width * bytes_per_pixel"),
210            Self::StrideNotPixelAligned => {
211                write!(f, "stride is not a multiple of bytes_per_pixel")
212            }
213            Self::InvalidDimensions => write!(f, "width or height is zero or causes overflow"),
214            Self::IncompatibleDescriptor => {
215                write!(f, "new descriptor has different bytes_per_pixel")
216            }
217            Self::AllocationFailed => write!(f, "buffer allocation failed"),
218        }
219    }
220}
221
222impl core::error::Error for BufferError {}
223
224// ---------------------------------------------------------------------------
225// Helper functions
226// ---------------------------------------------------------------------------
227
228/// Round `val` up to the next multiple of `align` (must be a power of 2).
229const fn align_up(val: usize, align: usize) -> usize {
230    (val + align - 1) & !(align - 1)
231}
232
233/// Compute the byte offset needed to align `ptr` to `align`.
234fn align_offset(ptr: *const u8, align: usize) -> usize {
235    let addr = ptr as usize;
236    align_up(addr, align) - addr
237}
238
239/// Try to allocate a zeroed `Vec<u8>` of the given size.
240///
241/// Returns [`BufferError::AllocationFailed`] if the allocation fails.
242fn try_alloc_zeroed(size: usize) -> Result<Vec<u8>, BufferError> {
243    let mut data = Vec::new();
244    data.try_reserve_exact(size)
245        .map_err(|_| BufferError::AllocationFailed)?;
246    data.resize(size, 0);
247    Ok(data)
248}
249
250/// Validate slice parameters (shared by erased and typed constructors).
251fn validate_slice(
252    data_len: usize,
253    data_ptr: *const u8,
254    width: u32,
255    rows: u32,
256    stride_bytes: usize,
257    descriptor: &PixelDescriptor,
258) -> Result<(), BufferError> {
259    let bpp = descriptor.bytes_per_pixel();
260    let min_stride = (width as usize)
261        .checked_mul(bpp)
262        .ok_or(BufferError::InvalidDimensions)?;
263    if stride_bytes < min_stride {
264        return Err(BufferError::StrideTooSmall);
265    }
266    #[allow(clippy::manual_is_multiple_of)] // is_multiple_of stabilized 1.87; MSRV is 1.85
267    if bpp > 0 && stride_bytes % bpp != 0 {
268        return Err(BufferError::StrideNotPixelAligned);
269    }
270    if rows > 0 {
271        let required = required_bytes(rows, stride_bytes, min_stride)?;
272        if data_len < required {
273            return Err(BufferError::InsufficientData);
274        }
275    }
276    let align = descriptor.min_alignment();
277    #[allow(clippy::manual_is_multiple_of)] // is_multiple_of stabilized 1.87; MSRV is 1.85
278    if (data_ptr as usize) % align != 0 {
279        return Err(BufferError::AlignmentViolation);
280    }
281    Ok(())
282}
283
284/// Minimum bytes needed: `(rows - 1) * stride + min_stride`.
285fn required_bytes(rows: u32, stride: usize, min_stride: usize) -> Result<usize, BufferError> {
286    let preceding = (rows as usize - 1)
287        .checked_mul(stride)
288        .ok_or(BufferError::InvalidDimensions)?;
289    preceding
290        .checked_add(min_stride)
291        .ok_or(BufferError::InvalidDimensions)
292}
293
294/// Convert `Vec<P>` to `Vec<u8>`. Zero-copy when alignment matches (u8-component
295/// types), copies via `cast_slice` otherwise.
296#[cfg(feature = "rgb")]
297fn pixels_to_bytes<P: bytemuck::Pod>(pixels: Vec<P>) -> Vec<u8> {
298    match bytemuck::try_cast_vec(pixels) {
299        Ok(bytes) => bytes,
300        Err((_err, pixels)) => bytemuck::cast_slice::<P, u8>(&pixels).to_vec(),
301    }
302}
303
304// ---------------------------------------------------------------------------
305// PixelSlice (borrowed, immutable)
306// ---------------------------------------------------------------------------
307
308/// Borrowed view of pixel data.
309///
310/// Represents a contiguous region of pixel rows, possibly a sub-region
311/// of a larger buffer. All rows share the same stride.
312///
313/// The type parameter `P` tracks the pixel format at compile time:
314/// - `PixelSlice<'a, Rgb<u8>>` — known to be RGB8 pixels
315/// - `PixelSlice<'a>` (= `PixelSlice<'a, ()>`) — type-erased, format in descriptor
316///
317/// Use [`new_typed()`](PixelSlice::new_typed) to create a typed slice, or
318/// [`erase()`](PixelSlice::erase) / [`try_typed()`](PixelSlice::try_typed)
319/// to convert between typed and erased forms.
320///
321/// Optionally carries [`ColorContext`] to track source color metadata
322/// through the processing pipeline.
323#[non_exhaustive]
324pub struct PixelSlice<'a, P = ()> {
325    data: &'a [u8],
326    width: u32,
327    rows: u32,
328    stride: usize,
329    descriptor: PixelDescriptor,
330    color: Option<Arc<ColorContext>>,
331    _pixel: PhantomData<P>,
332}
333
334impl<'a> PixelSlice<'a> {
335    /// Create a new pixel slice with validation.
336    ///
337    /// `stride_bytes` is the byte distance between the start of consecutive rows.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if the data is too small, the stride is too small,
342    /// or the data is not aligned for the channel type.
343    #[track_caller]
344    pub fn new(
345        data: &'a [u8],
346        width: u32,
347        rows: u32,
348        stride_bytes: usize,
349        descriptor: PixelDescriptor,
350    ) -> Result<Self, At<BufferError>> {
351        validate_slice(
352            data.len(),
353            data.as_ptr(),
354            width,
355            rows,
356            stride_bytes,
357            &descriptor,
358        )
359        .map_err(|e| whereat::at!(e))?;
360        Ok(Self {
361            data,
362            width,
363            rows,
364            stride: stride_bytes,
365            descriptor,
366
367            color: None,
368            _pixel: PhantomData,
369        })
370    }
371}
372
373impl<'a, P> PixelSlice<'a, P> {
374    /// Erase the pixel type, returning a type-erased slice.
375    ///
376    /// This is a zero-cost operation that just changes the type parameter.
377    pub fn erase(self) -> PixelSlice<'a> {
378        PixelSlice {
379            data: self.data,
380            width: self.width,
381            rows: self.rows,
382            stride: self.stride,
383            descriptor: self.descriptor,
384
385            color: self.color,
386            _pixel: PhantomData,
387        }
388    }
389
390    /// Try to reinterpret as a typed pixel slice.
391    ///
392    /// Succeeds if the descriptors are layout-compatible (same channel type
393    /// and layout). Transfer function and alpha mode are metadata, not
394    /// layout constraints.
395    pub fn try_typed<Q: Pixel>(self) -> Option<PixelSlice<'a, Q>> {
396        if self.descriptor.layout_compatible(Q::DESCRIPTOR) {
397            Some(PixelSlice {
398                data: self.data,
399                width: self.width,
400                rows: self.rows,
401                stride: self.stride,
402                descriptor: self.descriptor,
403
404                color: self.color,
405                _pixel: PhantomData,
406            })
407        } else {
408            None
409        }
410    }
411
412    /// Replace the descriptor with a layout-compatible one.
413    ///
414    /// Use after a transform that changes pixel metadata without changing
415    /// the buffer layout (e.g., transfer function change, alpha mode change,
416    /// signal range expansion).
417    ///
418    /// For per-field updates, prefer the specific setters: [`with_transfer()`](Self::with_transfer),
419    /// [`with_primaries()`](Self::with_primaries), [`with_signal_range()`](Self::with_signal_range),
420    /// [`with_alpha_mode()`](Self::with_alpha_mode).
421    ///
422    /// # Panics
423    ///
424    /// Panics if the new descriptor is not layout-compatible (different
425    /// `channel_type` or `layout`). Use [`reinterpret()`](Self::reinterpret)
426    /// for genuine layout changes.
427    #[inline]
428    #[must_use]
429    pub fn with_descriptor(mut self, descriptor: PixelDescriptor) -> Self {
430        assert!(
431            self.descriptor.layout_compatible(descriptor),
432            "with_descriptor() cannot change physical layout ({} -> {}); \
433             use reinterpret() for layout changes",
434            self.descriptor,
435            descriptor
436        );
437        self.descriptor = descriptor;
438        self
439    }
440
441    /// Reinterpret the buffer with a different physical layout.
442    ///
443    /// Unlike [`with_descriptor()`](Self::with_descriptor), this allows
444    /// changing `channel_type` and `layout`. The new descriptor must have
445    /// the same `bytes_per_pixel()` as the current one.
446    ///
447    /// Use cases: treating RGBA8 data as BGRA8, RGBX8 as RGBA8.
448    #[track_caller]
449    pub fn reinterpret(mut self, descriptor: PixelDescriptor) -> Result<Self, At<BufferError>> {
450        if self.descriptor.bytes_per_pixel() != descriptor.bytes_per_pixel() {
451            return Err(whereat::at!(BufferError::IncompatibleDescriptor));
452        }
453        self.descriptor = descriptor;
454        Ok(self)
455    }
456
457    /// Return a copy with a different transfer function.
458    #[inline]
459    #[must_use]
460    pub fn with_transfer(mut self, tf: TransferFunction) -> Self {
461        self.descriptor.transfer = tf;
462        self
463    }
464
465    /// Return a copy with different color primaries.
466    #[inline]
467    #[must_use]
468    pub fn with_primaries(mut self, cp: ColorPrimaries) -> Self {
469        self.descriptor.primaries = cp;
470        self
471    }
472
473    /// Return a copy with a different signal range.
474    #[inline]
475    #[must_use]
476    pub fn with_signal_range(mut self, sr: SignalRange) -> Self {
477        self.descriptor.signal_range = sr;
478        self
479    }
480
481    /// Return a copy with a different alpha mode.
482    #[inline]
483    #[must_use]
484    pub fn with_alpha_mode(mut self, am: Option<AlphaMode>) -> Self {
485        self.descriptor.alpha = am;
486        self
487    }
488
489    /// Image width in pixels.
490    #[inline]
491    pub fn width(&self) -> u32 {
492        self.width
493    }
494
495    /// Number of rows in this slice.
496    #[inline]
497    pub fn rows(&self) -> u32 {
498        self.rows
499    }
500
501    /// Byte stride between row starts.
502    #[inline]
503    pub fn stride(&self) -> usize {
504        self.stride
505    }
506
507    /// Pixel format descriptor.
508    #[inline]
509    pub fn descriptor(&self) -> PixelDescriptor {
510        self.descriptor
511    }
512
513    /// Source color context (ICC/CICP metadata), if set.
514    #[inline]
515    pub fn color_context(&self) -> Option<&Arc<ColorContext>> {
516        self.color.as_ref()
517    }
518
519    /// Return a copy of this slice with a color context attached.
520    #[inline]
521    #[must_use]
522    pub fn with_color_context(mut self, ctx: Arc<ColorContext>) -> Self {
523        self.color = Some(ctx);
524        self
525    }
526
527    /// Whether rows are tightly packed (no stride padding).
528    ///
529    /// When true, the entire pixel data is contiguous in memory and
530    /// [`as_contiguous_bytes()`](Self::as_contiguous_bytes) returns `Some`.
531    #[inline]
532    pub fn is_contiguous(&self) -> bool {
533        self.stride == self.width as usize * self.descriptor.bytes_per_pixel()
534    }
535
536    /// Zero-copy access to the raw pixel bytes when rows are tightly packed.
537    ///
538    /// Returns `Some(&[u8])` if `stride == width * bpp` (no padding),
539    /// `None` if rows have stride padding.
540    ///
541    /// Use this to avoid `collect_contiguous_bytes()` copies when passing
542    /// pixel data to FFI or other APIs that need a flat buffer.
543    #[inline]
544    pub fn as_contiguous_bytes(&self) -> Option<&'a [u8]> {
545        if self.is_contiguous() {
546            let total = self.rows as usize * self.stride;
547            Some(&self.data[..total])
548        } else {
549            None
550        }
551    }
552
553    /// Raw backing bytes including inter-row stride padding.
554    ///
555    /// Returns a `&[u8]` covering all rows with stride padding between
556    /// them. Includes trailing padding after the last row when the
557    /// backing buffer is large enough (e.g., from a full allocation),
558    /// but not for sub-row views where the last row may be trimmed.
559    ///
560    /// Use with [`stride()`](Self::stride) for APIs that accept a byte
561    /// buffer plus a stride value (GPU uploads, codec writers, etc).
562    #[inline]
563    pub fn as_strided_bytes(&self) -> &'a [u8] {
564        if self.rows == 0 {
565            return &[];
566        }
567        // Use rows*stride if the backing buffer is large enough (includes
568        // trailing padding), otherwise trim the last row to width*bpp.
569        let full = self.rows as usize * self.stride;
570        if full <= self.data.len() {
571            &self.data[..full]
572        } else {
573            let bpp = self.descriptor.bytes_per_pixel();
574            let trimmed = (self.rows as usize - 1) * self.stride + self.width as usize * bpp;
575            &self.data[..trimmed]
576        }
577    }
578
579    /// Get the raw pixel bytes, copying only if stride padding exists.
580    ///
581    /// Returns `Cow::Borrowed` when rows are contiguous (zero-copy),
582    /// `Cow::Owned` when stride padding must be stripped.
583    pub fn contiguous_bytes(&self) -> alloc::borrow::Cow<'a, [u8]> {
584        if let Some(bytes) = self.as_contiguous_bytes() {
585            alloc::borrow::Cow::Borrowed(bytes)
586        } else {
587            let bpp = self.descriptor.bytes_per_pixel();
588            let row_bytes = self.width as usize * bpp;
589            let mut buf = Vec::with_capacity(row_bytes * self.rows as usize);
590            for y in 0..self.rows {
591                buf.extend_from_slice(self.row(y));
592            }
593            alloc::borrow::Cow::Owned(buf)
594        }
595    }
596
597    /// Pixel bytes for row `y` (no padding, exactly `width * bpp` bytes).
598    ///
599    /// # Panics
600    ///
601    /// Panics if `y >= rows`.
602    #[inline]
603    pub fn row(&self, y: u32) -> &[u8] {
604        assert!(
605            y < self.rows,
606            "row index {y} out of bounds (rows: {})",
607            self.rows
608        );
609        let start = y as usize * self.stride;
610        let len = self.width as usize * self.descriptor.bytes_per_pixel();
611        &self.data[start..start + len]
612    }
613
614    /// Full stride bytes for row `y` (including any padding).
615    ///
616    /// # Panics
617    ///
618    /// Panics if `y >= rows` or if the underlying data does not contain
619    /// a full stride for this row (can happen on the last row of a
620    /// cropped view).
621    #[inline]
622    pub fn row_with_stride(&self, y: u32) -> &[u8] {
623        assert!(
624            y < self.rows,
625            "row index {y} out of bounds (rows: {})",
626            self.rows
627        );
628        let start = y as usize * self.stride;
629        &self.data[start..start + self.stride]
630    }
631
632    /// Borrow a sub-range of rows.
633    ///
634    /// # Panics
635    ///
636    /// Panics if `y + count > rows`.
637    pub fn sub_rows(&self, y: u32, count: u32) -> PixelSlice<'_, P> {
638        assert!(
639            y.checked_add(count).is_some_and(|end| end <= self.rows),
640            "sub_rows({y}, {count}) out of bounds (rows: {})",
641            self.rows
642        );
643        if count == 0 {
644            return PixelSlice {
645                data: &[],
646                width: self.width,
647                rows: 0,
648                stride: self.stride,
649                descriptor: self.descriptor,
650
651                color: self.color.clone(),
652                _pixel: PhantomData,
653            };
654        }
655        let bpp = self.descriptor.bytes_per_pixel();
656        let start = y as usize * self.stride;
657        let end = (y as usize + count as usize - 1) * self.stride + self.width as usize * bpp;
658        PixelSlice {
659            data: &self.data[start..end],
660            width: self.width,
661            rows: count,
662            stride: self.stride,
663            descriptor: self.descriptor,
664
665            color: self.color.clone(),
666            _pixel: PhantomData,
667        }
668    }
669
670    /// Zero-copy crop view. Adjusts the data pointer and width; stride
671    /// remains the same as the parent.
672    ///
673    /// # Panics
674    ///
675    /// Panics if the crop region is out of bounds.
676    pub fn crop_view(&self, x: u32, y: u32, w: u32, h: u32) -> PixelSlice<'_, P> {
677        assert!(
678            x.checked_add(w).is_some_and(|end| end <= self.width),
679            "crop x={x} w={w} exceeds width {}",
680            self.width
681        );
682        assert!(
683            y.checked_add(h).is_some_and(|end| end <= self.rows),
684            "crop y={y} h={h} exceeds rows {}",
685            self.rows
686        );
687        if h == 0 || w == 0 {
688            return PixelSlice {
689                data: &[],
690                width: w,
691                rows: h,
692                stride: self.stride,
693                descriptor: self.descriptor,
694
695                color: self.color.clone(),
696                _pixel: PhantomData,
697            };
698        }
699        let bpp = self.descriptor.bytes_per_pixel();
700        let start = y as usize * self.stride + x as usize * bpp;
701        let end = (y as usize + h as usize - 1) * self.stride + (x as usize + w as usize) * bpp;
702        PixelSlice {
703            data: &self.data[start..end],
704            width: w,
705            rows: h,
706            stride: self.stride,
707            descriptor: self.descriptor,
708
709            color: self.color.clone(),
710            _pixel: PhantomData,
711        }
712    }
713}
714
715impl<'a, P: Pixel> PixelSlice<'a, P> {
716    /// Create a typed pixel slice.
717    ///
718    /// `stride_pixels` is the number of pixels per row (>= width).
719    /// The byte stride is `stride_pixels * size_of::<P>()`.
720    ///
721    /// # Compile-time safety
722    ///
723    /// Includes a compile-time assertion that `size_of::<P>()` matches
724    /// `P::DESCRIPTOR.bytes_per_pixel()`, catching bad `Pixel` impls.
725    #[track_caller]
726    pub fn new_typed(
727        data: &'a [u8],
728        width: u32,
729        rows: u32,
730        stride_pixels: u32,
731    ) -> Result<Self, At<BufferError>> {
732        const { assert!(core::mem::size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()) }
733        let stride_bytes = stride_pixels as usize * core::mem::size_of::<P>();
734        validate_slice(
735            data.len(),
736            data.as_ptr(),
737            width,
738            rows,
739            stride_bytes,
740            &P::DESCRIPTOR,
741        )
742        .map_err(|e| whereat::at!(e))?;
743        Ok(Self {
744            data,
745            width,
746            rows,
747            stride: stride_bytes,
748            descriptor: P::DESCRIPTOR,
749
750            color: None,
751            _pixel: PhantomData,
752        })
753    }
754}
755
756impl<P> fmt::Debug for PixelSlice<'_, P> {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        write!(
759            f,
760            "PixelSlice({}x{}, {:?} {:?})",
761            self.width,
762            self.rows,
763            self.descriptor.layout(),
764            self.descriptor.channel_type()
765        )
766    }
767}
768
769// ---------------------------------------------------------------------------
770// PixelSliceMut (borrowed, mutable)
771// ---------------------------------------------------------------------------
772
773/// Mutable borrowed view of pixel data.
774///
775/// Same semantics as [`PixelSlice`] but allows writing to rows.
776/// The type parameter `P` tracks pixel format at compile time.
777#[non_exhaustive]
778pub struct PixelSliceMut<'a, P = ()> {
779    data: &'a mut [u8],
780    width: u32,
781    rows: u32,
782    stride: usize,
783    descriptor: PixelDescriptor,
784    color: Option<Arc<ColorContext>>,
785    _pixel: PhantomData<P>,
786}
787
788impl<'a> PixelSliceMut<'a> {
789    /// Create a new mutable pixel slice with validation.
790    ///
791    /// `stride_bytes` is the byte distance between the start of consecutive rows.
792    ///
793    /// # Errors
794    ///
795    /// Returns an error if the data is too small, the stride is too small,
796    /// or the data is not aligned for the channel type.
797    #[track_caller]
798    pub fn new(
799        data: &'a mut [u8],
800        width: u32,
801        rows: u32,
802        stride_bytes: usize,
803        descriptor: PixelDescriptor,
804    ) -> Result<Self, At<BufferError>> {
805        validate_slice(
806            data.len(),
807            data.as_ptr(),
808            width,
809            rows,
810            stride_bytes,
811            &descriptor,
812        )
813        .map_err(|e| whereat::at!(e))?;
814        Ok(Self {
815            data,
816            width,
817            rows,
818            stride: stride_bytes,
819            descriptor,
820
821            color: None,
822            _pixel: PhantomData,
823        })
824    }
825}
826
827impl<'a, P> PixelSliceMut<'a, P> {
828    /// Erase the pixel type, returning a type-erased mutable slice.
829    pub fn erase(self) -> PixelSliceMut<'a> {
830        PixelSliceMut {
831            data: self.data,
832            width: self.width,
833            rows: self.rows,
834            stride: self.stride,
835            descriptor: self.descriptor,
836
837            color: self.color,
838            _pixel: PhantomData,
839        }
840    }
841
842    /// Try to reinterpret as a typed mutable pixel slice.
843    ///
844    /// Succeeds if the descriptors are layout-compatible.
845    pub fn try_typed<Q: Pixel>(self) -> Option<PixelSliceMut<'a, Q>> {
846        if self.descriptor.layout_compatible(Q::DESCRIPTOR) {
847            Some(PixelSliceMut {
848                data: self.data,
849                width: self.width,
850                rows: self.rows,
851                stride: self.stride,
852                descriptor: self.descriptor,
853
854                color: self.color,
855                _pixel: PhantomData,
856            })
857        } else {
858            None
859        }
860    }
861
862    /// Replace the descriptor with a layout-compatible one.
863    ///
864    /// See [`PixelSlice::with_descriptor()`] for details.
865    #[inline]
866    #[must_use]
867    pub fn with_descriptor(mut self, descriptor: PixelDescriptor) -> Self {
868        assert!(
869            self.descriptor.layout_compatible(descriptor),
870            "with_descriptor() cannot change physical layout ({} -> {}); \
871             use reinterpret() for layout changes",
872            self.descriptor,
873            descriptor
874        );
875        self.descriptor = descriptor;
876        self
877    }
878
879    /// Reinterpret the buffer with a different physical layout.
880    ///
881    /// See [`PixelSlice::reinterpret()`] for details.
882    #[track_caller]
883    pub fn reinterpret(mut self, descriptor: PixelDescriptor) -> Result<Self, At<BufferError>> {
884        if self.descriptor.bytes_per_pixel() != descriptor.bytes_per_pixel() {
885            return Err(whereat::at!(BufferError::IncompatibleDescriptor));
886        }
887        self.descriptor = descriptor;
888        Ok(self)
889    }
890
891    /// Return a copy with a different transfer function.
892    #[inline]
893    #[must_use]
894    pub fn with_transfer(mut self, tf: TransferFunction) -> Self {
895        self.descriptor.transfer = tf;
896        self
897    }
898
899    /// Return a copy with different color primaries.
900    #[inline]
901    #[must_use]
902    pub fn with_primaries(mut self, cp: ColorPrimaries) -> Self {
903        self.descriptor.primaries = cp;
904        self
905    }
906
907    /// Return a copy with a different signal range.
908    #[inline]
909    #[must_use]
910    pub fn with_signal_range(mut self, sr: SignalRange) -> Self {
911        self.descriptor.signal_range = sr;
912        self
913    }
914
915    /// Return a copy with a different alpha mode.
916    #[inline]
917    #[must_use]
918    pub fn with_alpha_mode(mut self, am: Option<AlphaMode>) -> Self {
919        self.descriptor.alpha = am;
920        self
921    }
922
923    /// Image width in pixels.
924    #[inline]
925    pub fn width(&self) -> u32 {
926        self.width
927    }
928
929    /// Number of rows in this slice.
930    #[inline]
931    pub fn rows(&self) -> u32 {
932        self.rows
933    }
934
935    /// Byte stride between row starts.
936    #[inline]
937    pub fn stride(&self) -> usize {
938        self.stride
939    }
940
941    /// Pixel format descriptor.
942    #[inline]
943    pub fn descriptor(&self) -> PixelDescriptor {
944        self.descriptor
945    }
946
947    /// Source color context (ICC/CICP metadata), if set.
948    #[inline]
949    pub fn color_context(&self) -> Option<&Arc<ColorContext>> {
950        self.color.as_ref()
951    }
952
953    /// Return a copy of this slice with a color context attached.
954    #[inline]
955    #[must_use]
956    pub fn with_color_context(mut self, ctx: Arc<ColorContext>) -> Self {
957        self.color = Some(ctx);
958        self
959    }
960
961    /// Reborrow as an immutable [`PixelSlice`] (zero-copy).
962    ///
963    /// The returned slice borrows from `self`, so the mutable slice
964    /// cannot be used while the immutable reborrow is alive.
965    #[inline]
966    pub fn as_pixel_slice(&self) -> PixelSlice<'_, P> {
967        PixelSlice {
968            data: self.data,
969            width: self.width,
970            rows: self.rows,
971            stride: self.stride,
972            descriptor: self.descriptor,
973            color: self.color.clone(),
974            _pixel: PhantomData,
975        }
976    }
977
978    /// Zero-copy access to the raw backing bytes, including any stride padding.
979    ///
980    /// Unlike [`PixelSlice::as_strided_bytes()`] (which clips to the image
981    /// extent), this returns the full backing buffer so callers can write
982    /// stride padding (zeroing, codec requirements, etc.).
983    #[inline]
984    pub fn as_strided_bytes(&self) -> &[u8] {
985        self.data
986    }
987
988    /// Mutable access to the raw backing bytes, including any stride padding.
989    ///
990    /// Returns the full backing buffer so callers can write stride padding.
991    #[inline]
992    pub fn as_strided_bytes_mut(&mut self) -> &mut [u8] {
993        self.data
994    }
995
996    /// Pixel bytes for row `y` (immutable, no padding).
997    ///
998    /// # Panics
999    ///
1000    /// Panics if `y >= rows`.
1001    #[inline]
1002    pub fn row(&self, y: u32) -> &[u8] {
1003        assert!(
1004            y < self.rows,
1005            "row index {y} out of bounds (rows: {})",
1006            self.rows
1007        );
1008        let start = y as usize * self.stride;
1009        let len = self.width as usize * self.descriptor.bytes_per_pixel();
1010        &self.data[start..start + len]
1011    }
1012
1013    /// Mutable pixel bytes for row `y` (no padding).
1014    ///
1015    /// # Panics
1016    ///
1017    /// Panics if `y >= rows`.
1018    #[inline]
1019    pub fn row_mut(&mut self, y: u32) -> &mut [u8] {
1020        assert!(
1021            y < self.rows,
1022            "row index {y} out of bounds (rows: {})",
1023            self.rows
1024        );
1025        let start = y as usize * self.stride;
1026        let len = self.width as usize * self.descriptor.bytes_per_pixel();
1027        &mut self.data[start..start + len]
1028    }
1029
1030    /// Borrow a mutable sub-range of rows.
1031    ///
1032    /// # Panics
1033    ///
1034    /// Panics if `y + count > rows`.
1035    pub fn sub_rows_mut(&mut self, y: u32, count: u32) -> PixelSliceMut<'_, P> {
1036        assert!(
1037            y.checked_add(count).is_some_and(|end| end <= self.rows),
1038            "sub_rows_mut({y}, {count}) out of bounds (rows: {})",
1039            self.rows
1040        );
1041        if count == 0 {
1042            return PixelSliceMut {
1043                data: &mut [],
1044                width: self.width,
1045                rows: 0,
1046                stride: self.stride,
1047                descriptor: self.descriptor,
1048
1049                color: self.color.clone(),
1050                _pixel: PhantomData,
1051            };
1052        }
1053        let bpp = self.descriptor.bytes_per_pixel();
1054        let start = y as usize * self.stride;
1055        let end = (y as usize + count as usize - 1) * self.stride + self.width as usize * bpp;
1056        PixelSliceMut {
1057            data: &mut self.data[start..end],
1058            width: self.width,
1059            rows: count,
1060            stride: self.stride,
1061            descriptor: self.descriptor,
1062
1063            color: self.color.clone(),
1064            _pixel: PhantomData,
1065        }
1066    }
1067}
1068
1069impl<'a, P: Pixel> PixelSliceMut<'a, P> {
1070    /// Create a typed mutable pixel slice.
1071    ///
1072    /// `stride_pixels` is the number of pixels per row (>= width).
1073    /// The byte stride is `stride_pixels * size_of::<P>()`.
1074    #[track_caller]
1075    pub fn new_typed(
1076        data: &'a mut [u8],
1077        width: u32,
1078        rows: u32,
1079        stride_pixels: u32,
1080    ) -> Result<Self, At<BufferError>> {
1081        const { assert!(core::mem::size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()) }
1082        let stride_bytes = stride_pixels as usize * core::mem::size_of::<P>();
1083        validate_slice(
1084            data.len(),
1085            data.as_ptr(),
1086            width,
1087            rows,
1088            stride_bytes,
1089            &P::DESCRIPTOR,
1090        )
1091        .map_err(|e| whereat::at!(e))?;
1092        Ok(Self {
1093            data,
1094            width,
1095            rows,
1096            stride: stride_bytes,
1097            descriptor: P::DESCRIPTOR,
1098
1099            color: None,
1100            _pixel: PhantomData,
1101        })
1102    }
1103}
1104
1105impl<P> fmt::Debug for PixelSliceMut<'_, P> {
1106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1107        write!(
1108            f,
1109            "PixelSliceMut({}x{}, {:?} {:?})",
1110            self.width,
1111            self.rows,
1112            self.descriptor.layout(),
1113            self.descriptor.channel_type()
1114        )
1115    }
1116}
1117
1118// ---------------------------------------------------------------------------
1119// In-place 32-bit pixel format conversions on PixelSliceMut
1120// ---------------------------------------------------------------------------
1121
1122/// Helper: iterate over pixel rows, calling `f` on each 4-byte pixel.
1123fn for_each_pixel_4bpp(
1124    data: &mut [u8],
1125    width: u32,
1126    rows: u32,
1127    stride: usize,
1128    mut f: impl FnMut(&mut [u8; 4]),
1129) {
1130    let row_bytes = width as usize * 4;
1131    for y in 0..rows as usize {
1132        let row_start = y * stride;
1133        let row = &mut data[row_start..row_start + row_bytes];
1134        for chunk in row.chunks_exact_mut(4) {
1135            let px: &mut [u8; 4] = chunk.try_into().unwrap();
1136            f(px);
1137        }
1138    }
1139}
1140
1141impl<'a> PixelSliceMut<'a, Rgbx> {
1142    /// Byte-swap R<->B channels in place, converting to BGRX.
1143    #[must_use]
1144    pub fn swap_to_bgrx(self) -> PixelSliceMut<'a, Bgrx> {
1145        let width = self.width;
1146        let rows = self.rows;
1147        let stride = self.stride;
1148
1149        let color = self.color;
1150        let data = self.data;
1151        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1152            px.swap(0, 2);
1153        });
1154        PixelSliceMut {
1155            data,
1156            width,
1157            rows,
1158            stride,
1159            descriptor: PixelDescriptor::BGRX8_SRGB,
1160
1161            color,
1162            _pixel: PhantomData,
1163        }
1164    }
1165}
1166
1167#[cfg(feature = "rgb")]
1168impl<'a> PixelSliceMut<'a, Rgbx> {
1169    /// Upgrade to RGBA by setting all padding bytes to 255 (fully opaque).
1170    #[must_use]
1171    pub fn upgrade_to_rgba(self) -> PixelSliceMut<'a, Rgba<u8>> {
1172        let width = self.width;
1173        let rows = self.rows;
1174        let stride = self.stride;
1175
1176        let color = self.color;
1177        let data = self.data;
1178        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1179            px[3] = 255;
1180        });
1181        PixelSliceMut {
1182            data,
1183            width,
1184            rows,
1185            stride,
1186            descriptor: PixelDescriptor::RGBA8_SRGB,
1187
1188            color,
1189            _pixel: PhantomData,
1190        }
1191    }
1192}
1193
1194impl<'a> PixelSliceMut<'a, Bgrx> {
1195    /// Byte-swap B<->R channels in place, converting to RGBX.
1196    #[must_use]
1197    pub fn swap_to_rgbx(self) -> PixelSliceMut<'a, Rgbx> {
1198        let width = self.width;
1199        let rows = self.rows;
1200        let stride = self.stride;
1201
1202        let color = self.color;
1203        let data = self.data;
1204        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1205            px.swap(0, 2);
1206        });
1207        PixelSliceMut {
1208            data,
1209            width,
1210            rows,
1211            stride,
1212            descriptor: PixelDescriptor::RGBX8_SRGB,
1213
1214            color,
1215            _pixel: PhantomData,
1216        }
1217    }
1218}
1219
1220#[cfg(feature = "rgb")]
1221impl<'a> PixelSliceMut<'a, Bgrx> {
1222    /// Upgrade to BGRA by setting all padding bytes to 255 (fully opaque).
1223    #[must_use]
1224    pub fn upgrade_to_bgra(self) -> PixelSliceMut<'a, BGRA<u8>> {
1225        let width = self.width;
1226        let rows = self.rows;
1227        let stride = self.stride;
1228
1229        let color = self.color;
1230        let data = self.data;
1231        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1232            px[3] = 255;
1233        });
1234        PixelSliceMut {
1235            data,
1236            width,
1237            rows,
1238            stride,
1239            descriptor: PixelDescriptor::BGRA8_SRGB,
1240
1241            color,
1242            _pixel: PhantomData,
1243        }
1244    }
1245}
1246
1247#[cfg(feature = "rgb")]
1248impl<'a> PixelSliceMut<'a, Rgba<u8>> {
1249    /// Matte alpha against a solid RGB background, producing RGBX.
1250    ///
1251    /// Each pixel is composited: `out = src * alpha/255 + bg * (255 - alpha)/255`.
1252    /// The alpha byte becomes padding.
1253    #[must_use]
1254    pub fn matte_to_rgbx(self, bg: Rgb<u8>) -> PixelSliceMut<'a, Rgbx> {
1255        let width = self.width;
1256        let rows = self.rows;
1257        let stride = self.stride;
1258
1259        let color = self.color;
1260        let data = self.data;
1261        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1262            let a = px[3] as u16;
1263            let inv_a = 255 - a;
1264            px[0] = ((px[0] as u16 * a + bg.r as u16 * inv_a + 127) / 255) as u8;
1265            px[1] = ((px[1] as u16 * a + bg.g as u16 * inv_a + 127) / 255) as u8;
1266            px[2] = ((px[2] as u16 * a + bg.b as u16 * inv_a + 127) / 255) as u8;
1267            px[3] = 0;
1268        });
1269        PixelSliceMut {
1270            data,
1271            width,
1272            rows,
1273            stride,
1274            descriptor: PixelDescriptor::RGBX8_SRGB,
1275
1276            color,
1277            _pixel: PhantomData,
1278        }
1279    }
1280
1281    /// Strip alpha to RGBX without compositing (just mark as padding).
1282    ///
1283    /// The alpha byte value is preserved in memory but semantically ignored.
1284    /// Use when you know alpha is already 255 or don't care about the values.
1285    #[must_use]
1286    pub fn strip_alpha_to_rgbx(self) -> PixelSliceMut<'a, Rgbx> {
1287        PixelSliceMut {
1288            data: self.data,
1289            width: self.width,
1290            rows: self.rows,
1291            stride: self.stride,
1292            descriptor: PixelDescriptor::RGBX8_SRGB,
1293
1294            color: self.color,
1295            _pixel: PhantomData,
1296        }
1297    }
1298
1299    /// Byte-swap R<->B channels in place, converting to BGRA.
1300    #[must_use]
1301    pub fn swap_to_bgra(self) -> PixelSliceMut<'a, BGRA<u8>> {
1302        let width = self.width;
1303        let rows = self.rows;
1304        let stride = self.stride;
1305
1306        let color = self.color;
1307        let data = self.data;
1308        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1309            px.swap(0, 2);
1310        });
1311        PixelSliceMut {
1312            data,
1313            width,
1314            rows,
1315            stride,
1316            descriptor: PixelDescriptor::BGRA8_SRGB,
1317
1318            color,
1319            _pixel: PhantomData,
1320        }
1321    }
1322}
1323
1324#[cfg(feature = "rgb")]
1325impl<'a> PixelSliceMut<'a, BGRA<u8>> {
1326    /// Matte alpha against a solid RGB background, producing BGRX.
1327    ///
1328    /// Each pixel is composited: `out = src * alpha/255 + bg * (255 - alpha)/255`.
1329    /// The alpha byte becomes padding.
1330    #[must_use]
1331    pub fn matte_to_bgrx(self, bg: Rgb<u8>) -> PixelSliceMut<'a, Bgrx> {
1332        let width = self.width;
1333        let rows = self.rows;
1334        let stride = self.stride;
1335
1336        let color = self.color;
1337        let data = self.data;
1338        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1339            let a = px[3] as u16;
1340            let inv_a = 255 - a;
1341            // BGRA layout: [B, G, R, A]
1342            px[0] = ((px[0] as u16 * a + bg.b as u16 * inv_a + 127) / 255) as u8;
1343            px[1] = ((px[1] as u16 * a + bg.g as u16 * inv_a + 127) / 255) as u8;
1344            px[2] = ((px[2] as u16 * a + bg.r as u16 * inv_a + 127) / 255) as u8;
1345            px[3] = 0;
1346        });
1347        PixelSliceMut {
1348            data,
1349            width,
1350            rows,
1351            stride,
1352            descriptor: PixelDescriptor::BGRX8_SRGB,
1353
1354            color,
1355            _pixel: PhantomData,
1356        }
1357    }
1358
1359    /// Strip alpha to BGRX without compositing (just mark as padding).
1360    #[must_use]
1361    pub fn strip_alpha_to_bgrx(self) -> PixelSliceMut<'a, Bgrx> {
1362        PixelSliceMut {
1363            data: self.data,
1364            width: self.width,
1365            rows: self.rows,
1366            stride: self.stride,
1367            descriptor: PixelDescriptor::BGRX8_SRGB,
1368
1369            color: self.color,
1370            _pixel: PhantomData,
1371        }
1372    }
1373
1374    /// Byte-swap B<->R channels in place, converting to RGBA.
1375    #[must_use]
1376    pub fn swap_to_rgba(self) -> PixelSliceMut<'a, Rgba<u8>> {
1377        let width = self.width;
1378        let rows = self.rows;
1379        let stride = self.stride;
1380
1381        let color = self.color;
1382        let data = self.data;
1383        for_each_pixel_4bpp(data, width, rows, stride, |px| {
1384            px.swap(0, 2);
1385        });
1386        PixelSliceMut {
1387            data,
1388            width,
1389            rows,
1390            stride,
1391            descriptor: PixelDescriptor::RGBA8_SRGB,
1392
1393            color,
1394            _pixel: PhantomData,
1395        }
1396    }
1397}
1398
1399// ---------------------------------------------------------------------------
1400// PixelBuffer (owned, pool-friendly)
1401// ---------------------------------------------------------------------------
1402
1403/// Owned pixel buffer with format metadata.
1404///
1405/// Wraps a `Vec<u8>` with an optional alignment offset so that pixel
1406/// rows start at the correct alignment for the channel type. The
1407/// backing vec can be recovered with [`into_vec`](Self::into_vec) for
1408/// pool reuse.
1409///
1410/// The type parameter `P` tracks pixel format at compile time, same as
1411/// [`PixelSlice`].
1412#[non_exhaustive]
1413pub struct PixelBuffer<P = ()> {
1414    data: Vec<u8>,
1415    /// Byte offset from `data` start to the first aligned pixel.
1416    offset: usize,
1417    width: u32,
1418    height: u32,
1419    stride: usize,
1420    descriptor: PixelDescriptor,
1421    color: Option<Arc<ColorContext>>,
1422    _pixel: PhantomData<P>,
1423}
1424
1425impl PixelBuffer {
1426    /// Allocate a zero-filled buffer for the given dimensions and format.
1427    ///
1428    /// The default path is infallible for speed — see the
1429    /// [allocation policy](crate#allocation-policy) for the rationale and
1430    /// for guidance on when to reach for the `try_*` sibling.
1431    ///
1432    /// # Panics
1433    ///
1434    /// Panics if allocation fails. Use [`try_new`](Self::try_new) for
1435    /// fallible allocation when handling untrusted sizes or when OOM must
1436    /// be recoverable.
1437    pub fn new(width: u32, height: u32, descriptor: PixelDescriptor) -> Self {
1438        Self::try_new(width, height, descriptor).expect("pixel buffer allocation failed")
1439    }
1440
1441    /// Try to allocate a zero-filled buffer for the given dimensions and format.
1442    ///
1443    /// Returns [`BufferError::InvalidDimensions`] if the total size overflows,
1444    /// or [`BufferError::AllocationFailed`] if allocation fails.
1445    #[track_caller]
1446    pub fn try_new(
1447        width: u32,
1448        height: u32,
1449        descriptor: PixelDescriptor,
1450    ) -> Result<Self, At<BufferError>> {
1451        let stride = descriptor.aligned_stride(width);
1452        let total = stride
1453            .checked_mul(height as usize)
1454            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1455        let align = descriptor.min_alignment();
1456        let alloc_size = total
1457            .checked_add(align - 1)
1458            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1459        let data = try_alloc_zeroed(alloc_size).map_err(|e| whereat::at!(e))?;
1460        let offset = align_offset(data.as_ptr(), align);
1461        Ok(Self {
1462            data,
1463            offset,
1464            width,
1465            height,
1466            stride,
1467            descriptor,
1468
1469            color: None,
1470            _pixel: PhantomData,
1471        })
1472    }
1473
1474    /// Allocate a SIMD-aligned buffer for the given dimensions and format.
1475    ///
1476    /// Row stride is a multiple of `lcm(bpp, simd_align)`, ensuring every
1477    /// row start is both pixel-aligned and SIMD-aligned when the buffer
1478    /// itself starts at a SIMD-aligned address.
1479    ///
1480    /// `simd_align` must be a power of 2 (e.g. 16, 32, 64).
1481    ///
1482    /// The default path is infallible for speed — see the
1483    /// [allocation policy](crate#allocation-policy) for the rationale and
1484    /// for guidance on when to reach for the `try_*` sibling.
1485    ///
1486    /// # Panics
1487    ///
1488    /// Panics if allocation fails. Use
1489    /// [`try_new_simd_aligned`](Self::try_new_simd_aligned) for fallible
1490    /// allocation when handling untrusted sizes or when OOM must be
1491    /// recoverable.
1492    pub fn new_simd_aligned(
1493        width: u32,
1494        height: u32,
1495        descriptor: PixelDescriptor,
1496        simd_align: usize,
1497    ) -> Self {
1498        Self::try_new_simd_aligned(width, height, descriptor, simd_align)
1499            .expect("pixel buffer SIMD-aligned allocation failed")
1500    }
1501
1502    /// Try to allocate a SIMD-aligned buffer for the given dimensions and format.
1503    ///
1504    /// Returns [`BufferError::InvalidDimensions`] if the total size overflows,
1505    /// or [`BufferError::AllocationFailed`] if allocation fails.
1506    #[track_caller]
1507    pub fn try_new_simd_aligned(
1508        width: u32,
1509        height: u32,
1510        descriptor: PixelDescriptor,
1511        simd_align: usize,
1512    ) -> Result<Self, At<BufferError>> {
1513        let stride = descriptor.simd_aligned_stride(width, simd_align);
1514        let total = stride
1515            .checked_mul(height as usize)
1516            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1517        let alloc_size = total
1518            .checked_add(simd_align - 1)
1519            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1520        let data = try_alloc_zeroed(alloc_size).map_err(|e| whereat::at!(e))?;
1521        let offset = align_offset(data.as_ptr(), simd_align);
1522        Ok(Self {
1523            data,
1524            offset,
1525            width,
1526            height,
1527            stride,
1528            descriptor,
1529
1530            color: None,
1531            _pixel: PhantomData,
1532        })
1533    }
1534
1535    /// Wrap an existing `Vec<u8>` as a pixel buffer.
1536    ///
1537    /// The vec must be large enough to hold `aligned_stride(width) * height`
1538    /// bytes (plus any alignment offset). Stride is computed from the
1539    /// descriptor -- rows are assumed tightly packed.
1540    ///
1541    /// # Errors
1542    ///
1543    /// Returns [`BufferError::InsufficientData`] if the vec is too small.
1544    #[track_caller]
1545    pub fn from_vec(
1546        data: Vec<u8>,
1547        width: u32,
1548        height: u32,
1549        descriptor: PixelDescriptor,
1550    ) -> Result<Self, At<BufferError>> {
1551        let stride = descriptor.aligned_stride(width);
1552        let total = stride
1553            .checked_mul(height as usize)
1554            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1555        let align = descriptor.min_alignment();
1556        let offset = align_offset(data.as_ptr(), align);
1557        if data.len() < offset + total {
1558            return Err(whereat::at!(BufferError::InsufficientData));
1559        }
1560        Ok(Self {
1561            data,
1562            offset,
1563            width,
1564            height,
1565            stride,
1566            descriptor,
1567
1568            color: None,
1569            _pixel: PhantomData,
1570        })
1571    }
1572}
1573
1574impl<P: Pixel> PixelBuffer<P> {
1575    /// Allocate a typed zero-filled buffer for the given dimensions.
1576    ///
1577    /// The descriptor is derived from `P::DESCRIPTOR`.
1578    ///
1579    /// The default path is infallible for speed — see the
1580    /// [allocation policy](crate#allocation-policy) for the rationale and
1581    /// for guidance on when to reach for the `try_*` sibling.
1582    ///
1583    /// # Panics
1584    ///
1585    /// Panics if allocation fails. Use
1586    /// [`try_new_typed`](Self::try_new_typed) for fallible allocation when
1587    /// handling untrusted sizes or when OOM must be recoverable.
1588    pub fn new_typed(width: u32, height: u32) -> Self {
1589        Self::try_new_typed(width, height).expect("typed pixel buffer allocation failed")
1590    }
1591
1592    /// Try to allocate a typed zero-filled buffer for the given dimensions.
1593    ///
1594    /// Returns [`BufferError::InvalidDimensions`] if the total size overflows,
1595    /// or [`BufferError::AllocationFailed`] if allocation fails.
1596    #[track_caller]
1597    pub fn try_new_typed(width: u32, height: u32) -> Result<Self, At<BufferError>> {
1598        const { assert!(core::mem::size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()) }
1599        let descriptor = P::DESCRIPTOR;
1600        let stride = descriptor.aligned_stride(width);
1601        let total = stride
1602            .checked_mul(height as usize)
1603            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1604        let align = descriptor.min_alignment();
1605        let alloc_size = total
1606            .checked_add(align - 1)
1607            .ok_or_else(|| whereat::at!(BufferError::InvalidDimensions))?;
1608        let data = try_alloc_zeroed(alloc_size).map_err(|e| whereat::at!(e))?;
1609        let offset = align_offset(data.as_ptr(), align);
1610        Ok(Self {
1611            data,
1612            offset,
1613            width,
1614            height,
1615            stride,
1616            descriptor,
1617
1618            color: None,
1619            _pixel: PhantomData,
1620        })
1621    }
1622}
1623
1624#[cfg(feature = "rgb")]
1625impl<P: Pixel> PixelBuffer<P> {
1626    /// Construct from a typed pixel `Vec`.
1627    ///
1628    /// Zero-copy when `P` has alignment 1 (u8-component types like `Rgb<u8>`).
1629    /// Copies the data for types with higher alignment (`Rgb<u16>`, `Rgb<f32>`, etc.)
1630    /// because `Vec` tracks allocation alignment and `Vec<u8>` requires alignment 1.
1631    ///
1632    /// # Errors
1633    ///
1634    /// Returns [`BufferError::InvalidDimensions`] if `pixels.len() != width * height`.
1635    #[track_caller]
1636    pub fn from_pixels(pixels: Vec<P>, width: u32, height: u32) -> Result<Self, At<BufferError>> {
1637        const { assert!(core::mem::size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()) }
1638        let expected = width as usize * height as usize;
1639        if pixels.len() != expected {
1640            return Err(whereat::at!(BufferError::InvalidDimensions));
1641        }
1642        let descriptor = P::DESCRIPTOR;
1643        let stride = descriptor.aligned_stride(width);
1644        let data: Vec<u8> = pixels_to_bytes(pixels);
1645        Ok(Self {
1646            data,
1647            offset: 0,
1648            width,
1649            height,
1650            stride,
1651            descriptor,
1652
1653            color: None,
1654            _pixel: PhantomData,
1655        })
1656    }
1657}
1658
1659#[cfg(feature = "imgref")]
1660impl<P: Pixel> PixelBuffer<P> {
1661    /// Construct from a typed `ImgVec`.
1662    ///
1663    /// Zero-copy when `P` has alignment 1 (u8-component types).
1664    /// Copies for higher-alignment types.
1665    ///
1666    /// # Panics
1667    ///
1668    /// Panics if the copy path (triggered for `P` with alignment > 1) fails
1669    /// to allocate. There is no fallible sibling today; if you need one,
1670    /// pre-convert the `ImgVec` into a `Vec<P>` and use
1671    /// [`try_new_typed`](Self::try_new_typed) followed by a manual copy, or
1672    /// file a request against the crate (see the
1673    /// [allocation policy](crate#allocation-policy)).
1674    pub fn from_imgvec(img: ImgVec<P>) -> Self {
1675        const { assert!(core::mem::size_of::<P>() == P::DESCRIPTOR.bytes_per_pixel()) }
1676        let width = img.width() as u32;
1677        let height = img.height() as u32;
1678        let stride_pixels = img.stride();
1679        let descriptor = P::DESCRIPTOR;
1680        let stride_bytes = stride_pixels * core::mem::size_of::<P>();
1681        let (buf, ..) = img.into_contiguous_buf();
1682        let data: Vec<u8> = pixels_to_bytes(buf);
1683        Self {
1684            data,
1685            offset: 0,
1686            width,
1687            height,
1688            stride: stride_bytes,
1689            descriptor,
1690
1691            color: None,
1692            _pixel: PhantomData,
1693        }
1694    }
1695}
1696
1697#[cfg(feature = "imgref")]
1698impl<P: Pixel> PixelBuffer<P> {
1699    /// Borrow the buffer as an [`ImgRef`].
1700    ///
1701    /// Zero-copy: reinterprets the raw bytes as typed pixels via
1702    /// [`bytemuck::cast_slice`].
1703    ///
1704    /// # Panics
1705    ///
1706    /// Panics if the stride is not pixel-aligned (always succeeds for
1707    /// buffers created via `new_typed()`, `from_pixels()`, or `from_imgvec()`).
1708    pub fn as_imgref(&self) -> ImgRef<'_, P> {
1709        let total_bytes = if self.height == 0 {
1710            0
1711        } else {
1712            (self.height as usize - 1) * self.stride
1713                + self.width as usize * core::mem::size_of::<P>()
1714        };
1715        let data = &self.data[self.offset..self.offset + total_bytes];
1716        let pixels: &[P] = bytemuck::cast_slice(data);
1717        let stride_px = self.stride / core::mem::size_of::<P>();
1718        imgref::Img::new_stride(pixels, self.width as usize, self.height as usize, stride_px)
1719    }
1720
1721    /// Borrow the buffer as a mutable [`ImgRefMut`](imgref::ImgRefMut).
1722    ///
1723    /// Zero-copy: reinterprets the raw bytes as typed pixels.
1724    pub fn as_imgref_mut(&mut self) -> imgref::ImgRefMut<'_, P> {
1725        let total_bytes = if self.height == 0 {
1726            0
1727        } else {
1728            (self.height as usize - 1) * self.stride
1729                + self.width as usize * core::mem::size_of::<P>()
1730        };
1731        let offset = self.offset;
1732        let data = &mut self.data[offset..offset + total_bytes];
1733        let pixels: &mut [P] = bytemuck::cast_slice_mut(data);
1734        let stride_px = self.stride / core::mem::size_of::<P>();
1735        imgref::Img::new_stride(pixels, self.width as usize, self.height as usize, stride_px)
1736    }
1737}
1738
1739/// Type-erased typed pixel construction and access.
1740#[cfg(feature = "rgb")]
1741impl PixelBuffer {
1742    /// Zero-copy construction from typed pixels, returning an erased `PixelBuffer`.
1743    ///
1744    /// Equivalent to `PixelBuffer::<P>::from_pixels(pixels, w, h)?.into()` but
1745    /// avoids the intermediate typed `PixelBuffer`.
1746    ///
1747    /// # Errors
1748    ///
1749    /// Returns [`BufferError::InvalidDimensions`] if `pixels.len() != width * height`.
1750    #[track_caller]
1751    pub fn from_pixels_erased<P: Pixel>(
1752        pixels: Vec<P>,
1753        width: u32,
1754        height: u32,
1755    ) -> Result<Self, At<BufferError>> {
1756        PixelBuffer::<P>::from_pixels(pixels, width, height)
1757            .at()
1758            .map(PixelBuffer::from)
1759    }
1760
1761    /// Zero-copy access to the pixel data as a typed slice.
1762    ///
1763    /// Returns `Some(&[P])` if the descriptor is layout-compatible with `P`
1764    /// and rows are tightly packed (no stride padding). Returns `None` otherwise.
1765    ///
1766    /// This is a convenience for the common case where you just need a flat
1767    /// pixel slice without imgref metadata. For strided access, use
1768    /// [`try_as_imgref()`](Self::try_as_imgref).
1769    pub fn as_contiguous_pixels<P: Pixel>(&self) -> Option<&[P]> {
1770        if !self.descriptor.layout_compatible(P::DESCRIPTOR) {
1771            return None;
1772        }
1773        let pixel_size = core::mem::size_of::<P>();
1774        let row_bytes = self.width as usize * pixel_size;
1775        if pixel_size == 0 || self.stride != row_bytes {
1776            return None;
1777        }
1778        let total = row_bytes * self.height as usize;
1779        let data = &self.data[self.offset..self.offset + total];
1780        Some(bytemuck::cast_slice(data))
1781    }
1782
1783    /// Consume the buffer and return the pixels as a typed `Vec<P>`.
1784    ///
1785    /// Returns `None` if the descriptor is not layout-compatible with `P`.
1786    /// Strips stride padding if present. Zero-copy when the buffer is
1787    /// tightly packed and `P` has alignment 1 (u8-component types like
1788    /// `Rgb<u8>`, `Rgba<u8>`); copies otherwise.
1789    pub fn into_contiguous_pixels<P: Pixel>(self) -> Option<Vec<P>> {
1790        if !self.descriptor.layout_compatible(P::DESCRIPTOR) {
1791            return None;
1792        }
1793        let pixel_size = core::mem::size_of::<P>();
1794        if pixel_size == 0 {
1795            return None;
1796        }
1797        let row_bytes = self.width as usize * pixel_size;
1798        let total_pixels = self.width as usize * self.height as usize;
1799
1800        if self.stride == row_bytes && self.offset == 0 {
1801            // Fast path: tightly packed, no offset -- try zero-copy reinterpret
1802            let mut data = self.data;
1803            data.truncate(total_pixels * pixel_size);
1804            match bytemuck::try_cast_vec(data) {
1805                Ok(pixels) => return Some(pixels),
1806                Err((_err, data)) => {
1807                    // Alignment mismatch -- copy
1808                    return Some(
1809                        bytemuck::cast_slice::<u8, P>(&data[..total_pixels * pixel_size]).to_vec(),
1810                    );
1811                }
1812            }
1813        }
1814
1815        // Slow path: has offset or stride padding -- copy row by row
1816        let mut out = Vec::with_capacity(total_pixels);
1817        for y in 0..self.height as usize {
1818            let row_start = self.offset + y * self.stride;
1819            let row_data = &self.data[row_start..row_start + row_bytes];
1820            out.extend_from_slice(bytemuck::cast_slice(row_data));
1821        }
1822        Some(out)
1823    }
1824}
1825
1826/// Imgref interop for type-erased PixelBuffer.
1827#[cfg(feature = "imgref")]
1828impl PixelBuffer {
1829    /// Try to borrow the buffer as a typed [`ImgRef`].
1830    ///
1831    /// Returns `None` if the descriptor is not layout-compatible with `P`.
1832    pub fn try_as_imgref<P: Pixel>(&self) -> Option<ImgRef<'_, P>> {
1833        if !self.descriptor.layout_compatible(P::DESCRIPTOR) {
1834            return None;
1835        }
1836        let pixel_size = core::mem::size_of::<P>();
1837        #[allow(clippy::manual_is_multiple_of)] // is_multiple_of stabilized 1.87; MSRV is 1.85
1838        if pixel_size == 0 || self.stride % pixel_size != 0 {
1839            return None;
1840        }
1841        let total_bytes = if self.height == 0 {
1842            0
1843        } else {
1844            (self.height as usize - 1) * self.stride + self.width as usize * pixel_size
1845        };
1846        let data = &self.data[self.offset..self.offset + total_bytes];
1847        let pixels: &[P] = bytemuck::cast_slice(data);
1848        let stride_px = self.stride / pixel_size;
1849        Some(imgref::Img::new_stride(
1850            pixels,
1851            self.width as usize,
1852            self.height as usize,
1853            stride_px,
1854        ))
1855    }
1856
1857    /// Try to borrow the buffer as a typed mutable [`ImgRefMut`](imgref::ImgRefMut).
1858    ///
1859    /// Returns `None` if the descriptor is not layout-compatible with `P`.
1860    pub fn try_as_imgref_mut<P: Pixel>(&mut self) -> Option<imgref::ImgRefMut<'_, P>> {
1861        if !self.descriptor.layout_compatible(P::DESCRIPTOR) {
1862            return None;
1863        }
1864        let pixel_size = core::mem::size_of::<P>();
1865        #[allow(clippy::manual_is_multiple_of)] // is_multiple_of stabilized 1.87; MSRV is 1.85
1866        if pixel_size == 0 || self.stride % pixel_size != 0 {
1867            return None;
1868        }
1869        let total_bytes = if self.height == 0 {
1870            0
1871        } else {
1872            (self.height as usize - 1) * self.stride + self.width as usize * pixel_size
1873        };
1874        let offset = self.offset;
1875        let data = &mut self.data[offset..offset + total_bytes];
1876        let pixels: &mut [P] = bytemuck::cast_slice_mut(data);
1877        let stride_px = self.stride / pixel_size;
1878        Some(imgref::Img::new_stride(
1879            pixels,
1880            self.width as usize,
1881            self.height as usize,
1882            stride_px,
1883        ))
1884    }
1885}
1886
1887impl<P> PixelBuffer<P> {
1888    /// Erase the pixel type, returning a type-erased buffer.
1889    pub fn erase(self) -> PixelBuffer {
1890        PixelBuffer {
1891            data: self.data,
1892            offset: self.offset,
1893            width: self.width,
1894            height: self.height,
1895            stride: self.stride,
1896            descriptor: self.descriptor,
1897
1898            color: self.color,
1899            _pixel: PhantomData,
1900        }
1901    }
1902
1903    /// Try to reinterpret as a typed pixel buffer.
1904    ///
1905    /// Succeeds if the descriptors are layout-compatible.
1906    pub fn try_typed<Q: Pixel>(self) -> Option<PixelBuffer<Q>> {
1907        if self.descriptor.layout_compatible(Q::DESCRIPTOR) {
1908            Some(PixelBuffer {
1909                data: self.data,
1910                offset: self.offset,
1911                width: self.width,
1912                height: self.height,
1913                stride: self.stride,
1914                descriptor: self.descriptor,
1915
1916                color: self.color,
1917                _pixel: PhantomData,
1918            })
1919        } else {
1920            None
1921        }
1922    }
1923
1924    /// Replace the descriptor with a layout-compatible one.
1925    ///
1926    /// See [`PixelSlice::with_descriptor()`] for details.
1927    #[inline]
1928    #[must_use]
1929    pub fn with_descriptor(mut self, descriptor: PixelDescriptor) -> Self {
1930        assert!(
1931            self.descriptor.layout_compatible(descriptor),
1932            "with_descriptor() cannot change physical layout ({} -> {}); \
1933             use reinterpret() for layout changes",
1934            self.descriptor,
1935            descriptor
1936        );
1937        self.descriptor = descriptor;
1938        self
1939    }
1940
1941    /// Reinterpret the buffer with a different physical layout.
1942    ///
1943    /// See [`PixelSlice::reinterpret()`] for details.
1944    #[track_caller]
1945    pub fn reinterpret(mut self, descriptor: PixelDescriptor) -> Result<Self, At<BufferError>> {
1946        if self.descriptor.bytes_per_pixel() != descriptor.bytes_per_pixel() {
1947            return Err(whereat::at!(BufferError::IncompatibleDescriptor));
1948        }
1949        self.descriptor = descriptor;
1950        Ok(self)
1951    }
1952
1953    /// Return a copy with a different transfer function.
1954    #[inline]
1955    #[must_use]
1956    pub fn with_transfer(mut self, tf: TransferFunction) -> Self {
1957        self.descriptor.transfer = tf;
1958        self
1959    }
1960
1961    /// Return a copy with different color primaries.
1962    #[inline]
1963    #[must_use]
1964    pub fn with_primaries(mut self, cp: ColorPrimaries) -> Self {
1965        self.descriptor.primaries = cp;
1966        self
1967    }
1968
1969    /// Return a copy with a different signal range.
1970    #[inline]
1971    #[must_use]
1972    pub fn with_signal_range(mut self, sr: SignalRange) -> Self {
1973        self.descriptor.signal_range = sr;
1974        self
1975    }
1976
1977    /// Return a copy with a different alpha mode.
1978    #[inline]
1979    #[must_use]
1980    pub fn with_alpha_mode(mut self, am: Option<AlphaMode>) -> Self {
1981        self.descriptor.alpha = am;
1982        self
1983    }
1984
1985    /// Whether this buffer carries meaningful alpha data.
1986    #[inline]
1987    pub fn has_alpha(&self) -> bool {
1988        self.descriptor.has_alpha()
1989    }
1990
1991    /// Whether this buffer is grayscale (Gray or GrayAlpha layout).
1992    #[inline]
1993    pub fn is_grayscale(&self) -> bool {
1994        self.descriptor.is_grayscale()
1995    }
1996
1997    /// Consume the buffer and return the backing `Vec<u8>` for pool reuse.
1998    pub fn into_vec(self) -> Vec<u8> {
1999        self.data
2000    }
2001
2002    /// Zero-copy access to the raw pixel bytes when rows are tightly packed.
2003    ///
2004    /// Returns `Some(&[u8])` if `stride == width * bpp` (no padding),
2005    /// `None` if rows have stride padding.
2006    #[inline]
2007    pub fn as_contiguous_bytes(&self) -> Option<&[u8]> {
2008        let bpp = self.descriptor.bytes_per_pixel();
2009        let row_bytes = self.width as usize * bpp;
2010        if self.stride == row_bytes {
2011            let total = row_bytes * self.height as usize;
2012            Some(&self.data[self.offset..self.offset + total])
2013        } else {
2014            None
2015        }
2016    }
2017
2018    /// Copy pixel data to a new contiguous byte `Vec` without stride padding.
2019    ///
2020    /// Returns exactly `width * height * bytes_per_pixel` bytes in row-major order.
2021    /// For buffers already tightly packed (stride == width * bpp), this is a single memcpy.
2022    /// For padded buffers, this strips the padding row by row.
2023    pub fn copy_to_contiguous_bytes(&self) -> Vec<u8> {
2024        let bpp = self.descriptor.bytes_per_pixel();
2025        let row_bytes = self.width as usize * bpp;
2026        let total = row_bytes * self.height as usize;
2027
2028        // Fast path: already contiguous
2029        if self.stride == row_bytes {
2030            let start = self.offset;
2031            return self.data[start..start + total].to_vec();
2032        }
2033
2034        // Slow path: strip padding
2035        let mut out = Vec::with_capacity(total);
2036        let slice = self.as_slice();
2037        for y in 0..self.height {
2038            out.extend_from_slice(slice.row(y));
2039        }
2040        out
2041    }
2042
2043    /// Image width in pixels.
2044    #[inline]
2045    pub fn width(&self) -> u32 {
2046        self.width
2047    }
2048
2049    /// Image height in pixels.
2050    #[inline]
2051    pub fn height(&self) -> u32 {
2052        self.height
2053    }
2054
2055    /// Byte stride between row starts.
2056    #[inline]
2057    pub fn stride(&self) -> usize {
2058        self.stride
2059    }
2060
2061    /// Pixel format descriptor.
2062    #[inline]
2063    pub fn descriptor(&self) -> PixelDescriptor {
2064        self.descriptor
2065    }
2066
2067    /// Source color context (ICC/CICP metadata), if set.
2068    #[inline]
2069    pub fn color_context(&self) -> Option<&Arc<ColorContext>> {
2070        self.color.as_ref()
2071    }
2072
2073    /// Set the color context on this buffer.
2074    #[inline]
2075    #[must_use]
2076    pub fn with_color_context(mut self, ctx: Arc<ColorContext>) -> Self {
2077        self.color = Some(ctx);
2078        self
2079    }
2080
2081    /// Borrow the full buffer as an immutable [`PixelSlice`].
2082    pub fn as_slice(&self) -> PixelSlice<'_, P> {
2083        let total = self.stride * self.height as usize;
2084        PixelSlice {
2085            data: &self.data[self.offset..self.offset + total],
2086            width: self.width,
2087            rows: self.height,
2088            stride: self.stride,
2089            descriptor: self.descriptor,
2090
2091            color: self.color.clone(),
2092            _pixel: PhantomData,
2093        }
2094    }
2095
2096    /// Borrow the full buffer as a mutable [`PixelSliceMut`].
2097    pub fn as_slice_mut(&mut self) -> PixelSliceMut<'_, P> {
2098        let total = self.stride * self.height as usize;
2099        let offset = self.offset;
2100        PixelSliceMut {
2101            data: &mut self.data[offset..offset + total],
2102            width: self.width,
2103            rows: self.height,
2104            stride: self.stride,
2105            descriptor: self.descriptor,
2106
2107            color: self.color.clone(),
2108            _pixel: PhantomData,
2109        }
2110    }
2111
2112    /// Borrow a range of rows as an immutable [`PixelSlice`].
2113    ///
2114    /// # Panics
2115    ///
2116    /// Panics if `y + count > height`.
2117    pub fn rows(&self, y: u32, count: u32) -> PixelSlice<'_, P> {
2118        assert!(
2119            y.checked_add(count).is_some_and(|end| end <= self.height),
2120            "rows({y}, {count}) out of bounds (height: {})",
2121            self.height
2122        );
2123        if count == 0 {
2124            return PixelSlice {
2125                data: &[],
2126                width: self.width,
2127                rows: 0,
2128                stride: self.stride,
2129                descriptor: self.descriptor,
2130
2131                color: self.color.clone(),
2132                _pixel: PhantomData,
2133            };
2134        }
2135        let bpp = self.descriptor.bytes_per_pixel();
2136        let start = self.offset + y as usize * self.stride;
2137        let end = self.offset
2138            + (y as usize + count as usize - 1) * self.stride
2139            + self.width as usize * bpp;
2140        PixelSlice {
2141            data: &self.data[start..end],
2142            width: self.width,
2143            rows: count,
2144            stride: self.stride,
2145            descriptor: self.descriptor,
2146
2147            color: self.color.clone(),
2148            _pixel: PhantomData,
2149        }
2150    }
2151
2152    /// Borrow a range of rows as a mutable [`PixelSliceMut`].
2153    ///
2154    /// # Panics
2155    ///
2156    /// Panics if `y + count > height`.
2157    pub fn rows_mut(&mut self, y: u32, count: u32) -> PixelSliceMut<'_, P> {
2158        assert!(
2159            y.checked_add(count).is_some_and(|end| end <= self.height),
2160            "rows_mut({y}, {count}) out of bounds (height: {})",
2161            self.height
2162        );
2163        if count == 0 {
2164            return PixelSliceMut {
2165                data: &mut [],
2166                width: self.width,
2167                rows: 0,
2168                stride: self.stride,
2169                descriptor: self.descriptor,
2170
2171                color: self.color.clone(),
2172                _pixel: PhantomData,
2173            };
2174        }
2175        let bpp = self.descriptor.bytes_per_pixel();
2176        let start = self.offset + y as usize * self.stride;
2177        let end = self.offset
2178            + (y as usize + count as usize - 1) * self.stride
2179            + self.width as usize * bpp;
2180        PixelSliceMut {
2181            data: &mut self.data[start..end],
2182            width: self.width,
2183            rows: count,
2184            stride: self.stride,
2185            descriptor: self.descriptor,
2186
2187            color: self.color.clone(),
2188            _pixel: PhantomData,
2189        }
2190    }
2191
2192    /// Zero-copy sub-region view (immutable).
2193    ///
2194    /// # Panics
2195    ///
2196    /// Panics if the crop region is out of bounds.
2197    pub fn crop_view(&self, x: u32, y: u32, w: u32, h: u32) -> PixelSlice<'_, P> {
2198        assert!(
2199            x.checked_add(w).is_some_and(|end| end <= self.width),
2200            "crop x={x} w={w} exceeds width {}",
2201            self.width
2202        );
2203        assert!(
2204            y.checked_add(h).is_some_and(|end| end <= self.height),
2205            "crop y={y} h={h} exceeds height {}",
2206            self.height
2207        );
2208        if h == 0 || w == 0 {
2209            return PixelSlice {
2210                data: &[],
2211                width: w,
2212                rows: h,
2213                stride: self.stride,
2214                descriptor: self.descriptor,
2215
2216                color: self.color.clone(),
2217                _pixel: PhantomData,
2218            };
2219        }
2220        let bpp = self.descriptor.bytes_per_pixel();
2221        let start = self.offset + y as usize * self.stride + x as usize * bpp;
2222        let end = self.offset
2223            + (y as usize + h as usize - 1) * self.stride
2224            + (x as usize + w as usize) * bpp;
2225        PixelSlice {
2226            data: &self.data[start..end],
2227            width: w,
2228            rows: h,
2229            stride: self.stride,
2230            descriptor: self.descriptor,
2231
2232            color: self.color.clone(),
2233            _pixel: PhantomData,
2234        }
2235    }
2236
2237    /// Copy a sub-region into a new, tightly-packed [`PixelBuffer`].
2238    ///
2239    /// # Panics
2240    ///
2241    /// Panics if the crop region is out of bounds.
2242    pub fn crop_copy(&self, x: u32, y: u32, w: u32, h: u32) -> PixelBuffer<P> {
2243        let src = self.crop_view(x, y, w, h);
2244        let stride = self.descriptor.aligned_stride(w);
2245        let total = stride * h as usize;
2246        let align = self.descriptor.min_alignment();
2247        let alloc_size = total + align - 1;
2248        let data = vec![0u8; alloc_size];
2249        let offset = align_offset(data.as_ptr(), align);
2250        let mut dst = PixelBuffer {
2251            data,
2252            offset,
2253            width: w,
2254            height: h,
2255            stride,
2256            descriptor: self.descriptor,
2257
2258            color: self.color.clone(),
2259            _pixel: PhantomData,
2260        };
2261        let bpp = self.descriptor.bytes_per_pixel();
2262        let row_bytes = w as usize * bpp;
2263        for row_y in 0..h {
2264            let src_row = src.row(row_y);
2265            let dst_start = dst.offset + row_y as usize * dst.stride;
2266            dst.data[dst_start..dst_start + row_bytes].copy_from_slice(&src_row[..row_bytes]);
2267        }
2268        dst
2269    }
2270}
2271
2272impl<P> fmt::Debug for PixelBuffer<P> {
2273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2274        write!(
2275            f,
2276            "PixelBuffer({}x{}, {:?} {:?})",
2277            self.width,
2278            self.height,
2279            self.descriptor.layout(),
2280            self.descriptor.channel_type()
2281        )
2282    }
2283}
2284
2285// ---------------------------------------------------------------------------
2286// ImgRef -> PixelSlice (zero-copy From impls) -- imgref feature only
2287// ---------------------------------------------------------------------------
2288
2289#[cfg(feature = "imgref")]
2290macro_rules! impl_from_imgref {
2291    ($pixel:ty, $descriptor:expr) => {
2292        impl<'a> From<ImgRef<'a, $pixel>> for PixelSlice<'a, $pixel> {
2293            fn from(img: ImgRef<'a, $pixel>) -> Self {
2294                let bytes: &[u8] = bytemuck::cast_slice(img.buf());
2295                let byte_stride = img.stride() * core::mem::size_of::<$pixel>();
2296                PixelSlice {
2297                    data: bytes,
2298                    width: img.width() as u32,
2299                    rows: img.height() as u32,
2300                    stride: byte_stride,
2301                    descriptor: $descriptor,
2302
2303                    color: None,
2304                    _pixel: PhantomData,
2305                }
2306            }
2307        }
2308    };
2309}
2310
2311// u8 types are conventionally sRGB, f32 types are conventionally linear.
2312// u16 types have no standard convention so use transfer-agnostic descriptors.
2313#[cfg(feature = "imgref")]
2314impl_from_imgref!(Rgb<u8>, PixelDescriptor::RGB8_SRGB);
2315#[cfg(feature = "imgref")]
2316impl_from_imgref!(Rgba<u8>, PixelDescriptor::RGBA8_SRGB);
2317#[cfg(feature = "imgref")]
2318impl_from_imgref!(Rgb<u16>, PixelDescriptor::RGB16);
2319#[cfg(feature = "imgref")]
2320impl_from_imgref!(Rgba<u16>, PixelDescriptor::RGBA16);
2321#[cfg(feature = "imgref")]
2322impl_from_imgref!(Rgb<f32>, PixelDescriptor::RGBF32_LINEAR);
2323#[cfg(feature = "imgref")]
2324impl_from_imgref!(Rgba<f32>, PixelDescriptor::RGBAF32_LINEAR);
2325#[cfg(feature = "imgref")]
2326impl_from_imgref!(Gray<u8>, PixelDescriptor::GRAY8_SRGB);
2327#[cfg(feature = "imgref")]
2328impl_from_imgref!(Gray<u16>, PixelDescriptor::GRAY16);
2329#[cfg(feature = "imgref")]
2330impl_from_imgref!(Gray<f32>, PixelDescriptor::GRAYF32_LINEAR);
2331#[cfg(feature = "imgref")]
2332impl_from_imgref!(BGRA<u8>, PixelDescriptor::BGRA8_SRGB);
2333
2334// ---------------------------------------------------------------------------
2335// ImgRefMut -> PixelSliceMut (zero-copy From impls) -- imgref feature only
2336// ---------------------------------------------------------------------------
2337
2338#[cfg(feature = "imgref")]
2339macro_rules! impl_from_imgref_mut {
2340    ($pixel:ty, $descriptor:expr) => {
2341        impl<'a> From<imgref::ImgRefMut<'a, $pixel>> for PixelSliceMut<'a, $pixel> {
2342            fn from(img: imgref::ImgRefMut<'a, $pixel>) -> Self {
2343                let width = img.width() as u32;
2344                let rows = img.height() as u32;
2345                let byte_stride = img.stride() * core::mem::size_of::<$pixel>();
2346                let buf = img.into_buf();
2347                let bytes: &mut [u8] = bytemuck::cast_slice_mut(buf);
2348                PixelSliceMut {
2349                    data: bytes,
2350                    width,
2351                    rows,
2352                    stride: byte_stride,
2353                    descriptor: $descriptor,
2354
2355                    color: None,
2356                    _pixel: PhantomData,
2357                }
2358            }
2359        }
2360    };
2361}
2362
2363#[cfg(feature = "imgref")]
2364impl_from_imgref_mut!(Rgb<u8>, PixelDescriptor::RGB8_SRGB);
2365#[cfg(feature = "imgref")]
2366impl_from_imgref_mut!(Rgba<u8>, PixelDescriptor::RGBA8_SRGB);
2367#[cfg(feature = "imgref")]
2368impl_from_imgref_mut!(Rgb<u16>, PixelDescriptor::RGB16);
2369#[cfg(feature = "imgref")]
2370impl_from_imgref_mut!(Rgba<u16>, PixelDescriptor::RGBA16);
2371#[cfg(feature = "imgref")]
2372impl_from_imgref_mut!(Rgb<f32>, PixelDescriptor::RGBF32_LINEAR);
2373#[cfg(feature = "imgref")]
2374impl_from_imgref_mut!(Rgba<f32>, PixelDescriptor::RGBAF32_LINEAR);
2375#[cfg(feature = "imgref")]
2376impl_from_imgref_mut!(Gray<u8>, PixelDescriptor::GRAY8_SRGB);
2377#[cfg(feature = "imgref")]
2378impl_from_imgref_mut!(Gray<u16>, PixelDescriptor::GRAY16);
2379#[cfg(feature = "imgref")]
2380impl_from_imgref_mut!(Gray<f32>, PixelDescriptor::GRAYF32_LINEAR);
2381#[cfg(feature = "imgref")]
2382impl_from_imgref_mut!(BGRA<u8>, PixelDescriptor::BGRA8_SRGB);
2383
2384// ---------------------------------------------------------------------------
2385// Typed -> Erased blanket From impls (erase via From)
2386// ---------------------------------------------------------------------------
2387
2388impl<'a, P: Pixel> From<PixelSlice<'a, P>> for PixelSlice<'a> {
2389    fn from(typed: PixelSlice<'a, P>) -> Self {
2390        typed.erase()
2391    }
2392}
2393
2394impl<'a, P: Pixel> From<PixelSliceMut<'a, P>> for PixelSliceMut<'a> {
2395    fn from(typed: PixelSliceMut<'a, P>) -> Self {
2396        typed.erase()
2397    }
2398}
2399
2400impl<P: Pixel> From<PixelBuffer<P>> for PixelBuffer {
2401    fn from(typed: PixelBuffer<P>) -> Self {
2402        typed.erase()
2403    }
2404}
2405
2406// ---------------------------------------------------------------------------
2407// Mutable -> Immutable conversions (zero-copy reborrow / move)
2408// ---------------------------------------------------------------------------
2409
2410/// Consume a mutable pixel slice and produce an immutable one (zero-copy).
2411impl<'a, P> From<PixelSliceMut<'a, P>> for PixelSlice<'a, P> {
2412    fn from(mut_slice: PixelSliceMut<'a, P>) -> Self {
2413        PixelSlice {
2414            data: mut_slice.data,
2415            width: mut_slice.width,
2416            rows: mut_slice.rows,
2417            stride: mut_slice.stride,
2418            descriptor: mut_slice.descriptor,
2419            color: mut_slice.color,
2420            _pixel: PhantomData,
2421        }
2422    }
2423}
2424
2425// ---------------------------------------------------------------------------
2426// Tests
2427// ---------------------------------------------------------------------------
2428
2429#[cfg(test)]
2430mod tests {
2431    use super::*;
2432    use crate::descriptor::{ChannelLayout, ChannelType};
2433    use alloc::format;
2434    use alloc::vec;
2435
2436    // --- PixelBuffer allocation and row access ---
2437
2438    #[test]
2439    fn pixel_buffer_new_rgb8() {
2440        let buf = PixelBuffer::new(10, 5, PixelDescriptor::RGB8_SRGB);
2441        assert_eq!(buf.width(), 10);
2442        assert_eq!(buf.height(), 5);
2443        assert_eq!(buf.stride(), 30);
2444        assert_eq!(buf.descriptor(), PixelDescriptor::RGB8_SRGB);
2445        // All zeros
2446        let slice = buf.as_slice();
2447        assert_eq!(slice.row(0), &[0u8; 30]);
2448        assert_eq!(slice.row(4), &[0u8; 30]);
2449    }
2450
2451    #[test]
2452    fn pixel_buffer_from_vec() {
2453        let data = vec![0u8; 30 * 5];
2454        let buf = PixelBuffer::from_vec(data, 10, 5, PixelDescriptor::RGB8_SRGB).unwrap();
2455        assert_eq!(buf.width(), 10);
2456        assert_eq!(buf.height(), 5);
2457    }
2458
2459    #[test]
2460    fn pixel_buffer_from_vec_too_small() {
2461        let data = vec![0u8; 10];
2462        let err = PixelBuffer::from_vec(data, 10, 5, PixelDescriptor::RGB8_SRGB);
2463        assert_eq!(*err.unwrap_err().error(), BufferError::InsufficientData);
2464    }
2465
2466    #[test]
2467    fn pixel_buffer_into_vec_roundtrip() {
2468        let buf = PixelBuffer::new(4, 4, PixelDescriptor::RGBA8_SRGB);
2469        let v = buf.into_vec();
2470        // Can re-wrap it
2471        let buf2 = PixelBuffer::from_vec(v, 4, 4, PixelDescriptor::RGBA8_SRGB).unwrap();
2472        assert_eq!(buf2.width(), 4);
2473    }
2474
2475    #[test]
2476    fn pixel_buffer_write_and_read() {
2477        let mut buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2478        {
2479            let mut slice = buf.as_slice_mut();
2480            let row = slice.row_mut(0);
2481            row[0] = 255;
2482            row[1] = 128;
2483            row[2] = 64;
2484        }
2485        let slice = buf.as_slice();
2486        assert_eq!(&slice.row(0)[..3], &[255, 128, 64]);
2487        assert_eq!(&slice.row(1)[..3], &[0, 0, 0]);
2488    }
2489
2490    #[test]
2491    fn pixel_buffer_simd_aligned() {
2492        let buf = PixelBuffer::new_simd_aligned(10, 5, PixelDescriptor::RGBA8_SRGB, 64);
2493        assert_eq!(buf.width(), 10);
2494        assert_eq!(buf.height(), 5);
2495        // RGBA8 bpp=4, lcm(4,64)=64, raw=40 -> stride=64
2496        assert_eq!(buf.stride(), 64);
2497        // First row should be 64-byte aligned
2498        let slice = buf.as_slice();
2499        assert_eq!(slice.data.as_ptr() as usize % 64, 0);
2500    }
2501
2502    // --- PixelSlice crop_view ---
2503
2504    #[test]
2505    fn pixel_slice_crop_view() {
2506        // 4x4 RGB8 buffer, fill each row with row index
2507        let mut buf = PixelBuffer::new(4, 4, PixelDescriptor::RGB8_SRGB);
2508        {
2509            let mut slice = buf.as_slice_mut();
2510            for y in 0..4u32 {
2511                let row = slice.row_mut(y);
2512                for byte in row.iter_mut() {
2513                    *byte = y as u8;
2514                }
2515            }
2516        }
2517        // Crop 2x2 starting at (1, 1)
2518        let crop = buf.crop_view(1, 1, 2, 2);
2519        assert_eq!(crop.width(), 2);
2520        assert_eq!(crop.rows(), 2);
2521        // Row 0 of crop = row 1 of original, should be all 1s
2522        assert_eq!(crop.row(0), &[1, 1, 1, 1, 1, 1]);
2523        // Row 1 of crop = row 2 of original, should be all 2s
2524        assert_eq!(crop.row(1), &[2, 2, 2, 2, 2, 2]);
2525    }
2526
2527    #[test]
2528    fn pixel_slice_crop_copy() {
2529        let mut buf = PixelBuffer::new(4, 4, PixelDescriptor::RGB8_SRGB);
2530        {
2531            let mut slice = buf.as_slice_mut();
2532            for y in 0..4u32 {
2533                let row = slice.row_mut(y);
2534                for (i, byte) in row.iter_mut().enumerate() {
2535                    *byte = (y * 100 + i as u32) as u8;
2536                }
2537            }
2538        }
2539        let cropped = buf.crop_copy(1, 1, 2, 2);
2540        assert_eq!(cropped.width(), 2);
2541        assert_eq!(cropped.height(), 2);
2542        // Row 0: original row 1, pixels 1-2 -> bytes [103,104,105, 106,107,108]
2543        assert_eq!(cropped.as_slice().row(0), &[103, 104, 105, 106, 107, 108]);
2544    }
2545
2546    #[test]
2547    fn pixel_slice_sub_rows() {
2548        let mut buf = PixelBuffer::new(2, 4, PixelDescriptor::GRAY8_SRGB);
2549        {
2550            let mut slice = buf.as_slice_mut();
2551            for y in 0..4u32 {
2552                let row = slice.row_mut(y);
2553                row[0] = y as u8 * 10;
2554                row[1] = y as u8 * 10 + 1;
2555            }
2556        }
2557        let sub = buf.rows(1, 2);
2558        assert_eq!(sub.rows(), 2);
2559        assert_eq!(sub.row(0), &[10, 11]);
2560        assert_eq!(sub.row(1), &[20, 21]);
2561    }
2562
2563    // --- PixelSlice validation ---
2564
2565    #[test]
2566    fn pixel_slice_stride_too_small() {
2567        let data = [0u8; 100];
2568        let err = PixelSlice::new(&data, 10, 1, 2, PixelDescriptor::RGB8_SRGB);
2569        assert_eq!(*err.unwrap_err().error(), BufferError::StrideTooSmall);
2570    }
2571
2572    #[test]
2573    fn pixel_slice_insufficient_data() {
2574        let data = [0u8; 10];
2575        let err = PixelSlice::new(&data, 10, 1, 30, PixelDescriptor::RGB8_SRGB);
2576        assert_eq!(*err.unwrap_err().error(), BufferError::InsufficientData);
2577    }
2578
2579    #[test]
2580    fn pixel_slice_zero_rows() {
2581        let data = [0u8; 0];
2582        let slice = PixelSlice::new(&data, 10, 0, 30, PixelDescriptor::RGB8_SRGB).unwrap();
2583        assert_eq!(slice.rows(), 0);
2584    }
2585
2586    #[test]
2587    fn stride_not_pixel_aligned_rejected() {
2588        // RGB8 bpp=3, stride=32 is not a multiple of 3
2589        let data = [0u8; 128];
2590        let err = PixelSlice::new(&data, 10, 1, 32, PixelDescriptor::RGB8_SRGB);
2591        assert_eq!(
2592            *err.unwrap_err().error(),
2593            BufferError::StrideNotPixelAligned
2594        );
2595
2596        // stride=33 IS a multiple of 3 -> accepted
2597        let ok = PixelSlice::new(&data, 10, 1, 33, PixelDescriptor::RGB8_SRGB);
2598        assert!(ok.is_ok());
2599    }
2600
2601    #[test]
2602    fn stride_pixel_aligned_accepted() {
2603        // RGBA8 bpp=4, stride=48 is a multiple of 4
2604        let data = [0u8; 256];
2605        let ok = PixelSlice::new(&data, 10, 2, 48, PixelDescriptor::RGBA8_SRGB);
2606        assert!(ok.is_ok());
2607        let s = ok.unwrap();
2608        assert_eq!(s.stride(), 48);
2609    }
2610
2611    // --- Debug formatting ---
2612
2613    #[test]
2614    fn debug_formats() {
2615        let buf = PixelBuffer::new(10, 5, PixelDescriptor::RGB8_SRGB);
2616        assert_eq!(format!("{buf:?}"), "PixelBuffer(10x5, Rgb U8)");
2617
2618        let slice = buf.as_slice();
2619        assert_eq!(format!("{slice:?}"), "PixelSlice(10x5, Rgb U8)");
2620
2621        let mut buf = PixelBuffer::new(3, 3, PixelDescriptor::RGBA16_SRGB);
2622        let slice_mut = buf.as_slice_mut();
2623        assert_eq!(format!("{slice_mut:?}"), "PixelSliceMut(3x3, Rgba U16)");
2624    }
2625
2626    // --- BufferError Display ---
2627
2628    #[test]
2629    fn buffer_error_display() {
2630        let msg = format!("{}", BufferError::StrideTooSmall);
2631        assert!(msg.contains("stride"));
2632    }
2633
2634    // --- Edge cases ---
2635
2636    #[test]
2637    fn bgrx8_srgb_properties() {
2638        let d = PixelDescriptor::BGRX8_SRGB;
2639        assert_eq!(d.channel_type(), ChannelType::U8);
2640        assert_eq!(d.layout(), ChannelLayout::Bgra);
2641        assert_eq!(d.alpha(), Some(AlphaMode::Undefined));
2642        assert_eq!(d.transfer(), TransferFunction::Srgb);
2643        assert_eq!(d.bytes_per_pixel(), 4);
2644        assert_eq!(d.min_alignment(), 1);
2645        // Layout-compatible with BGRA8
2646        assert!(d.layout_compatible(PixelDescriptor::BGRA8_SRGB));
2647        // BGRX has no meaningful alpha -- the fourth byte is padding
2648        assert!(!d.has_alpha());
2649        // BGRA does have meaningful alpha
2650        assert!(PixelDescriptor::BGRA8_SRGB.has_alpha());
2651        // The layout itself reports an alpha-position channel
2652        assert!(d.layout().has_alpha());
2653    }
2654
2655    #[test]
2656    fn zero_size_buffer() {
2657        let buf = PixelBuffer::new(0, 0, PixelDescriptor::RGB8_SRGB);
2658        assert_eq!(buf.width(), 0);
2659        assert_eq!(buf.height(), 0);
2660        let slice = buf.as_slice();
2661        assert_eq!(slice.rows(), 0);
2662    }
2663
2664    #[test]
2665    fn crop_empty() {
2666        let buf = PixelBuffer::new(4, 4, PixelDescriptor::RGB8_SRGB);
2667        let crop = buf.crop_view(0, 0, 0, 0);
2668        assert_eq!(crop.width(), 0);
2669        assert_eq!(crop.rows(), 0);
2670    }
2671
2672    #[test]
2673    fn sub_rows_empty() {
2674        let buf = PixelBuffer::new(4, 4, PixelDescriptor::RGB8_SRGB);
2675        let sub = buf.rows(2, 0);
2676        assert_eq!(sub.rows(), 0);
2677    }
2678
2679    // --- with_descriptor assertion ---
2680
2681    #[test]
2682    fn with_descriptor_metadata_change_succeeds() {
2683        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2684        // Changing transfer function is metadata-only -- should succeed
2685        let buf2 = buf.with_descriptor(PixelDescriptor::RGB8);
2686        assert_eq!(buf2.descriptor(), PixelDescriptor::RGB8);
2687    }
2688
2689    #[test]
2690    #[should_panic(expected = "with_descriptor() cannot change physical layout")]
2691    fn with_descriptor_layout_change_panics() {
2692        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8);
2693        // Trying to change from RGB8 to RGBA8 -- different layout, should panic
2694        let _ = buf.with_descriptor(PixelDescriptor::RGBA8);
2695    }
2696
2697    #[test]
2698    fn with_descriptor_slice_assertion() {
2699        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2700        let slice = buf.as_slice();
2701        // Metadata change OK
2702        let s2 = slice.with_descriptor(PixelDescriptor::RGB8);
2703        assert_eq!(s2.descriptor(), PixelDescriptor::RGB8);
2704    }
2705
2706    #[test]
2707    #[should_panic(expected = "with_descriptor() cannot change physical layout")]
2708    fn with_descriptor_slice_layout_change_panics() {
2709        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8);
2710        let slice = buf.as_slice();
2711        let _ = slice.with_descriptor(PixelDescriptor::RGBA8);
2712    }
2713
2714    // --- reinterpret ---
2715
2716    #[test]
2717    fn reinterpret_same_bpp_succeeds() {
2718        // RGBA8 -> BGRA8: same 4 bpp, different layout
2719        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8);
2720        let buf2 = buf.reinterpret(PixelDescriptor::BGRA8).unwrap();
2721        assert_eq!(buf2.descriptor().layout(), ChannelLayout::Bgra);
2722    }
2723
2724    #[test]
2725    fn reinterpret_different_bpp_fails() {
2726        // RGB8 (3 bpp) -> RGBA8 (4 bpp): different bytes_per_pixel
2727        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8);
2728        let err = buf.reinterpret(PixelDescriptor::RGBA8);
2729        assert_eq!(
2730            *err.unwrap_err().error(),
2731            BufferError::IncompatibleDescriptor
2732        );
2733    }
2734
2735    #[test]
2736    fn reinterpret_rgbx_to_rgba() {
2737        // RGBX8 -> RGBA8: same bpp (4), reinterpret padding as alpha
2738        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBX8);
2739        let buf2 = buf.reinterpret(PixelDescriptor::RGBA8).unwrap();
2740        assert!(buf2.descriptor().has_alpha());
2741    }
2742
2743    // --- Per-field metadata setters ---
2744
2745    #[test]
2746    fn per_field_setters() {
2747        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8);
2748        let buf = buf.with_transfer(TransferFunction::Srgb);
2749        assert_eq!(buf.descriptor().transfer(), TransferFunction::Srgb);
2750        let buf = buf.with_primaries(ColorPrimaries::DisplayP3);
2751        assert_eq!(buf.descriptor().primaries, ColorPrimaries::DisplayP3);
2752        let buf = buf.with_signal_range(SignalRange::Narrow);
2753        assert!(matches!(buf.descriptor().signal_range, SignalRange::Narrow));
2754        let buf = buf.with_alpha_mode(Some(AlphaMode::Premultiplied));
2755        assert_eq!(buf.descriptor().alpha(), Some(AlphaMode::Premultiplied));
2756    }
2757
2758    // --- copy_to_contiguous_bytes ---
2759
2760    #[test]
2761    fn copy_to_contiguous_bytes_tight() {
2762        let mut buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2763        {
2764            let mut s = buf.as_slice_mut();
2765            s.row_mut(0).copy_from_slice(&[1, 2, 3, 4, 5, 6]);
2766            s.row_mut(1).copy_from_slice(&[7, 8, 9, 10, 11, 12]);
2767        }
2768        let bytes = buf.copy_to_contiguous_bytes();
2769        assert_eq!(bytes, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
2770    }
2771
2772    #[test]
2773    fn copy_to_contiguous_bytes_padded() {
2774        // Use SIMD-aligned buffer which will have stride padding for small widths
2775        let mut buf = PixelBuffer::new_simd_aligned(2, 2, PixelDescriptor::RGB8_SRGB, 16);
2776        let stride = buf.stride();
2777        // Stride should be >= 6 (2 pixels * 3 bytes) and aligned to lcm(3, 16) = 48
2778        assert!(stride >= 6);
2779        {
2780            let mut s = buf.as_slice_mut();
2781            s.row_mut(0).copy_from_slice(&[1, 2, 3, 4, 5, 6]);
2782            s.row_mut(1).copy_from_slice(&[7, 8, 9, 10, 11, 12]);
2783        }
2784        let bytes = buf.copy_to_contiguous_bytes();
2785        // Should only contain the actual pixel data, no padding
2786        assert_eq!(bytes.len(), 12);
2787        assert_eq!(bytes, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
2788    }
2789
2790    // --- BufferError Display for all variants ---
2791
2792    #[test]
2793    fn buffer_error_display_alignment_violation() {
2794        let msg = format!("{}", BufferError::AlignmentViolation);
2795        assert_eq!(msg, "data is not aligned for the channel type");
2796    }
2797
2798    #[test]
2799    fn buffer_error_display_insufficient_data() {
2800        let msg = format!("{}", BufferError::InsufficientData);
2801        assert_eq!(msg, "data slice is too small for the given dimensions");
2802    }
2803
2804    #[test]
2805    fn buffer_error_display_invalid_dimensions() {
2806        let msg = format!("{}", BufferError::InvalidDimensions);
2807        assert_eq!(msg, "width or height is zero or causes overflow");
2808    }
2809
2810    #[test]
2811    fn buffer_error_display_incompatible_descriptor() {
2812        let msg = format!("{}", BufferError::IncompatibleDescriptor);
2813        assert_eq!(msg, "new descriptor has different bytes_per_pixel");
2814    }
2815
2816    #[test]
2817    fn buffer_error_display_allocation_failed() {
2818        let msg = format!("{}", BufferError::AllocationFailed);
2819        assert_eq!(msg, "buffer allocation failed");
2820    }
2821
2822    // --- PixelSlice::is_contiguous ---
2823
2824    #[test]
2825    fn pixel_slice_is_contiguous_tight() {
2826        // Tight buffer: stride == width * bpp
2827        let buf = PixelBuffer::new(4, 3, PixelDescriptor::RGBA8_SRGB);
2828        let slice = buf.as_slice();
2829        // stride should be 4 * 4 = 16
2830        assert_eq!(slice.stride(), 16);
2831        assert!(slice.is_contiguous());
2832    }
2833
2834    // --- PixelSlice::as_contiguous_bytes ---
2835
2836    #[test]
2837    fn pixel_slice_as_contiguous_bytes_tight() {
2838        let mut buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8_SRGB);
2839        {
2840            let mut s = buf.as_slice_mut();
2841            s.row_mut(0).copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
2842            s.row_mut(1)
2843                .copy_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
2844        }
2845        let slice = buf.as_slice();
2846        let bytes = slice.as_contiguous_bytes();
2847        assert!(bytes.is_some());
2848        assert_eq!(
2849            bytes.unwrap(),
2850            &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
2851        );
2852    }
2853
2854    #[test]
2855    fn pixel_slice_as_contiguous_bytes_padded_returns_none() {
2856        // SIMD-aligned buffer will have stride padding for small widths
2857        let buf = PixelBuffer::new_simd_aligned(2, 2, PixelDescriptor::RGB8_SRGB, 16);
2858        let slice = buf.as_slice();
2859        // stride > width * bpp, so not contiguous
2860        assert!(slice.stride() > 6);
2861        assert!(!slice.is_contiguous());
2862        assert!(slice.as_contiguous_bytes().is_none());
2863    }
2864
2865    // --- PixelSlice::as_strided_bytes ---
2866
2867    #[test]
2868    fn pixel_slice_as_strided_bytes_tight() {
2869        let mut buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8_SRGB);
2870        {
2871            let mut s = buf.as_slice_mut();
2872            s.row_mut(0).copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
2873            s.row_mut(1)
2874                .copy_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
2875        }
2876        let slice = buf.as_slice();
2877        let bytes = slice.as_strided_bytes();
2878        // Tight layout: strided bytes == contiguous bytes
2879        assert_eq!(
2880            bytes,
2881            &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
2882        );
2883    }
2884
2885    #[test]
2886    fn pixel_slice_as_strided_bytes_padded() {
2887        let buf = PixelBuffer::new_simd_aligned(2, 2, PixelDescriptor::RGB8_SRGB, 16);
2888        let slice = buf.as_slice();
2889        let stride = slice.stride();
2890        assert!(stride > 6, "expected padding for SIMD alignment");
2891        let bytes = slice.as_strided_bytes();
2892        // Total length includes stride padding between and after rows
2893        assert_eq!(bytes.len(), stride * 2);
2894    }
2895
2896    #[test]
2897    fn pixel_slice_as_strided_bytes_sub_rows() {
2898        let buf = PixelBuffer::new_simd_aligned(2, 4, PixelDescriptor::RGB8_SRGB, 16);
2899        let slice = buf.as_slice();
2900        let stride = slice.stride();
2901        let sub = slice.sub_rows(1, 2);
2902        let bytes = sub.as_strided_bytes();
2903        // sub_rows trims trailing padding on last row: (count-1)*stride + width*bpp
2904        let expected_len = stride + 2 * 3; // 1 stride + 6 pixel bytes
2905        assert_eq!(bytes.len(), expected_len);
2906    }
2907
2908    #[test]
2909    fn pixel_slice_mut_as_strided_bytes() {
2910        let mut buf = PixelBuffer::new_simd_aligned(2, 2, PixelDescriptor::RGB8_SRGB, 16);
2911        let mut slice = buf.as_slice_mut();
2912        let stride = slice.stride();
2913        // Write via as_strided_bytes_mut
2914        let bytes = slice.as_strided_bytes_mut();
2915        assert_eq!(bytes.len(), stride * 2);
2916        bytes[0] = 42;
2917        // Verify through row accessor
2918        assert_eq!(slice.row(0)[0], 42);
2919    }
2920
2921    // --- PixelSlice::row_with_stride ---
2922
2923    #[test]
2924    fn pixel_slice_row_with_stride_padded() {
2925        // Create a padded buffer via SIMD alignment
2926        let buf = PixelBuffer::new_simd_aligned(2, 2, PixelDescriptor::RGBA8_SRGB, 64);
2927        let slice = buf.as_slice();
2928        let stride = slice.stride();
2929        // stride should be >= 8 (2 * 4 bpp) and aligned
2930        assert!(stride >= 8);
2931        // row_with_stride returns the full stride bytes including padding
2932        let full_row = slice.row_with_stride(0);
2933        assert_eq!(full_row.len(), stride);
2934        // row() returns only the pixel data (no padding)
2935        let pixel_row = slice.row(0);
2936        assert_eq!(pixel_row.len(), 8); // 2 pixels * 4 bytes
2937    }
2938
2939    // --- PixelBuffer::has_alpha ---
2940
2941    #[test]
2942    fn pixel_buffer_has_alpha_rgba8() {
2943        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8_SRGB);
2944        assert!(buf.has_alpha());
2945    }
2946
2947    #[test]
2948    fn pixel_buffer_has_alpha_rgb8_false() {
2949        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2950        assert!(!buf.has_alpha());
2951    }
2952
2953    // --- PixelBuffer::is_grayscale ---
2954
2955    #[test]
2956    fn pixel_buffer_is_grayscale_gray8() {
2957        let buf = PixelBuffer::new(2, 2, PixelDescriptor::GRAY8_SRGB);
2958        assert!(buf.is_grayscale());
2959    }
2960
2961    #[test]
2962    fn pixel_buffer_is_grayscale_rgb8_false() {
2963        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
2964        assert!(!buf.is_grayscale());
2965    }
2966
2967    // --- PixelSlice::try_typed (requires "rgb" feature) ---
2968
2969    #[cfg(feature = "rgb")]
2970    #[test]
2971    fn pixel_slice_try_typed_success() {
2972        use rgb::Rgba;
2973
2974        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8_SRGB);
2975        let slice = buf.as_slice();
2976        let typed: Option<PixelSlice<'_, Rgba<u8>>> = slice.try_typed();
2977        assert!(typed.is_some());
2978        let typed = typed.unwrap();
2979        assert_eq!(typed.width(), 2);
2980        assert_eq!(typed.rows(), 2);
2981    }
2982
2983    // --- PixelBuffer::try_typed ---
2984
2985    #[cfg(feature = "rgb")]
2986    #[test]
2987    fn pixel_buffer_try_typed_success() {
2988        use rgb::Rgba;
2989
2990        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGBA8_SRGB);
2991        let typed: Option<PixelBuffer<Rgba<u8>>> = buf.try_typed();
2992        assert!(typed.is_some());
2993        let typed = typed.unwrap();
2994        assert_eq!(typed.width(), 2);
2995        assert_eq!(typed.height(), 2);
2996    }
2997
2998    #[cfg(feature = "rgb")]
2999    #[test]
3000    fn pixel_buffer_try_typed_failure_wrong_layout() {
3001        use rgb::Rgba;
3002
3003        // RGB8 buffer cannot be typed as Rgba<u8>
3004        let buf = PixelBuffer::new(2, 2, PixelDescriptor::RGB8_SRGB);
3005        let typed: Option<PixelBuffer<Rgba<u8>>> = buf.try_typed();
3006        assert!(typed.is_none());
3007    }
3008}
3009
3010#[cfg(all(test, feature = "imgref"))]
3011mod buffer_tests {
3012    use super::*;
3013    use alloc::vec;
3014    use alloc::vec::Vec;
3015    use rgb::{Gray, Rgb};
3016
3017    // --- ImgRef -> PixelSlice -> row access ---
3018
3019    #[test]
3020    fn imgref_to_pixel_slice_rgb8() {
3021        let pixels: Vec<Rgb<u8>> = vec![
3022            Rgb {
3023                r: 10,
3024                g: 20,
3025                b: 30,
3026            },
3027            Rgb {
3028                r: 40,
3029                g: 50,
3030                b: 60,
3031            },
3032            Rgb {
3033                r: 70,
3034                g: 80,
3035                b: 90,
3036            },
3037            Rgb {
3038                r: 100,
3039                g: 110,
3040                b: 120,
3041            },
3042        ];
3043        let img = imgref::Img::new(pixels.as_slice(), 2, 2);
3044        let slice: PixelSlice<'_, Rgb<u8>> = img.into();
3045        assert_eq!(slice.width(), 2);
3046        assert_eq!(slice.rows(), 2);
3047        assert_eq!(slice.row(0), &[10, 20, 30, 40, 50, 60]);
3048        assert_eq!(slice.row(1), &[70, 80, 90, 100, 110, 120]);
3049    }
3050
3051    #[test]
3052    fn imgref_to_pixel_slice_gray16() {
3053        let pixels = vec![Gray::new(1000u16), Gray::new(2000u16)];
3054        let img = imgref::Img::new(pixels.as_slice(), 2, 1);
3055        let slice: PixelSlice<'_, Gray<u16>> = img.into();
3056        assert_eq!(slice.width(), 2);
3057        assert_eq!(slice.rows(), 1);
3058        assert_eq!(slice.descriptor(), PixelDescriptor::GRAY16);
3059        // Bytes should be native-endian u16
3060        let row = slice.row(0);
3061        assert_eq!(row.len(), 4);
3062        let v0 = u16::from_ne_bytes([row[0], row[1]]);
3063        let v1 = u16::from_ne_bytes([row[2], row[3]]);
3064        assert_eq!(v0, 1000);
3065        assert_eq!(v1, 2000);
3066    }
3067
3068    // --- from_pixels_erased ---
3069
3070    #[test]
3071    fn from_pixels_erased_matches_manual() {
3072        let pixels1: Vec<Rgb<u8>> = vec![
3073            Rgb {
3074                r: 10,
3075                g: 20,
3076                b: 30,
3077            },
3078            Rgb {
3079                r: 40,
3080                g: 50,
3081                b: 60,
3082            },
3083        ];
3084        let pixels2 = pixels1.clone();
3085
3086        // Manual: from_pixels + into
3087        let manual: PixelBuffer = PixelBuffer::from_pixels(pixels1, 2, 1).unwrap().into();
3088
3089        // Erased: from_pixels_erased
3090        let erased = PixelBuffer::from_pixels_erased(pixels2, 2, 1).unwrap();
3091
3092        assert_eq!(manual.width(), erased.width());
3093        assert_eq!(manual.height(), erased.height());
3094        assert_eq!(manual.descriptor(), erased.descriptor());
3095        assert_eq!(manual.as_slice().row(0), erased.as_slice().row(0));
3096    }
3097
3098    #[test]
3099    fn from_pixels_erased_dimension_mismatch() {
3100        let pixels: Vec<Rgb<u8>> = vec![Rgb { r: 1, g: 2, b: 3 }];
3101        let err = PixelBuffer::from_pixels_erased(pixels, 2, 1);
3102        assert_eq!(*err.unwrap_err().error(), BufferError::InvalidDimensions);
3103    }
3104}