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}