Skip to main content

stenoxide_core/image_io/
buffer.rs

1//! Owned image buffer and the pixel-access abstraction used by every layer
2//! above image I/O.
3//!
4//! An [`ImageBuffer`] can only be produced by the validation type-state in
5//! [`crate::image_io::validate`]: its constructor is `pub(crate)` and the
6//! intermediate states of the automaton are private to that module. Downstream
7//! layers therefore receive images that are guaranteed to have passed every
8//! validation gate.
9
10use zeroize::Zeroize;
11
12/// Pixel layout of a decoded container image.
13///
14/// Only layouts that expose an integer-valued, losslessly representable sample
15/// per channel are supported. Floating point layouts are rejected during
16/// validation because LSB embedding is not well defined on them.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ColorSpace {
19    /// Three 8-bit channels: red, green, blue.
20    Rgb8,
21    /// Three 16-bit channels stored as little-endian byte pairs.
22    Rgb16,
23    /// Four 8-bit channels: red, green, blue, alpha.
24    Rgba8,
25    /// A single 8-bit grayscale channel.
26    Luma8,
27}
28
29impl ColorSpace {
30    /// Number of bytes occupied by one pixel in this layout.
31    ///
32    /// For [`ColorSpace::Rgb16`] this counts the two bytes of each 16-bit
33    /// sample, not the sample itself.
34    pub fn bytes_per_pixel(&self) -> usize {
35        match self {
36            ColorSpace::Rgb8 => 3,
37            ColorSpace::Rgb16 => 6,
38            ColorSpace::Rgba8 => 4,
39            ColorSpace::Luma8 => 1,
40        }
41    }
42
43    /// Whether the layout carries a dedicated red channel.
44    ///
45    /// Grayscale images do not, which matters to the layers above: they cannot
46    /// treat the single luma channel as if it were a colour component.
47    pub fn has_explicit_red_channel(&self) -> bool {
48        match self {
49            ColorSpace::Rgb8 | ColorSpace::Rgb16 | ColorSpace::Rgba8 => true,
50            ColorSpace::Luma8 => false,
51        }
52    }
53}
54
55/// Read/write access to the raw samples of a container image.
56///
57/// The trait exists so that the cost and embedding layers can operate on any
58/// pixel source without owning the concrete buffer type. Implementors must
59/// guarantee that [`CoverSource::pixels`] holds exactly
60/// `pixel_count() * color_space().bytes_per_pixel()` elements, laid out in
61/// row-major order with no padding between rows.
62pub trait CoverSource {
63    /// Image dimensions as `(width, height)`, in pixels.
64    fn dimensions(&self) -> (u32, u32);
65
66    /// Raw sample bytes in row-major order.
67    fn pixels(&self) -> &[u8];
68
69    /// Mutable view of the raw sample bytes, used by the embedding layer.
70    fn pixels_mut(&mut self) -> &mut [u8];
71
72    /// Pixel layout of the underlying samples.
73    fn color_space(&self) -> ColorSpace;
74
75    /// Total number of pixels in the image.
76    fn pixel_count(&self) -> usize {
77        let (width, height) = self.dimensions();
78        width as usize * height as usize
79    }
80
81    /// Byte offset of the first sample of the pixel at `(x, y)`.
82    ///
83    /// The coordinates are not bounds-checked; callers must keep them inside
84    /// the range reported by [`CoverSource::dimensions`].
85    fn pixel_offset(&self, x: u32, y: u32) -> usize {
86        let (width, _) = self.dimensions();
87        (y as usize * width as usize + x as usize) * self.color_space().bytes_per_pixel()
88    }
89}
90
91/// A decoded, fully validated container image owned as a flat byte buffer.
92///
93/// The type deliberately does not implement [`Clone`]. Container images are
94/// several megabytes large, and an accidental copy would leave a second image
95/// in memory that no layer is responsible for wiping.
96#[derive(Debug)]
97pub struct ImageBuffer {
98    pixels: Vec<u8>,
99    width: u32,
100    height: u32,
101    color_space: ColorSpace,
102}
103
104impl ImageBuffer {
105    /// Builds a buffer from already validated components.
106    ///
107    /// Restricted to the crate on purpose: outside code must go through
108    /// [`crate::image_io::validate::load_and_validate`], the only path that
109    /// runs every validation gate.
110    pub(crate) fn new(pixels: Vec<u8>, width: u32, height: u32, color_space: ColorSpace) -> Self {
111        Self {
112            pixels,
113            width,
114            height,
115            color_space,
116        }
117    }
118}
119
120impl CoverSource for ImageBuffer {
121    fn dimensions(&self) -> (u32, u32) {
122        (self.width, self.height)
123    }
124
125    fn pixels(&self) -> &[u8] {
126        &self.pixels
127    }
128
129    fn pixels_mut(&mut self) -> &mut [u8] {
130        &mut self.pixels
131    }
132
133    fn color_space(&self) -> ColorSpace {
134        self.color_space
135    }
136}
137
138impl Zeroize for ImageBuffer {
139    /// Overwrites the sample buffer in place.
140    ///
141    /// Written by hand rather than derived: only `pixels` holds data worth
142    /// wiping, whereas the derive would also reset the dimensions, which are
143    /// metadata a caller may still need while inspecting the wiped buffer.
144    fn zeroize(&mut self) {
145        self.pixels.zeroize();
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    /// Every layout, with the stride it promises.
154    const LAYOUTS: [(ColorSpace, usize); 4] = [
155        (ColorSpace::Rgb8, 3),
156        (ColorSpace::Rgb16, 6),
157        (ColorSpace::Rgba8, 4),
158        (ColorSpace::Luma8, 1),
159    ];
160
161    /// The stride of each layout, which the length contract of
162    /// [`CoverSource::pixels`] is expressed in.
163    #[test]
164    fn each_layout_reports_its_own_stride() {
165        for (layout, stride) in LAYOUTS {
166            assert_eq!(layout.bytes_per_pixel(), stride, "layout {layout:?}");
167        }
168    }
169
170    /// Only the colour layouts have a red plane for the cost layer to protect.
171    #[test]
172    fn only_colour_layouts_carry_a_red_channel() {
173        for (layout, _) in LAYOUTS {
174            assert_eq!(
175                layout.has_explicit_red_channel(),
176                layout != ColorSpace::Luma8,
177                "layout {layout:?}"
178            );
179        }
180    }
181
182    /// The two provided methods of the trait, which no implementor overrides.
183    #[test]
184    fn offsets_follow_row_major_order() {
185        let image = ImageBuffer::new(vec![0u8; 6 * 4 * 3], 6, 4, ColorSpace::Rgb8);
186
187        assert_eq!(image.dimensions(), (6, 4));
188        assert_eq!(image.pixel_count(), 24);
189        assert_eq!(image.pixel_offset(0, 0), 0);
190        assert_eq!(image.pixel_offset(1, 0), 3);
191        // One row down and one column across: six pixels of stride, plus one.
192        assert_eq!(image.pixel_offset(1, 1), 21);
193    }
194
195    /// Wiping a buffer clears the samples and leaves the geometry readable.
196    #[test]
197    fn zeroizing_clears_the_samples_and_keeps_the_geometry() {
198        let mut image = ImageBuffer::new(vec![0xAAu8; 12], 4, 3, ColorSpace::Luma8);
199
200        image.pixels_mut()[0] = 0xFF;
201        assert!(image.pixels().iter().any(|&sample| sample != 0));
202
203        image.zeroize();
204
205        assert!(image.pixels().iter().all(|&sample| sample == 0));
206        assert_eq!(image.dimensions(), (4, 3));
207        assert_eq!(image.color_space(), ColorSpace::Luma8);
208    }
209}