gamut_core/lib.rs
1//! Core traits, image buffers, dimensions, and error types shared across the gamut codecs.
2//!
3//! This crate is dependency-free with respect to the format crates: every codec in the
4//! workspace builds on the [`EncodeImage`] / [`DecodeImage`] traits, the branded [`ImageRef`] /
5//! [`ImageBuf`] buffers, and the [`Error`] type defined here, so that callers get a single,
6//! consistent error surface regardless of format.
7#![forbid(unsafe_code)]
8
9mod image;
10mod pixel;
11
12pub use image::{ImageBuf, ImageRef};
13pub use pixel::{
14 Bilevel, Cmyk8, ColorModel, Gray8, Gray16, Indexed8, Pixel, Rgb8, Rgb16, Rgba8, Rgba16, Sample,
15};
16
17/// Errors produced by gamut encoders and decoders.
18///
19/// Marked `#[non_exhaustive]` so additional variants can be added as formats land without a
20/// breaking change.
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum Error {
24 /// The input data was malformed, truncated, or otherwise not valid for the format.
25 #[error("invalid input: {0}")]
26 InvalidInput(&'static str),
27 /// The requested format, profile, or feature is not yet supported.
28 #[error("unsupported: {0}")]
29 Unsupported(&'static str),
30}
31
32/// Convenience result type for gamut operations.
33pub type Result<T> = core::result::Result<T, Error>;
34
35/// Width and height of an image, in pixels.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct Dimensions {
38 /// Image width in pixels.
39 pub width: u32,
40 /// Image height in pixels.
41 pub height: u32,
42}
43
44impl Dimensions {
45 /// Creates dimensions, rejecting a zero width or height.
46 ///
47 /// The fields stay public for ergonomic struct literals; this constructor is the validated
48 /// path that buffer types ([`crate::ImageRef`]) and codecs use so an empty image is caught
49 /// once, at the boundary, rather than re-checked in every encoder.
50 ///
51 /// # Errors
52 ///
53 /// Returns [`Error::InvalidInput`] if either dimension is zero.
54 pub fn new(width: u32, height: u32) -> Result<Self> {
55 if width == 0 || height == 0 {
56 return Err(Error::InvalidInput("zero-sized image"));
57 }
58 Ok(Self { width, height })
59 }
60
61 /// The pixel count `width * height`, or `None` if it overflows `usize`.
62 #[must_use]
63 pub fn num_pixels(self) -> Option<usize> {
64 (self.width as usize).checked_mul(self.height as usize)
65 }
66
67 /// The sample count for an interleaved buffer of `channels` samples per pixel
68 /// (`width * height * channels`), or `None` on overflow. The length an [`crate::ImageRef`]
69 /// validates against.
70 #[must_use]
71 pub fn sample_count(self, channels: usize) -> Option<usize> {
72 self.num_pixels()?.checked_mul(channels)
73 }
74
75 /// Whether either dimension is zero.
76 #[must_use]
77 pub fn is_empty(self) -> bool {
78 self.width == 0 || self.height == 0
79 }
80}
81
82/// Encodes an [`ImageRef`] of a specific pixel layout `P` into a compressed byte stream.
83///
84/// A codec implements this once per pixel layout it supports (`impl EncodeImage<Rgb8> for …`,
85/// `impl EncodeImage<Cmyk8> for …`, …), so asking it to encode an unsupported layout is a compile
86/// error rather than a runtime `Unsupported`. The input is pre-validated by [`ImageRef::new`], so an
87/// implementation never re-checks the buffer length. Bytes are appended to `out` to keep callers
88/// that reuse a scratch buffer allocation-conscious.
89pub trait EncodeImage<P: Pixel> {
90 /// Encode `image` into `out` (appended), returning the number of bytes written.
91 ///
92 /// # Errors
93 ///
94 /// Returns [`Error::Unsupported`] if the requested encoder configuration is not implemented, or
95 /// [`Error::InvalidInput`] if the image violates a format constraint (e.g. a dimension limit).
96 fn encode_image(&self, image: ImageRef<'_, P>, out: &mut Vec<u8>) -> Result<usize>;
97}
98
99/// Decodes a compressed byte stream into an owned [`ImageBuf`] of pixel layout `P`.
100///
101/// `P` selects the layout the caller wants back; a codec implements this for each layout it can
102/// present (converting internally as needed, e.g. grayscale → [`Rgb8`]). Returning an owned
103/// [`ImageBuf`] keeps the dimensions, samples, and layout brand together so the result can't be
104/// misinterpreted.
105pub trait DecodeImage<P: Pixel> {
106 /// Decode `data` into a fresh [`ImageBuf`].
107 ///
108 /// # Errors
109 ///
110 /// Returns [`Error::InvalidInput`] if `data` is malformed, or [`Error::Unsupported`] if it uses
111 /// a feature that is not implemented or cannot be presented as `P`.
112 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<P>>;
113
114 /// Decode `data` into `dst`, reusing its allocation where possible.
115 ///
116 /// The default forwards to [`DecodeImage::decode_image`]; a codec may override it to refill
117 /// `dst`'s backing storage in place across repeated calls.
118 ///
119 /// # Errors
120 ///
121 /// As [`DecodeImage::decode_image`].
122 fn decode_image_into(&self, data: &[u8], dst: &mut ImageBuf<P>) -> Result<()> {
123 *dst = self.decode_image(data)?;
124 Ok(())
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn error_displays_and_dimensions_fields() {
134 assert!(!Error::Unsupported("x").to_string().is_empty());
135 assert!(!Error::InvalidInput("y").to_string().is_empty());
136 let d = Dimensions {
137 width: 1920,
138 height: 1080,
139 };
140 assert_eq!(d.width, 1920);
141 assert_eq!(d.height, 1080);
142 }
143
144 #[test]
145 fn dimensions_new_rejects_zero() {
146 assert!(Dimensions::new(0, 4).is_err());
147 assert!(Dimensions::new(4, 0).is_err());
148 assert!(Dimensions::new(0, 0).is_err());
149 let d = Dimensions::new(4, 3).unwrap();
150 assert_eq!((d.width, d.height), (4, 3));
151 }
152
153 #[test]
154 fn dimensions_pixel_and_sample_counts() {
155 let d = Dimensions {
156 width: 4,
157 height: 3,
158 };
159 assert_eq!(d.num_pixels(), Some(12));
160 assert_eq!(d.sample_count(3), Some(36));
161 assert_eq!(d.sample_count(1), Some(12));
162 assert!(!d.is_empty());
163 }
164
165 #[test]
166 fn dimensions_is_empty() {
167 assert!(
168 Dimensions {
169 width: 0,
170 height: 5
171 }
172 .is_empty()
173 );
174 assert!(
175 Dimensions {
176 width: 5,
177 height: 0
178 }
179 .is_empty()
180 );
181 assert!(
182 !Dimensions {
183 width: 5,
184 height: 5
185 }
186 .is_empty()
187 );
188 }
189
190 #[test]
191 fn dimensions_sample_count_overflow_is_none() {
192 // 65535*65535 fits in a 32-bit usize, so num_pixels is Some on every target...
193 let d = Dimensions {
194 width: 0xFFFF,
195 height: 0xFFFF,
196 };
197 assert_eq!(d.num_pixels(), Some(0xFFFF * 0xFFFF));
198 // ...but scaling by usize::MAX channels overflows on any platform.
199 assert_eq!(d.sample_count(usize::MAX), None);
200 }
201}
202
203#[cfg(test)]
204mod trait_tests {
205 use super::*;
206
207 /// A trivial codec: encodes by copying the samples out, decodes a fixed 1x1 gray pixel.
208 /// Exists only to exercise the trait defaults and object-safety.
209 struct Trivial;
210
211 impl EncodeImage<Gray8> for Trivial {
212 fn encode_image(&self, image: ImageRef<'_, Gray8>, out: &mut Vec<u8>) -> Result<usize> {
213 out.extend_from_slice(image.as_samples());
214 Ok(image.as_samples().len())
215 }
216 }
217
218 impl DecodeImage<Gray8> for Trivial {
219 fn decode_image(&self, _data: &[u8]) -> Result<ImageBuf<Gray8>> {
220 ImageBuf::<Gray8>::new(vec![42u8], Dimensions::new(1, 1)?)
221 }
222 }
223
224 #[test]
225 fn encode_image_appends_and_counts() {
226 let img = ImageBuf::<Gray8>::new(vec![1, 2, 3, 4], Dimensions::new(2, 2).unwrap()).unwrap();
227 let mut out = vec![0xFF];
228 let n = Trivial.encode_image(img.as_ref(), &mut out).unwrap();
229 assert_eq!(n, 4);
230 assert_eq!(out, vec![0xFF, 1, 2, 3, 4]);
231 }
232
233 #[test]
234 fn decode_image_into_default_forwards() {
235 let mut dst = ImageBuf::<Gray8>::zeroed(Dimensions::new(1, 1).unwrap()).unwrap();
236 Trivial.decode_image_into(&[], &mut dst).unwrap();
237 assert_eq!(dst.as_samples(), &[42]);
238 }
239
240 #[test]
241 fn traits_are_object_safe() {
242 // Compiles and runs only while both traits stay object-safe (e.g. for `Box<dyn …>`).
243 let enc: &dyn EncodeImage<Gray8> = &Trivial;
244 let dec: &dyn DecodeImage<Gray8> = &Trivial;
245 let img = ImageBuf::<Gray8>::new(vec![7u8], Dimensions::new(1, 1).unwrap()).unwrap();
246 let mut out = Vec::new();
247 assert_eq!(enc.encode_image(img.as_ref(), &mut out).unwrap(), 1);
248 assert_eq!(dec.decode_image(&[]).unwrap().as_samples(), &[42]);
249 }
250}