Skip to main content

rasterrocket_render/
types.rs

1//! Raster-local enums and workspace-wide constants.
2//!
3//! Re-exports all [`color`] public types so that downstream modules within
4//! this crate only need `use crate::types::*`.
5
6use std::borrow::Cow;
7
8/// Re-export every public type from the [`color`] crate, including all
9/// arithmetic helpers from [`color::convert`].
10///
11/// Downstream modules within this crate can import everything they need with
12/// a single `use crate::types::*`.
13pub use color::{
14    AnyColor, Cmyk8, DeviceN8, Gray8, NCOMPS, Pixel, PixelMode, Rgb8, Rgba8, TransferLut,
15    convert::*,
16};
17
18// ── Rasterizer constants ──────────────────────────────────────────────────────
19
20/// Supersampling factor for anti-aliasing.
21///
22/// The AA buffer is `bitmap_width × AA_SIZE` pixels wide and `AA_SIZE` rows
23/// tall. Changing this value requires corresponding changes to all AA-buffer
24/// allocation and compositing logic.
25///
26/// Unit: pixels (linear).
27pub const AA_SIZE: i32 = 4;
28
29/// Maximum number of De Casteljau subdivisions when flattening a Bézier curve.
30///
31/// A value of 1024 gives sub-pixel accuracy for all practical PDF coordinate
32/// ranges. Increasing this value raises stack and array allocation costs
33/// quadratically; decreasing it degrades curve quality.
34///
35/// Unit: dimensionless iteration count.
36pub const MAX_CURVE_SPLITS: i32 = 1024;
37
38/// Control-point ratio for approximating a quarter-circle with a cubic Bézier.
39///
40/// Derivation: `4 * (√2 − 1) / 3 ≈ 0.552_284_75`. Four cubic Bézier segments
41/// with this control-point ratio approximate a unit circle with a maximum
42/// radial error below 0.03 %.
43///
44/// Unit: dimensionless ratio (fraction of the radius).
45pub const BEZIER_CIRCLE: f64 = 0.552_284_75;
46
47/// Number of spot color channels in a [`DeviceN8`] pixel.
48///
49/// A `DeviceN8` pixel is laid out as `[C, M, Y, K, S0, S1, S2, S3]`, giving
50/// 4 process + 4 spot = 8 bytes per pixel total.
51pub const SPOT_NCOMPS: usize = 4;
52
53// ── Enums ─────────────────────────────────────────────────────────────────────
54
55/// Thin-line rendering treatment.
56///
57/// Controls how strokes whose device-space width is less than one pixel are
58/// drawn. The default preserves the hairline width via shape anti-aliasing
59/// when stroke adjustment (`SA`) is on.
60///
61/// # Extension policy
62///
63/// This enum is **not** `#[non_exhaustive]` because exhaustive matches on it
64/// are used throughout the rasterizer to guarantee every mode is handled; the
65/// compiler enforces this at every call site. Adding a new variant is an
66/// intentional breaking change that forces all match sites to be updated.
67#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
68pub enum ThinLineMode {
69    /// Preserve the hairline width.
70    ///
71    /// When stroke adjustment is on and the device-space line width is less
72    /// than half a pixel, the line is drawn as a shaped (anti-aliased) stroke;
73    /// otherwise it is drawn solid.
74    #[default]
75    Default,
76    /// Render as a solid opaque 1-pixel line regardless of width.
77    Solid,
78    /// Always draw with shape anti-aliasing even for sub-pixel widths.
79    Shape,
80}
81
82/// Stroke line-cap style.
83///
84/// Determines how open sub-path endpoints are capped. Corresponds directly to
85/// the PDF `lineCap` graphics-state parameter (PDF 32000-1:2008, §8.4.3.3).
86///
87/// # Extension policy
88///
89/// Not `#[non_exhaustive]`: PDF specifies exactly three cap styles; exhaustive
90/// matching is a compile-time guarantee that all are handled.
91#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
92pub enum LineCap {
93    /// Square end flush with the endpoint (PDF cap style 0).
94    #[default]
95    Butt,
96    /// Semicircle centred on the endpoint, radius = half line-width (PDF cap style 1).
97    Round,
98    /// Square extending half the line-width beyond the endpoint (PDF cap style 2).
99    Projecting,
100}
101
102/// Stroke line-join style.
103///
104/// Determines how two stroke segments meet at a shared vertex. Corresponds to
105/// the PDF `lineJoin` graphics-state parameter (PDF 32000-1:2008, §8.4.3.4).
106///
107/// # Extension policy
108///
109/// Not `#[non_exhaustive]`: PDF specifies exactly three join styles.
110#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
111pub enum LineJoin {
112    /// Sharp mitered corner, clipped at the miter limit (PDF join style 0).
113    #[default]
114    Miter,
115    /// Rounded join — a filled arc centred on the vertex (PDF join style 1).
116    Round,
117    /// Flat bevel — the outside corner is cut off (PDF join style 2).
118    Bevel,
119}
120
121/// Halftone screen type used by [`ScreenParams`].
122///
123/// # Extension policy
124///
125/// Not `#[non_exhaustive]`: the screen-building code uses exhaustive match; the
126/// compiler ensures new screen types are handled everywhere.
127#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
128pub enum ScreenType {
129    /// Bayer-style dispersed-dot ordered dither.
130    ///
131    /// Chosen automatically below 300 dpi (4 × 4 matrix). The default.
132    #[default]
133    Dispersed,
134    /// Clustered-dot halftone screen.
135    Clustered,
136    /// Stochastic clustered-dot screen (64 × 64 matrix, used ≥ 300 dpi).
137    StochasticClustered,
138}
139
140/// Parameters for constructing a halftone screen.
141///
142/// # Valid values
143///
144/// - [`kind`](ScreenParams::kind): any [`ScreenType`] variant.
145/// - [`size`](ScreenParams::size): must be a **power of two** and **≥ 2**
146///   (e.g. 2, 4, 8, 16 …). The screen matrix is `size × size` cells. Values
147///   that are not powers of two, or values less than 2, produce an undefined
148///   screen pattern. Call [`validate`](ScreenParams::validate) after construction
149///   to enforce these constraints.
150/// - [`dot_radius`](ScreenParams::dot_radius): meaningful only for
151///   [`ScreenType::StochasticClustered`]; must be ≥ 1. Ignored for other screen
152///   types but must still be positive.
153///
154/// # Default
155///
156/// `{ Dispersed, size: 2, dot_radius: 2 }`.
157#[derive(Copy, Clone, Debug)]
158pub struct ScreenParams {
159    /// The halftone algorithm to use.
160    pub kind: ScreenType,
161
162    /// Screen matrix dimension in cells.
163    ///
164    /// Must be a power of two and ≥ 2. Typical values: 2, 4, 8, 16, 32, 64.
165    pub size: i32,
166
167    /// Dot radius for [`ScreenType::StochasticClustered`] screens.
168    ///
169    /// Must be ≥ 1. Ignored (but still validated) for other screen types.
170    pub dot_radius: i32,
171}
172
173impl Default for ScreenParams {
174    /// Returns the default screen parameters: `{ Dispersed, size: 2, dot_radius: 2 }`.
175    fn default() -> Self {
176        Self {
177            kind: ScreenType::Dispersed,
178            size: 2,
179            dot_radius: 2,
180        }
181    }
182}
183
184impl ScreenParams {
185    /// Validates that the parameter values are within their documented ranges.
186    ///
187    /// # Constraints checked
188    ///
189    /// - `size` must be ≥ 2.
190    /// - `size` must be a power of two.
191    /// - `dot_radius` must be ≥ 1.
192    ///
193    /// # Errors
194    ///
195    /// Returns `Err` with a human-readable [`Cow<'static, str>`] message
196    /// describing the first constraint violated.  All current error messages
197    /// are static string literals (`Cow::Borrowed`), so no allocation occurs.
198    /// Future callers that need dynamic messages (e.g. including the offending
199    /// field value) can return `Cow::Owned(format!(...))` without a breaking
200    /// API change.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// # use raster::types::{ScreenParams, ScreenType};
206    /// assert!(ScreenParams::default().validate().is_ok());
207    ///
208    /// let bad = ScreenParams { size: 3, ..ScreenParams::default() };
209    /// assert!(bad.validate().is_err());
210    /// ```
211    pub const fn validate(&self) -> Result<(), Cow<'static, str>> {
212        if self.size < 2 {
213            return Err(Cow::Borrowed("ScreenParams::size must be >= 2"));
214        }
215        // Casting to u32 is safe: we already checked size >= 2 > 0, so the
216        // sign bit is clear and no bits are lost.
217        #[expect(
218            clippy::cast_sign_loss,
219            reason = "size >= 2 has been asserted above; the value is positive, \
220                      so the cast to u32 loses no bits"
221        )]
222        let size_u32 = self.size as u32;
223        if !size_u32.is_power_of_two() {
224            return Err(Cow::Borrowed("ScreenParams::size must be a power of two"));
225        }
226        if self.dot_radius < 1 {
227            return Err(Cow::Borrowed("ScreenParams::dot_radius must be >= 1"));
228        }
229        Ok(())
230    }
231}
232
233// ── BlendMode ─────────────────────────────────────────────────────────────────
234
235/// PDF compositing blend mode (PDF 32000-1:2008, §11.3.5).
236///
237/// Blend mode is a typed enum; dispatch happens in `pipe::blend`.
238///
239/// # Separable vs. non-separable
240///
241/// The first twelve variants (`Normal` through `Exclusion`) are *separable*:
242/// the blend function operates independently on each colour channel.
243/// The last four (`Hue` through `Luminosity`) are *non-separable*: they operate
244/// on the full RGB triple (with CMYK converted to additive space first).
245///
246/// `Normal` is by far the most common; the pipeline fast-paths for it.
247#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
248pub enum BlendMode {
249    /// Standard Porter-Duff over (gfxBlendNormal).
250    #[default]
251    Normal,
252    /// `Cs × Cd` (gfxBlendMultiply).
253    Multiply,
254    /// `Cs + Cd - Cs × Cd` (gfxBlendScreen).
255    Screen,
256    /// Hard-light of Cd over Cs (gfxBlendOverlay).
257    Overlay,
258    /// `min(Cs, Cd)` (gfxBlendDarken).
259    Darken,
260    /// `max(Cs, Cd)` (gfxBlendLighten).
261    Lighten,
262    /// Brighten Cd to reflect Cs (gfxBlendColorDodge).
263    ColorDodge,
264    /// Darken Cd to reflect Cs (gfxBlendColorBurn).
265    ColorBurn,
266    /// Multiply or screen depending on Cs < 0.5 (gfxBlendHardLight).
267    HardLight,
268    /// Soft version of `HardLight` (gfxBlendSoftLight).
269    SoftLight,
270    /// `|Cd - Cs|` (gfxBlendDifference).
271    Difference,
272    /// `Cs + Cd - 2 × Cs × Cd` (gfxBlendExclusion).
273    Exclusion,
274    /// Hue of Cs, saturation and luminosity of Cd (non-separable, gfxBlendHue).
275    Hue,
276    /// Saturation of Cs, hue and luminosity of Cd (non-separable, gfxBlendSaturation).
277    Saturation,
278    /// Hue and saturation of Cs, luminosity of Cd (non-separable, gfxBlendColor).
279    Color,
280    /// Luminosity of Cs, hue and saturation of Cd (non-separable, gfxBlendLuminosity).
281    Luminosity,
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn screen_params_default_matches_cpp() {
290        let p = ScreenParams::default();
291        assert_eq!(p.kind, ScreenType::Dispersed);
292        assert_eq!(p.size, 2);
293        assert_eq!(p.dot_radius, 2);
294        assert!(p.validate().is_ok());
295    }
296
297    #[test]
298    fn screen_params_validate_rejects_non_power_of_two() {
299        let p = ScreenParams {
300            size: 3,
301            ..ScreenParams::default()
302        };
303        assert!(p.validate().is_err());
304    }
305
306    #[test]
307    fn screen_params_validate_rejects_size_less_than_2() {
308        let p = ScreenParams {
309            size: 1,
310            ..ScreenParams::default()
311        };
312        assert!(p.validate().is_err());
313        let p = ScreenParams {
314            size: 0,
315            ..ScreenParams::default()
316        };
317        assert!(p.validate().is_err());
318        let p = ScreenParams {
319            size: -1,
320            ..ScreenParams::default()
321        };
322        assert!(p.validate().is_err());
323    }
324
325    #[test]
326    fn screen_params_validate_rejects_zero_dot_radius() {
327        let p = ScreenParams {
328            dot_radius: 0,
329            ..ScreenParams::default()
330        };
331        assert!(p.validate().is_err());
332    }
333
334    /// `validate` errors are `Cow::Borrowed` (static string literals) — no
335    /// heap allocation occurs for any of the three failure paths.
336    #[test]
337    fn screen_params_validate_errors_are_borrowed() {
338        use std::borrow::Cow;
339
340        let size_small = ScreenParams {
341            size: 1,
342            ..ScreenParams::default()
343        };
344        assert!(
345            matches!(size_small.validate(), Err(Cow::Borrowed(_))),
346            "size < 2 error should be Cow::Borrowed"
347        );
348
349        let size_non_pow2 = ScreenParams {
350            size: 3,
351            ..ScreenParams::default()
352        };
353        assert!(
354            matches!(size_non_pow2.validate(), Err(Cow::Borrowed(_))),
355            "non-power-of-two error should be Cow::Borrowed"
356        );
357
358        let bad_radius = ScreenParams {
359            dot_radius: 0,
360            ..ScreenParams::default()
361        };
362        assert!(
363            matches!(bad_radius.validate(), Err(Cow::Borrowed(_))),
364            "dot_radius < 1 error should be Cow::Borrowed"
365        );
366    }
367
368    /// `validate` error messages contain the field name so they are human-readable.
369    #[test]
370    fn screen_params_validate_error_messages_mention_field() {
371        let size_small = ScreenParams {
372            size: 0,
373            ..ScreenParams::default()
374        };
375        let msg = size_small.validate().unwrap_err();
376        assert!(
377            msg.contains("size"),
378            "error for small size should mention 'size', got: {msg}"
379        );
380
381        let size_non_pow2 = ScreenParams {
382            size: 3,
383            ..ScreenParams::default()
384        };
385        let msg = size_non_pow2.validate().unwrap_err();
386        assert!(
387            msg.contains("size"),
388            "error for non-power-of-two should mention 'size', got: {msg}"
389        );
390
391        let bad_radius = ScreenParams {
392            dot_radius: 0,
393            ..ScreenParams::default()
394        };
395        let msg = bad_radius.validate().unwrap_err();
396        assert!(
397            msg.contains("dot_radius"),
398            "error for bad radius should mention 'dot_radius', got: {msg}"
399        );
400    }
401
402    #[test]
403    fn screen_params_validate_accepts_valid_power_of_two_sizes() {
404        for exp in 1u32..=6 {
405            let size = 1i32 << exp; // 2, 4, 8, 16, 32, 64
406            let p = ScreenParams {
407                size,
408                dot_radius: 1,
409                ..ScreenParams::default()
410            };
411            assert!(p.validate().is_ok(), "size={size} should be valid");
412        }
413    }
414
415    #[test]
416    fn spot_ncomps_matches_cpp() {
417        // SPOT_NCOMPS = 4, matching #define SPOT_NCOMPS 4 in SplashTypes.h.
418        assert_eq!(SPOT_NCOMPS, 4);
419    }
420
421    #[test]
422    fn aa_size_matches_cpp() {
423        // splashAASize = 4 in SplashTypes.h.
424        assert_eq!(AA_SIZE, 4);
425    }
426
427    #[test]
428    fn max_curve_splits_matches_cpp() {
429        // splashMaxCurveSplits = 1 << 10 = 1024 in SplashXPath.h.
430        assert_eq!(MAX_CURVE_SPLITS, 1 << 10);
431    }
432
433    #[test]
434    fn bezier_circle_matches_cpp() {
435        // bezierCircle = 0.55228475 defined in Splash.cc.
436        // Check to 7 significant figures.
437        assert!((BEZIER_CIRCLE - 0.552_284_75_f64).abs() < 1e-9);
438    }
439}