Skip to main content

edgeparse_core/pdf/
graphics_state.rs

1//! PDF graphics state tracking.
2//!
3//! Manages the graphics state stack including the Current Transformation Matrix (CTM),
4//! text state parameters, and color state needed for text extraction.
5
6/// 2D affine transformation matrix: [a b c d e f]
7/// Represents the transform: x' = a*x + c*y + e, y' = b*x + d*y + f
8#[derive(Debug, Clone, Copy)]
9pub struct Matrix {
10    /// Horizontal scaling component.
11    pub a: f64,
12    /// Horizontal skewing component.
13    pub b: f64,
14    /// Vertical skewing component.
15    pub c: f64,
16    /// Vertical scaling component.
17    pub d: f64,
18    /// Horizontal translation.
19    pub e: f64,
20    /// Vertical translation.
21    pub f: f64,
22}
23
24impl Matrix {
25    /// Identity matrix.
26    pub fn identity() -> Self {
27        Self {
28            a: 1.0,
29            b: 0.0,
30            c: 0.0,
31            d: 1.0,
32            e: 0.0,
33            f: 0.0,
34        }
35    }
36
37    /// Translate matrix.
38    pub fn translate(tx: f64, ty: f64) -> Self {
39        Self {
40            a: 1.0,
41            b: 0.0,
42            c: 0.0,
43            d: 1.0,
44            e: tx,
45            f: ty,
46        }
47    }
48
49    /// Multiply this matrix by another: self × other.
50    pub fn multiply(&self, other: &Matrix) -> Matrix {
51        Matrix {
52            a: self.a * other.a + self.b * other.c,
53            b: self.a * other.b + self.b * other.d,
54            c: self.c * other.a + self.d * other.c,
55            d: self.c * other.b + self.d * other.d,
56            e: self.e * other.a + self.f * other.c + other.e,
57            f: self.e * other.b + self.f * other.d + other.f,
58        }
59    }
60
61    /// Transform a point (x, y) by this matrix.
62    pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
63        (
64            self.a * x + self.c * y + self.e,
65            self.b * x + self.d * y + self.f,
66        )
67    }
68
69    /// Get the effective font size (vertical scaling factor).
70    pub fn font_size_factor(&self) -> f64 {
71        (self.b * self.b + self.d * self.d).sqrt()
72    }
73}
74
75impl Default for Matrix {
76    fn default() -> Self {
77        Self::identity()
78    }
79}
80
81/// Text state parameters tracked during content stream processing.
82#[derive(Debug, Clone)]
83pub struct TextState {
84    /// Current font name (resource name like "F1")
85    pub font_name: String,
86    /// Font size in text space units
87    pub font_size: f64,
88    /// Character spacing (Tc)
89    pub char_spacing: f64,
90    /// Word spacing (Tw)
91    pub word_spacing: f64,
92    /// Horizontal scaling (Tz) as percentage (default 100)
93    pub horizontal_scaling: f64,
94    /// Text leading (TL)
95    pub leading: f64,
96    /// Text rise (Ts)
97    pub rise: f64,
98    /// Text rendering mode (Tr)
99    pub render_mode: i32,
100}
101
102impl Default for TextState {
103    fn default() -> Self {
104        Self {
105            font_name: String::new(),
106            font_size: 0.0,
107            char_spacing: 0.0,
108            word_spacing: 0.0,
109            horizontal_scaling: 100.0,
110            leading: 0.0,
111            rise: 0.0,
112            render_mode: 0,
113        }
114    }
115}
116
117/// Full graphics state for PDF content stream processing.
118#[derive(Debug, Clone)]
119pub struct GraphicsState {
120    /// Current transformation matrix
121    pub ctm: Matrix,
122    /// Text matrix (set by BT and text positioning operators)
123    pub text_matrix: Matrix,
124    /// Text line matrix (set by text line positioning operators)
125    pub text_line_matrix: Matrix,
126    /// Text state parameters
127    pub text_state: TextState,
128    /// Fill color — original PDF color components (1=Gray, 3=RGB, 4=CMYK)
129    pub fill_color: Vec<f64>,
130    /// Stroke color — original PDF color components
131    pub stroke_color: Vec<f64>,
132    /// Number of components in current non-stroking color space (1=Gray, 3=RGB, 4=CMYK)
133    pub fill_color_space_components: u8,
134    /// Number of components in current stroking color space
135    pub stroke_color_space_components: u8,
136}
137
138impl Default for GraphicsState {
139    fn default() -> Self {
140        Self {
141            ctm: Matrix::identity(),
142            text_matrix: Matrix::identity(),
143            text_line_matrix: Matrix::identity(),
144            text_state: TextState::default(),
145            fill_color: vec![0.0], // Black (default DeviceGray per PDF spec)
146            stroke_color: vec![0.0],
147            fill_color_space_components: 1, // Default: DeviceGray
148            stroke_color_space_components: 1,
149        }
150    }
151}
152
153impl GraphicsState {
154    /// Begin text object: reset text matrix and text line matrix.
155    pub fn begin_text(&mut self) {
156        self.text_matrix = Matrix::identity();
157        self.text_line_matrix = Matrix::identity();
158    }
159
160    /// Get the combined text rendering matrix: text_state.font_size × text_matrix × CTM.
161    pub fn text_rendering_matrix(&self) -> Matrix {
162        let font_matrix = Matrix {
163            a: self.text_state.font_size * (self.text_state.horizontal_scaling / 100.0),
164            b: 0.0,
165            c: 0.0,
166            d: self.text_state.font_size,
167            e: 0.0,
168            f: self.text_state.rise,
169        };
170        let tm_ctm = self.text_matrix.multiply(&self.ctm);
171        font_matrix.multiply(&tm_ctm)
172    }
173
174    /// Get the current text position in user space.
175    pub fn text_position(&self) -> (f64, f64) {
176        let trm = self.text_rendering_matrix();
177        (trm.e, trm.f)
178    }
179
180    /// Get the effective font size in user space.
181    pub fn effective_font_size(&self) -> f64 {
182        let trm = self.text_rendering_matrix();
183        trm.font_size_factor()
184    }
185
186    /// Apply Td (translate text position).
187    pub fn translate_text(&mut self, tx: f64, ty: f64) {
188        let translation = Matrix::translate(tx, ty);
189        self.text_line_matrix = translation.multiply(&self.text_line_matrix);
190        self.text_matrix = self.text_line_matrix;
191    }
192
193    /// Apply Tm (set text matrix directly).
194    pub fn set_text_matrix(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
195        self.text_matrix = Matrix { a, b, c, d, e, f };
196        self.text_line_matrix = self.text_matrix;
197    }
198
199    /// Apply T* (move to start of next line).
200    pub fn next_line(&mut self) {
201        self.translate_text(0.0, -self.text_state.leading);
202    }
203
204    /// Advance the text position after showing text (Tj displacement).
205    pub fn advance_text(&mut self, displacement: f64) {
206        let scaled = displacement * self.text_state.horizontal_scaling / 100.0;
207        self.text_matrix.e += scaled * self.text_matrix.a;
208        self.text_matrix.f += scaled * self.text_matrix.b;
209    }
210}
211
212/// Graphics state stack for q/Q save/restore operations.
213#[derive(Default)]
214pub struct GraphicsStateStack {
215    stack: Vec<GraphicsState>,
216    /// Current active graphics state.
217    pub current: GraphicsState,
218}
219
220impl GraphicsStateStack {
221    /// Save current state (q operator).
222    pub fn save(&mut self) {
223        self.stack.push(self.current.clone());
224    }
225
226    /// Restore saved state (Q operator).
227    pub fn restore(&mut self) {
228        if let Some(state) = self.stack.pop() {
229            self.current = state;
230        }
231    }
232
233    /// Apply CTM concatenation (cm operator).
234    pub fn concat_ctm(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
235        let new_matrix = Matrix { a, b, c, d, e, f };
236        self.current.ctm = new_matrix.multiply(&self.current.ctm);
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_matrix_identity() {
246        let m = Matrix::identity();
247        let (x, y) = m.transform_point(10.0, 20.0);
248        assert!((x - 10.0).abs() < 1e-10);
249        assert!((y - 20.0).abs() < 1e-10);
250    }
251
252    #[test]
253    fn test_matrix_translate() {
254        let m = Matrix::translate(100.0, 200.0);
255        let (x, y) = m.transform_point(10.0, 20.0);
256        assert!((x - 110.0).abs() < 1e-10);
257        assert!((y - 220.0).abs() < 1e-10);
258    }
259
260    #[test]
261    fn test_matrix_multiply() {
262        let a = Matrix::translate(10.0, 20.0);
263        let b = Matrix::translate(30.0, 40.0);
264        let c = a.multiply(&b);
265        let (x, y) = c.transform_point(0.0, 0.0);
266        assert!((x - 40.0).abs() < 1e-10);
267        assert!((y - 60.0).abs() < 1e-10);
268    }
269
270    #[test]
271    fn test_text_translate() {
272        let mut gs = GraphicsState::default();
273        gs.text_state.font_size = 12.0;
274        gs.begin_text();
275        gs.translate_text(100.0, 700.0);
276        let (x, y) = gs.text_position();
277        assert!((x - 100.0 * 12.0).abs() < 1e-6 || (x - 100.0).abs() < 1e-6);
278        // The text position should reflect the translation
279        assert!(y.abs() > 0.0 || y.abs() < 1e-6);
280    }
281
282    #[test]
283    fn test_graphics_state_stack() {
284        let mut stack = GraphicsStateStack::default();
285        stack.current.text_state.font_size = 12.0;
286        stack.save();
287        stack.current.text_state.font_size = 24.0;
288        assert!((stack.current.text_state.font_size - 24.0).abs() < 1e-10);
289        stack.restore();
290        assert!((stack.current.text_state.font_size - 12.0).abs() < 1e-10);
291    }
292}