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}
449
450/// Color space carried through the display list for native shading output.
451///
452/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
453/// wildcard arm.
454#[derive(Clone, Debug)]
455#[non_exhaustive]
456pub enum ShadingColorSpace {
457    DeviceGray,
458    DeviceRGB,
459    DeviceCMYK,
460    ICCBased {
461        n: u32,
462        profile_hash: ProfileHash,
463        profile_data: Arc<Vec<u8>>,
464    },
465    CalRGB {
466        white_point: [f64; 3],
467        matrix: Option<[f64; 9]>,
468        gamma: Option<[f64; 3]>,
469    },
470    CalGray {
471        white_point: [f64; 3],
472        gamma: Option<f64>,
473    },
474}
475
476impl ShadingColorSpace {
477    /// Number of color components in this color space.
478    pub fn num_components(&self) -> usize {
479        match self {
480            ShadingColorSpace::DeviceGray | ShadingColorSpace::CalGray { .. } => 1,
481            ShadingColorSpace::DeviceRGB | ShadingColorSpace::CalRGB { .. } => 3,
482            ShadingColorSpace::DeviceCMYK => 4,
483            ShadingColorSpace::ICCBased { n, .. } => *n as usize,
484        }
485    }
486}
487
488/// A single color stop in a gradient.
489///
490/// New fields may be added without notice; pattern-matching consumers
491/// should use `..` to ignore unmatched fields.
492#[derive(Clone, Debug)]
493pub struct ColorStop {
494    pub position: f64,
495    pub color: DeviceColor,
496    pub raw_components: Vec<f64>,
497}
498
499/// Parameters for axial (linear) gradient shading (Type 2).
500///
501/// New fields may be added without notice; pattern-matching consumers
502/// should use `..` to ignore unmatched fields.
503#[derive(Clone, Debug)]
504pub struct AxialShadingParams {
505    pub x0: f64,
506    pub y0: f64,
507    pub x1: f64,
508    pub y1: f64,
509    pub color_stops: Vec<ColorStop>,
510    pub extend_start: bool,
511    pub extend_end: bool,
512    pub ctm: Matrix,
513    pub bbox: Option<[f64; 4]>,
514    pub color_space: ShadingColorSpace,
515    pub overprint: bool,
516    pub painted_channels: u8,
517    /// Fill alpha from graphics state (0.0–1.0).
518    pub alpha: f64,
519    /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
520    pub blend_mode: u8,
521    /// PDF `AIS` (alpha-is-shape). Default false.
522    pub alpha_is_shape: bool,
523    /// True when this shading uses a Separation/DeviceN color space with a
524    /// CMYK alternate AND at least one non-process spot colorant.  The
525    /// renderer composites the per-pixel CMYK from the gradient stops with
526    /// the tracked CMYK buffer multiplicatively, preserving underlying CMYK
527    /// paints under the gradient (e.g. green checkmarks under a green→cyan
528    /// DeviceN strip survive).
529    pub spot_tint_blend: bool,
530}
531
532/// Parameters for radial gradient shading (Type 3).
533///
534/// New fields may be added without notice; pattern-matching consumers
535/// should use `..` to ignore unmatched fields.
536#[derive(Clone, Debug)]
537pub struct RadialShadingParams {
538    pub x0: f64,
539    pub y0: f64,
540    pub r0: f64,
541    pub x1: f64,
542    pub y1: f64,
543    pub r1: f64,
544    pub color_stops: Vec<ColorStop>,
545    pub extend_start: bool,
546    pub extend_end: bool,
547    pub ctm: Matrix,
548    pub bbox: Option<[f64; 4]>,
549    pub color_space: ShadingColorSpace,
550    pub overprint: bool,
551    pub painted_channels: u8,
552    /// Fill alpha from graphics state (0.0–1.0).
553    pub alpha: f64,
554    /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
555    pub blend_mode: u8,
556    /// PDF `AIS` (alpha-is-shape). Default false.
557    pub alpha_is_shape: bool,
558    /// See [`AxialShadingParams::spot_tint_blend`].
559    pub spot_tint_blend: bool,
560}
561
562/// A vertex in a shading triangle mesh.
563#[derive(Clone, Debug)]
564pub struct ShadingVertex {
565    pub x: f64,
566    pub y: f64,
567    pub color: DeviceColor,
568    pub raw_components: Vec<f64>,
569}
570
571/// A triangle in a shading mesh.
572#[derive(Clone, Debug)]
573pub struct ShadingTriangle {
574    pub v0: ShadingVertex,
575    pub v1: ShadingVertex,
576    pub v2: ShadingVertex,
577}
578
579/// Parameters for Gouraud-shaded triangle mesh shading (Types 4 & 5).
580///
581/// New fields may be added without notice; pattern-matching consumers
582/// should use `..` to ignore unmatched fields.
583#[derive(Clone, Debug)]
584pub struct MeshShadingParams {
585    pub triangles: Vec<ShadingTriangle>,
586    pub ctm: Matrix,
587    pub bbox: Option<[f64; 4]>,
588    pub color_space: ShadingColorSpace,
589    pub overprint: bool,
590    pub painted_channels: u8,
591    /// Pre-sampled color LUT for function-based mesh shadings.
592    /// When present, vertex `raw_components[0]` holds a normalized `[0,1]`
593    /// function input. The renderer interpolates this per-pixel, then
594    /// indexes the LUT instead of Gouraud-interpolating DeviceColor.
595    pub color_lut: Option<Arc<Vec<DeviceColor>>>,
596    /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
597    pub alpha: f64,
598    /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
599    pub blend_mode: u8,
600    /// PDF `AIS` (alpha-is-shape). Default false.
601    pub alpha_is_shape: bool,
602}
603
604/// A patch in a Coons or tensor-product patch mesh.
605#[derive(Clone, Debug)]
606pub struct ShadingPatch {
607    pub points: Vec<(f64, f64)>,
608    pub colors: [DeviceColor; 4],
609    pub raw_colors: [Vec<f64>; 4],
610}
611
612/// Parameters for Coons/tensor-product patch mesh shading (Types 6 & 7).
613///
614/// New fields may be added without notice; pattern-matching consumers
615/// should use `..` to ignore unmatched fields.
616#[derive(Clone, Debug)]
617pub struct PatchShadingParams {
618    pub patches: Vec<ShadingPatch>,
619    pub ctm: Matrix,
620    pub bbox: Option<[f64; 4]>,
621    pub color_space: ShadingColorSpace,
622    pub overprint: bool,
623    pub painted_channels: u8,
624    /// When present, vertex `raw_colors[i][0]` holds a normalized `[0,1]`
625    /// function input. The renderer interpolates this per-pixel, then
626    /// indexes the LUT for per-pixel non-linear function evaluation.
627    pub color_lut: Option<Arc<Vec<DeviceColor>>>,
628    /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
629    pub alpha: f64,
630    /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
631    pub blend_mode: u8,
632    /// PDF `AIS` (alpha-is-shape). Default false.
633    pub alpha_is_shape: bool,
634}
635
636/// Parameters for a tiled pattern fill.
637#[derive(Clone)]
638pub struct PatternFillParams {
639    /// The path to fill with the pattern.
640    pub path: PsPath,
641    /// Fill rule for the path.
642    pub fill_rule: FillRule,
643    /// Pre-rendered display list for a single tile.
644    pub tile: DisplayList,
645    /// Pattern matrix (pattern space → device space).
646    pub pattern_matrix: Matrix,
647    /// Bounding box of one tile in pattern space.
648    pub bbox: [f64; 4],
649    /// Horizontal step between tile origins.
650    pub xstep: f64,
651    /// Vertical step between tile origins.
652    pub ystep: f64,
653    /// Paint type: 1 = colored, 2 = uncolored.
654    pub paint_type: i32,
655    /// For uncolored patterns, the fill color.
656    pub underlying_color: Option<DeviceColor>,
657    /// Unique pattern ID from pattern_store (for dedup in PDF output).
658    pub pattern_id: u32,
659    /// When true, tile display list elements have CTMs in device space
660    /// (the pattern matrix is already baked into element transforms).
661    /// When false, elements are in pattern space and the renderer applies
662    /// the pattern_matrix during rendering.
663    pub device_space_tile: bool,
664    /// When true, the tile content was designed for a Y-flipped coordinate
665    /// system (pattern matrix had negative d). The pre-rendered tile must
666    /// be vertically flipped before stamping.
667    pub flip_tile_y: bool,
668    /// For pattern strokes: stroke parameters to expand the centerline path
669    /// into a fill outline for masking. When Some, `path` is a user-space
670    /// stroke centerline rather than a fill path.
671    pub stroke_params: Option<StrokeParams>,
672    /// PDF overprint mode (0 or 1). When 1, CMYK(0,0,0,0) pixels in tile
673    /// images are transparent (no ink = don't paint).
674    pub overprint_mode: i32,
675}
676
677// ---------------------------------------------------------------------------
678// Default impls
679//
680// The `Default` impls below pair with `#[non_exhaustive]` on each type:
681// downstream consumers (and other workspace crates) construct values via
682// `FillParams { color, ..Default::default() }`-style functional update so
683// new fields can be added without breaking call sites. The defaults are
684// chosen for ergonomics (alpha = 1.0, blend mode = Normal, identity CTM,
685// solid black colour, no transfer/halftone/spot state) — not as
686// semantically meaningful "blank records".
687// ---------------------------------------------------------------------------
688
689impl Default for FillParams {
690    fn default() -> Self {
691        Self {
692            color: DeviceColor::default(),
693            fill_rule: FillRule::default(),
694            ctm: Matrix::default(),
695            is_text_glyph: false,
696            overprint: false,
697            overprint_mode: 0,
698            opm_paired: false,
699            painted_channels: 0,
700            is_device_cmyk: false,
701            spot_color: None,
702            rendering_intent: 0,
703            transfer: TransferState::default(),
704            halftone: HalftoneState::default(),
705            bg_ucr: BgUcrState::default(),
706            alpha: 1.0,
707            blend_mode: 0,
708            alpha_is_shape: false,
709        }
710    }
711}
712
713impl Default for StrokeParams {
714    fn default() -> Self {
715        Self {
716            color: DeviceColor::default(),
717            line_width: 1.0,
718            line_cap: LineCap::default(),
719            line_join: LineJoin::default(),
720            miter_limit: 10.0,
721            dash_pattern: DashPattern::default(),
722            ctm: Matrix::default(),
723            stroke_adjust: false,
724            is_text_glyph: false,
725            overprint: false,
726            overprint_mode: 0,
727            opm_paired: false,
728            painted_channels: 0,
729            is_device_cmyk: false,
730            spot_color: None,
731            rendering_intent: 0,
732            transfer: TransferState::default(),
733            halftone: HalftoneState::default(),
734            bg_ucr: BgUcrState::default(),
735            alpha: 1.0,
736            blend_mode: 0,
737            alpha_is_shape: false,
738        }
739    }
740}
741
742impl Default for ClipParams {
743    fn default() -> Self {
744        Self {
745            fill_rule: FillRule::default(),
746            ctm: Matrix::default(),
747            stroke_params: None,
748        }
749    }
750}
751
752impl Default for TextParams {
753    fn default() -> Self {
754        Self {
755            text: Vec::new(),
756            start_x: 0.0,
757            start_y: 0.0,
758            font_entity: 0,
759            font_name: Vec::new(),
760            font_type: 1,
761            font_size: 0.0,
762            color: DeviceColor::default(),
763            ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
764            font_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
765            paint_type: 0,
766            stroke_width: 0.0,
767            spot_color: None,
768            rendering_intent: 0,
769            transfer: TransferState::default(),
770            halftone: HalftoneState::default(),
771            bg_ucr: BgUcrState::default(),
772            fill_opacity: 1.0,
773            stroke_opacity: 1.0,
774            blend_mode: 0,
775            alpha_is_shape: false,
776            text_knockout: true,
777        }
778    }
779}
780
781impl Default for ImageColorSpace {
782    fn default() -> Self {
783        ImageColorSpace::DeviceGray
784    }
785}
786
787impl Default for ImageParams {
788    fn default() -> Self {
789        Self {
790            width: 0,
791            height: 0,
792            color_space: ImageColorSpace::default(),
793            bits_per_component: 8,
794            ctm: Matrix::default(),
795            image_matrix: Matrix::default(),
796            interpolate: false,
797            mask_color: None,
798            alpha: 1.0,
799            blend_mode: 0,
800            overprint: false,
801            overprint_mode: 0,
802            opm_paired: false,
803            painted_channels: 0,
804            alpha_is_shape: false,
805        }
806    }
807}
808
809impl Default for ShadingColorSpace {
810    fn default() -> Self {
811        ShadingColorSpace::DeviceRGB
812    }
813}
814
815impl Default for ColorStop {
816    fn default() -> Self {
817        Self {
818            position: 0.0,
819            color: DeviceColor::default(),
820            raw_components: Vec::new(),
821        }
822    }
823}
824
825impl Default for AxialShadingParams {
826    fn default() -> Self {
827        Self {
828            x0: 0.0,
829            y0: 0.0,
830            x1: 0.0,
831            y1: 0.0,
832            color_stops: Vec::new(),
833            extend_start: false,
834            extend_end: false,
835            ctm: Matrix::default(),
836            bbox: None,
837            color_space: ShadingColorSpace::default(),
838            overprint: false,
839            painted_channels: 0,
840            alpha: 1.0,
841            blend_mode: 0,
842            alpha_is_shape: false,
843            spot_tint_blend: false,
844        }
845    }
846}
847
848impl Default for RadialShadingParams {
849    fn default() -> Self {
850        Self {
851            x0: 0.0,
852            y0: 0.0,
853            r0: 0.0,
854            x1: 0.0,
855            y1: 0.0,
856            r1: 0.0,
857            color_stops: Vec::new(),
858            extend_start: false,
859            extend_end: false,
860            ctm: Matrix::default(),
861            bbox: None,
862            color_space: ShadingColorSpace::default(),
863            overprint: false,
864            painted_channels: 0,
865            alpha: 1.0,
866            blend_mode: 0,
867            alpha_is_shape: false,
868            spot_tint_blend: false,
869        }
870    }
871}
872
873impl Default for MeshShadingParams {
874    fn default() -> Self {
875        Self {
876            triangles: Vec::new(),
877            ctm: Matrix::default(),
878            bbox: None,
879            color_space: ShadingColorSpace::default(),
880            overprint: false,
881            painted_channels: 0,
882            color_lut: None,
883            alpha: 1.0,
884            blend_mode: 0,
885            alpha_is_shape: false,
886        }
887    }
888}
889
890impl Default for PatchShadingParams {
891    fn default() -> Self {
892        Self {
893            patches: Vec::new(),
894            ctm: Matrix::default(),
895            bbox: None,
896            color_space: ShadingColorSpace::default(),
897            overprint: false,
898            painted_channels: 0,
899            color_lut: None,
900            alpha: 1.0,
901            blend_mode: 0,
902            alpha_is_shape: false,
903        }
904    }
905}
906
907/// Trait for consuming rendered page pixel data.
908pub trait PageSink: Send {
909    /// Start a new page with the given pixel dimensions.
910    fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String>;
911
912    /// Write one or more rows of RGBA pixel data (4 bytes per pixel, row-major).
913    fn write_rows(&mut self, rgba_rows: &[u8], num_rows: u32) -> Result<(), String>;
914
915    /// Finish the current page. May block (e.g., viewer waits for user input).
916    fn end_page(&mut self) -> Result<(), String>;
917}
918
919/// Factory for creating per-page sinks.
920pub trait PageSinkFactory: Send + Sync {
921    /// Create a new sink for a single page.
922    fn create_sink(&self, output_path: &str) -> Result<Box<dyn PageSink>, String>;
923}