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