Skip to main content

pdfplumber_parse/
interpreter_state.rs

1//! Graphics state stack for the content stream interpreter.
2//!
3//! Implements the PDF graphics state model: a stack of states managed by
4//! `q` (save) and `Q` (restore) operators, with CTM management via `cm`,
5//! and color setting via G/g, RG/rg, K/k, SC/SCN/sc/scn operators.
6
7use pdfplumber_core::geometry::Ctm;
8use pdfplumber_core::painting::{Color, GraphicsState};
9
10/// Full interpreter state that combines the CTM with the graphics state.
11///
12/// This is the interpreter-level state that tracks everything needed
13/// during content stream processing. The `q` operator pushes a copy
14/// onto the stack; `Q` restores from the stack.
15#[derive(Debug, Clone, PartialEq)]
16pub struct InterpreterState {
17    /// Current transformation matrix.
18    ctm: Ctm,
19    /// Current graphics state (colors, line width, dash, alpha).
20    graphics_state: GraphicsState,
21    /// Saved state stack for q/Q operators.
22    stack: Vec<SavedState>,
23}
24
25/// A snapshot of the interpreter state saved by the `q` operator.
26#[derive(Debug, Clone, PartialEq)]
27struct SavedState {
28    ctm: Ctm,
29    graphics_state: GraphicsState,
30}
31
32impl Default for InterpreterState {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl InterpreterState {
39    /// Create a new interpreter state with identity CTM and default graphics state.
40    pub fn new() -> Self {
41        Self {
42            ctm: Ctm::identity(),
43            graphics_state: GraphicsState::default(),
44            stack: Vec::new(),
45        }
46    }
47
48    /// Get the current transformation matrix.
49    pub fn ctm(&self) -> &Ctm {
50        &self.ctm
51    }
52
53    /// Get the current CTM as a 6-element array `[a, b, c, d, e, f]`.
54    pub fn ctm_array(&self) -> [f64; 6] {
55        [
56            self.ctm.a, self.ctm.b, self.ctm.c, self.ctm.d, self.ctm.e, self.ctm.f,
57        ]
58    }
59
60    /// Get the current graphics state.
61    pub fn graphics_state(&self) -> &GraphicsState {
62        &self.graphics_state
63    }
64
65    /// Get a mutable reference to the current graphics state.
66    pub fn graphics_state_mut(&mut self) -> &mut GraphicsState {
67        &mut self.graphics_state
68    }
69
70    /// Returns the current stack depth.
71    pub fn stack_depth(&self) -> usize {
72        self.stack.len()
73    }
74
75    // --- q/Q operators ---
76
77    /// `q` operator: save the current graphics state onto the stack.
78    pub fn save_state(&mut self) {
79        self.stack.push(SavedState {
80            ctm: self.ctm,
81            graphics_state: self.graphics_state.clone(),
82        });
83    }
84
85    /// `Q` operator: restore the most recently saved graphics state.
86    ///
87    /// Returns `false` if the stack is empty (unbalanced Q).
88    pub fn restore_state(&mut self) -> bool {
89        if let Some(saved) = self.stack.pop() {
90            self.ctm = saved.ctm;
91            self.graphics_state = saved.graphics_state;
92            true
93        } else {
94            false
95        }
96    }
97
98    // --- cm operator ---
99
100    /// `cm` operator: concatenate a matrix with the current CTM.
101    ///
102    /// The new matrix is pre-multiplied: CTM' = new_matrix × CTM_current.
103    /// This follows the PDF spec where `cm` modifies the CTM by pre-concatenating.
104    pub fn concat_matrix(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
105        let new_matrix = Ctm::new(a, b, c, d, e, f);
106        self.ctm = new_matrix.concat(&self.ctm);
107    }
108
109    // --- w operator ---
110
111    /// `w` operator: set line width.
112    pub fn set_line_width(&mut self, width: f64) {
113        self.graphics_state.line_width = width;
114    }
115
116    // --- d operator ---
117
118    /// `d` operator: set dash pattern.
119    pub fn set_dash_pattern(&mut self, dash_array: Vec<f64>, dash_phase: f64) {
120        self.graphics_state.set_dash_pattern(dash_array, dash_phase);
121    }
122
123    // --- Color operators ---
124
125    /// `G` operator: set stroking color to DeviceGray.
126    pub fn set_stroking_gray(&mut self, gray: f32) {
127        self.graphics_state.stroke_color = Color::Gray(gray);
128    }
129
130    /// `g` operator: set non-stroking color to DeviceGray.
131    pub fn set_non_stroking_gray(&mut self, gray: f32) {
132        self.graphics_state.fill_color = Color::Gray(gray);
133    }
134
135    /// `RG` operator: set stroking color to DeviceRGB.
136    pub fn set_stroking_rgb(&mut self, r: f32, g: f32, b: f32) {
137        self.graphics_state.stroke_color = Color::Rgb(r, g, b);
138    }
139
140    /// `rg` operator: set non-stroking color to DeviceRGB.
141    pub fn set_non_stroking_rgb(&mut self, r: f32, g: f32, b: f32) {
142        self.graphics_state.fill_color = Color::Rgb(r, g, b);
143    }
144
145    /// `K` operator: set stroking color to DeviceCMYK.
146    pub fn set_stroking_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) {
147        self.graphics_state.stroke_color = Color::Cmyk(c, m, y, k);
148    }
149
150    /// `k` operator: set non-stroking color to DeviceCMYK.
151    pub fn set_non_stroking_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) {
152        self.graphics_state.fill_color = Color::Cmyk(c, m, y, k);
153    }
154
155    /// `SC`/`SCN` operator: set stroking color from components.
156    ///
157    /// Interprets component count to determine color space:
158    /// - 1 component → Gray
159    /// - 3 components → RGB
160    /// - 4 components → CMYK
161    /// - other → Other
162    pub fn set_stroking_color(&mut self, components: &[f32]) {
163        self.graphics_state.stroke_color = color_from_components(components);
164    }
165
166    /// `sc`/`scn` operator: set non-stroking color from components.
167    ///
168    /// Interprets component count to determine color space:
169    /// - 1 component → Gray
170    /// - 3 components → RGB
171    /// - 4 components → CMYK
172    /// - other → Other
173    pub fn set_non_stroking_color(&mut self, components: &[f32]) {
174        self.graphics_state.fill_color = color_from_components(components);
175    }
176}
177
178/// Convert a slice of color components to a `Color` value.
179fn color_from_components(components: &[f32]) -> Color {
180    match components.len() {
181        1 => Color::Gray(components[0]),
182        3 => Color::Rgb(components[0], components[1], components[2]),
183        4 => Color::Cmyk(components[0], components[1], components[2], components[3]),
184        _ => Color::Other(components.to_vec()),
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use pdfplumber_core::geometry::Point;
192    use pdfplumber_core::painting::DashPattern;
193
194    // --- Construction and defaults ---
195
196    #[test]
197    fn test_new_has_identity_ctm() {
198        let state = InterpreterState::new();
199        assert_eq!(*state.ctm(), Ctm::identity());
200    }
201
202    #[test]
203    fn test_new_has_default_graphics_state() {
204        let state = InterpreterState::new();
205        let gs = state.graphics_state();
206        assert_eq!(gs.line_width, 1.0);
207        assert_eq!(gs.stroke_color, Color::black());
208        assert_eq!(gs.fill_color, Color::black());
209        assert!(gs.dash_pattern.is_solid());
210        assert_eq!(gs.stroke_alpha, 1.0);
211        assert_eq!(gs.fill_alpha, 1.0);
212    }
213
214    #[test]
215    fn test_new_has_empty_stack() {
216        let state = InterpreterState::new();
217        assert_eq!(state.stack_depth(), 0);
218    }
219
220    #[test]
221    fn test_default_equals_new() {
222        assert_eq!(InterpreterState::default(), InterpreterState::new());
223    }
224
225    #[test]
226    fn test_ctm_array() {
227        let state = InterpreterState::new();
228        assert_eq!(state.ctm_array(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
229    }
230
231    // --- q/Q: push/pop state ---
232
233    #[test]
234    fn test_save_state_increments_depth() {
235        let mut state = InterpreterState::new();
236        state.save_state();
237        assert_eq!(state.stack_depth(), 1);
238        state.save_state();
239        assert_eq!(state.stack_depth(), 2);
240    }
241
242    #[test]
243    fn test_restore_state_decrements_depth() {
244        let mut state = InterpreterState::new();
245        state.save_state();
246        state.save_state();
247        assert_eq!(state.stack_depth(), 2);
248
249        assert!(state.restore_state());
250        assert_eq!(state.stack_depth(), 1);
251
252        assert!(state.restore_state());
253        assert_eq!(state.stack_depth(), 0);
254    }
255
256    #[test]
257    fn test_restore_on_empty_stack_returns_false() {
258        let mut state = InterpreterState::new();
259        assert!(!state.restore_state());
260    }
261
262    #[test]
263    fn test_save_restore_preserves_ctm() {
264        let mut state = InterpreterState::new();
265
266        // Save, then modify CTM
267        state.save_state();
268        state.concat_matrix(2.0, 0.0, 0.0, 2.0, 10.0, 20.0);
269        assert_ne!(*state.ctm(), Ctm::identity());
270
271        // Restore: CTM should be back to identity
272        state.restore_state();
273        assert_eq!(*state.ctm(), Ctm::identity());
274    }
275
276    #[test]
277    fn test_save_restore_preserves_graphics_state() {
278        let mut state = InterpreterState::new();
279
280        // Save, then modify state
281        state.save_state();
282        state.set_line_width(5.0);
283        state.set_stroking_rgb(1.0, 0.0, 0.0);
284        state.set_non_stroking_gray(0.5);
285        state.set_dash_pattern(vec![3.0, 2.0], 1.0);
286
287        // Verify changes took effect
288        assert_eq!(state.graphics_state().line_width, 5.0);
289        assert_eq!(
290            state.graphics_state().stroke_color,
291            Color::Rgb(1.0, 0.0, 0.0)
292        );
293        assert_eq!(state.graphics_state().fill_color, Color::Gray(0.5));
294
295        // Restore: all should be back to defaults
296        state.restore_state();
297        assert_eq!(state.graphics_state().line_width, 1.0);
298        assert_eq!(state.graphics_state().stroke_color, Color::black());
299        assert_eq!(state.graphics_state().fill_color, Color::black());
300        assert!(state.graphics_state().dash_pattern.is_solid());
301    }
302
303    #[test]
304    fn test_nested_save_restore() {
305        let mut state = InterpreterState::new();
306
307        // Level 0: set red stroke
308        state.set_stroking_rgb(1.0, 0.0, 0.0);
309
310        // Save level 0
311        state.save_state();
312
313        // Level 1: set blue stroke
314        state.set_stroking_rgb(0.0, 0.0, 1.0);
315        assert_eq!(
316            state.graphics_state().stroke_color,
317            Color::Rgb(0.0, 0.0, 1.0)
318        );
319
320        // Save level 1
321        state.save_state();
322
323        // Level 2: set green stroke
324        state.set_stroking_rgb(0.0, 1.0, 0.0);
325        assert_eq!(
326            state.graphics_state().stroke_color,
327            Color::Rgb(0.0, 1.0, 0.0)
328        );
329
330        // Restore to level 1: blue
331        state.restore_state();
332        assert_eq!(
333            state.graphics_state().stroke_color,
334            Color::Rgb(0.0, 0.0, 1.0)
335        );
336
337        // Restore to level 0: red
338        state.restore_state();
339        assert_eq!(
340            state.graphics_state().stroke_color,
341            Color::Rgb(1.0, 0.0, 0.0)
342        );
343    }
344
345    // --- cm: CTM multiplication ---
346
347    #[test]
348    fn test_concat_matrix_translation() {
349        let mut state = InterpreterState::new();
350        state.concat_matrix(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
351
352        let p = state.ctm().transform_point(Point::new(0.0, 0.0));
353        assert_approx(p.x, 100.0);
354        assert_approx(p.y, 200.0);
355    }
356
357    #[test]
358    fn test_concat_matrix_scaling() {
359        let mut state = InterpreterState::new();
360        state.concat_matrix(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
361
362        let p = state.ctm().transform_point(Point::new(5.0, 10.0));
363        assert_approx(p.x, 10.0);
364        assert_approx(p.y, 30.0);
365    }
366
367    #[test]
368    fn test_concat_matrix_cumulative() {
369        let mut state = InterpreterState::new();
370
371        // First: scale by 2x
372        state.concat_matrix(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
373        // Second: translate by (10, 20) — in the scaled coordinate system
374        state.concat_matrix(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
375
376        // Point (0,0) in user space:
377        // After translate: (10, 20) in intermediate space
378        // After scale: (20, 40) in device space
379        let p = state.ctm().transform_point(Point::new(0.0, 0.0));
380        assert_approx(p.x, 20.0);
381        assert_approx(p.y, 40.0);
382    }
383
384    #[test]
385    fn test_concat_identity_no_change() {
386        let mut state = InterpreterState::new();
387        state.concat_matrix(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
388        let ctm_before = *state.ctm();
389
390        // Concatenate identity — no change
391        state.concat_matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
392        assert_eq!(*state.ctm(), ctm_before);
393    }
394
395    #[test]
396    fn test_ctm_array_after_concat() {
397        let mut state = InterpreterState::new();
398        state.concat_matrix(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
399        assert_eq!(state.ctm_array(), [2.0, 0.0, 0.0, 3.0, 10.0, 20.0]);
400    }
401
402    // --- w: line width ---
403
404    #[test]
405    fn test_set_line_width() {
406        let mut state = InterpreterState::new();
407        state.set_line_width(3.5);
408        assert_eq!(state.graphics_state().line_width, 3.5);
409    }
410
411    #[test]
412    fn test_set_line_width_zero() {
413        let mut state = InterpreterState::new();
414        state.set_line_width(0.0);
415        assert_eq!(state.graphics_state().line_width, 0.0);
416    }
417
418    // --- d: dash pattern ---
419
420    #[test]
421    fn test_set_dash_pattern() {
422        let mut state = InterpreterState::new();
423        state.set_dash_pattern(vec![3.0, 2.0], 1.0);
424
425        let dp = &state.graphics_state().dash_pattern;
426        assert_eq!(dp.dash_array, vec![3.0, 2.0]);
427        assert_eq!(dp.dash_phase, 1.0);
428        assert!(!dp.is_solid());
429    }
430
431    #[test]
432    fn test_set_dash_pattern_solid() {
433        let mut state = InterpreterState::new();
434        state.set_dash_pattern(vec![3.0, 2.0], 0.0);
435        assert!(!state.graphics_state().dash_pattern.is_solid());
436
437        state.set_dash_pattern(vec![], 0.0);
438        assert!(state.graphics_state().dash_pattern.is_solid());
439    }
440
441    // --- G/g: DeviceGray color ---
442
443    #[test]
444    fn test_set_stroking_gray() {
445        let mut state = InterpreterState::new();
446        state.set_stroking_gray(0.5);
447        assert_eq!(state.graphics_state().stroke_color, Color::Gray(0.5));
448    }
449
450    #[test]
451    fn test_set_non_stroking_gray() {
452        let mut state = InterpreterState::new();
453        state.set_non_stroking_gray(0.75);
454        assert_eq!(state.graphics_state().fill_color, Color::Gray(0.75));
455    }
456
457    // --- RG/rg: DeviceRGB color ---
458
459    #[test]
460    fn test_set_stroking_rgb() {
461        let mut state = InterpreterState::new();
462        state.set_stroking_rgb(1.0, 0.0, 0.0);
463        assert_eq!(
464            state.graphics_state().stroke_color,
465            Color::Rgb(1.0, 0.0, 0.0)
466        );
467    }
468
469    #[test]
470    fn test_set_non_stroking_rgb() {
471        let mut state = InterpreterState::new();
472        state.set_non_stroking_rgb(0.0, 1.0, 0.0);
473        assert_eq!(state.graphics_state().fill_color, Color::Rgb(0.0, 1.0, 0.0));
474    }
475
476    // --- K/k: DeviceCMYK color ---
477
478    #[test]
479    fn test_set_stroking_cmyk() {
480        let mut state = InterpreterState::new();
481        state.set_stroking_cmyk(0.1, 0.2, 0.3, 0.4);
482        assert_eq!(
483            state.graphics_state().stroke_color,
484            Color::Cmyk(0.1, 0.2, 0.3, 0.4)
485        );
486    }
487
488    #[test]
489    fn test_set_non_stroking_cmyk() {
490        let mut state = InterpreterState::new();
491        state.set_non_stroking_cmyk(0.5, 0.6, 0.7, 0.8);
492        assert_eq!(
493            state.graphics_state().fill_color,
494            Color::Cmyk(0.5, 0.6, 0.7, 0.8)
495        );
496    }
497
498    // --- SC/SCN/sc/scn: generic color operators ---
499
500    #[test]
501    fn test_set_stroking_color_1_component_is_gray() {
502        let mut state = InterpreterState::new();
503        state.set_stroking_color(&[0.5]);
504        assert_eq!(state.graphics_state().stroke_color, Color::Gray(0.5));
505    }
506
507    #[test]
508    fn test_set_stroking_color_3_components_is_rgb() {
509        let mut state = InterpreterState::new();
510        state.set_stroking_color(&[1.0, 0.0, 0.0]);
511        assert_eq!(
512            state.graphics_state().stroke_color,
513            Color::Rgb(1.0, 0.0, 0.0)
514        );
515    }
516
517    #[test]
518    fn test_set_stroking_color_4_components_is_cmyk() {
519        let mut state = InterpreterState::new();
520        state.set_stroking_color(&[0.1, 0.2, 0.3, 0.4]);
521        assert_eq!(
522            state.graphics_state().stroke_color,
523            Color::Cmyk(0.1, 0.2, 0.3, 0.4)
524        );
525    }
526
527    #[test]
528    fn test_set_stroking_color_other_component_count() {
529        let mut state = InterpreterState::new();
530        state.set_stroking_color(&[0.1, 0.2]);
531        assert_eq!(
532            state.graphics_state().stroke_color,
533            Color::Other(vec![0.1, 0.2])
534        );
535    }
536
537    #[test]
538    fn test_set_non_stroking_color_1_component() {
539        let mut state = InterpreterState::new();
540        state.set_non_stroking_color(&[0.3]);
541        assert_eq!(state.graphics_state().fill_color, Color::Gray(0.3));
542    }
543
544    #[test]
545    fn test_set_non_stroking_color_3_components() {
546        let mut state = InterpreterState::new();
547        state.set_non_stroking_color(&[0.0, 0.0, 1.0]);
548        assert_eq!(state.graphics_state().fill_color, Color::Rgb(0.0, 0.0, 1.0));
549    }
550
551    #[test]
552    fn test_set_non_stroking_color_5_components_is_other() {
553        let mut state = InterpreterState::new();
554        state.set_non_stroking_color(&[0.1, 0.2, 0.3, 0.4, 0.5]);
555        assert_eq!(
556            state.graphics_state().fill_color,
557            Color::Other(vec![0.1, 0.2, 0.3, 0.4, 0.5])
558        );
559    }
560
561    // --- Color state independence ---
562
563    #[test]
564    fn test_stroking_and_non_stroking_independent() {
565        let mut state = InterpreterState::new();
566        state.set_stroking_rgb(1.0, 0.0, 0.0);
567        state.set_non_stroking_rgb(0.0, 0.0, 1.0);
568
569        assert_eq!(
570            state.graphics_state().stroke_color,
571            Color::Rgb(1.0, 0.0, 0.0)
572        );
573        assert_eq!(state.graphics_state().fill_color, Color::Rgb(0.0, 0.0, 1.0));
574    }
575
576    #[test]
577    fn test_color_changes_across_color_spaces() {
578        let mut state = InterpreterState::new();
579
580        // Start gray
581        state.set_stroking_gray(0.5);
582        assert_eq!(state.graphics_state().stroke_color, Color::Gray(0.5));
583
584        // Switch to RGB
585        state.set_stroking_rgb(1.0, 0.0, 0.0);
586        assert_eq!(
587            state.graphics_state().stroke_color,
588            Color::Rgb(1.0, 0.0, 0.0)
589        );
590
591        // Switch to CMYK
592        state.set_stroking_cmyk(0.0, 1.0, 0.0, 0.0);
593        assert_eq!(
594            state.graphics_state().stroke_color,
595            Color::Cmyk(0.0, 1.0, 0.0, 0.0)
596        );
597    }
598
599    // --- Combined q/Q with all state changes ---
600
601    #[test]
602    fn test_full_state_save_restore_cycle() {
603        let mut state = InterpreterState::new();
604
605        // Set up initial state
606        state.concat_matrix(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
607        state.set_line_width(2.0);
608        state.set_stroking_rgb(1.0, 0.0, 0.0);
609        state.set_non_stroking_gray(0.5);
610        state.set_dash_pattern(vec![5.0, 3.0], 0.0);
611
612        // Save (q)
613        state.save_state();
614
615        // Modify everything
616        state.concat_matrix(1.0, 0.0, 0.0, 1.0, 50.0, 50.0);
617        state.set_line_width(0.5);
618        state.set_stroking_cmyk(0.0, 0.0, 0.0, 1.0);
619        state.set_non_stroking_rgb(0.0, 1.0, 0.0);
620        state.set_dash_pattern(vec![], 0.0);
621
622        // Verify modifications
623        assert_eq!(state.graphics_state().line_width, 0.5);
624        assert_eq!(
625            state.graphics_state().stroke_color,
626            Color::Cmyk(0.0, 0.0, 0.0, 1.0)
627        );
628        assert!(state.graphics_state().dash_pattern.is_solid());
629
630        // Restore (Q) — should revert to pre-save state
631        state.restore_state();
632
633        // Check CTM was restored (scale 2x only)
634        assert_eq!(state.ctm_array(), [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]);
635
636        // Check graphics state was restored
637        assert_eq!(state.graphics_state().line_width, 2.0);
638        assert_eq!(
639            state.graphics_state().stroke_color,
640            Color::Rgb(1.0, 0.0, 0.0)
641        );
642        assert_eq!(state.graphics_state().fill_color, Color::Gray(0.5));
643        assert_eq!(
644            state.graphics_state().dash_pattern,
645            DashPattern::new(vec![5.0, 3.0], 0.0)
646        );
647    }
648
649    #[test]
650    fn test_multiple_unbalanced_restores_return_false() {
651        let mut state = InterpreterState::new();
652        state.save_state();
653
654        assert!(state.restore_state());
655        assert!(!state.restore_state()); // empty stack
656        assert!(!state.restore_state()); // still empty
657    }
658
659    #[test]
660    fn test_graphics_state_mut_access() {
661        let mut state = InterpreterState::new();
662        state.graphics_state_mut().stroke_alpha = 0.5;
663        assert_eq!(state.graphics_state().stroke_alpha, 0.5);
664    }
665
666    // --- Helper ---
667
668    fn assert_approx(actual: f64, expected: f64) {
669        assert!(
670            (actual - expected).abs() < 1e-10,
671            "expected {expected}, got {actual}"
672        );
673    }
674}