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}
82
83impl PartialEq for ColorSpace {
84    fn eq(&self, other: &Self) -> bool {
85        use ColorSpace::*;
86        match (self, other) {
87            (DeviceGray, DeviceGray) | (DeviceRGB, DeviceRGB) | (DeviceCMYK, DeviceCMYK) => true,
88            (
89                Indexed {
90                    base: b1,
91                    hival: h1,
92                    lookup: l1,
93                    ..
94                },
95                Indexed {
96                    base: b2,
97                    hival: h2,
98                    lookup: l2,
99                    ..
100                },
101            ) => b1 == b2 && h1 == h2 && l1 == l2,
102            (
103                CIEBasedABC {
104                    dict_entity: d1, ..
105                },
106                CIEBasedABC {
107                    dict_entity: d2, ..
108                },
109            ) => d1 == d2,
110            (
111                CIEBasedA {
112                    dict_entity: d1, ..
113                },
114                CIEBasedA {
115                    dict_entity: d2, ..
116                },
117            ) => d1 == d2,
118            (
119                CIEBasedDEF {
120                    dict_entity: d1, ..
121                },
122                CIEBasedDEF {
123                    dict_entity: d2, ..
124                },
125            ) => d1 == d2,
126            (
127                CIEBasedDEFG {
128                    dict_entity: d1, ..
129                },
130                CIEBasedDEFG {
131                    dict_entity: d2, ..
132                },
133            ) => d1 == d2,
134            (
135                ICCBased {
136                    dict_entity: d1,
137                    n: n1,
138                    ..
139                },
140                ICCBased {
141                    dict_entity: d2,
142                    n: n2,
143                    ..
144                },
145            ) => d1 == d2 && n1 == n2,
146            (
147                Separation {
148                    name: name1,
149                    alt_space: a1,
150                    tint_transform: t1,
151                    num_alt_components: n1,
152                },
153                Separation {
154                    name: name2,
155                    alt_space: a2,
156                    tint_transform: t2,
157                    num_alt_components: n2,
158                },
159            ) => name1 == name2 && a1 == a2 && t1 == t2 && n1 == n2,
160            (
161                DeviceN {
162                    names: names1,
163                    num_colorants: nc1,
164                    alt_space: a1,
165                    tint_transform: t1,
166                    num_alt_components: n1,
167                },
168                DeviceN {
169                    names: names2,
170                    num_colorants: nc2,
171                    alt_space: a2,
172                    tint_transform: t2,
173                    num_alt_components: n2,
174                },
175            ) => names1 == names2 && nc1 == nc2 && a1 == a2 && t1 == t2 && n1 == n2,
176            _ => false,
177        }
178    }
179}
180
181/// Pattern instance data created by `makepattern`.
182#[derive(Clone)]
183pub struct PatternData {
184    /// Pattern type: 1 = tiling, 2 = shading.
185    pub pattern_type: i32,
186    /// Paint type: 1 = colored, 2 = uncolored.
187    pub paint_type: i32,
188    /// Tiling type: 1 = constant spacing, 2 = no distortion, 3 = fast.
189    pub tiling_type: i32,
190    /// Bounding box [llx, lly, urx, ury] in pattern space.
191    pub bbox: [f64; 4],
192    /// X step between tile origins.
193    pub xstep: f64,
194    /// Y step between tile origins.
195    pub ystep: f64,
196    /// Combined matrix: matrix_arg × CTM at makepattern time.
197    pub pattern_matrix: Matrix,
198    /// Pre-rendered display list from executing PaintProc.
199    pub cached_display_list: DisplayList,
200}
201
202/// Entry on the graphics state stack, tracking whether it was created by
203/// `save` (implicit gsave) or `gsave`.
204#[derive(Clone, Debug)]
205pub struct GstateEntry {
206    pub state: GraphicsState,
207    /// True if created by `save`, false if by `gsave`.
208    /// `grestore` skips save-created entries; `grestoreall` stops at them.
209    pub saved_by_save: bool,
210}
211
212/// Complete graphics state (cloned for gsave/grestore).
213#[derive(Clone, Debug)]
214pub struct GraphicsState {
215    pub ctm: Matrix,
216    pub color: DeviceColor,
217    pub color_space: ColorSpace,
218    pub path: PsPath,
219    pub current_point: Option<(f64, f64)>,
220    pub clip_path: Option<PsPath>,
221    pub clip_path_version: u32,
222    pub line_width: f64,
223    pub line_cap: LineCap,
224    pub line_join: LineJoin,
225    pub miter_limit: f64,
226    pub dash_pattern: DashPattern,
227    pub flatness: f64,
228    pub stroke_adjust: bool,
229    pub overprint: bool,
230    pub smoothness: f64,
231    pub default_ctm: Matrix,
232
233    // Clip save/restore stack (per graphics state)
234    pub clip_stack: Vec<Option<PsPath>>,
235
236    // Current font (set by setfont, used by show operators)
237    pub current_font: Option<crate::object::PsObject>,
238
239    // Root font for composite font hierarchy (set during Type 0 rendering).
240    // rootfont returns this if set, otherwise falls back to current_font.
241    pub root_font: Option<crate::object::PsObject>,
242
243    // Page device dict (EntityId into DictStore)
244    pub page_device: Option<crate::object::EntityId>,
245
246    // Halftone screen parameters (set by setscreen/setcolorscreen/sethalftone)
247    pub screen_freq: f64,
248    pub screen_angle: f64,
249    pub screen_proc: Option<crate::object::PsObject>,
250    /// Per-component color screen: [red, green, blue, gray] × (freq, angle, proc)
251    pub color_screen: Option<[(f64, f64, crate::object::PsObject); 4]>,
252    /// Halftone dictionary (set by sethalftone)
253    pub halftone: Option<crate::object::PsObject>,
254
255    // Transfer functions
256    pub transfer_function: Option<crate::object::PsObject>,
257    /// Per-component transfer: [red, green, blue, gray]
258    pub color_transfer: Option<[crate::object::PsObject; 4]>,
259    /// Pre-sampled transfer function (256 entries). None = identity.
260    pub sampled_transfer: Option<Arc<Vec<f64>>>,
261    /// Pre-sampled per-component transfer \[R, G, B, Gray\].
262    pub sampled_color_transfer: Option<[Option<Arc<Vec<f64>>>; 4]>,
263    /// Pre-computed halftone screen for PDF output. None = default (suppressed).
264    pub precomputed_halftone: Option<Arc<crate::device::HalftoneScreen>>,
265    /// Pre-computed per-component halftone \[R, G, B, Gray\] (from setcolorscreen).
266    pub precomputed_color_halftone: Option<[Option<Arc<crate::device::HalftoneScreen>>; 4]>,
267
268    // Black generation / undercolor removal
269    pub black_generation: Option<crate::object::PsObject>,
270    pub undercolor_removal: Option<crate::object::PsObject>,
271    /// Pre-sampled black generation function (256 entries, domain `[0,1]` → range `[0,1]`).
272    pub sampled_black_generation: Option<Arc<Vec<f64>>>,
273    /// Pre-sampled undercolor removal function (256 entries, domain `[0,1]` → range `[-1,1]`).
274    pub sampled_ucr: Option<Arc<Vec<f64>>>,
275
276    // Color rendering dictionary
277    pub color_rendering: Option<crate::object::PsObject>,
278
279    /// Rendering intent: 0=RelativeColorimetric, 1=AbsoluteColorimetric,
280    /// 2=Perceptual, 3=Saturation. Default is RelativeColorimetric.
281    pub rendering_intent: u8,
282
283    // Pattern state (set by setpattern, consumed by fill/eofill)
284    /// Index into `Context.pattern_store` for the active tiling pattern.
285    pub current_pattern: Option<u32>,
286    /// Underlying color for uncolored (PaintType 2) patterns.
287    pub pattern_underlying_color: Option<DeviceColor>,
288
289    // Userpath bounding box (set by setbbox, cleared by newpath)
290    pub bbox: Option<[f64; 4]>,
291
292    /// Tint values from the most recent setcolor (for Separation/DeviceN).
293    /// 1 value for Separation, N values for DeviceN. None for device color spaces.
294    pub tint_values: Option<Vec<f64>>,
295
296    /// Cached pre-sampled tint lookup table for the current Separation/DeviceN color space.
297    /// Set when setcolorspace installs a Separation/DeviceN space.
298    pub cached_tint_table: Option<Arc<crate::device::TintLookupTable>>,
299
300    /// Constant fill opacity (PDF `ca`). Range \[0,1\]. Default 1.0.
301    pub fill_opacity: f64,
302    /// Constant stroke opacity (PDF `CA`). Range \[0,1\]. Default 1.0.
303    pub stroke_opacity: f64,
304    /// Blend mode index. 0=Normal, 1=Multiply, …, 15=Luminosity. Default 0.
305    pub blend_mode: u8,
306    /// Alpha-is-shape flag (PDF `AIS`). Default false.
307    pub alpha_is_shape: bool,
308    /// Text knockout flag (PDF `TK`). Default true.
309    pub text_knockout: bool,
310}
311
312impl GraphicsState {
313    /// Create default graphics state (PostScript initial state).
314    pub fn new() -> Self {
315        Self {
316            ctm: Matrix::identity(),
317            color: DeviceColor::black(),
318            color_space: ColorSpace::DeviceGray,
319            path: PsPath::new(),
320            current_point: None,
321            clip_path: None,
322            clip_path_version: 0,
323            line_width: 1.0,
324            line_cap: LineCap::Butt,
325            line_join: LineJoin::Miter,
326            miter_limit: 10.0,
327            dash_pattern: DashPattern::solid(),
328            flatness: 1.0,
329            stroke_adjust: false,
330            overprint: false,
331            smoothness: 1.0,
332            default_ctm: Matrix::identity(),
333            clip_stack: Vec::new(),
334            current_font: None,
335            root_font: None,
336            page_device: None,
337            screen_freq: 60.0,
338            screen_angle: 45.0,
339            screen_proc: None,
340            color_screen: None,
341            halftone: None,
342            transfer_function: None,
343            color_transfer: None,
344            sampled_transfer: None,
345            sampled_color_transfer: None,
346            precomputed_halftone: None,
347            precomputed_color_halftone: None,
348            black_generation: None,
349            undercolor_removal: None,
350            sampled_black_generation: None,
351            sampled_ucr: None,
352            color_rendering: None,
353            rendering_intent: 0, // RelativeColorimetric
354            current_pattern: None,
355            pattern_underlying_color: None,
356            bbox: None,
357            tint_values: None,
358            cached_tint_table: None,
359            fill_opacity: 1.0,
360            stroke_opacity: 1.0,
361            blend_mode: 0,
362            alpha_is_shape: false,
363            text_knockout: true,
364        }
365    }
366}
367
368impl Default for GraphicsState {
369    fn default() -> Self {
370        Self::new()
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_default_graphics_state() {
380        let gs = GraphicsState::new();
381        assert_eq!(gs.line_width, 1.0);
382        assert_eq!(gs.line_cap, LineCap::Butt);
383        assert_eq!(gs.line_join, LineJoin::Miter);
384        assert_eq!(gs.miter_limit, 10.0);
385        assert!(gs.path.is_empty());
386        assert!(gs.current_point.is_none());
387        assert!(gs.clip_path.is_none());
388        assert_eq!(gs.flatness, 1.0);
389        assert!(!gs.stroke_adjust);
390    }
391}