Skip to main content

stet_core/
graphics_state.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Graphics state: transforms, paths, colors, and rendering parameters.
6
7use crate::display_list::DisplayList;
8use crate::object::PsObject;
9use std::sync::Arc;
10
11// ── Re-exports from stet-fonts and stet-graphics ────────────────────────────
12// These types used to be defined here. Re-export for backward compatibility.
13pub use stet_fonts::geometry::{Matrix, PathSegment, PsPath, round10};
14pub use stet_graphics::color::{
15    CieAParams, CieAbcParams, CieDefParams, CieDefgParams, DashPattern, DeviceColor, FillRule,
16    LineCap, LineJoin,
17};
18
19// ── Types that remain in stet-core (depend on PS VM types) ──────────────────
20
21/// Color space identifier.
22#[derive(Clone, Debug)]
23pub enum ColorSpace {
24    DeviceGray,
25    DeviceRGB,
26    DeviceCMYK,
27    /// Indexed color space: `[/Indexed base hival lookup]`.
28    /// `lookup_proc` is `Some(proc_object)` when the lookup is a procedure that
29    /// needs to be pre-evaluated via exec_sync during setcolorspace.
30    Indexed {
31        base: Box<ColorSpace>,
32        hival: u32,
33        lookup: Vec<u8>,
34        lookup_proc: Option<PsObject>,
35    },
36    /// CIE-based ABC color space (3 components): `[/CIEBasedABC dict]`.
37    CIEBasedABC {
38        params: Arc<CieAbcParams>,
39        dict_entity: crate::object::EntityId,
40    },
41    /// CIE-based A color space (1 component): `[/CIEBasedA dict]`.
42    CIEBasedA {
43        params: Arc<CieAParams>,
44        dict_entity: crate::object::EntityId,
45    },
46    /// CIE-based DEF color space (3 components → 3D table → ABC → sRGB).
47    CIEBasedDEF {
48        params: Arc<CieDefParams>,
49        dict_entity: crate::object::EntityId,
50    },
51    /// CIE-based DEFG color space (4 components → 4D table → ABC → sRGB).
52    CIEBasedDEFG {
53        params: Arc<CieDefgParams>,
54        dict_entity: crate::object::EntityId,
55    },
56    /// ICC-based color space: `[/ICCBased dict]` where dict has /N components.
57    /// When `profile_hash` is Some, colors are converted through the ICC profile.
58    /// Falls back to device space based on N (1=Gray, 3=RGB, 4=CMYK).
59    ICCBased {
60        dict_entity: crate::object::EntityId,
61        n: u32,
62        profile_hash: Option<crate::icc::ProfileHash>,
63    },
64    /// Separation color space: `[/Separation name alternativeSpace tintTransform]`.
65    /// Single tint component mapped to alternative space via tint transform procedure.
66    Separation {
67        name: Vec<u8>,
68        alt_space: Box<ColorSpace>,
69        tint_transform: crate::object::PsObject,
70        num_alt_components: u32,
71    },
72    /// DeviceN color space: `[/DeviceN names alternativeSpace tintTransform]`.
73    /// N tint components mapped to alternative space via tint transform procedure.
74    DeviceN {
75        names: Vec<Vec<u8>>,
76        num_colorants: u32,
77        alt_space: Box<ColorSpace>,
78        tint_transform: crate::object::PsObject,
79        num_alt_components: u32,
80    },
81    /// Pattern color space: `/Pattern`, `[/Pattern]` or `[/Pattern base]`.
82    ///
83    /// PLRM 4.9.6. `base` is `None` for a space that can only hold colored
84    /// (PaintType 1) patterns, and `Some(space)` for one that also accepts
85    /// uncolored (PaintType 2) patterns, whose color comes from `base`.
86    /// A Pattern space may not itself be used as `base`.
87    Pattern {
88        base: Option<Box<ColorSpace>>,
89    },
90}
91
92impl PartialEq for ColorSpace {
93    fn eq(&self, other: &Self) -> bool {
94        use ColorSpace::*;
95        match (self, other) {
96            (DeviceGray, DeviceGray) | (DeviceRGB, DeviceRGB) | (DeviceCMYK, DeviceCMYK) => true,
97            (
98                Indexed {
99                    base: b1,
100                    hival: h1,
101                    lookup: l1,
102                    ..
103                },
104                Indexed {
105                    base: b2,
106                    hival: h2,
107                    lookup: l2,
108                    ..
109                },
110            ) => b1 == b2 && h1 == h2 && l1 == l2,
111            (
112                CIEBasedABC {
113                    dict_entity: d1, ..
114                },
115                CIEBasedABC {
116                    dict_entity: d2, ..
117                },
118            ) => d1 == d2,
119            (
120                CIEBasedA {
121                    dict_entity: d1, ..
122                },
123                CIEBasedA {
124                    dict_entity: d2, ..
125                },
126            ) => d1 == d2,
127            (
128                CIEBasedDEF {
129                    dict_entity: d1, ..
130                },
131                CIEBasedDEF {
132                    dict_entity: d2, ..
133                },
134            ) => d1 == d2,
135            (
136                CIEBasedDEFG {
137                    dict_entity: d1, ..
138                },
139                CIEBasedDEFG {
140                    dict_entity: d2, ..
141                },
142            ) => d1 == d2,
143            (
144                ICCBased {
145                    dict_entity: d1,
146                    n: n1,
147                    ..
148                },
149                ICCBased {
150                    dict_entity: d2,
151                    n: n2,
152                    ..
153                },
154            ) => d1 == d2 && n1 == n2,
155            (
156                Separation {
157                    name: name1,
158                    alt_space: a1,
159                    tint_transform: t1,
160                    num_alt_components: n1,
161                },
162                Separation {
163                    name: name2,
164                    alt_space: a2,
165                    tint_transform: t2,
166                    num_alt_components: n2,
167                },
168            ) => name1 == name2 && a1 == a2 && t1 == t2 && n1 == n2,
169            (
170                DeviceN {
171                    names: names1,
172                    num_colorants: nc1,
173                    alt_space: a1,
174                    tint_transform: t1,
175                    num_alt_components: n1,
176                },
177                DeviceN {
178                    names: names2,
179                    num_colorants: nc2,
180                    alt_space: a2,
181                    tint_transform: t2,
182                    num_alt_components: n2,
183                },
184            ) => names1 == names2 && nc1 == nc2 && a1 == a2 && t1 == t2 && n1 == n2,
185            (Pattern { base: b1 }, Pattern { base: b2 }) => b1 == b2,
186            _ => false,
187        }
188    }
189}
190
191/// Pattern instance data created by `makepattern`.
192#[derive(Clone)]
193pub struct PatternData {
194    /// Pattern type: 1 = tiling, 2 = shading.
195    pub pattern_type: i32,
196    /// Paint type: 1 = colored, 2 = uncolored.
197    pub paint_type: i32,
198    /// Tiling type: 1 = constant spacing, 2 = no distortion, 3 = fast.
199    pub tiling_type: i32,
200    /// Bounding box [llx, lly, urx, ury] in pattern space.
201    pub bbox: [f64; 4],
202    /// X step between tile origins.
203    pub xstep: f64,
204    /// Y step between tile origins.
205    pub ystep: f64,
206    /// Combined matrix: matrix_arg × CTM at makepattern time.
207    pub pattern_matrix: Matrix,
208    /// Pre-rendered display list from executing PaintProc.
209    pub cached_display_list: DisplayList,
210}
211
212/// Entry on the graphics state stack, tracking whether it was created by
213/// `save` (implicit gsave) or `gsave`.
214#[derive(Clone, Debug)]
215pub struct GstateEntry {
216    pub state: GraphicsState,
217    /// True if created by `save`, false if by `gsave`.
218    /// `grestore` skips save-created entries; `grestoreall` stops at them.
219    pub saved_by_save: bool,
220}
221
222/// Complete graphics state (cloned for gsave/grestore).
223#[derive(Clone, Debug)]
224pub struct GraphicsState {
225    pub ctm: Matrix,
226    pub color: DeviceColor,
227    pub color_space: ColorSpace,
228    pub path: PsPath,
229    pub current_point: Option<(f64, f64)>,
230    pub clip_path: Option<PsPath>,
231    pub clip_path_version: u32,
232    pub line_width: f64,
233    pub line_cap: LineCap,
234    pub line_join: LineJoin,
235    pub miter_limit: f64,
236    pub dash_pattern: DashPattern,
237    pub flatness: f64,
238    pub stroke_adjust: bool,
239    pub overprint: bool,
240    pub smoothness: f64,
241    pub default_ctm: Matrix,
242
243    // Clip save/restore stack (per graphics state)
244    pub clip_stack: Vec<Option<PsPath>>,
245
246    // Current font (set by setfont, used by show operators)
247    pub current_font: Option<crate::object::PsObject>,
248
249    // Root font for composite font hierarchy (set during Type 0 rendering).
250    // rootfont returns this if set, otherwise falls back to current_font.
251    pub root_font: Option<crate::object::PsObject>,
252
253    // Page device dict (EntityId into DictStore)
254    pub page_device: Option<crate::object::EntityId>,
255
256    // Halftone screen parameters (set by setscreen/setcolorscreen/sethalftone)
257    pub screen_freq: f64,
258    pub screen_angle: f64,
259    pub screen_proc: Option<crate::object::PsObject>,
260    /// Per-component color screen: [red, green, blue, gray] × (freq, angle, proc)
261    pub color_screen: Option<[(f64, f64, crate::object::PsObject); 4]>,
262    /// Halftone dictionary (set by sethalftone)
263    pub halftone: Option<crate::object::PsObject>,
264
265    // Transfer functions
266    pub transfer_function: Option<crate::object::PsObject>,
267    /// Per-component transfer: [red, green, blue, gray]
268    pub color_transfer: Option<[crate::object::PsObject; 4]>,
269    /// Pre-sampled transfer function (256 entries). None = identity.
270    pub sampled_transfer: Option<Arc<Vec<f64>>>,
271    /// Pre-sampled per-component transfer \[R, G, B, Gray\].
272    pub sampled_color_transfer: Option<[Option<Arc<Vec<f64>>>; 4]>,
273    /// Pre-computed halftone screen for PDF output. None = default (suppressed).
274    pub precomputed_halftone: Option<Arc<crate::device::HalftoneScreen>>,
275    /// Pre-computed per-component halftone \[R, G, B, Gray\] (from setcolorscreen).
276    pub precomputed_color_halftone: Option<[Option<Arc<crate::device::HalftoneScreen>>; 4]>,
277
278    // Black generation / undercolor removal
279    pub black_generation: Option<crate::object::PsObject>,
280    pub undercolor_removal: Option<crate::object::PsObject>,
281    /// Pre-sampled black generation function (256 entries, domain `[0,1]` → range `[0,1]`).
282    pub sampled_black_generation: Option<Arc<Vec<f64>>>,
283    /// Pre-sampled undercolor removal function (256 entries, domain `[0,1]` → range `[-1,1]`).
284    pub sampled_ucr: Option<Arc<Vec<f64>>>,
285
286    // Color rendering dictionary
287    pub color_rendering: Option<crate::object::PsObject>,
288
289    /// Rendering intent: 0=RelativeColorimetric, 1=AbsoluteColorimetric,
290    /// 2=Perceptual, 3=Saturation. Default is RelativeColorimetric.
291    pub rendering_intent: u8,
292
293    // Pattern state (set by setpattern, consumed by fill/eofill)
294    /// Index into `Context.pattern_store` for the active tiling pattern.
295    pub current_pattern: Option<u32>,
296    /// Underlying color for uncolored (PaintType 2) patterns.
297    pub pattern_underlying_color: Option<DeviceColor>,
298    /// The pattern dictionary installed by `setpattern` (or by `setcolor` in a
299    /// Pattern color space). PLRM 4.9.6 makes the pattern part of the current
300    /// color, so `currentcolor` has to hand the same object back.
301    pub current_pattern_dict: Option<crate::object::EntityId>,
302    /// Components of the underlying color for an uncolored (PaintType 2)
303    /// pattern, in the Pattern space's base color space. Empty for colored
304    /// patterns; `currentcolor` pushes these ahead of the pattern dictionary.
305    pub pattern_components: Vec<f64>,
306
307    // Userpath bounding box (set by setbbox, cleared by newpath)
308    pub bbox: Option<[f64; 4]>,
309
310    /// Tint values from the most recent setcolor (for Separation/DeviceN).
311    /// 1 value for Separation, N values for DeviceN. None for device color spaces.
312    pub tint_values: Option<Vec<f64>>,
313
314    /// Cached pre-sampled tint lookup table for the current Separation/DeviceN color space.
315    /// Set when setcolorspace installs a Separation/DeviceN space.
316    pub cached_tint_table: Option<Arc<crate::device::TintLookupTable>>,
317
318    /// Constant fill opacity (PDF `ca`). Range \[0,1\]. Default 1.0.
319    pub fill_opacity: f64,
320    /// Constant stroke opacity (PDF `CA`). Range \[0,1\]. Default 1.0.
321    pub stroke_opacity: f64,
322    /// Blend mode index. 0=Normal, 1=Multiply, …, 15=Luminosity. Default 0.
323    pub blend_mode: u8,
324    /// Alpha-is-shape flag (PDF `AIS`). Default false.
325    pub alpha_is_shape: bool,
326    /// Text knockout flag (PDF `TK`). Default true.
327    pub text_knockout: bool,
328}
329
330impl GraphicsState {
331    /// Create default graphics state (PostScript initial state).
332    pub fn new() -> Self {
333        Self {
334            ctm: Matrix::identity(),
335            color: DeviceColor::black(),
336            color_space: ColorSpace::DeviceGray,
337            path: PsPath::new(),
338            current_point: None,
339            clip_path: None,
340            clip_path_version: 0,
341            line_width: 1.0,
342            line_cap: LineCap::Butt,
343            line_join: LineJoin::Miter,
344            miter_limit: 10.0,
345            dash_pattern: DashPattern::solid(),
346            flatness: 1.0,
347            stroke_adjust: false,
348            overprint: false,
349            smoothness: 1.0,
350            default_ctm: Matrix::identity(),
351            clip_stack: Vec::new(),
352            current_font: None,
353            root_font: None,
354            page_device: None,
355            screen_freq: 60.0,
356            screen_angle: 45.0,
357            screen_proc: None,
358            color_screen: None,
359            halftone: None,
360            transfer_function: None,
361            color_transfer: None,
362            sampled_transfer: None,
363            sampled_color_transfer: None,
364            precomputed_halftone: None,
365            precomputed_color_halftone: None,
366            black_generation: None,
367            undercolor_removal: None,
368            sampled_black_generation: None,
369            sampled_ucr: None,
370            color_rendering: None,
371            rendering_intent: 0, // RelativeColorimetric
372            current_pattern: None,
373            pattern_underlying_color: None,
374            current_pattern_dict: None,
375            pattern_components: Vec::new(),
376            bbox: None,
377            tint_values: None,
378            cached_tint_table: None,
379            fill_opacity: 1.0,
380            stroke_opacity: 1.0,
381            blend_mode: 0,
382            alpha_is_shape: false,
383            text_knockout: true,
384        }
385    }
386}
387
388impl Default for GraphicsState {
389    fn default() -> Self {
390        Self::new()
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_default_graphics_state() {
400        let gs = GraphicsState::new();
401        assert_eq!(gs.line_width, 1.0);
402        assert_eq!(gs.line_cap, LineCap::Butt);
403        assert_eq!(gs.line_join, LineJoin::Miter);
404        assert_eq!(gs.miter_limit, 10.0);
405        assert!(gs.path.is_empty());
406        assert!(gs.current_point.is_none());
407        assert!(gs.clip_path.is_none());
408        assert_eq!(gs.flatness, 1.0);
409        assert!(!gs.stroke_adjust);
410    }
411}