1use pdfplumber_core::geometry::Ctm;
8use pdfplumber_core::painting::{Color, GraphicsState};
9
10#[derive(Debug, Clone, PartialEq)]
16pub struct InterpreterState {
17 ctm: Ctm,
19 graphics_state: GraphicsState,
21 stack: Vec<SavedState>,
23}
24
25#[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 pub fn new() -> Self {
41 Self {
42 ctm: Ctm::identity(),
43 graphics_state: GraphicsState::default(),
44 stack: Vec::new(),
45 }
46 }
47
48 pub fn ctm(&self) -> &Ctm {
50 &self.ctm
51 }
52
53 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 pub fn graphics_state(&self) -> &GraphicsState {
62 &self.graphics_state
63 }
64
65 pub fn graphics_state_mut(&mut self) -> &mut GraphicsState {
67 &mut self.graphics_state
68 }
69
70 pub fn stack_depth(&self) -> usize {
72 self.stack.len()
73 }
74
75 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 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 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 pub fn set_line_width(&mut self, width: f64) {
113 self.graphics_state.line_width = width;
114 }
115
116 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 pub fn set_stroking_gray(&mut self, gray: f32) {
127 self.graphics_state.stroke_color = Color::Gray(gray);
128 }
129
130 pub fn set_non_stroking_gray(&mut self, gray: f32) {
132 self.graphics_state.fill_color = Color::Gray(gray);
133 }
134
135 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 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 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 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 pub fn set_stroking_color(&mut self, components: &[f32]) {
163 self.graphics_state.stroke_color = color_from_components(components);
164 }
165
166 pub fn set_non_stroking_color(&mut self, components: &[f32]) {
174 self.graphics_state.fill_color = color_from_components(components);
175 }
176}
177
178fn 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 #[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 #[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 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 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 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 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 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 state.set_stroking_rgb(1.0, 0.0, 0.0);
309
310 state.save_state();
312
313 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 state.save_state();
322
323 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 state.restore_state();
332 assert_eq!(
333 state.graphics_state().stroke_color,
334 Color::Rgb(0.0, 0.0, 1.0)
335 );
336
337 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 #[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 state.concat_matrix(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
373 state.concat_matrix(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
375
376 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 state.set_stroking_gray(0.5);
582 assert_eq!(state.graphics_state().stroke_color, Color::Gray(0.5));
583
584 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 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 #[test]
602 fn test_full_state_save_restore_cycle() {
603 let mut state = InterpreterState::new();
604
605 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 state.save_state();
614
615 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 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 state.restore_state();
632
633 assert_eq!(state.ctm_array(), [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]);
635
636 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()); assert!(!state.restore_state()); }
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 fn assert_approx(actual: f64, expected: f64) {
669 assert!(
670 (actual - expected).abs() < 1e-10,
671 "expected {expected}, got {actual}"
672 );
673 }
674}