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}