Skip to main content

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}