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}