rasterrocket_color/pixel.rs
1//! Pixel types and the [`Pixel`] trait.
2//!
3//! # Design overview
4//!
5//! Each concrete type is a `#[repr(C)]` struct that also implements
6//! [`bytemuck::Pod`] and [`bytemuck::Zeroable`], allowing zero-copy casts
7//! between `&[u8]` row buffers and typed pixel slices via
8//! `bytemuck::cast_slice`.
9//!
10//! ## The `Pixel` trait
11//!
12//! [`Pixel`] is the generic bound used by `Bitmap<P>` and the rasterizer
13//! pipeline. It requires `Copy + Pod + Zeroable + Send + Sync + 'static` so
14//! that pixel buffers can be shared across threads without additional
15//! synchronisation. Every implementation must keep `BYTES` equal to
16//! `std::mem::size_of::<Self>()` — the module-level compile-time assertions
17//! enforce this.
18//!
19//! ## Monomorphization
20//!
21//! The `Pixel` bound is used as a generic parameter on `Bitmap<P>` and the hot
22//! rasterizer loops. The compiler generates one specialised code path per pixel
23//! format, eliminating runtime mode dispatch in the inner loop.
24//!
25//! ## `AnyColor` vs `Pixel`
26//!
27//! Use [`AnyColor`] when you need to carry a pixel value alongside its mode at
28//! runtime (e.g. paper colour, graphics-state default colour). Use a concrete
29//! `impl Pixel` type — or a `Pixel`-bounded generic — everywhere else.
30
31use bytemuck::{Pod, Zeroable};
32
33use crate::mode::PixelMode;
34
35// ── Compile-time size assertions ──────────────────────────────────────────────
36//
37// Each assertion fires at compile time if `BYTES` disagrees with the actual
38// struct size. This catches padding surprises that the runtime test would only
39// catch after the binary is built.
40
41const _: () = assert!(std::mem::size_of::<Rgb8>() == Rgb8::BYTES);
42const _: () = assert!(std::mem::size_of::<Rgba8>() == Rgba8::BYTES);
43const _: () = assert!(std::mem::size_of::<Gray8>() == Gray8::BYTES);
44const _: () = assert!(std::mem::size_of::<Cmyk8>() == Cmyk8::BYTES);
45const _: () = assert!(std::mem::size_of::<DeviceN8>() == DeviceN8::BYTES);
46
47// ── Pixel trait ───────────────────────────────────────────────────────────────
48
49/// A typed pixel value that can be stored in a `Bitmap<P>` row buffer.
50///
51/// All implementations are `Copy + Pod`, enabling zero-copy row access via
52/// `bytemuck::cast_slice`. `BYTES` must match `std::mem::size_of::<Self>()`;
53/// compile-time assertions in this module enforce the invariant.
54pub trait Pixel: Copy + Pod + Zeroable + Send + Sync + 'static {
55 /// The [`PixelMode`] variant that identifies this pixel format at runtime.
56 const MODE: PixelMode;
57 /// Byte width of one pixel. Must equal `std::mem::size_of::<Self>()`.
58 const BYTES: usize;
59}
60
61// ── Concrete pixel types ──────────────────────────────────────────────────────
62
63/// 8-bit RGB, 3 bytes/pixel, wire layout `[R, G, B]`.
64///
65/// The most common rasterizer output format, matching `SplashModRGB8` in the
66/// C++ side.
67#[repr(C)]
68#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Pod, Zeroable)]
69pub struct Rgb8 {
70 /// Red channel, `0` = minimum, `255` = full intensity.
71 pub r: u8,
72 /// Green channel, `0` = minimum, `255` = full intensity.
73 pub g: u8,
74 /// Blue channel, `0` = minimum, `255` = full intensity.
75 pub b: u8,
76}
77
78impl Pixel for Rgb8 {
79 const MODE: PixelMode = PixelMode::Rgb8;
80 const BYTES: usize = 3;
81}
82
83/// 8-bit RGBA, 4 bytes/pixel, wire layout `[R, G, B, A]`.
84///
85/// This is the working format for transparency groups. The `MODE` constant is
86/// set to [`PixelMode::Xbgr8`] as an intentional approximation: the rasterizer
87/// internally uses this struct for transparency groups and the mode field is
88/// only used for external dispatch (e.g. choosing a blitter). The actual byte
89/// layout is `[R, G, B, A]`, **not** `[X, B, G, R]`; callers that perform
90/// memory-layout-sensitive operations must use the struct fields directly
91/// rather than relying on the `MODE` variant.
92#[repr(C)]
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Pod, Zeroable)]
94pub struct Rgba8 {
95 /// Red channel, `0` = minimum, `255` = full intensity.
96 pub r: u8,
97 /// Green channel, `0` = minimum, `255` = full intensity.
98 pub g: u8,
99 /// Blue channel, `0` = minimum, `255` = full intensity.
100 pub b: u8,
101 /// Alpha channel, `0` = fully transparent, `255` = fully opaque.
102 pub a: u8,
103}
104
105impl Pixel for Rgba8 {
106 // Intentional approximation — see struct doc comment above.
107 const MODE: PixelMode = PixelMode::Xbgr8;
108 const BYTES: usize = 4;
109}
110
111/// 8-bit grayscale, 1 byte/pixel, wire layout `[Y]`.
112///
113/// Used for `-gray` output. RGB→luminance uses BT.709 coefficients.
114#[repr(C)]
115#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Pod, Zeroable)]
116pub struct Gray8 {
117 /// Luminance, `0` = black, `255` = white.
118 pub v: u8,
119}
120
121impl Pixel for Gray8 {
122 const MODE: PixelMode = PixelMode::Mono8;
123 const BYTES: usize = 1;
124}
125
126/// 8-bit CMYK, 4 bytes/pixel, wire layout `[C, M, Y, K]`.
127///
128/// Used for `-jpegcmyk` and overprint modes.
129#[repr(C)]
130#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Pod, Zeroable)]
131pub struct Cmyk8 {
132 /// Cyan ink, `0` = no ink, `255` = full coverage.
133 pub c: u8,
134 /// Magenta ink, `0` = no ink, `255` = full coverage.
135 pub m: u8,
136 /// Yellow ink, `0` = no ink, `255` = full coverage.
137 pub y: u8,
138 /// Black (key) ink, `0` = no ink, `255` = full coverage.
139 pub k: u8,
140}
141
142impl Pixel for Cmyk8 {
143 const MODE: PixelMode = PixelMode::Cmyk8;
144 const BYTES: usize = 4;
145}
146
147/// CMYK + 4 spot channels, 8 bytes/pixel, wire layout `[C, M, Y, K, S0, S1, S2, S3]`.
148///
149/// Used with `-overprint`. `SPOT_NCOMPS = 4` is fixed at compile time,
150/// matching the C++ default.
151#[repr(C)]
152#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Pod, Zeroable)]
153pub struct DeviceN8 {
154 /// Process CMYK channels.
155 pub cmyk: Cmyk8,
156 /// Spot channels `S0`–`S3`, `0` = no ink, `255` = full coverage.
157 pub spots: [u8; 4],
158}
159
160impl Pixel for DeviceN8 {
161 const MODE: PixelMode = PixelMode::DeviceN8;
162 const BYTES: usize = 8;
163}
164
165// ── Erased pixel buffer ───────────────────────────────────────────────────────
166
167/// A mode-erased pixel value carrying up to 8 bytes (matching `SplashColor`).
168///
169/// # When to use `AnyColor` vs a concrete `Pixel` type
170///
171/// Prefer a concrete `impl Pixel` type — or a `Pixel`-bounded generic — in
172/// every performance-sensitive path; the monomorphized code paths avoid runtime
173/// dispatch. Use `AnyColor` only in the small number of places that must handle
174/// **all** modes at runtime without monomorphizing the entire call stack: paper
175/// colour, graphics-state default colour, and similar configuration values.
176#[derive(Copy, Clone, Debug, Default)]
177pub struct AnyColor {
178 /// Raw pixel bytes; only the first `mode.bytes_per_pixel()` entries are meaningful.
179 pub bytes: [u8; 8],
180 /// The pixel format that determines how `bytes` should be interpreted.
181 pub mode: PixelMode,
182}
183
184impl AnyColor {
185 /// Return the black (zero-ink / zero-intensity) colour for `mode`.
186 #[must_use]
187 pub const fn black(mode: PixelMode) -> Self {
188 Self {
189 bytes: [0; 8],
190 mode,
191 }
192 }
193
194 /// Return the white colour for `mode`.
195 ///
196 /// # Per-mode encoding
197 ///
198 /// | Mode | White encoding |
199 /// |------|----------------|
200 /// | `Mono1` | `bytes[0] = 0xFF` — all 8 bits set = all pixels white (MSB-first packed format) |
201 /// | `Mono8` | `bytes[0] = 255` |
202 /// | `Rgb8`, `Bgr8` | `bytes[0..3] = [255, 255, 255]` |
203 /// | `Xbgr8` | `bytes[0..4] = [255, 255, 255, 255]` — `byte[3]` is the ignored X/padding byte, set to 255 for consistency so the full 4-byte value reads as opaque white in any RGBA interpretation |
204 /// | `Cmyk8`, `DeviceN8` | all bytes zero — CMYK white is zero ink on all channels |
205 #[must_use]
206 pub const fn white(mode: PixelMode) -> Self {
207 let mut bytes = [0u8; 8];
208 match mode {
209 // Mono1: 0xFF means all 8 packed bits = 1 = white (MSB-first format).
210 // Mono8: 255 = maximum luminance = white.
211 PixelMode::Mono1 | PixelMode::Mono8 => bytes[0] = 255,
212 PixelMode::Rgb8 | PixelMode::Bgr8 => {
213 bytes[0] = 255;
214 bytes[1] = 255;
215 bytes[2] = 255;
216 }
217 PixelMode::Xbgr8 => {
218 bytes[0] = 255;
219 bytes[1] = 255;
220 bytes[2] = 255;
221 // byte[3] is the X (ignored/padding) byte in XBGR. Setting it
222 // to 255 ensures the 4-byte word reads as fully-opaque white
223 // when interpreted as any RGBA variant, and avoids leaving
224 // uninitialised-looking padding in the output buffer.
225 bytes[3] = 255;
226 }
227 // CMYK/DeviceN white = no ink on any channel = all zeros.
228 PixelMode::Cmyk8 | PixelMode::DeviceN8 => {}
229 }
230 Self { bytes, mode }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 // The compile-time assertions at the top of the module already enforce
239 // BYTES == size_of, but the runtime test provides a readable failure message
240 // during `cargo test` in case someone adds a new type and forgets the const.
241 #[test]
242 fn sizes_match_bytes_const() {
243 assert_eq!(std::mem::size_of::<Rgb8>(), Rgb8::BYTES);
244 assert_eq!(std::mem::size_of::<Rgba8>(), Rgba8::BYTES);
245 assert_eq!(std::mem::size_of::<Gray8>(), Gray8::BYTES);
246 assert_eq!(std::mem::size_of::<Cmyk8>(), Cmyk8::BYTES);
247 assert_eq!(std::mem::size_of::<DeviceN8>(), DeviceN8::BYTES);
248 }
249
250 #[test]
251 fn cmyk8_black() {
252 let px = Cmyk8 {
253 c: 0,
254 m: 0,
255 y: 0,
256 k: 255,
257 };
258 let (r, g, b) = crate::convert::cmyk_to_rgb(px.c, px.m, px.y, px.k);
259 assert_eq!((r, g, b), (0, 0, 0));
260 }
261
262 /// White for `Mono1` must be `0xFF` — all 8 packed bits set to 1.
263 #[test]
264 fn any_color_white_mono1_is_all_bits_set() {
265 let w = AnyColor::white(PixelMode::Mono1);
266 assert_eq!(w.bytes[0], 0xFF, "Mono1 white must be 0xFF (all bits = 1)");
267 }
268
269 /// White for `Xbgr8` must set all four bytes including the padding X byte.
270 #[test]
271 fn any_color_white_xbgr8_sets_padding_byte() {
272 let w = AnyColor::white(PixelMode::Xbgr8);
273 assert_eq!(w.bytes, [255, 255, 255, 255, 0, 0, 0, 0]);
274 }
275
276 /// CMYK/DeviceN white is zero ink on all channels.
277 #[test]
278 fn any_color_white_cmyk_is_zero() {
279 let w = AnyColor::white(PixelMode::Cmyk8);
280 assert_eq!(w.bytes, [0u8; 8]);
281 }
282}