Skip to main content

folio_content/
gstate.rs

1//! PDF Graphics State — tracks all visual properties during content stream processing.
2
3use folio_core::Matrix2D;
4
5/// The complete graphics state at any point during content stream processing.
6#[derive(Debug, Clone)]
7pub struct GraphicsState {
8    /// Current transformation matrix.
9    pub ctm: Matrix2D,
10    /// Line width.
11    pub line_width: f64,
12    /// Line cap style (0=butt, 1=round, 2=square).
13    pub line_cap: i32,
14    /// Line join style (0=miter, 1=round, 2=bevel).
15    pub line_join: i32,
16    /// Miter limit.
17    pub miter_limit: f64,
18    /// Dash pattern.
19    pub dash_array: Vec<f64>,
20    /// Dash phase.
21    pub dash_phase: f64,
22    /// Fill color components.
23    pub fill_color: Vec<f64>,
24    /// Stroke color components.
25    pub stroke_color: Vec<f64>,
26    /// Fill color space name.
27    pub fill_color_space: Vec<u8>,
28    /// Stroke color space name.
29    pub stroke_color_space: Vec<u8>,
30    /// Character spacing.
31    pub char_spacing: f64,
32    /// Word spacing.
33    pub word_spacing: f64,
34    /// Horizontal scaling (percentage, default 100).
35    pub horiz_scaling: f64,
36    /// Text leading.
37    pub text_leading: f64,
38    /// Current font name (resource name, e.g., "F1").
39    pub font_name: Vec<u8>,
40    /// Current font size.
41    pub font_size: f64,
42    /// Text rendering mode (0-7).
43    pub text_render_mode: i32,
44    /// Text rise.
45    pub text_rise: f64,
46    /// Text matrix (set by Tm, modified by Td/TD/T*).
47    pub text_matrix: Matrix2D,
48    /// Text line matrix (set at start of line, used by T* and ').
49    pub text_line_matrix: Matrix2D,
50    /// Fill opacity (0.0 - 1.0).
51    pub fill_opacity: f64,
52    /// Stroke opacity (0.0 - 1.0).
53    pub stroke_opacity: f64,
54}
55
56impl Default for GraphicsState {
57    fn default() -> Self {
58        Self {
59            ctm: Matrix2D::identity(),
60            line_width: 1.0,
61            line_cap: 0,
62            line_join: 0,
63            miter_limit: 10.0,
64            dash_array: Vec::new(),
65            dash_phase: 0.0,
66            fill_color: vec![0.0],
67            stroke_color: vec![0.0],
68            fill_color_space: b"DeviceGray".to_vec(),
69            stroke_color_space: b"DeviceGray".to_vec(),
70            char_spacing: 0.0,
71            word_spacing: 0.0,
72            horiz_scaling: 100.0,
73            text_leading: 0.0,
74            font_name: Vec::new(),
75            font_size: 0.0,
76            text_render_mode: 0,
77            text_rise: 0.0,
78            text_matrix: Matrix2D::identity(),
79            text_line_matrix: Matrix2D::identity(),
80            fill_opacity: 1.0,
81            stroke_opacity: 1.0,
82        }
83    }
84}
85
86/// A graphics state stack that supports save/restore (q/Q operators).
87#[derive(Debug)]
88pub struct GraphicsStateStack {
89    current: GraphicsState,
90    stack: Vec<GraphicsState>,
91}
92
93impl GraphicsStateStack {
94    pub fn new() -> Self {
95        Self {
96            current: GraphicsState::default(),
97            stack: Vec::new(),
98        }
99    }
100
101    /// Get the current graphics state.
102    pub fn current(&self) -> &GraphicsState {
103        &self.current
104    }
105
106    /// Get a mutable reference to the current graphics state.
107    pub fn current_mut(&mut self) -> &mut GraphicsState {
108        &mut self.current
109    }
110
111    /// Save the current state (q operator).
112    pub fn save(&mut self) {
113        self.stack.push(self.current.clone());
114    }
115
116    /// Restore the previous state (Q operator).
117    pub fn restore(&mut self) {
118        if let Some(prev) = self.stack.pop() {
119            self.current = prev;
120        }
121    }
122
123    /// Stack depth (number of saved states).
124    pub fn depth(&self) -> usize {
125        self.stack.len()
126    }
127}
128
129impl Default for GraphicsStateStack {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_save_restore() {
141        let mut stack = GraphicsStateStack::new();
142        stack.current_mut().line_width = 5.0;
143        stack.save();
144        stack.current_mut().line_width = 10.0;
145        assert_eq!(stack.current().line_width, 10.0);
146        stack.restore();
147        assert_eq!(stack.current().line_width, 5.0);
148    }
149
150    #[test]
151    fn test_restore_empty() {
152        let mut stack = GraphicsStateStack::new();
153        stack.current_mut().line_width = 5.0;
154        stack.restore(); // Should be a no-op
155        assert_eq!(stack.current().line_width, 5.0);
156    }
157
158    #[test]
159    fn test_nested() {
160        let mut stack = GraphicsStateStack::new();
161        stack.current_mut().line_width = 1.0;
162        stack.save();
163        stack.current_mut().line_width = 2.0;
164        stack.save();
165        stack.current_mut().line_width = 3.0;
166        assert_eq!(stack.depth(), 2);
167        stack.restore();
168        assert_eq!(stack.current().line_width, 2.0);
169        stack.restore();
170        assert_eq!(stack.current().line_width, 1.0);
171    }
172}