1use pdfplumber_core::geometry::Ctm;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum TextRenderMode {
14 #[default]
16 Fill = 0,
17 Stroke = 1,
19 FillStroke = 2,
21 Invisible = 3,
23 FillClip = 4,
25 StrokeClip = 5,
27 FillStrokeClip = 6,
29 Clip = 7,
31}
32
33impl TextRenderMode {
34 pub fn from_i64(value: i64) -> Option<Self> {
37 match value {
38 0 => Some(Self::Fill),
39 1 => Some(Self::Stroke),
40 2 => Some(Self::FillStroke),
41 3 => Some(Self::Invisible),
42 4 => Some(Self::FillClip),
43 5 => Some(Self::StrokeClip),
44 6 => Some(Self::FillStrokeClip),
45 7 => Some(Self::Clip),
46 _ => None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
57pub struct TextState {
58 pub char_spacing: f64,
60 pub word_spacing: f64,
62 pub h_scaling: f64,
65 pub leading: f64,
67 pub font_name: String,
69 pub font_size: f64,
71 pub render_mode: TextRenderMode,
73 pub rise: f64,
75 in_text_object: bool,
77 text_matrix: Ctm,
79 line_matrix: Ctm,
81}
82
83impl Default for TextState {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl TextState {
90 pub fn new() -> Self {
92 Self {
93 char_spacing: 0.0,
94 word_spacing: 0.0,
95 h_scaling: 100.0,
96 leading: 0.0,
97 font_name: String::new(),
98 font_size: 0.0,
99 render_mode: TextRenderMode::default(),
100 rise: 0.0,
101 in_text_object: false,
102 text_matrix: Ctm::identity(),
103 line_matrix: Ctm::identity(),
104 }
105 }
106
107 pub fn in_text_object(&self) -> bool {
109 self.in_text_object
110 }
111
112 pub fn text_matrix(&self) -> &Ctm {
114 &self.text_matrix
115 }
116
117 pub fn text_matrix_array(&self) -> [f64; 6] {
119 [
120 self.text_matrix.a,
121 self.text_matrix.b,
122 self.text_matrix.c,
123 self.text_matrix.d,
124 self.text_matrix.e,
125 self.text_matrix.f,
126 ]
127 }
128
129 pub fn line_matrix(&self) -> &Ctm {
131 &self.line_matrix
132 }
133
134 pub fn h_scaling_normalized(&self) -> f64 {
136 self.h_scaling / 100.0
137 }
138
139 pub fn begin_text(&mut self) {
146 self.text_matrix = Ctm::identity();
147 self.line_matrix = Ctm::identity();
148 self.in_text_object = true;
149 }
150
151 pub fn end_text(&mut self) {
158 self.in_text_object = false;
159 }
160
161 pub fn set_font(&mut self, font_name: String, font_size: f64) {
165 self.font_name = font_name;
166 self.font_size = font_size;
167 }
168
169 pub fn set_char_spacing(&mut self, spacing: f64) {
173 self.char_spacing = spacing;
174 }
175
176 pub fn set_word_spacing(&mut self, spacing: f64) {
180 self.word_spacing = spacing;
181 }
182
183 pub fn set_h_scaling(&mut self, scale: f64) {
187 self.h_scaling = scale;
188 }
189
190 pub fn set_leading(&mut self, leading: f64) {
194 self.leading = leading;
195 }
196
197 pub fn set_render_mode(&mut self, mode: TextRenderMode) {
201 self.render_mode = mode;
202 }
203
204 pub fn set_rise(&mut self, rise: f64) {
208 self.rise = rise;
209 }
210
211 pub fn set_text_matrix(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
218 let m = Ctm::new(a, b, c, d, e, f);
219 self.text_matrix = m;
220 self.line_matrix = m;
221 }
222
223 pub fn move_text_position(&mut self, tx: f64, ty: f64) {
230 let translation = Ctm::new(1.0, 0.0, 0.0, 1.0, tx, ty);
231 self.line_matrix = translation.concat(&self.line_matrix);
232 self.text_matrix = self.line_matrix;
233 }
234
235 pub fn move_text_position_and_set_leading(&mut self, tx: f64, ty: f64) {
242 self.leading = -ty;
243 self.move_text_position(tx, ty);
244 }
245
246 pub fn move_to_next_line(&mut self) {
252 let leading = self.leading;
253 self.move_text_position(0.0, -leading);
254 }
255
256 pub fn advance_text_position(&mut self, tx: f64) {
264 let translation = Ctm::new(1.0, 0.0, 0.0, 1.0, tx, 0.0);
266 self.text_matrix = translation.concat(&self.text_matrix);
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn assert_approx(actual: f64, expected: f64) {
275 assert!(
276 (actual - expected).abs() < 1e-10,
277 "expected {expected}, got {actual}"
278 );
279 }
280
281 fn assert_matrix_approx(ctm: &Ctm, expected: [f64; 6]) {
282 assert_approx(ctm.a, expected[0]);
283 assert_approx(ctm.b, expected[1]);
284 assert_approx(ctm.c, expected[2]);
285 assert_approx(ctm.d, expected[3]);
286 assert_approx(ctm.e, expected[4]);
287 assert_approx(ctm.f, expected[5]);
288 }
289
290 #[test]
293 fn test_render_mode_from_i64_valid() {
294 assert_eq!(TextRenderMode::from_i64(0), Some(TextRenderMode::Fill));
295 assert_eq!(TextRenderMode::from_i64(1), Some(TextRenderMode::Stroke));
296 assert_eq!(
297 TextRenderMode::from_i64(2),
298 Some(TextRenderMode::FillStroke)
299 );
300 assert_eq!(TextRenderMode::from_i64(3), Some(TextRenderMode::Invisible));
301 assert_eq!(TextRenderMode::from_i64(4), Some(TextRenderMode::FillClip));
302 assert_eq!(
303 TextRenderMode::from_i64(5),
304 Some(TextRenderMode::StrokeClip)
305 );
306 assert_eq!(
307 TextRenderMode::from_i64(6),
308 Some(TextRenderMode::FillStrokeClip)
309 );
310 assert_eq!(TextRenderMode::from_i64(7), Some(TextRenderMode::Clip));
311 }
312
313 #[test]
314 fn test_render_mode_from_i64_invalid() {
315 assert_eq!(TextRenderMode::from_i64(-1), None);
316 assert_eq!(TextRenderMode::from_i64(8), None);
317 assert_eq!(TextRenderMode::from_i64(100), None);
318 }
319
320 #[test]
321 fn test_render_mode_default_is_fill() {
322 assert_eq!(TextRenderMode::default(), TextRenderMode::Fill);
323 }
324
325 #[test]
328 fn test_new_defaults() {
329 let ts = TextState::new();
330 assert_eq!(ts.char_spacing, 0.0);
331 assert_eq!(ts.word_spacing, 0.0);
332 assert_eq!(ts.h_scaling, 100.0);
333 assert_eq!(ts.leading, 0.0);
334 assert_eq!(ts.font_name, "");
335 assert_eq!(ts.font_size, 0.0);
336 assert_eq!(ts.render_mode, TextRenderMode::Fill);
337 assert_eq!(ts.rise, 0.0);
338 assert!(!ts.in_text_object());
339 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
340 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
341 }
342
343 #[test]
344 fn test_default_equals_new() {
345 assert_eq!(TextState::default(), TextState::new());
346 }
347
348 #[test]
349 fn test_h_scaling_normalized() {
350 let mut ts = TextState::new();
351 assert_approx(ts.h_scaling_normalized(), 1.0);
352
353 ts.set_h_scaling(50.0);
354 assert_approx(ts.h_scaling_normalized(), 0.5);
355
356 ts.set_h_scaling(200.0);
357 assert_approx(ts.h_scaling_normalized(), 2.0);
358 }
359
360 #[test]
363 fn test_begin_text_sets_in_text_object() {
364 let mut ts = TextState::new();
365 assert!(!ts.in_text_object());
366
367 ts.begin_text();
368 assert!(ts.in_text_object());
369 }
370
371 #[test]
372 fn test_end_text_clears_in_text_object() {
373 let mut ts = TextState::new();
374 ts.begin_text();
375 assert!(ts.in_text_object());
376
377 ts.end_text();
378 assert!(!ts.in_text_object());
379 }
380
381 #[test]
382 fn test_begin_text_resets_matrices_to_identity() {
383 let mut ts = TextState::new();
384 ts.begin_text();
385
386 ts.move_text_position(100.0, 200.0);
388 assert_ne!(*ts.text_matrix(), Ctm::identity());
389
390 ts.begin_text();
392 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
393 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
394 }
395
396 #[test]
399 fn test_set_font() {
400 let mut ts = TextState::new();
401 ts.set_font("Helvetica".to_string(), 12.0);
402
403 assert_eq!(ts.font_name, "Helvetica");
404 assert_eq!(ts.font_size, 12.0);
405 }
406
407 #[test]
408 fn test_set_font_changes_both_name_and_size() {
409 let mut ts = TextState::new();
410 ts.set_font("Helvetica".to_string(), 12.0);
411 ts.set_font("Times-Roman".to_string(), 14.0);
412
413 assert_eq!(ts.font_name, "Times-Roman");
414 assert_eq!(ts.font_size, 14.0);
415 }
416
417 #[test]
420 fn test_set_char_spacing() {
421 let mut ts = TextState::new();
422 ts.set_char_spacing(0.5);
423 assert_eq!(ts.char_spacing, 0.5);
424 }
425
426 #[test]
429 fn test_set_word_spacing() {
430 let mut ts = TextState::new();
431 ts.set_word_spacing(2.0);
432 assert_eq!(ts.word_spacing, 2.0);
433 }
434
435 #[test]
438 fn test_set_h_scaling() {
439 let mut ts = TextState::new();
440 ts.set_h_scaling(150.0);
441 assert_eq!(ts.h_scaling, 150.0);
442 }
443
444 #[test]
447 fn test_set_leading() {
448 let mut ts = TextState::new();
449 ts.set_leading(14.0);
450 assert_eq!(ts.leading, 14.0);
451 }
452
453 #[test]
456 fn test_set_render_mode() {
457 let mut ts = TextState::new();
458 ts.set_render_mode(TextRenderMode::Stroke);
459 assert_eq!(ts.render_mode, TextRenderMode::Stroke);
460 }
461
462 #[test]
465 fn test_set_rise() {
466 let mut ts = TextState::new();
467 ts.set_rise(5.0);
468 assert_eq!(ts.rise, 5.0);
469 }
470
471 #[test]
472 fn test_set_rise_negative() {
473 let mut ts = TextState::new();
474 ts.set_rise(-3.0);
475 assert_eq!(ts.rise, -3.0);
476 }
477
478 #[test]
481 fn test_set_text_matrix() {
482 let mut ts = TextState::new();
483 ts.begin_text();
484 ts.set_text_matrix(12.0, 0.0, 0.0, 12.0, 72.0, 720.0);
485
486 assert_matrix_approx(ts.text_matrix(), [12.0, 0.0, 0.0, 12.0, 72.0, 720.0]);
487 assert_matrix_approx(ts.line_matrix(), [12.0, 0.0, 0.0, 12.0, 72.0, 720.0]);
489 }
490
491 #[test]
492 fn test_set_text_matrix_replaces_not_concatenates() {
493 let mut ts = TextState::new();
494 ts.begin_text();
495
496 ts.set_text_matrix(2.0, 0.0, 0.0, 2.0, 100.0, 200.0);
498
499 ts.set_text_matrix(1.0, 0.0, 0.0, 1.0, 50.0, 60.0);
501
502 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 50.0, 60.0]);
503 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 50.0, 60.0]);
504 }
505
506 #[test]
507 fn test_text_matrix_array() {
508 let mut ts = TextState::new();
509 ts.begin_text();
510 ts.set_text_matrix(12.0, 0.0, 0.0, 12.0, 72.0, 720.0);
511
512 assert_eq!(ts.text_matrix_array(), [12.0, 0.0, 0.0, 12.0, 72.0, 720.0]);
513 }
514
515 #[test]
518 fn test_move_text_position_simple() {
519 let mut ts = TextState::new();
520 ts.begin_text();
521
522 ts.move_text_position(100.0, 700.0);
523
524 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 100.0, 700.0]);
526 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 100.0, 700.0]);
528 }
529
530 #[test]
531 fn test_move_text_position_cumulative() {
532 let mut ts = TextState::new();
533 ts.begin_text();
534
535 ts.move_text_position(100.0, 700.0);
536 ts.move_text_position(0.0, -14.0);
537
538 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 100.0, 686.0]);
540 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 100.0, 686.0]);
541 }
542
543 #[test]
544 fn test_move_text_position_after_tm() {
545 let mut ts = TextState::new();
546 ts.begin_text();
547
548 ts.set_text_matrix(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
550
551 ts.move_text_position(50.0, 100.0);
553
554 assert_matrix_approx(ts.text_matrix(), [2.0, 0.0, 0.0, 2.0, 100.0, 200.0]);
557 assert_matrix_approx(ts.line_matrix(), [2.0, 0.0, 0.0, 2.0, 100.0, 200.0]);
558 }
559
560 #[test]
563 fn test_move_text_position_and_set_leading() {
564 let mut ts = TextState::new();
565 ts.begin_text();
566
567 ts.move_text_position_and_set_leading(0.0, -14.0);
569
570 assert_eq!(ts.leading, 14.0); assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, -14.0]);
572 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, -14.0]);
573 }
574
575 #[test]
576 fn test_td_sets_leading_positive_ty() {
577 let mut ts = TextState::new();
578 ts.begin_text();
579
580 ts.move_text_position_and_set_leading(5.0, 10.0);
582
583 assert_eq!(ts.leading, -10.0);
584 }
585
586 #[test]
589 fn test_move_to_next_line() {
590 let mut ts = TextState::new();
591 ts.begin_text();
592 ts.set_leading(14.0);
593
594 ts.move_text_position(72.0, 700.0);
596
597 ts.move_to_next_line();
599
600 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 686.0]);
601 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 686.0]);
602 }
603
604 #[test]
605 fn test_move_to_next_line_multiple_times() {
606 let mut ts = TextState::new();
607 ts.begin_text();
608 ts.set_leading(12.0);
609
610 ts.move_text_position(72.0, 700.0);
611 ts.move_to_next_line();
612 ts.move_to_next_line();
613 ts.move_to_next_line();
614
615 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 664.0]);
617 }
618
619 #[test]
620 fn test_move_to_next_line_zero_leading() {
621 let mut ts = TextState::new();
622 ts.begin_text();
623 ts.move_text_position(72.0, 700.0);
625 ts.move_to_next_line();
626
627 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 700.0]);
629 }
630
631 #[test]
634 fn test_text_state_params_persist_across_bt_et() {
635 let mut ts = TextState::new();
636
637 ts.set_font("Helvetica".to_string(), 12.0);
639 ts.set_char_spacing(0.5);
640 ts.set_word_spacing(1.0);
641 ts.set_h_scaling(110.0);
642 ts.set_leading(14.0);
643 ts.set_render_mode(TextRenderMode::Stroke);
644 ts.set_rise(3.0);
645
646 ts.begin_text();
648 ts.end_text();
649
650 assert_eq!(ts.font_name, "Helvetica");
652 assert_eq!(ts.font_size, 12.0);
653 assert_eq!(ts.char_spacing, 0.5);
654 assert_eq!(ts.word_spacing, 1.0);
655 assert_eq!(ts.h_scaling, 110.0);
656 assert_eq!(ts.leading, 14.0);
657 assert_eq!(ts.render_mode, TextRenderMode::Stroke);
658 assert_eq!(ts.rise, 3.0);
659 }
660
661 #[test]
662 fn test_bt_resets_matrices_not_params() {
663 let mut ts = TextState::new();
664 ts.set_font("Helvetica".to_string(), 12.0);
665 ts.set_leading(14.0);
666
667 ts.begin_text();
668 ts.set_text_matrix(12.0, 0.0, 0.0, 12.0, 72.0, 720.0);
669 ts.end_text();
670
671 ts.begin_text();
673 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
674 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
675 assert_eq!(ts.font_name, "Helvetica");
676 assert_eq!(ts.font_size, 12.0);
677 assert_eq!(ts.leading, 14.0);
678 }
679
680 #[test]
683 fn test_advance_text_position() {
684 let mut ts = TextState::new();
685 ts.begin_text();
686 ts.move_text_position(72.0, 700.0);
687
688 ts.advance_text_position(10.0);
690
691 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 82.0, 700.0]);
693 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 700.0]);
694 }
695
696 #[test]
697 fn test_advance_text_position_does_not_change_line_matrix() {
698 let mut ts = TextState::new();
699 ts.begin_text();
700 ts.move_text_position(72.0, 700.0);
701
702 let line_matrix_before = *ts.line_matrix();
703 ts.advance_text_position(50.0);
704
705 assert_eq!(*ts.line_matrix(), line_matrix_before);
706 }
707
708 #[test]
709 fn test_advance_text_position_cumulative() {
710 let mut ts = TextState::new();
711 ts.begin_text();
712 ts.move_text_position(72.0, 700.0);
713
714 ts.advance_text_position(10.0);
715 ts.advance_text_position(5.0);
716 ts.advance_text_position(8.0);
717
718 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 95.0, 700.0]);
719 }
720
721 #[test]
722 fn test_advance_text_position_with_scaled_matrix() {
723 let mut ts = TextState::new();
724 ts.begin_text();
725 ts.set_text_matrix(12.0, 0.0, 0.0, 12.0, 72.0, 700.0);
727
728 ts.advance_text_position(10.0);
735
736 assert_matrix_approx(ts.text_matrix(), [12.0, 0.0, 0.0, 12.0, 192.0, 700.0]);
737 }
738
739 #[test]
742 fn test_realistic_text_rendering_sequence() {
743 let mut ts = TextState::new();
744
745 ts.set_font("Helvetica".to_string(), 12.0);
747 ts.set_leading(14.0);
748
749 ts.begin_text();
751 assert!(ts.in_text_object());
752
753 ts.move_text_position(72.0, 700.0);
755 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 700.0]);
756
757 ts.advance_text_position(30.0); assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 102.0, 700.0]);
760
761 ts.move_to_next_line();
763 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 686.0]);
764 assert_matrix_approx(ts.line_matrix(), [1.0, 0.0, 0.0, 1.0, 72.0, 686.0]);
766
767 ts.advance_text_position(32.0);
769 assert_matrix_approx(ts.text_matrix(), [1.0, 0.0, 0.0, 1.0, 104.0, 686.0]);
770
771 ts.end_text();
773 assert!(!ts.in_text_object());
774 }
775
776 #[test]
777 fn test_td_td_sequence_with_tm() {
778 let mut ts = TextState::new();
779 ts.begin_text();
780
781 ts.set_text_matrix(10.0, 0.0, 0.0, 10.0, 100.0, 500.0);
783
784 ts.move_text_position(5.0, -12.0);
786 assert_matrix_approx(ts.text_matrix(), [10.0, 0.0, 0.0, 10.0, 150.0, 380.0]);
790 }
791}