Skip to main content

gamut_core/
pixel.rs

1//! Compile-time pixel vocabulary: the [`Sample`] storage primitives, the [`ColorModel`] tag, and
2//! the sealed [`Pixel`] trait with one zero-sized marker per supported interleaved layout.
3//!
4//! These types brand the otherwise-opaque sample buffers in [`crate::ImageRef`] / [`crate::ImageBuf`]
5//! so a layout mismatch — handing CMYK bytes to an RGBA encoder, or grayscale luminance where
6//! palette indices are expected — is a compile error rather than a runtime length check. The markers
7//! carry no data; they exist only to select an encoder/decoder impl and to expose the layout
8//! constants ([`Pixel::CHANNELS`], [`Pixel::MODEL`], [`Pixel::BYTES_PER_PIXEL`]).
9
10mod sample_sealed {
11    pub trait Sealed {}
12    impl Sealed for u8 {}
13    impl Sealed for u16 {}
14}
15
16/// A pixel-sample storage primitive: `u8` (8-bit) or `u16` (10/12/16-bit, high-bit-depth).
17///
18/// Sealed — only `u8` and `u16` implement it. The supertrait bounds are chosen so that `P::Sample`
19/// transitively gives buffer types everything they need (copy, zero-fill via `Default`, ordering)
20/// without callers repeating `where P::Sample: …` clauses.
21pub trait Sample:
22    sample_sealed::Sealed + Copy + Default + Ord + core::fmt::Debug + 'static
23{
24    /// Bits the primitive stores (8 or 16). Distinct from a stream's *coded* bit depth (e.g. 10 or
25    /// 12), which is a codec concern carried separately (see `gamut_color::BitDepth`).
26    const STORAGE_BITS: u32;
27}
28
29impl Sample for u8 {
30    const STORAGE_BITS: u32 = 8;
31}
32impl Sample for u16 {
33    const STORAGE_BITS: u32 = 16;
34}
35
36/// The colour interpretation of a pixel's channels.
37///
38/// Distinguishes layouts that share a channel count: [`ColorModel::Rgba`] and [`ColorModel::Cmyk`]
39/// are both four channels but must never be interchanged, and [`ColorModel::Gray`],
40/// [`ColorModel::Bilevel`], and [`ColorModel::Indexed`] are all one channel with different meanings.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[non_exhaustive]
43pub enum ColorModel {
44    /// Single luminance channel.
45    Gray,
46    /// Luminance plus an alpha channel.
47    GrayAlpha,
48    /// Red, green, blue.
49    Rgb,
50    /// Red, green, blue, alpha (unassociated).
51    Rgba,
52    /// Cyan, magenta, yellow, black ink separations.
53    Cmyk,
54    /// One channel, `0` = black and any non-zero value = white (a 1-bit image carried as one byte
55    /// per pixel).
56    Bilevel,
57    /// One channel of indices into a separate colour palette.
58    Indexed,
59}
60
61mod pixel_sealed {
62    pub trait Sealed {}
63}
64
65/// Compile-time description of one interleaved pixel layout.
66///
67/// Sealed: implemented only by the zero-sized marker types in this module. A buffer is branded with
68/// a `Pixel` type so its channel count, sample primitive, and colour model are known statically;
69/// codecs implement [`crate::EncodeImage<P>`] / [`crate::DecodeImage<P>`] for exactly the `P` they
70/// support, making an unsupported format a compile error.
71pub trait Pixel: pixel_sealed::Sealed + Copy + 'static {
72    /// The storage primitive of each sample (`u8` or `u16`).
73    type Sample: Sample;
74    /// Samples per pixel.
75    const CHANNELS: usize;
76    /// The colour interpretation of those samples.
77    const MODEL: ColorModel;
78    /// Bytes one pixel occupies in an interleaved buffer (`CHANNELS * size_of::<Sample>()`).
79    const BYTES_PER_PIXEL: usize = Self::CHANNELS * core::mem::size_of::<Self::Sample>();
80}
81
82/// Defines a zero-sized pixel marker and its [`Pixel`] impl from a compact table.
83macro_rules! define_pixels {
84    ($(
85        $(#[$meta:meta])*
86        $name:ident => $sample:ty, $channels:expr, $model:expr;
87    )*) => {
88        $(
89            $(#[$meta])*
90            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91            pub struct $name;
92
93            impl pixel_sealed::Sealed for $name {}
94            impl Pixel for $name {
95                type Sample = $sample;
96                const CHANNELS: usize = $channels;
97                const MODEL: ColorModel = $model;
98            }
99        )*
100    };
101}
102
103define_pixels! {
104    /// 8-bit grayscale: one luminance byte per pixel.
105    Gray8 => u8, 1, ColorModel::Gray;
106    /// 8-bit bilevel: one byte per pixel, `0` = black and non-zero = white. Distinct from [`Gray8`]
107    /// so a grayscale buffer cannot be mistaken for a 1-bit image.
108    Bilevel => u8, 1, ColorModel::Bilevel;
109    /// 8-bit palette indices: one index byte per pixel into a separate colour table. Distinct from
110    /// [`Gray8`] so indices cannot be mistaken for luminance.
111    Indexed8 => u8, 1, ColorModel::Indexed;
112    /// 8-bit RGB: three interleaved bytes per pixel, row-major.
113    Rgb8 => u8, 3, ColorModel::Rgb;
114    /// 8-bit RGBA: four interleaved bytes per pixel (unassociated alpha).
115    Rgba8 => u8, 4, ColorModel::Rgba;
116    /// 8-bit CMYK: four interleaved ink bytes per pixel. Distinct from [`Rgba8`] despite the shared
117    /// channel count.
118    Cmyk8 => u8, 4, ColorModel::Cmyk;
119    /// 16-bit grayscale: one `u16` luminance sample per pixel (high-bit-depth).
120    Gray16 => u16, 1, ColorModel::Gray;
121    /// 16-bit RGB: three interleaved `u16` samples per pixel (high-bit-depth).
122    Rgb16 => u16, 3, ColorModel::Rgb;
123    /// 16-bit RGBA: four interleaved `u16` samples per pixel (high-bit-depth).
124    Rgba16 => u16, 4, ColorModel::Rgba;
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn sample_storage_bits() {
133        assert_eq!(<u8 as Sample>::STORAGE_BITS, 8);
134        assert_eq!(<u16 as Sample>::STORAGE_BITS, 16);
135    }
136
137    #[test]
138    fn pixel_layout_constants() {
139        assert_eq!((Gray8::CHANNELS, Gray8::BYTES_PER_PIXEL), (1, 1));
140        assert_eq!(Gray8::MODEL, ColorModel::Gray);
141        assert_eq!((Bilevel::CHANNELS, Bilevel::BYTES_PER_PIXEL), (1, 1));
142        assert_eq!(Bilevel::MODEL, ColorModel::Bilevel);
143        assert_eq!((Indexed8::CHANNELS, Indexed8::BYTES_PER_PIXEL), (1, 1));
144        assert_eq!(Indexed8::MODEL, ColorModel::Indexed);
145        assert_eq!((Rgb8::CHANNELS, Rgb8::BYTES_PER_PIXEL), (3, 3));
146        assert_eq!(Rgb8::MODEL, ColorModel::Rgb);
147        assert_eq!((Rgba8::CHANNELS, Rgba8::BYTES_PER_PIXEL), (4, 4));
148        assert_eq!(Rgba8::MODEL, ColorModel::Rgba);
149        assert_eq!((Cmyk8::CHANNELS, Cmyk8::BYTES_PER_PIXEL), (4, 4));
150        assert_eq!(Cmyk8::MODEL, ColorModel::Cmyk);
151        assert_eq!((Gray16::CHANNELS, Gray16::BYTES_PER_PIXEL), (1, 2));
152        assert_eq!((Rgb16::CHANNELS, Rgb16::BYTES_PER_PIXEL), (3, 6));
153        assert_eq!((Rgba16::CHANNELS, Rgba16::BYTES_PER_PIXEL), (4, 8));
154    }
155
156    #[test]
157    fn distinct_models_share_channel_count() {
158        // Exactly the footgun the type system now prevents: same shape, different meaning.
159        assert_eq!(Rgba8::CHANNELS, Cmyk8::CHANNELS);
160        assert_ne!(Rgba8::MODEL, Cmyk8::MODEL);
161        assert_eq!(Gray8::CHANNELS, Indexed8::CHANNELS);
162        assert_ne!(Gray8::MODEL, Indexed8::MODEL);
163        assert_ne!(Gray8::MODEL, Bilevel::MODEL);
164    }
165}