Skip to main content

gamut_core/
image.rs

1//! Branded, length-validated interleaved image buffers: [`ImageRef`] (borrowed) and [`ImageBuf`]
2//! (owned).
3//!
4//! Both wrap a flat sample slice/vec of `P::Sample` (`u8` or `u16`) plus [`Dimensions`], branded
5//! with a [`Pixel`] marker `P`. The invariant `len == width * height * P::CHANNELS` (with non-zero
6//! dimensions) is checked **once**, at construction, so codecs receive a buffer that is already
7//! known-good and can pull the raw slice back out via [`ImageRef::as_samples`] with zero overhead —
8//! their hot loops are byte-identical to operating on a bare `&[u8]`. The brand is what makes a
9//! format mismatch (e.g. feeding [`crate::Cmyk8`] data to an [`crate::Rgba8`] encoder) a compile
10//! error instead of a silent reinterpretation.
11
12use core::marker::PhantomData;
13
14use crate::{Dimensions, Error, Pixel, Result};
15
16/// The required sample count for `dims` at `P`'s channel count, rejecting empty or overflowing
17/// dimensions. The single validation shared by every buffer constructor.
18fn expected_len<P: Pixel>(dims: Dimensions) -> Result<usize> {
19    if dims.is_empty() {
20        return Err(Error::InvalidInput("zero-sized image"));
21    }
22    dims.sample_count(P::CHANNELS)
23        .ok_or(Error::InvalidInput("image dimensions overflow usize"))
24}
25
26/// A borrowed, length-validated view of an interleaved image of pixel type `P`.
27///
28/// Cheap to copy (a slice + [`Dimensions`] + a zero-sized marker). Construct one at an API boundary
29/// with [`ImageRef::new`]; pass it to an [`crate::EncodeImage`] implementation.
30#[derive(Debug, Clone, Copy)]
31pub struct ImageRef<'a, P: Pixel> {
32    data: &'a [P::Sample],
33    dims: Dimensions,
34    _p: PhantomData<P>,
35}
36
37impl<'a, P: Pixel> ImageRef<'a, P> {
38    /// Brands `data` as an image of `dims`, validating that its length is exactly
39    /// `width * height * P::CHANNELS`.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`Error::InvalidInput`] if `dims` is zero-sized, if `width * height * channels`
44    /// overflows `usize`, or if `data.len()` does not equal that product.
45    pub fn new(data: &'a [P::Sample], dims: Dimensions) -> Result<Self> {
46        let want = expected_len::<P>(dims)?;
47        if data.len() != want {
48            return Err(Error::InvalidInput(
49                "image buffer length does not match dimensions",
50            ));
51        }
52        Ok(Self {
53            data,
54            dims,
55            _p: PhantomData,
56        })
57    }
58
59    /// The image dimensions.
60    #[must_use]
61    pub fn dimensions(self) -> Dimensions {
62        self.dims
63    }
64
65    /// Image width in pixels.
66    #[must_use]
67    pub fn width(self) -> u32 {
68        self.dims.width
69    }
70
71    /// Image height in pixels.
72    #[must_use]
73    pub fn height(self) -> u32 {
74        self.dims.height
75    }
76
77    /// The raw interleaved samples (`width * height * P::CHANNELS` of them, row-major). The
78    /// zero-cost escape hatch a codec uses to feed its existing slice-based hot path.
79    #[must_use]
80    pub fn as_samples(self) -> &'a [P::Sample] {
81        self.data
82    }
83
84    /// Row `y` as a `width * P::CHANNELS`-sample slice.
85    ///
86    /// # Panics
87    ///
88    /// Panics if `y >= height`.
89    #[must_use]
90    pub fn row(self, y: u32) -> &'a [P::Sample] {
91        let row_len = self.dims.width as usize * P::CHANNELS;
92        let start = y as usize * row_len;
93        &self.data[start..start + row_len]
94    }
95
96    /// Iterates the rows top to bottom, each a `width * P::CHANNELS`-sample slice.
97    #[must_use]
98    pub fn rows(self) -> impl ExactSizeIterator<Item = &'a [P::Sample]> {
99        let row_len = self.dims.width as usize * P::CHANNELS;
100        self.data.chunks_exact(row_len)
101    }
102
103    /// The `P::CHANNELS` samples of the pixel at `(x, y)`.
104    ///
105    /// # Panics
106    ///
107    /// Panics if `x >= width` or `y >= height`.
108    #[must_use]
109    pub fn pixel(self, x: u32, y: u32) -> &'a [P::Sample] {
110        let i = (y as usize * self.dims.width as usize + x as usize) * P::CHANNELS;
111        &self.data[i..i + P::CHANNELS]
112    }
113}
114
115/// An owned, length-validated interleaved image of pixel type `P`.
116///
117/// The owning counterpart of [`ImageRef`]; the natural return of a decoder, carrying its
118/// dimensions, samples, and layout brand as one unit so a caller can never misinterpret the result.
119#[derive(Debug, Clone)]
120#[must_use]
121pub struct ImageBuf<P: Pixel> {
122    data: Vec<P::Sample>,
123    dims: Dimensions,
124    _p: PhantomData<P>,
125}
126
127impl<P: Pixel> ImageBuf<P> {
128    /// Takes ownership of `data` as an image of `dims`, validating its length the same way as
129    /// [`ImageRef::new`].
130    ///
131    /// # Errors
132    ///
133    /// Returns [`Error::InvalidInput`] if `dims` is zero-sized, if the sample count overflows
134    /// `usize`, or if `data.len()` does not equal `width * height * P::CHANNELS`.
135    pub fn new(data: Vec<P::Sample>, dims: Dimensions) -> Result<Self> {
136        // Reuse the single source of truth for the length invariant.
137        ImageRef::<P>::new(&data, dims)?;
138        Ok(Self {
139            data,
140            dims,
141            _p: PhantomData,
142        })
143    }
144
145    /// An all-zero image of `dims` (every sample `P::Sample::default()`).
146    ///
147    /// # Errors
148    ///
149    /// Returns [`Error::InvalidInput`] if `dims` is zero-sized or the sample count overflows `usize`.
150    pub fn zeroed(dims: Dimensions) -> Result<Self> {
151        let want = expected_len::<P>(dims)?;
152        Ok(Self {
153            data: vec![P::Sample::default(); want],
154            dims,
155            _p: PhantomData,
156        })
157    }
158
159    /// Borrows this image as an [`ImageRef`]. Infallible — the invariant already holds.
160    #[must_use]
161    pub fn as_ref(&self) -> ImageRef<'_, P> {
162        ImageRef {
163            data: &self.data,
164            dims: self.dims,
165            _p: PhantomData,
166        }
167    }
168
169    /// The image dimensions.
170    #[must_use]
171    pub fn dimensions(&self) -> Dimensions {
172        self.dims
173    }
174
175    /// The raw interleaved samples, row-major.
176    #[must_use]
177    pub fn as_samples(&self) -> &[P::Sample] {
178        &self.data
179    }
180
181    /// Consumes the image, returning its backing sample vector.
182    #[must_use]
183    pub fn into_samples(self) -> Vec<P::Sample> {
184        self.data
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::{Rgb8, Rgb16};
192
193    fn dims(w: u32, h: u32) -> Dimensions {
194        Dimensions {
195            width: w,
196            height: h,
197        }
198    }
199
200    #[test]
201    fn new_validates_length() {
202        let rgb = vec![0u8; 2 * 2 * 3];
203        let img = ImageRef::<Rgb8>::new(&rgb, dims(2, 2)).unwrap();
204        assert_eq!(img.dimensions(), dims(2, 2));
205        assert_eq!((img.width(), img.height()), (2, 2));
206        assert_eq!(img.as_samples().len(), 12);
207        // Too short and too long both rejected.
208        assert!(ImageRef::<Rgb8>::new(&rgb[..11], dims(2, 2)).is_err());
209        let long = vec![0u8; 13];
210        assert!(ImageRef::<Rgb8>::new(&long, dims(2, 2)).is_err());
211    }
212
213    #[test]
214    fn new_rejects_zero_sized() {
215        let empty: [u8; 0] = [];
216        assert!(ImageRef::<Rgb8>::new(&empty, dims(0, 4)).is_err());
217        assert!(ImageRef::<Rgb8>::new(&empty, dims(4, 0)).is_err());
218    }
219
220    #[test]
221    fn rejects_overflowing_dimensions() {
222        // width*height fits usize on 64-bit but *3 channels overflows; on 32-bit width*height
223        // already overflows. Either way expected_len returns the overflow error.
224        let big = dims(u32::MAX, u32::MAX);
225        assert!(ImageRef::<Rgb8>::new(&[], big).is_err());
226        assert!(ImageBuf::<Rgb8>::zeroed(big).is_err());
227    }
228
229    #[test]
230    fn row_and_pixel_access() {
231        // 3x2 RGB, pixel (x,y) tagged so we can spot misindexing.
232        let mut rgb = vec![0u8; 3 * 2 * 3];
233        for y in 0..2u32 {
234            for x in 0..3u32 {
235                let i = (y as usize * 3 + x as usize) * 3;
236                rgb[i] = x as u8;
237                rgb[i + 1] = y as u8;
238                rgb[i + 2] = 0xAA;
239            }
240        }
241        let img = ImageRef::<Rgb8>::new(&rgb, dims(3, 2)).unwrap();
242        assert_eq!(img.row(1).len(), 9);
243        assert_eq!(img.pixel(2, 1), &[2, 1, 0xAA]);
244        assert_eq!(img.pixel(0, 0), &[0, 0, 0xAA]);
245        let rows: Vec<_> = img.rows().collect();
246        assert_eq!(rows.len(), 2);
247        assert!(rows.iter().all(|r| r.len() == 9));
248    }
249
250    #[test]
251    fn high_bit_depth_samples() {
252        // 2x1 RGB16 = 2 pixels * 3 channels = 6 u16 samples.
253        let rgb16 = vec![1000u16; 6];
254        let img = ImageRef::<Rgb16>::new(&rgb16, dims(2, 1)).unwrap();
255        assert_eq!(img.as_samples().len(), 6);
256        assert_eq!(img.pixel(1, 0), &[1000, 1000, 1000]);
257        // A u16 buffer of the wrong length is still rejected.
258        assert!(ImageRef::<Rgb16>::new(&[0u16; 5], dims(2, 1)).is_err());
259    }
260
261    #[test]
262    fn owned_buffer_roundtrips() {
263        let buf = ImageBuf::<Rgb8>::new(vec![7u8; 12], dims(2, 2)).unwrap();
264        assert_eq!(buf.dimensions(), dims(2, 2));
265        assert_eq!(buf.as_samples().len(), 12);
266        assert_eq!(buf.as_ref().pixel(0, 0), &[7, 7, 7]);
267        assert_eq!(buf.into_samples(), vec![7u8; 12]);
268        // Wrong length rejected on the owned path too.
269        assert!(ImageBuf::<Rgb8>::new(vec![0u8; 11], dims(2, 2)).is_err());
270    }
271
272    #[test]
273    fn zeroed_is_all_default_and_correct_length() {
274        let buf = ImageBuf::<Rgb8>::zeroed(dims(4, 3)).unwrap();
275        assert_eq!(buf.as_samples().len(), 4 * 3 * 3);
276        assert!(buf.as_samples().iter().all(|&s| s == 0));
277        assert!(ImageBuf::<Rgb8>::zeroed(dims(0, 3)).is_err());
278    }
279}