Skip to main content

spatialrust_image/
lib.rs

1//! Typed, CPU-resident image buffers and zero-copy strided views.
2//!
3//! Channel count is part of the type. Packed interleaved ownership is the
4//! default; planar ownership and explicitly-strided views are also available.
5//! Device-backed images belong in dedicated GPU crates so transfers remain
6//! explicit.
7
8#![deny(unsafe_code)]
9#![warn(missing_docs)]
10
11use std::ops::{Index, IndexMut};
12
13/// Errors raised while constructing or indexing images.
14#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
15pub enum ImageError {
16    /// A zero channel image type was requested.
17    #[error("image channel count must be greater than zero")]
18    ZeroChannels,
19    /// Image dimensions overflowed `usize` arithmetic.
20    #[error("image dimensions overflow addressable memory")]
21    DimensionOverflow,
22    /// The provided storage does not match the image layout.
23    #[error("image storage is too short: need at least {required} elements, found {found}")]
24    StorageTooShort {
25        /// Minimum required element count.
26        required: usize,
27        /// Provided element count.
28        found: usize,
29    },
30    /// A row stride cannot hold one row of pixels.
31    #[error("row stride {stride} is smaller than packed row width {minimum}")]
32    InvalidStride {
33        /// Provided stride in scalar elements.
34        stride: usize,
35        /// Packed row size in scalar elements.
36        minimum: usize,
37    },
38    /// A planar channel stride overlaps the preceding channel plane.
39    #[error("plane stride {stride} is smaller than one plane span {minimum}")]
40    InvalidPlaneStride {
41        /// Provided plane stride in scalar elements.
42        stride: usize,
43        /// Minimum non-overlapping plane span.
44        minimum: usize,
45    },
46    /// Color metadata is incompatible with the compile-time channel count.
47    #[error("color space {color_space:?} requires {expected} channels, image has {found}")]
48    MetadataChannelMismatch {
49        /// Declared color space.
50        color_space: ColorSpace,
51        /// Required channel count.
52        expected: usize,
53        /// Image channel count.
54        found: usize,
55    },
56    /// A requested image region lies outside its parent image.
57    #[error("region ({x}, {y}, {region_width}, {region_height}) exceeds image bounds {image_width}x{image_height}")]
58    InvalidRegion {
59        /// Region x origin.
60        x: usize,
61        /// Region y origin.
62        y: usize,
63        /// Region width.
64        region_width: usize,
65        /// Region height.
66        region_height: usize,
67        /// Parent image width.
68        image_width: usize,
69        /// Parent image height.
70        image_height: usize,
71    },
72    /// Pixel coordinates were outside the image.
73    #[error("pixel ({x}, {y}) is outside image bounds {width}x{height}")]
74    OutOfBounds {
75        /// Pixel x coordinate.
76        x: usize,
77        /// Pixel y coordinate.
78        y: usize,
79        /// Image width.
80        width: usize,
81        /// Image height.
82        height: usize,
83    },
84}
85
86/// Physical channel arrangement in CPU storage.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
88pub enum ImageLayout {
89    /// Pixel channels are adjacent (`RGBRGB...`).
90    #[default]
91    Interleaved,
92    /// Each channel occupies a separate plane (`RR...GG...BB...`).
93    Planar,
94}
95
96/// Semantic interpretation of image channels.
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
98pub enum ColorSpace {
99    /// Channel semantics are not specified.
100    #[default]
101    Unknown,
102    /// One-channel luminance.
103    Gray,
104    /// Nonlinear red, green, blue.
105    Rgb,
106    /// Nonlinear blue, green, red.
107    Bgr,
108    /// Nonlinear red, green, blue, alpha.
109    Rgba,
110    /// Nonlinear blue, green, red, alpha.
111    Bgra,
112    /// Linear-light red, green, blue.
113    LinearRgb,
114    /// Hue, saturation, value.
115    Hsv,
116    /// Metric or sensor depth values.
117    Depth,
118    /// Integer semantic or instance labels.
119    Label,
120}
121
122impl ColorSpace {
123    /// Returns the required channel count when the color space fixes one.
124    #[must_use]
125    pub const fn required_channels(self) -> Option<usize> {
126        match self {
127            Self::Unknown => None,
128            Self::Gray | Self::Depth | Self::Label => Some(1),
129            Self::Rgb | Self::Bgr | Self::LinearRgb | Self::Hsv => Some(3),
130            Self::Rgba | Self::Bgra => Some(4),
131        }
132    }
133}
134
135/// Numeric range convention associated with image channels.
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
137pub enum ColorRange {
138    /// Range is not specified or is naturally unbounded (for example depth).
139    #[default]
140    Unspecified,
141    /// Full dtype or normalized range.
142    Full,
143    /// Video-range encoding such as limited-range YUV.
144    Limited,
145}
146
147/// Alpha-channel interpretation.
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
149pub enum AlphaMode {
150    /// No alpha channel is declared.
151    #[default]
152    None,
153    /// Straight (unassociated) alpha.
154    Straight,
155    /// RGB values are premultiplied by alpha.
156    Premultiplied,
157}
158
159/// Lightweight semantic metadata carried by images and borrowed views.
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
161pub struct ImageMetadata {
162    /// Channel color/depth/label interpretation.
163    pub color_space: ColorSpace,
164    /// Numeric range convention.
165    pub color_range: ColorRange,
166    /// Alpha convention.
167    pub alpha_mode: AlphaMode,
168}
169
170impl ImageMetadata {
171    /// Validates metadata against a compile-time channel count.
172    pub fn validate<const CHANNELS: usize>(self) -> Result<(), ImageError> {
173        if CHANNELS == 0 {
174            return Err(ImageError::ZeroChannels);
175        }
176        if let Some(expected) = self.color_space.required_channels() {
177            if expected != CHANNELS {
178                return Err(ImageError::MetadataChannelMismatch {
179                    color_space: self.color_space,
180                    expected,
181                    found: CHANNELS,
182                });
183            }
184        }
185        Ok(())
186    }
187}
188
189/// A checked rectangular image region.
190#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
191pub struct ImageRegion {
192    /// Horizontal origin.
193    pub x: usize,
194    /// Vertical origin.
195    pub y: usize,
196    /// Region width.
197    pub width: usize,
198    /// Region height.
199    pub height: usize,
200}
201
202impl ImageRegion {
203    /// Creates a rectangular region.
204    #[must_use]
205    pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
206        Self { x, y, width, height }
207    }
208
209    fn validate(self, image_width: usize, image_height: usize) -> Result<(), ImageError> {
210        let end_x = self.x.checked_add(self.width).ok_or(ImageError::DimensionOverflow)?;
211        let end_y = self.y.checked_add(self.height).ok_or(ImageError::DimensionOverflow)?;
212        if end_x > image_width || end_y > image_height {
213            return Err(ImageError::InvalidRegion {
214                x: self.x,
215                y: self.y,
216                region_width: self.width,
217                region_height: self.height,
218                image_width,
219                image_height,
220            });
221        }
222        Ok(())
223    }
224}
225
226fn packed_len<const CHANNELS: usize>(width: usize, height: usize) -> Result<usize, ImageError> {
227    if CHANNELS == 0 {
228        return Err(ImageError::ZeroChannels);
229    }
230    width
231        .checked_mul(height)
232        .and_then(|value| value.checked_mul(CHANNELS))
233        .ok_or(ImageError::DimensionOverflow)
234}
235
236fn strided_span(height: usize, row_stride: usize, packed_row: usize) -> Result<usize, ImageError> {
237    if height == 0 {
238        return Ok(0);
239    }
240    row_stride
241        .checked_mul(height - 1)
242        .and_then(|offset| offset.checked_add(packed_row))
243        .ok_or(ImageError::DimensionOverflow)
244}
245
246/// An owning, densely packed, interleaved image.
247#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct Image<T, const CHANNELS: usize> {
249    width: usize,
250    height: usize,
251    data: Vec<T>,
252    metadata: ImageMetadata,
253}
254
255impl<T, const CHANNELS: usize> Image<T, CHANNELS> {
256    /// Creates an image from densely packed, interleaved scalar elements.
257    pub fn try_new(width: usize, height: usize, data: Vec<T>) -> Result<Self, ImageError> {
258        let required = packed_len::<CHANNELS>(width, height)?;
259        if data.len() != required {
260            return Err(ImageError::StorageTooShort { required, found: data.len() });
261        }
262        Ok(Self { width, height, data, metadata: ImageMetadata::default() })
263    }
264
265    /// Creates a packed image and validates its semantic metadata.
266    pub fn try_new_with_metadata(
267        width: usize,
268        height: usize,
269        data: Vec<T>,
270        metadata: ImageMetadata,
271    ) -> Result<Self, ImageError> {
272        metadata.validate::<CHANNELS>()?;
273        let mut image = Self::try_new(width, height, data)?;
274        image.metadata = metadata;
275        Ok(image)
276    }
277
278    /// Returns image width in pixels.
279    #[must_use]
280    pub const fn width(&self) -> usize {
281        self.width
282    }
283
284    /// Returns image height in pixels.
285    #[must_use]
286    pub const fn height(&self) -> usize {
287        self.height
288    }
289
290    /// Returns the packed row stride in scalar elements.
291    #[must_use]
292    pub const fn row_stride(&self) -> usize {
293        self.width * CHANNELS
294    }
295
296    /// Returns physical channel layout.
297    #[must_use]
298    pub const fn layout(&self) -> ImageLayout {
299        ImageLayout::Interleaved
300    }
301
302    /// Returns semantic image metadata.
303    #[must_use]
304    pub const fn metadata(&self) -> ImageMetadata {
305        self.metadata
306    }
307
308    /// Replaces semantic metadata after validating the channel count.
309    pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
310        metadata.validate::<CHANNELS>()?;
311        self.metadata = metadata;
312        Ok(())
313    }
314
315    /// Returns the packed scalar storage.
316    #[must_use]
317    pub fn as_slice(&self) -> &[T] {
318        &self.data
319    }
320
321    /// Returns mutable packed scalar storage.
322    #[must_use]
323    pub fn as_mut_slice(&mut self) -> &mut [T] {
324        &mut self.data
325    }
326
327    /// Borrows this image as a zero-copy view.
328    #[must_use]
329    pub fn view(&self) -> ImageView<'_, T, CHANNELS> {
330        ImageView {
331            width: self.width,
332            height: self.height,
333            row_stride: self.row_stride(),
334            data: &self.data,
335            metadata: self.metadata,
336        }
337    }
338
339    /// Borrows this image as a mutable zero-copy view.
340    #[must_use]
341    pub fn view_mut(&mut self) -> ImageViewMut<'_, T, CHANNELS> {
342        ImageViewMut {
343            width: self.width,
344            height: self.height,
345            row_stride: self.width * CHANNELS,
346            data: &mut self.data,
347            metadata: self.metadata,
348        }
349    }
350
351    /// Returns one pixel, or `None` outside image bounds.
352    #[must_use]
353    pub fn get(&self, x: usize, y: usize) -> Option<&[T; CHANNELS]> {
354        self.view().get(x, y)
355    }
356
357    /// Returns one mutable pixel, or `None` outside image bounds.
358    #[must_use]
359    pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut [T; CHANNELS]> {
360        if x >= self.width || y >= self.height {
361            return None;
362        }
363        let offset = (y * self.width + x) * CHANNELS;
364        self.data.get_mut(offset..offset + CHANNELS)?.try_into().ok()
365    }
366
367    /// Consumes the image and returns its packed scalar storage.
368    #[must_use]
369    pub fn into_vec(self) -> Vec<T> {
370        self.data
371    }
372}
373
374impl<T: Clone, const CHANNELS: usize> Image<T, CHANNELS> {
375    /// Creates a densely packed image filled with one pixel value.
376    pub fn from_pixel(
377        width: usize,
378        height: usize,
379        pixel: [T; CHANNELS],
380    ) -> Result<Self, ImageError> {
381        let required = packed_len::<CHANNELS>(width, height)?;
382        let mut data = Vec::with_capacity(required);
383        for _ in 0..width.saturating_mul(height) {
384            data.extend_from_slice(&pixel);
385        }
386        Self::try_new(width, height, data)
387    }
388}
389
390impl<T, const CHANNELS: usize> Index<(usize, usize)> for Image<T, CHANNELS> {
391    type Output = [T; CHANNELS];
392
393    fn index(&self, (x, y): (usize, usize)) -> &Self::Output {
394        self.get(x, y).expect("image index out of bounds")
395    }
396}
397
398impl<T, const CHANNELS: usize> IndexMut<(usize, usize)> for Image<T, CHANNELS> {
399    fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Self::Output {
400        self.get_mut(x, y).expect("image index out of bounds")
401    }
402}
403
404/// A read-only, zero-copy image view with an explicit row stride.
405#[derive(Clone, Copy, Debug)]
406pub struct ImageView<'a, T, const CHANNELS: usize> {
407    width: usize,
408    height: usize,
409    row_stride: usize,
410    data: &'a [T],
411    metadata: ImageMetadata,
412}
413
414impl<'a, T, const CHANNELS: usize> ImageView<'a, T, CHANNELS> {
415    /// Creates a view over interleaved storage.
416    ///
417    /// `row_stride` is measured in scalar elements, not bytes.
418    pub fn new(
419        width: usize,
420        height: usize,
421        row_stride: usize,
422        data: &'a [T],
423    ) -> Result<Self, ImageError> {
424        Self::new_with_metadata(width, height, row_stride, data, ImageMetadata::default())
425    }
426
427    /// Creates a view with validated semantic metadata.
428    pub fn new_with_metadata(
429        width: usize,
430        height: usize,
431        row_stride: usize,
432        data: &'a [T],
433        metadata: ImageMetadata,
434    ) -> Result<Self, ImageError> {
435        metadata.validate::<CHANNELS>()?;
436        let packed_row = width.checked_mul(CHANNELS).ok_or(ImageError::DimensionOverflow)?;
437        if row_stride < packed_row {
438            return Err(ImageError::InvalidStride { stride: row_stride, minimum: packed_row });
439        }
440        let required = if height == 0 {
441            0
442        } else {
443            row_stride
444                .checked_mul(height - 1)
445                .and_then(|offset| offset.checked_add(packed_row))
446                .ok_or(ImageError::DimensionOverflow)?
447        };
448        if data.len() < required {
449            return Err(ImageError::StorageTooShort { required, found: data.len() });
450        }
451        Ok(Self { width, height, row_stride, data, metadata })
452    }
453
454    /// Returns image width in pixels.
455    #[must_use]
456    pub const fn width(self) -> usize {
457        self.width
458    }
459
460    /// Returns image height in pixels.
461    #[must_use]
462    pub const fn height(self) -> usize {
463        self.height
464    }
465
466    /// Returns row stride in scalar elements.
467    #[must_use]
468    pub const fn row_stride(self) -> usize {
469        self.row_stride
470    }
471
472    /// Returns physical channel layout.
473    #[must_use]
474    pub const fn layout(self) -> ImageLayout {
475        ImageLayout::Interleaved
476    }
477
478    /// Returns semantic image metadata.
479    #[must_use]
480    pub const fn metadata(self) -> ImageMetadata {
481        self.metadata
482    }
483
484    /// Returns one pixel, or `None` outside image bounds.
485    #[must_use]
486    pub fn get(self, x: usize, y: usize) -> Option<&'a [T; CHANNELS]> {
487        if x >= self.width || y >= self.height {
488            return None;
489        }
490        let offset = y * self.row_stride + x * CHANNELS;
491        self.data.get(offset..offset + CHANNELS)?.try_into().ok()
492    }
493
494    /// Returns a packed row without its trailing padding.
495    #[must_use]
496    pub fn row(self, y: usize) -> Option<&'a [T]> {
497        if y >= self.height {
498            return None;
499        }
500        let start = y * self.row_stride;
501        self.data.get(start..start + self.width * CHANNELS)
502    }
503
504    /// Creates a checked zero-copy subview.
505    pub fn subview(self, region: ImageRegion) -> Result<Self, ImageError> {
506        region.validate(self.width, self.height)?;
507        if region.width == 0 || region.height == 0 {
508            return Ok(Self {
509                width: region.width,
510                height: region.height,
511                row_stride: self.row_stride,
512                data: &self.data[..0],
513                metadata: self.metadata,
514            });
515        }
516        let start = region.y * self.row_stride + region.x * CHANNELS;
517        let span = (region.height - 1) * self.row_stride + region.width * CHANNELS;
518        Ok(Self {
519            width: region.width,
520            height: region.height,
521            row_stride: self.row_stride,
522            data: &self.data[start..start + span],
523            metadata: self.metadata,
524        })
525    }
526}
527
528/// A mutable, zero-copy interleaved image view with an explicit row stride.
529#[derive(Debug)]
530pub struct ImageViewMut<'a, T, const CHANNELS: usize> {
531    width: usize,
532    height: usize,
533    row_stride: usize,
534    data: &'a mut [T],
535    metadata: ImageMetadata,
536}
537
538impl<'a, T, const CHANNELS: usize> ImageViewMut<'a, T, CHANNELS> {
539    /// Creates a mutable view over interleaved storage.
540    pub fn new(
541        width: usize,
542        height: usize,
543        row_stride: usize,
544        data: &'a mut [T],
545    ) -> Result<Self, ImageError> {
546        Self::new_with_metadata(width, height, row_stride, data, ImageMetadata::default())
547    }
548
549    /// Creates a mutable view with validated semantic metadata.
550    pub fn new_with_metadata(
551        width: usize,
552        height: usize,
553        row_stride: usize,
554        data: &'a mut [T],
555        metadata: ImageMetadata,
556    ) -> Result<Self, ImageError> {
557        metadata.validate::<CHANNELS>()?;
558        let packed_row = width.checked_mul(CHANNELS).ok_or(ImageError::DimensionOverflow)?;
559        if row_stride < packed_row {
560            return Err(ImageError::InvalidStride { stride: row_stride, minimum: packed_row });
561        }
562        let required = strided_span(height, row_stride, packed_row)?;
563        if data.len() < required {
564            return Err(ImageError::StorageTooShort { required, found: data.len() });
565        }
566        Ok(Self { width, height, row_stride, data, metadata })
567    }
568
569    /// Returns image width in pixels.
570    #[must_use]
571    pub const fn width(&self) -> usize {
572        self.width
573    }
574
575    /// Returns image height in pixels.
576    #[must_use]
577    pub const fn height(&self) -> usize {
578        self.height
579    }
580
581    /// Returns row stride in scalar elements.
582    #[must_use]
583    pub const fn row_stride(&self) -> usize {
584        self.row_stride
585    }
586
587    /// Returns semantic image metadata.
588    #[must_use]
589    pub const fn metadata(&self) -> ImageMetadata {
590        self.metadata
591    }
592
593    /// Replaces semantic metadata after validating the channel count.
594    pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
595        metadata.validate::<CHANNELS>()?;
596        self.metadata = metadata;
597        Ok(())
598    }
599
600    /// Reborrows this mutable view as read-only.
601    #[must_use]
602    pub fn as_view(&self) -> ImageView<'_, T, CHANNELS> {
603        ImageView {
604            width: self.width,
605            height: self.height,
606            row_stride: self.row_stride,
607            data: self.data,
608            metadata: self.metadata,
609        }
610    }
611
612    /// Returns one read-only pixel, or `None` outside image bounds.
613    #[must_use]
614    pub fn get(&self, x: usize, y: usize) -> Option<&[T; CHANNELS]> {
615        self.as_view().get(x, y)
616    }
617
618    /// Returns one mutable pixel, or `None` outside image bounds.
619    #[must_use]
620    pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut [T; CHANNELS]> {
621        if x >= self.width || y >= self.height {
622            return None;
623        }
624        let offset = y * self.row_stride + x * CHANNELS;
625        self.data.get_mut(offset..offset + CHANNELS)?.try_into().ok()
626    }
627
628    /// Returns a mutable packed row without trailing padding.
629    #[must_use]
630    pub fn row_mut(&mut self, y: usize) -> Option<&mut [T]> {
631        if y >= self.height {
632            return None;
633        }
634        let start = y * self.row_stride;
635        self.data.get_mut(start..start + self.width * CHANNELS)
636    }
637
638    /// Returns the complete mutable backing span, including inter-row padding.
639    ///
640    /// The final row has no required trailing padding, so the returned length is
641    /// the minimum checked span accepted by [`ImageViewMut::new`].
642    #[must_use]
643    pub fn as_mut_slice(&mut self) -> &mut [T] {
644        self.data
645    }
646
647    /// Creates a checked mutable zero-copy subview.
648    pub fn subview(
649        &mut self,
650        region: ImageRegion,
651    ) -> Result<ImageViewMut<'_, T, CHANNELS>, ImageError> {
652        region.validate(self.width, self.height)?;
653        if region.width == 0 || region.height == 0 {
654            return Ok(ImageViewMut {
655                width: region.width,
656                height: region.height,
657                row_stride: self.row_stride,
658                data: &mut self.data[..0],
659                metadata: self.metadata,
660            });
661        }
662        let start = region.y * self.row_stride + region.x * CHANNELS;
663        let span = (region.height - 1) * self.row_stride + region.width * CHANNELS;
664        Ok(ImageViewMut {
665            width: region.width,
666            height: region.height,
667            row_stride: self.row_stride,
668            data: &mut self.data[start..start + span],
669            metadata: self.metadata,
670        })
671    }
672}
673
674/// An owning, densely packed planar image.
675///
676/// Storage order is all values for channel 0, followed by channel 1, and so on.
677#[derive(Clone, Debug, PartialEq, Eq)]
678pub struct PlanarImage<T, const CHANNELS: usize> {
679    width: usize,
680    height: usize,
681    data: Vec<T>,
682    metadata: ImageMetadata,
683}
684
685impl<T, const CHANNELS: usize> PlanarImage<T, CHANNELS> {
686    /// Creates an image from densely packed planar scalar elements.
687    pub fn try_new(width: usize, height: usize, data: Vec<T>) -> Result<Self, ImageError> {
688        let required = packed_len::<CHANNELS>(width, height)?;
689        if data.len() != required {
690            return Err(ImageError::StorageTooShort { required, found: data.len() });
691        }
692        Ok(Self { width, height, data, metadata: ImageMetadata::default() })
693    }
694
695    /// Creates a planar image with validated semantic metadata.
696    pub fn try_new_with_metadata(
697        width: usize,
698        height: usize,
699        data: Vec<T>,
700        metadata: ImageMetadata,
701    ) -> Result<Self, ImageError> {
702        metadata.validate::<CHANNELS>()?;
703        let mut image = Self::try_new(width, height, data)?;
704        image.metadata = metadata;
705        Ok(image)
706    }
707
708    /// Returns image width in pixels.
709    #[must_use]
710    pub const fn width(&self) -> usize {
711        self.width
712    }
713
714    /// Returns image height in pixels.
715    #[must_use]
716    pub const fn height(&self) -> usize {
717        self.height
718    }
719
720    /// Returns physical channel layout.
721    #[must_use]
722    pub const fn layout(&self) -> ImageLayout {
723        ImageLayout::Planar
724    }
725
726    /// Returns packed row stride within each plane.
727    #[must_use]
728    pub const fn row_stride(&self) -> usize {
729        self.width
730    }
731
732    /// Returns the scalar distance between channel-plane origins.
733    #[must_use]
734    pub const fn plane_stride(&self) -> usize {
735        self.width * self.height
736    }
737
738    /// Returns semantic image metadata.
739    #[must_use]
740    pub const fn metadata(&self) -> ImageMetadata {
741        self.metadata
742    }
743
744    /// Replaces semantic metadata after validating the channel count.
745    pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
746        metadata.validate::<CHANNELS>()?;
747        self.metadata = metadata;
748        Ok(())
749    }
750
751    /// Returns planar scalar storage.
752    #[must_use]
753    pub fn as_slice(&self) -> &[T] {
754        &self.data
755    }
756
757    /// Returns mutable planar scalar storage.
758    #[must_use]
759    pub fn as_mut_slice(&mut self) -> &mut [T] {
760        &mut self.data
761    }
762
763    /// Borrows this image as a planar view.
764    #[must_use]
765    pub fn view(&self) -> PlanarImageView<'_, T, CHANNELS> {
766        PlanarImageView {
767            width: self.width,
768            height: self.height,
769            row_stride: self.width,
770            plane_stride: self.width * self.height,
771            data: &self.data,
772            metadata: self.metadata,
773        }
774    }
775
776    /// Returns one channel value, or `None` outside image bounds.
777    #[must_use]
778    pub fn get(&self, channel: usize, x: usize, y: usize) -> Option<&T> {
779        self.view().get(channel, x, y)
780    }
781
782    /// Returns one mutable channel value, or `None` outside image bounds.
783    #[must_use]
784    pub fn get_mut(&mut self, channel: usize, x: usize, y: usize) -> Option<&mut T> {
785        if channel >= CHANNELS || x >= self.width || y >= self.height {
786            return None;
787        }
788        let offset = channel * self.width * self.height + y * self.width + x;
789        self.data.get_mut(offset)
790    }
791
792    /// Consumes the image and returns planar scalar storage.
793    #[must_use]
794    pub fn into_vec(self) -> Vec<T> {
795        self.data
796    }
797}
798
799/// A read-only, zero-copy planar image view with explicit strides.
800#[derive(Clone, Copy, Debug)]
801pub struct PlanarImageView<'a, T, const CHANNELS: usize> {
802    width: usize,
803    height: usize,
804    row_stride: usize,
805    plane_stride: usize,
806    data: &'a [T],
807    metadata: ImageMetadata,
808}
809
810impl<'a, T, const CHANNELS: usize> PlanarImageView<'a, T, CHANNELS> {
811    /// Creates a planar view. Strides are measured in scalar elements.
812    pub fn new(
813        width: usize,
814        height: usize,
815        row_stride: usize,
816        plane_stride: usize,
817        data: &'a [T],
818    ) -> Result<Self, ImageError> {
819        Self::new_with_metadata(
820            width,
821            height,
822            row_stride,
823            plane_stride,
824            data,
825            ImageMetadata::default(),
826        )
827    }
828
829    /// Creates a planar view with validated semantic metadata.
830    pub fn new_with_metadata(
831        width: usize,
832        height: usize,
833        row_stride: usize,
834        plane_stride: usize,
835        data: &'a [T],
836        metadata: ImageMetadata,
837    ) -> Result<Self, ImageError> {
838        metadata.validate::<CHANNELS>()?;
839        if row_stride < width {
840            return Err(ImageError::InvalidStride { stride: row_stride, minimum: width });
841        }
842        let plane_span = strided_span(height, row_stride, width)?;
843        if plane_stride < plane_span {
844            return Err(ImageError::InvalidPlaneStride {
845                stride: plane_stride,
846                minimum: plane_span,
847            });
848        }
849        let required = if width == 0 || height == 0 {
850            0
851        } else {
852            plane_stride
853                .checked_mul(CHANNELS - 1)
854                .and_then(|offset| offset.checked_add(plane_span))
855                .ok_or(ImageError::DimensionOverflow)?
856        };
857        if data.len() < required {
858            return Err(ImageError::StorageTooShort { required, found: data.len() });
859        }
860        Ok(Self { width, height, row_stride, plane_stride, data, metadata })
861    }
862
863    /// Returns image width in pixels.
864    #[must_use]
865    pub const fn width(self) -> usize {
866        self.width
867    }
868
869    /// Returns image height in pixels.
870    #[must_use]
871    pub const fn height(self) -> usize {
872        self.height
873    }
874
875    /// Returns row stride within a plane.
876    #[must_use]
877    pub const fn row_stride(self) -> usize {
878        self.row_stride
879    }
880
881    /// Returns scalar distance between channel-plane origins.
882    #[must_use]
883    pub const fn plane_stride(self) -> usize {
884        self.plane_stride
885    }
886
887    /// Returns physical channel layout.
888    #[must_use]
889    pub const fn layout(self) -> ImageLayout {
890        ImageLayout::Planar
891    }
892
893    /// Returns semantic image metadata.
894    #[must_use]
895    pub const fn metadata(self) -> ImageMetadata {
896        self.metadata
897    }
898
899    /// Returns one channel value, or `None` outside image bounds.
900    #[must_use]
901    pub fn get(self, channel: usize, x: usize, y: usize) -> Option<&'a T> {
902        if channel >= CHANNELS || x >= self.width || y >= self.height {
903            return None;
904        }
905        self.data.get(channel * self.plane_stride + y * self.row_stride + x)
906    }
907
908    /// Copies one pixel from its channel planes.
909    #[must_use]
910    pub fn pixel(self, x: usize, y: usize) -> Option<[T; CHANNELS]>
911    where
912        T: Copy,
913    {
914        if x >= self.width || y >= self.height {
915            return None;
916        }
917        Some(std::array::from_fn(|channel| {
918            *self.get(channel, x, y).expect("validated planar coordinate")
919        }))
920    }
921
922    /// Creates a checked zero-copy planar subview.
923    pub fn subview(self, region: ImageRegion) -> Result<Self, ImageError> {
924        region.validate(self.width, self.height)?;
925        if region.width == 0 || region.height == 0 {
926            return Ok(Self {
927                width: region.width,
928                height: region.height,
929                row_stride: self.row_stride,
930                plane_stride: self.plane_stride,
931                data: &self.data[..0],
932                metadata: self.metadata,
933            });
934        }
935        let start = region.y * self.row_stride + region.x;
936        let span = (CHANNELS - 1) * self.plane_stride
937            + (region.height - 1) * self.row_stride
938            + region.width;
939        Ok(Self {
940            width: region.width,
941            height: region.height,
942            row_stride: self.row_stride,
943            plane_stride: self.plane_stride,
944            data: &self.data[start..start + span],
945            metadata: self.metadata,
946        })
947    }
948}
949
950/// A one-channel image.
951pub type GrayImage<T> = Image<T, 1>;
952/// A three-channel RGB image.
953pub type RgbImage<T> = Image<T, 3>;
954
955#[cfg(test)]
956mod tests {
957    use super::{
958        AlphaMode, ColorRange, ColorSpace, Image, ImageError, ImageMetadata, ImageRegion,
959        ImageView, ImageViewMut, PlanarImage, PlanarImageView,
960    };
961
962    #[test]
963    fn packed_image_indexes_pixels() {
964        let mut image = Image::<u8, 3>::try_new(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap();
965        assert_eq!(image[(1, 0)], [4, 5, 6]);
966        image[(0, 0)] = [7, 8, 9];
967        assert_eq!(image.as_slice(), &[7, 8, 9, 4, 5, 6]);
968    }
969
970    #[test]
971    fn strided_view_skips_padding() {
972        let data = [1_u16, 2, 99, 3, 4];
973        let view = ImageView::<u16, 1>::new(2, 2, 3, &data).unwrap();
974        assert_eq!(view.get(0, 1), Some(&[3]));
975        assert_eq!(view.row(0), Some(&[1, 2][..]));
976    }
977
978    #[test]
979    fn rejects_short_storage() {
980        assert_eq!(
981            Image::<u8, 1>::try_new(2, 2, vec![0; 3]).unwrap_err(),
982            ImageError::StorageTooShort { required: 4, found: 3 }
983        );
984    }
985
986    #[test]
987    fn mutable_roi_updates_only_selected_pixels() {
988        let mut data = [0_u8, 1, 2, 99, 3, 4, 5];
989        let mut view = ImageViewMut::<u8, 1>::new(3, 2, 4, &mut data).unwrap();
990        {
991            let mut roi = view.subview(ImageRegion::new(1, 0, 2, 2)).unwrap();
992            *roi.get_mut(0, 0).unwrap() = [10];
993            *roi.get_mut(1, 1).unwrap() = [20];
994        }
995        assert_eq!(data, [0, 10, 2, 99, 3, 4, 20]);
996    }
997
998    #[test]
999    fn immutable_roi_preserves_parent_stride() {
1000        let data = [0_u8, 1, 2, 99, 3, 4, 5];
1001        let view = ImageView::<u8, 1>::new(3, 2, 4, &data).unwrap();
1002        let roi = view.subview(ImageRegion::new(1, 0, 2, 2)).unwrap();
1003        assert_eq!(roi.row_stride(), 4);
1004        assert_eq!(roi.get(0, 0), Some(&[1]));
1005        assert_eq!(roi.get(1, 1), Some(&[5]));
1006        assert!(view.subview(ImageRegion::new(2, 1, 2, 1)).is_err());
1007    }
1008
1009    #[test]
1010    fn planar_image_reads_channels_and_subviews() {
1011        // R plane, G plane, B plane.
1012        let mut image = PlanarImage::<u8, 3>::try_new(
1013            2,
1014            2,
1015            vec![1, 2, 3, 4, 10, 20, 30, 40, 100, 110, 120, 130],
1016        )
1017        .unwrap();
1018        assert_eq!(image.view().pixel(1, 1), Some([4, 40, 130]));
1019        *image.get_mut(1, 0, 1).unwrap() = 31;
1020        let roi = image.view().subview(ImageRegion::new(0, 1, 2, 1)).unwrap();
1021        assert_eq!(roi.pixel(0, 0), Some([3, 31, 120]));
1022        assert_eq!(roi.pixel(1, 0), Some([4, 40, 130]));
1023    }
1024
1025    #[test]
1026    fn planar_view_honors_row_and_plane_padding() {
1027        let data = [1_u8, 2, 99, 3, 4, 88, 77, 10, 20, 99, 30, 40];
1028        let view = PlanarImageView::<u8, 2>::new(2, 2, 3, 7, &data).unwrap();
1029        assert_eq!(view.pixel(0, 1), Some([3, 30]));
1030        assert_eq!(view.pixel(1, 1), Some([4, 40]));
1031    }
1032
1033    #[test]
1034    fn validates_color_metadata_channel_count() {
1035        let metadata = ImageMetadata {
1036            color_space: ColorSpace::Rgb,
1037            color_range: ColorRange::Full,
1038            alpha_mode: AlphaMode::None,
1039        };
1040        let image = Image::<u8, 3>::try_new_with_metadata(1, 1, vec![1, 2, 3], metadata).unwrap();
1041        assert_eq!(image.metadata(), metadata);
1042        assert!(matches!(
1043            Image::<u8, 1>::try_new_with_metadata(1, 1, vec![1], metadata),
1044            Err(ImageError::MetadataChannelMismatch { .. })
1045        ));
1046    }
1047}