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}