Skip to main content

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}