stet_graphics/device.rs
1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Output device parameter types — pure data structures for rendering operations.
6
7use crate::color::{DashPattern, DeviceColor, FillRule, LineCap, LineJoin};
8use crate::display_list::DisplayList;
9use crate::icc::ProfileHash;
10use std::sync::Arc;
11use stet_fonts::geometry::{Matrix, PsPath};
12
13/// Pre-sampled transfer function (256 samples, domain `[0,1]` → range `[0,1]`).
14/// Arc for cheap clone across display list elements.
15pub type TransferTable = Arc<Vec<f64>>;
16
17/// Transfer function state captured at paint time.
18#[derive(Clone, Debug, Default)]
19pub struct TransferState {
20 /// Single-component transfer (from settransfer). None = identity.
21 pub gray: Option<TransferTable>,
22 /// Per-component color transfer \[R, G, B, Gray\] (from setcolortransfer).
23 /// When set, overrides `gray`.
24 pub color: Option<[Option<TransferTable>; 4]>,
25}
26
27impl TransferState {
28 /// Returns true if any non-identity transfer function is set.
29 pub fn has_functions(&self) -> bool {
30 if self.gray.is_some() {
31 return true;
32 }
33 if let Some(ref color) = self.color {
34 return color.iter().any(|t| t.is_some());
35 }
36 false
37 }
38}
39
40/// A pre-computed halftone screen for PDF output.
41#[derive(Clone, Debug)]
42pub struct HalftoneScreen {
43 pub frequency: f64,
44 pub angle: f64,
45 /// Spot function as PDF Type 4 calculator bytes (e.g., b"{ dup mul exch dup mul add 1 exch sub }").
46 /// None if conversion failed (falls back to sampled_2d).
47 pub type4_tokens: Option<Arc<Vec<u8>>>,
48 /// Spot function sampled on a 64×64 grid (4096 f64 values, domain `[-1,1]²`, range `[0,1]`).
49 /// Used when Type 4 decompilation fails.
50 pub sampled_2d: Option<Arc<Vec<f64>>>,
51}
52
53/// Pre-sampled black generation / undercolor removal state for PDF output.
54#[derive(Clone, Debug, Default)]
55pub struct BgUcrState {
56 /// Black generation function (256 samples, domain `[0,1]` → range `[0,1]`).
57 pub bg: Option<Arc<Vec<f64>>>,
58 /// Undercolor removal function (256 samples, domain `[0,1]` → range `[-1,1]`).
59 pub ucr: Option<Arc<Vec<f64>>>,
60}
61
62/// Pre-computed halftone state captured at paint time.
63#[derive(Clone, Debug, Default)]
64pub struct HalftoneState {
65 /// Single-component halftone (from setscreen). None = default (suppress).
66 pub gray: Option<Arc<HalftoneScreen>>,
67 /// Per-component \[R, G, B, Gray\] (from setcolorscreen). Emits Type 5 composite.
68 pub color: Option<[Option<Arc<HalftoneScreen>>; 4]>,
69}
70
71/// Native ICCBased fill/stroke color info for PDF output.
72///
73/// Preserves the raw component values from the source `sc`/`scn`
74/// operator plus the ICC profile bytes, so a PDF reader can capture the
75/// exact ICCBased paint at parse time and the PDF writer can emit a
76/// faithful `/CSn cs` + `c1 c2 c3 scn` round-trip — independent of the
77/// `DeviceColor` ICC-converted RGB that's used for rasterizing.
78#[derive(Clone, Debug)]
79pub struct IccColor {
80 /// Raw component values from `sc`/`scn` (length matches `color_space.n`).
81 pub components: Vec<f64>,
82 /// The ICC color space definition.
83 pub color_space: IccColorSpace,
84}
85
86/// Pre-resolved ICCBased color space ready for PDF emission.
87#[derive(Clone, Debug)]
88pub struct IccColorSpace {
89 /// Number of components (1, 3, or 4).
90 pub n: u32,
91 /// Raw ICC profile bytes (Arc-shared so multiple paints can dedup
92 /// to a single PDF stream).
93 pub profile_data: Arc<Vec<u8>>,
94 /// Profile hash, used both as a writer-side dedup key and to keep
95 /// the IccCache lookups in sync with the rasterizer.
96 pub profile_hash: ProfileHash,
97}
98
99/// Native Separation/DeviceN color info for PDF output.
100#[derive(Clone, Debug)]
101pub struct SpotColor {
102 /// Tint values from the most recent setcolor (1 for Separation, N for DeviceN).
103 pub tint_values: Vec<f64>,
104 /// Color space definition for this spot color.
105 pub color_space: SpotColorSpace,
106}
107
108/// Separation or DeviceN color space with pre-sampled tint function.
109///
110/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
111/// wildcard arm.
112#[derive(Clone, Debug)]
113#[non_exhaustive]
114pub enum SpotColorSpace {
115 Separation {
116 name: Vec<u8>,
117 alt: SimpleColorSpace,
118 tint_table: Arc<TintLookupTable>,
119 },
120 DeviceN {
121 names: Vec<Vec<u8>>,
122 alt: SimpleColorSpace,
123 tint_table: Arc<TintLookupTable>,
124 },
125}
126
127/// Simple device color space for alt-space references.
128#[derive(Clone, Debug, PartialEq, Eq, Hash)]
129pub enum SimpleColorSpace {
130 DeviceGray,
131 DeviceRGB,
132 DeviceCMYK,
133}
134
135/// Bitmask of CMYK channels painted by an overprint operation.
136/// Bits: 0=Cyan, 1=Magenta, 2=Yellow, 3=Black.
137pub const CMYK_C: u8 = 1 << 0;
138pub const CMYK_M: u8 = 1 << 1;
139pub const CMYK_Y: u8 = 1 << 2;
140pub const CMYK_K: u8 = 1 << 3;
141pub const CMYK_ALL: u8 = CMYK_C | CMYK_M | CMYK_Y | CMYK_K;
142
143/// Map a CMYK process color name to its channel bit.
144pub fn cmyk_channel_for_name(name: &[u8]) -> u8 {
145 match name {
146 b"Cyan" => CMYK_C,
147 b"Magenta" => CMYK_M,
148 b"Yellow" => CMYK_Y,
149 b"Black" => CMYK_K,
150 b"All" => CMYK_ALL,
151 b"None" => 0,
152 _ => 0,
153 }
154}
155
156/// Parameters for filling a path.
157///
158/// Constructed by interpreter/parser code (stet-ops, stet-pdf-reader)
159/// and read by renderers. New fields may be added without notice; pattern-
160/// matching consumers should use `..` to ignore unmatched fields.
161#[derive(Clone, Debug)]
162pub struct FillParams {
163 pub color: DeviceColor,
164 pub fill_rule: FillRule,
165 pub ctm: Matrix,
166 /// True when this fill is a text glyph from a show operator.
167 /// PDF device skips these (uses Text elements instead).
168 pub is_text_glyph: bool,
169 /// Overprint flag from graphics state (used by PDF output).
170 pub overprint: bool,
171 /// Overprint mode (0 or 1). With OPM 1 + DeviceCMYK, only non-zero channels are painted.
172 pub overprint_mode: i32,
173 /// True when /OPM was set together with /op or /OP in the same ExtGState
174 /// dict that configured this fill. Enables strict OPM-1 "preserve zero
175 /// components" behavior; when false, an all-zero CMYK source still
176 /// performs a full knockout (legacy Adobe compatibility).
177 pub opm_paired: bool,
178 /// Which CMYK channels this fill paints (bitmask of CMYK_C/M/Y/K).
179 pub painted_channels: u8,
180 /// True when color space is DeviceCMYK or ICCBased(4).
181 pub is_device_cmyk: bool,
182 /// Separation/DeviceN color for PDF output. None for device color spaces.
183 pub spot_color: Option<SpotColor>,
184 /// ICCBased color for PDF output. None for device color spaces and
185 /// for Separation/DeviceN paints (those round-trip through `spot_color`).
186 pub icc_color: Option<IccColor>,
187 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
188 pub rendering_intent: u8,
189 /// Pre-sampled transfer function state for PDF output.
190 pub transfer: TransferState,
191 /// Pre-computed halftone screen state for PDF output.
192 pub halftone: HalftoneState,
193 /// Pre-sampled black generation / undercolor removal for PDF output.
194 pub bg_ucr: BgUcrState,
195 /// Fill opacity (0.0–1.0, default 1.0). Used by PDF transparency.
196 pub alpha: f64,
197 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
198 pub blend_mode: u8,
199 /// PDF `AIS` (alpha-is-shape). When true, the source is interpreted as
200 /// shape rather than opacity. Default false.
201 pub alpha_is_shape: bool,
202}
203
204/// Parameters for a text element emitted by show operators.
205///
206/// The PDF device uses these for BT/ET/Tf/Tj text operators.
207/// The raster device ignores them (uses Fill elements for glyph paths).
208///
209/// New fields may be added without notice; pattern-matching consumers
210/// should use `..` to ignore unmatched fields.
211#[derive(Clone, Debug)]
212pub struct TextParams {
213 /// Character bytes (or 2-byte CID values for Type 0).
214 pub text: Vec<u8>,
215 /// Device-space X position at start of string.
216 pub start_x: f64,
217 /// Device-space Y position at start of string.
218 pub start_y: f64,
219 /// Font dict entity ID (raw u32 for VM independence).
220 pub font_entity: u32,
221 /// FontName bytes (e.g., b"Times-Roman").
222 pub font_name: Vec<u8>,
223 /// FontType (0, 1, 2, 3, 42).
224 pub font_type: i32,
225 /// Effective device-space font size.
226 pub font_size: f64,
227 /// Fill color at render time.
228 pub color: DeviceColor,
229 /// CTM at render time.
230 pub ctm: [f64; 6],
231 /// User-space font matrix (scaled to point units).
232 pub font_matrix: [f64; 6],
233 /// PaintType: 0 = fill (default), 2 = stroke (outlined glyphs).
234 pub paint_type: i32,
235 /// Device-space stroke width for PaintType 2 fonts.
236 pub stroke_width: f64,
237 /// Separation/DeviceN color for PDF output. None for device color spaces.
238 pub spot_color: Option<SpotColor>,
239 /// ICCBased color for PDF output. None for device color spaces and
240 /// for Separation/DeviceN paints (those round-trip through `spot_color`).
241 pub icc_color: Option<IccColor>,
242 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
243 pub rendering_intent: u8,
244 /// Pre-sampled transfer function state for PDF output.
245 pub transfer: TransferState,
246 /// Pre-computed halftone screen state for PDF output.
247 pub halftone: HalftoneState,
248 /// Pre-sampled black generation / undercolor removal for PDF output.
249 pub bg_ucr: BgUcrState,
250 /// Fill opacity (0.0–1.0, default 1.0). Used by PDF transparency.
251 pub fill_opacity: f64,
252 /// Stroke opacity (0.0–1.0, default 1.0). Applies to PaintType-2 fonts.
253 pub stroke_opacity: f64,
254 /// Blend mode (0=Normal, 1=Multiply, …, 15=Luminosity). Default 0.
255 pub blend_mode: u8,
256 /// Alpha-is-shape (PDF `AIS`). Default false.
257 pub alpha_is_shape: bool,
258 /// Text knockout (PDF `TK`). Default true.
259 pub text_knockout: bool,
260}
261
262/// Parameters for stroking a path.
263///
264/// New fields may be added without notice; pattern-matching consumers
265/// should use `..` to ignore unmatched fields.
266#[derive(Clone, Debug)]
267pub struct StrokeParams {
268 pub color: DeviceColor,
269 pub line_width: f64,
270 pub line_cap: LineCap,
271 pub line_join: LineJoin,
272 pub miter_limit: f64,
273 pub dash_pattern: DashPattern,
274 pub ctm: Matrix,
275 /// When true, snap thin stroke coordinates to device pixel centers.
276 pub stroke_adjust: bool,
277 /// True when this stroke is a text glyph from a show operator (PaintType 2).
278 pub is_text_glyph: bool,
279 /// Overprint flag from graphics state (used by PDF output).
280 pub overprint: bool,
281 /// Overprint mode (0 or 1).
282 pub overprint_mode: i32,
283 /// See FillParams::opm_paired. Strict OPM-1 preserve requires both
284 /// /OPM and /op|/OP set in the same ExtGState dict.
285 pub opm_paired: bool,
286 /// Which CMYK channels this stroke paints (bitmask of CMYK_C/M/Y/K).
287 pub painted_channels: u8,
288 /// True when stroke color space is DeviceCMYK or ICCBased(4) — OPM 1 only applies to these.
289 pub is_device_cmyk: bool,
290 /// Separation/DeviceN color for PDF output. None for device color spaces.
291 pub spot_color: Option<SpotColor>,
292 /// ICCBased color for PDF output. None for device color spaces and
293 /// for Separation/DeviceN paints (those round-trip through `spot_color`).
294 pub icc_color: Option<IccColor>,
295 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
296 pub rendering_intent: u8,
297 /// Pre-sampled transfer function state for PDF output.
298 pub transfer: TransferState,
299 /// Pre-computed halftone screen state for PDF output.
300 pub halftone: HalftoneState,
301 /// Pre-sampled black generation / undercolor removal for PDF output.
302 pub bg_ucr: BgUcrState,
303 /// Stroke opacity (0.0–1.0, default 1.0). Used by PDF transparency.
304 pub alpha: f64,
305 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
306 pub blend_mode: u8,
307 /// PDF `AIS` (alpha-is-shape). When true, the source is interpreted as
308 /// shape rather than opacity. Default false.
309 pub alpha_is_shape: bool,
310}
311
312/// Parameters for clipping.
313///
314/// New fields may be added without notice; pattern-matching consumers
315/// should use `..` to ignore unmatched fields.
316#[derive(Clone, Debug)]
317pub struct ClipParams {
318 pub fill_rule: FillRule,
319 pub ctm: Matrix,
320 /// For stroke-based clips: stroke parameters to expand the clip path
321 /// from a centerline to a stroke outline before rasterizing.
322 pub stroke_params: Option<StrokeParams>,
323}
324
325/// Pre-sampled tint transform: maps input tint values to alt-space components.
326#[derive(Clone, Debug)]
327pub struct TintLookupTable {
328 /// Number of input components (1 for Separation, N for DeviceN).
329 pub num_inputs: u32,
330 /// Number of output components (matches alternative space: 1/3/4).
331 pub num_outputs: u32,
332 /// Number of samples per dimension.
333 pub samples_per_dim: u32,
334 /// Flattened f32 data, row-major order. Length = samples_per_dim^num_inputs × num_outputs.
335 pub data: Vec<f32>,
336}
337
338impl TintLookupTable {
339 /// Linear interpolation lookup for 1D (Separation) tint transforms.
340 #[inline]
341 pub fn lookup_1d(&self, tint: f32, out: &mut [f32]) {
342 let n = self.samples_per_dim as usize;
343 let no = self.num_outputs as usize;
344 let idx = tint * (n - 1) as f32;
345 let i0 = (idx as usize).min(n - 2);
346 let frac = idx - i0 as f32;
347 let base0 = i0 * no;
348 let base1 = (i0 + 1) * no;
349 for (c, out_val) in out[..no].iter_mut().enumerate() {
350 *out_val = self.data[base0 + c] * (1.0 - frac) + self.data[base1 + c] * frac;
351 }
352 }
353
354 /// Multilinear interpolation lookup for N-D (DeviceN) tint transforms.
355 pub fn lookup_nd(&self, inputs: &[f32], out: &mut [f32]) {
356 let ni = self.num_inputs as usize;
357 let no = self.num_outputs as usize;
358 let n = self.samples_per_dim as usize;
359
360 let mut idx = [0usize; 8];
361 let mut frac = [0.0f32; 8];
362 for d in 0..ni {
363 let fi = inputs[d] * (n - 1) as f32;
364 idx[d] = (fi as usize).min(n - 2);
365 frac[d] = fi - idx[d] as f32;
366 }
367
368 let corners = 1usize << ni;
369 for out_val in out[..no].iter_mut() {
370 *out_val = 0.0;
371 }
372 for corner in 0..corners {
373 let mut weight = 1.0f32;
374 let mut linear_idx = 0usize;
375 for d in 0..ni {
376 let bit = (corner >> d) & 1;
377 let dim_idx = idx[d] + bit;
378 weight *= if bit == 1 { frac[d] } else { 1.0 - frac[d] };
379 let stride = n.pow((ni - 1 - d) as u32);
380 linear_idx += dim_idx * stride;
381 }
382 let base = linear_idx * no;
383 for (c, out_val) in out[..no].iter_mut().enumerate() {
384 *out_val += weight * self.data.get(base + c).copied().unwrap_or(0.0);
385 }
386 }
387 }
388}
389
390/// VM-free color space enum for images stored in the display list.
391///
392/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
393/// wildcard arm to remain forward-compatible.
394#[derive(Clone, Debug)]
395#[non_exhaustive]
396pub enum ImageColorSpace {
397 DeviceGray,
398 DeviceRGB,
399 DeviceCMYK,
400 ICCBased {
401 n: u32,
402 profile_hash: ProfileHash,
403 profile_data: Arc<Vec<u8>>,
404 },
405 Indexed {
406 base: Box<ImageColorSpace>,
407 hival: u32,
408 lookup: Vec<u8>,
409 },
410 CIEBasedABC {
411 params: Arc<crate::color::CieAbcParams>,
412 },
413 CIEBasedA {
414 params: Arc<crate::color::CieAParams>,
415 },
416 /// CIE L*a*b* color space (PDF /Lab or ICCBased Lab alternate).
417 ///
418 /// Sample byte layout: 3 components (L, a, b), 8-bit each. Decode
419 /// scales bytes: L = byte/255 × 100; a = byte/255 × (`range[1]`-`range[0]`) + `range[0]`;
420 /// b = byte/255 × (`range[3]`-`range[2]`) + `range[2]`.
421 Lab {
422 white_point: [f64; 3],
423 range: [f64; 4],
424 },
425 Separation {
426 name: Vec<u8>,
427 alt_space: Box<ImageColorSpace>,
428 tint_table: Arc<TintLookupTable>,
429 },
430 DeviceN {
431 names: Vec<Vec<u8>>,
432 alt_space: Box<ImageColorSpace>,
433 tint_table: Arc<TintLookupTable>,
434 },
435 Mask {
436 color: DeviceColor,
437 polarity: bool,
438 /// Optional Separation/DeviceN spot color carried alongside `color` so
439 /// the PDF writer can round-trip the imagemask's fill as `/CSn cs +
440 /// tint scn` instead of collapsing to a process-color paint.
441 /// `None` when the imagemask fill came from a Device/ICC space.
442 spot_color: Option<SpotColor>,
443 },
444 PreconvertedRGBA,
445}
446
447impl ImageColorSpace {
448 /// Number of components per sample.
449 pub fn num_components(&self) -> u32 {
450 match self {
451 ImageColorSpace::DeviceGray => 1,
452 ImageColorSpace::DeviceRGB => 3,
453 ImageColorSpace::DeviceCMYK => 4,
454 ImageColorSpace::ICCBased { n, .. } => *n,
455 ImageColorSpace::Indexed { .. } => 1,
456 ImageColorSpace::CIEBasedABC { .. } => 3,
457 ImageColorSpace::CIEBasedA { .. } => 1,
458 ImageColorSpace::Lab { .. } => 3,
459 ImageColorSpace::Separation { .. } => 1,
460 ImageColorSpace::DeviceN { tint_table, .. } => tint_table.num_inputs,
461 ImageColorSpace::Mask { .. } => 1,
462 ImageColorSpace::PreconvertedRGBA => 4,
463 }
464 }
465}
466
467/// Parameters for drawing an image.
468///
469/// New fields may be added without notice; pattern-matching consumers
470/// should use `..` to ignore unmatched fields.
471#[derive(Clone, Debug)]
472pub struct ImageParams {
473 pub width: u32,
474 pub height: u32,
475 pub color_space: ImageColorSpace,
476 pub bits_per_component: u8,
477 pub ctm: Matrix,
478 pub image_matrix: Matrix,
479 pub interpolate: bool,
480 pub mask_color: Option<Vec<u8>>,
481 pub alpha: f64,
482 pub blend_mode: u8,
483 pub overprint: bool,
484 pub overprint_mode: i32,
485 /// See FillParams::opm_paired.
486 pub opm_paired: bool,
487 pub painted_channels: u8,
488 /// PDF `AIS` (alpha-is-shape). Default false.
489 pub alpha_is_shape: bool,
490 /// Rendering intent that selects which `A2B*`/`B2A*` table the source
491 /// profile and the output-intent profile use when this image flows
492 /// through the proofing chain. Encoded as PDF byte: 0=Perceptual,
493 /// 1=RelativeColorimetric, 2=Saturation, 3=AbsoluteColorimetric.
494 /// Per ISO 32000 §11.3.4 a per-image `/Intent` overrides the gstate
495 /// `/RI`; PDF readers populate this from `/Intent` when present and
496 /// fall back to `gstate.rendering_intent` otherwise.
497 pub rendering_intent: u8,
498}
499
500/// A spot/DeviceN tint transform reduced to a uniform sampled grid.
501///
502/// Carries enough information for the PDF writer to round-trip the source's
503/// `/ColorSpace [/Separation … <tintFunc>]` or `/ColorSpace [/DeviceN … <tintFunc>]`
504/// dictionary as a SampledFunction without losing the spot identity.
505#[derive(Clone, Debug)]
506pub struct SpotTintFunction {
507 /// Number of input channels (1 for Separation, N for DeviceN).
508 pub input_dim: usize,
509 /// Samples per input dimension. For Separation this is the length of
510 /// `samples / 4`. For DeviceN this is the per-axis count of a grid of
511 /// total size `samples_per_dim.pow(input_dim)`.
512 pub samples_per_dim: usize,
513 /// Flat row-major grid of CMYK output samples, length
514 /// `samples_per_dim^input_dim * 4`. The reader builds this by evaluating
515 /// the source PDF's tint function at uniform input points; the writer
516 /// emits it back as a FunctionType-0 `/Function`.
517 pub cmyk_samples: Arc<Vec<f64>>,
518}
519
520/// Color space carried through the display list for native shading output.
521///
522/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
523/// wildcard arm.
524#[derive(Clone, Debug)]
525#[non_exhaustive]
526pub enum ShadingColorSpace {
527 DeviceGray,
528 DeviceRGB,
529 DeviceCMYK,
530 ICCBased {
531 n: u32,
532 profile_hash: ProfileHash,
533 profile_data: Arc<Vec<u8>>,
534 },
535 CalRGB {
536 white_point: [f64; 3],
537 matrix: Option<[f64; 9]>,
538 gamma: Option<[f64; 3]>,
539 },
540 CalGray {
541 white_point: [f64; 3],
542 gamma: Option<f64>,
543 },
544 /// Separation (single spot ink) with a CMYK alternate.
545 ///
546 /// Round-tripping this variant preserves the spot identity in the output
547 /// PDF; without it, the writer emits `/DeviceCMYK` and downstream readers
548 /// can't reconstruct the spot-tint-blend compositing behavior.
549 Separation {
550 /// Spot color name (PDF Name bytes, e.g. `"GWG Green"`).
551 name: Vec<u8>,
552 /// Alternate process color space. Typically `DeviceCMYK`.
553 alternate: SimpleColorSpace,
554 /// Sampled tint transform mapping spot tint `[0,1]` to alternate-space
555 /// components.
556 tint_function: SpotTintFunction,
557 },
558 /// DeviceN (multiple spot inks) with a CMYK alternate.
559 DeviceN {
560 /// Colorant names, in input-channel order.
561 names: Vec<Vec<u8>>,
562 /// Alternate process color space. Typically `DeviceCMYK`.
563 alternate: SimpleColorSpace,
564 /// Sampled tint transform mapping N spot tints to alternate-space
565 /// components.
566 tint_function: SpotTintFunction,
567 },
568}
569
570impl ShadingColorSpace {
571 /// Number of color components in this color space.
572 pub fn num_components(&self) -> usize {
573 match self {
574 ShadingColorSpace::DeviceGray | ShadingColorSpace::CalGray { .. } => 1,
575 ShadingColorSpace::DeviceRGB | ShadingColorSpace::CalRGB { .. } => 3,
576 ShadingColorSpace::DeviceCMYK => 4,
577 ShadingColorSpace::ICCBased { n, .. } => *n as usize,
578 ShadingColorSpace::Separation { .. } => 1,
579 ShadingColorSpace::DeviceN { names, .. } => names.len(),
580 }
581 }
582}
583
584/// A single color stop in a gradient.
585///
586/// New fields may be added without notice; pattern-matching consumers
587/// should use `..` to ignore unmatched fields.
588#[derive(Clone, Debug)]
589pub struct ColorStop {
590 pub position: f64,
591 pub color: DeviceColor,
592 pub raw_components: Vec<f64>,
593 /// Pre-tint-transform component values, in the shading's *source*
594 /// color space. For a Separation/DeviceN shading this holds the spot
595 /// tint(s) the source `/Function` evaluated to at this stop; the writer
596 /// emits these as the shading function's output so the round-trip PDF
597 /// preserves the spot input dimension. Empty when not applicable.
598 pub source_components: Vec<f64>,
599}
600
601/// Parameters for axial (linear) gradient shading (Type 2).
602///
603/// New fields may be added without notice; pattern-matching consumers
604/// should use `..` to ignore unmatched fields.
605#[derive(Clone, Debug)]
606pub struct AxialShadingParams {
607 pub x0: f64,
608 pub y0: f64,
609 pub x1: f64,
610 pub y1: f64,
611 pub color_stops: Vec<ColorStop>,
612 pub extend_start: bool,
613 pub extend_end: bool,
614 pub ctm: Matrix,
615 pub bbox: Option<[f64; 4]>,
616 pub color_space: ShadingColorSpace,
617 pub overprint: bool,
618 /// PDF `OPM` (overprint mode). 0 = standard (KO when a component is set);
619 /// 1 = Illustrator-style (a component explicitly set to 0 is preserved).
620 /// Must round-trip through PDF→DL→PDF or shadings painted onto an
621 /// underlay lose their preserved-component behavior.
622 pub overprint_mode: i32,
623 pub painted_channels: u8,
624 /// Fill alpha from graphics state (0.0–1.0).
625 pub alpha: f64,
626 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
627 pub blend_mode: u8,
628 /// PDF `AIS` (alpha-is-shape). Default false.
629 pub alpha_is_shape: bool,
630 /// True when this shading uses a Separation/DeviceN color space with a
631 /// CMYK alternate AND at least one non-process spot colorant. The
632 /// renderer composites the per-pixel CMYK from the gradient stops with
633 /// the tracked CMYK buffer multiplicatively, preserving underlying CMYK
634 /// paints under the gradient (e.g. green checkmarks under a green→cyan
635 /// DeviceN strip survive).
636 pub spot_tint_blend: bool,
637}
638
639/// Parameters for radial gradient shading (Type 3).
640///
641/// New fields may be added without notice; pattern-matching consumers
642/// should use `..` to ignore unmatched fields.
643#[derive(Clone, Debug)]
644pub struct RadialShadingParams {
645 pub x0: f64,
646 pub y0: f64,
647 pub r0: f64,
648 pub x1: f64,
649 pub y1: f64,
650 pub r1: f64,
651 pub color_stops: Vec<ColorStop>,
652 pub extend_start: bool,
653 pub extend_end: bool,
654 pub ctm: Matrix,
655 pub bbox: Option<[f64; 4]>,
656 pub color_space: ShadingColorSpace,
657 pub overprint: bool,
658 /// See [`AxialShadingParams::overprint_mode`].
659 pub overprint_mode: i32,
660 pub painted_channels: u8,
661 /// Fill alpha from graphics state (0.0–1.0).
662 pub alpha: f64,
663 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
664 pub blend_mode: u8,
665 /// PDF `AIS` (alpha-is-shape). Default false.
666 pub alpha_is_shape: bool,
667 /// See [`AxialShadingParams::spot_tint_blend`].
668 pub spot_tint_blend: bool,
669}
670
671/// A vertex in a shading triangle mesh.
672#[derive(Clone, Debug)]
673pub struct ShadingVertex {
674 pub x: f64,
675 pub y: f64,
676 pub color: DeviceColor,
677 pub raw_components: Vec<f64>,
678}
679
680/// A triangle in a shading mesh.
681#[derive(Clone, Debug)]
682pub struct ShadingTriangle {
683 pub v0: ShadingVertex,
684 pub v1: ShadingVertex,
685 pub v2: ShadingVertex,
686}
687
688/// Parameters for Gouraud-shaded triangle mesh shading (Types 4 & 5).
689///
690/// New fields may be added without notice; pattern-matching consumers
691/// should use `..` to ignore unmatched fields.
692#[derive(Clone, Debug)]
693pub struct MeshShadingParams {
694 pub triangles: Vec<ShadingTriangle>,
695 pub ctm: Matrix,
696 pub bbox: Option<[f64; 4]>,
697 pub color_space: ShadingColorSpace,
698 pub overprint: bool,
699 /// See [`AxialShadingParams::overprint_mode`].
700 pub overprint_mode: i32,
701 pub painted_channels: u8,
702 /// Pre-sampled color LUT for function-based mesh shadings.
703 /// When present, vertex `raw_components[0]` holds a normalized `[0,1]`
704 /// function input. The renderer interpolates this per-pixel, then
705 /// indexes the LUT instead of Gouraud-interpolating DeviceColor.
706 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
707 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
708 pub alpha: f64,
709 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
710 pub blend_mode: u8,
711 /// PDF `AIS` (alpha-is-shape). Default false.
712 pub alpha_is_shape: bool,
713}
714
715/// A patch in a Coons or tensor-product patch mesh.
716#[derive(Clone, Debug)]
717pub struct ShadingPatch {
718 pub points: Vec<(f64, f64)>,
719 pub colors: [DeviceColor; 4],
720 pub raw_colors: [Vec<f64>; 4],
721}
722
723/// Parameters for Coons/tensor-product patch mesh shading (Types 6 & 7).
724///
725/// New fields may be added without notice; pattern-matching consumers
726/// should use `..` to ignore unmatched fields.
727#[derive(Clone, Debug)]
728pub struct PatchShadingParams {
729 pub patches: Vec<ShadingPatch>,
730 pub ctm: Matrix,
731 pub bbox: Option<[f64; 4]>,
732 pub color_space: ShadingColorSpace,
733 pub overprint: bool,
734 /// See [`AxialShadingParams::overprint_mode`].
735 pub overprint_mode: i32,
736 pub painted_channels: u8,
737 /// When present, vertex `raw_colors[i][0]` holds a normalized `[0,1]`
738 /// function input. The renderer interpolates this per-pixel, then
739 /// indexes the LUT for per-pixel non-linear function evaluation.
740 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
741 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
742 pub alpha: f64,
743 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
744 pub blend_mode: u8,
745 /// PDF `AIS` (alpha-is-shape). Default false.
746 pub alpha_is_shape: bool,
747}
748
749/// Parameters for a tiled pattern fill.
750#[derive(Clone)]
751pub struct PatternFillParams {
752 /// The path to fill with the pattern.
753 pub path: PsPath,
754 /// Fill rule for the path.
755 pub fill_rule: FillRule,
756 /// Pre-rendered display list for a single tile.
757 pub tile: DisplayList,
758 /// Pattern matrix (pattern space → device space).
759 pub pattern_matrix: Matrix,
760 /// Bounding box of one tile in pattern space.
761 pub bbox: [f64; 4],
762 /// Horizontal step between tile origins.
763 pub xstep: f64,
764 /// Vertical step between tile origins.
765 pub ystep: f64,
766 /// Paint type: 1 = colored, 2 = uncolored.
767 pub paint_type: i32,
768 /// For uncolored patterns, the fill color.
769 pub underlying_color: Option<DeviceColor>,
770 /// Unique pattern ID from pattern_store (for dedup in PDF output).
771 pub pattern_id: u32,
772 /// When true, tile display list elements have CTMs in device space
773 /// (the pattern matrix is already baked into element transforms).
774 /// When false, elements are in pattern space and the renderer applies
775 /// the pattern_matrix during rendering.
776 pub device_space_tile: bool,
777 /// When true, the tile content was designed for a Y-flipped coordinate
778 /// system (pattern matrix had negative d). The pre-rendered tile must
779 /// be vertically flipped before stamping.
780 pub flip_tile_y: bool,
781 /// For pattern strokes: stroke parameters to expand the centerline path
782 /// into a fill outline for masking. When Some, `path` is a user-space
783 /// stroke centerline rather than a fill path.
784 pub stroke_params: Option<StrokeParams>,
785 /// PDF overprint mode (0 or 1). When 1, CMYK(0,0,0,0) pixels in tile
786 /// images are transparent (no ink = don't paint).
787 pub overprint_mode: i32,
788}
789
790// ---------------------------------------------------------------------------
791// Default impls
792//
793// The `Default` impls below pair with `#[non_exhaustive]` on each type:
794// downstream consumers (and other workspace crates) construct values via
795// `FillParams { color, ..Default::default() }`-style functional update so
796// new fields can be added without breaking call sites. The defaults are
797// chosen for ergonomics (alpha = 1.0, blend mode = Normal, identity CTM,
798// solid black colour, no transfer/halftone/spot state) — not as
799// semantically meaningful "blank records".
800// ---------------------------------------------------------------------------
801
802impl Default for FillParams {
803 fn default() -> Self {
804 Self {
805 color: DeviceColor::default(),
806 fill_rule: FillRule::default(),
807 ctm: Matrix::default(),
808 is_text_glyph: false,
809 overprint: false,
810 overprint_mode: 0,
811 opm_paired: false,
812 painted_channels: 0,
813 is_device_cmyk: false,
814 spot_color: None,
815 icc_color: None,
816 rendering_intent: 0,
817 transfer: TransferState::default(),
818 halftone: HalftoneState::default(),
819 bg_ucr: BgUcrState::default(),
820 alpha: 1.0,
821 blend_mode: 0,
822 alpha_is_shape: false,
823 }
824 }
825}
826
827impl Default for StrokeParams {
828 fn default() -> Self {
829 Self {
830 color: DeviceColor::default(),
831 line_width: 1.0,
832 line_cap: LineCap::default(),
833 line_join: LineJoin::default(),
834 miter_limit: 10.0,
835 dash_pattern: DashPattern::default(),
836 ctm: Matrix::default(),
837 stroke_adjust: false,
838 is_text_glyph: false,
839 overprint: false,
840 overprint_mode: 0,
841 opm_paired: false,
842 painted_channels: 0,
843 is_device_cmyk: false,
844 spot_color: None,
845 icc_color: None,
846 rendering_intent: 0,
847 transfer: TransferState::default(),
848 halftone: HalftoneState::default(),
849 bg_ucr: BgUcrState::default(),
850 alpha: 1.0,
851 blend_mode: 0,
852 alpha_is_shape: false,
853 }
854 }
855}
856
857impl Default for ClipParams {
858 fn default() -> Self {
859 Self {
860 fill_rule: FillRule::default(),
861 ctm: Matrix::default(),
862 stroke_params: None,
863 }
864 }
865}
866
867impl Default for TextParams {
868 fn default() -> Self {
869 Self {
870 text: Vec::new(),
871 start_x: 0.0,
872 start_y: 0.0,
873 font_entity: 0,
874 font_name: Vec::new(),
875 font_type: 1,
876 font_size: 0.0,
877 color: DeviceColor::default(),
878 ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
879 font_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
880 paint_type: 0,
881 stroke_width: 0.0,
882 spot_color: None,
883 icc_color: None,
884 rendering_intent: 0,
885 transfer: TransferState::default(),
886 halftone: HalftoneState::default(),
887 bg_ucr: BgUcrState::default(),
888 fill_opacity: 1.0,
889 stroke_opacity: 1.0,
890 blend_mode: 0,
891 alpha_is_shape: false,
892 text_knockout: true,
893 }
894 }
895}
896
897impl Default for ImageColorSpace {
898 fn default() -> Self {
899 ImageColorSpace::DeviceGray
900 }
901}
902
903impl Default for ImageParams {
904 fn default() -> Self {
905 Self {
906 width: 0,
907 height: 0,
908 color_space: ImageColorSpace::default(),
909 bits_per_component: 8,
910 ctm: Matrix::default(),
911 image_matrix: Matrix::default(),
912 interpolate: false,
913 mask_color: None,
914 alpha: 1.0,
915 blend_mode: 0,
916 overprint: false,
917 overprint_mode: 0,
918 opm_paired: false,
919 painted_channels: 0,
920 alpha_is_shape: false,
921 rendering_intent: 0,
922 }
923 }
924}
925
926impl Default for ShadingColorSpace {
927 fn default() -> Self {
928 ShadingColorSpace::DeviceRGB
929 }
930}
931
932impl Default for ColorStop {
933 fn default() -> Self {
934 Self {
935 position: 0.0,
936 color: DeviceColor::default(),
937 raw_components: Vec::new(),
938 source_components: Vec::new(),
939 }
940 }
941}
942
943impl Default for AxialShadingParams {
944 fn default() -> Self {
945 Self {
946 x0: 0.0,
947 y0: 0.0,
948 x1: 0.0,
949 y1: 0.0,
950 color_stops: Vec::new(),
951 extend_start: false,
952 extend_end: false,
953 ctm: Matrix::default(),
954 bbox: None,
955 color_space: ShadingColorSpace::default(),
956 overprint: false,
957 overprint_mode: 0,
958 painted_channels: 0,
959 alpha: 1.0,
960 blend_mode: 0,
961 alpha_is_shape: false,
962 spot_tint_blend: false,
963 }
964 }
965}
966
967impl Default for RadialShadingParams {
968 fn default() -> Self {
969 Self {
970 x0: 0.0,
971 y0: 0.0,
972 r0: 0.0,
973 x1: 0.0,
974 y1: 0.0,
975 r1: 0.0,
976 color_stops: Vec::new(),
977 extend_start: false,
978 extend_end: false,
979 ctm: Matrix::default(),
980 bbox: None,
981 color_space: ShadingColorSpace::default(),
982 overprint: false,
983 overprint_mode: 0,
984 painted_channels: 0,
985 alpha: 1.0,
986 blend_mode: 0,
987 alpha_is_shape: false,
988 spot_tint_blend: false,
989 }
990 }
991}
992
993impl Default for MeshShadingParams {
994 fn default() -> Self {
995 Self {
996 triangles: Vec::new(),
997 ctm: Matrix::default(),
998 bbox: None,
999 color_space: ShadingColorSpace::default(),
1000 overprint: false,
1001 overprint_mode: 0,
1002 painted_channels: 0,
1003 color_lut: None,
1004 alpha: 1.0,
1005 blend_mode: 0,
1006 alpha_is_shape: false,
1007 }
1008 }
1009}
1010
1011impl Default for PatchShadingParams {
1012 fn default() -> Self {
1013 Self {
1014 patches: Vec::new(),
1015 ctm: Matrix::default(),
1016 bbox: None,
1017 color_space: ShadingColorSpace::default(),
1018 overprint: false,
1019 overprint_mode: 0,
1020 painted_channels: 0,
1021 color_lut: None,
1022 alpha: 1.0,
1023 blend_mode: 0,
1024 alpha_is_shape: false,
1025 }
1026 }
1027}
1028
1029/// Trait for consuming rendered page pixel data.
1030pub trait PageSink: Send {
1031 /// Start a new page with the given pixel dimensions.
1032 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String>;
1033
1034 /// Write one or more rows of RGBA pixel data (4 bytes per pixel, row-major).
1035 fn write_rows(&mut self, rgba_rows: &[u8], num_rows: u32) -> Result<(), String>;
1036
1037 /// Finish the current page. May block (e.g., viewer waits for user input).
1038 fn end_page(&mut self) -> Result<(), String>;
1039}
1040
1041/// Factory for creating per-page sinks.
1042pub trait PageSinkFactory: Send + Sync {
1043 /// Create a new sink for a single page.
1044 fn create_sink(&self, output_path: &str) -> Result<Box<dyn PageSink>, String>;
1045}