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}