1use crate::error::Result;
2use crate::graphics::Color;
3use crate::page::Margins;
4use crate::text::metrics::{measure_text_with, FontMetricsStore};
5use crate::text::{split_into_words, Font};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum TextAlign {
10 Left,
11 Right,
12 Center,
13 Justified,
14}
15
16pub struct TextFlowContext {
17 operations: Vec<crate::graphics::ops::Op>,
18 current_font: Font,
19 font_size: f64,
20 line_height: f64,
21 cursor_x: f64,
22 cursor_y: f64,
23 alignment: TextAlign,
24 page_width: f64,
25 #[allow(dead_code)]
26 page_height: f64,
27 margins: Margins,
28 fill_color: Option<Color>,
34 character_spacing: Option<f64>,
39 word_spacing: Option<f64>,
40 horizontal_scaling: Option<f64>,
41 leading: Option<f64>,
42 text_rise: Option<f64>,
43 rendering_mode: Option<u8>,
44 stroke_color: Option<Color>,
45 used_characters_by_font: HashMap<String, HashSet<char>>,
50 pub(crate) font_metrics_store: Option<FontMetricsStore>,
54}
55
56impl TextFlowContext {
57 pub fn new(page_width: f64, page_height: f64, margins: Margins) -> Self {
58 Self {
59 operations: Vec::new(),
60 current_font: Font::Helvetica,
61 font_size: 12.0,
62 line_height: 1.2,
63 cursor_x: margins.left,
64 cursor_y: page_height - margins.top,
65 alignment: TextAlign::Left,
66 page_width,
67 page_height,
68 margins,
69 fill_color: None,
70 character_spacing: None,
71 word_spacing: None,
72 horizontal_scaling: None,
73 leading: None,
74 text_rise: None,
75 rendering_mode: None,
76 stroke_color: None,
77 used_characters_by_font: HashMap::new(),
78 font_metrics_store: None,
79 }
80 }
81
82 pub(crate) fn with_metrics_store(
88 page_width: f64,
89 page_height: f64,
90 margins: Margins,
91 store: Option<FontMetricsStore>,
92 ) -> Self {
93 let mut ctx = Self::new(page_width, page_height, margins);
94 ctx.font_metrics_store = store;
95 ctx
96 }
97
98 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
103 &self.used_characters_by_font
104 }
105
106 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
107 self.current_font = font;
108 self.font_size = size;
109 self
110 }
111
112 pub fn set_line_height(&mut self, multiplier: f64) -> &mut Self {
113 self.line_height = multiplier;
114 self
115 }
116
117 pub fn set_alignment(&mut self, alignment: TextAlign) -> &mut Self {
118 self.alignment = alignment;
119 self
120 }
121
122 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
128 self.fill_color = Some(color);
129 self
130 }
131
132 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
136 self.character_spacing = Some(spacing);
137 self
138 }
139
140 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
141 self.word_spacing = Some(spacing);
142 self
143 }
144
145 pub fn set_horizontal_scaling(&mut self, scale: f64) -> &mut Self {
150 self.horizontal_scaling = Some(scale);
151 self
152 }
153
154 pub fn set_leading(&mut self, leading: f64) -> &mut Self {
155 self.leading = Some(leading);
156 self
157 }
158
159 pub fn set_text_rise(&mut self, rise: f64) -> &mut Self {
160 self.text_rise = Some(rise);
161 self
162 }
163
164 pub fn set_rendering_mode(&mut self, mode: u8) -> &mut Self {
169 self.rendering_mode = Some(mode);
170 self
171 }
172
173 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
174 self.stroke_color = Some(color);
175 self
176 }
177
178 pub fn current_font(&self) -> &Font {
180 &self.current_font
181 }
182
183 pub fn font_size(&self) -> f64 {
185 self.font_size
186 }
187
188 pub fn fill_color(&self) -> Option<Color> {
190 self.fill_color
191 }
192
193 pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
194 self.cursor_x = x;
195 self.cursor_y = y;
196 self
197 }
198
199 pub fn content_width(&self) -> f64 {
200 self.page_width - self.margins.left - self.margins.right
201 }
202
203 pub fn available_width(&self) -> f64 {
209 (self.page_width - self.margins.right - self.cursor_x).max(0.0)
210 }
211
212 pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
213 let start_x = self.cursor_x;
214 let available_width = self.available_width();
215
216 let words = split_into_words(text);
218 let mut lines: Vec<Vec<&str>> = Vec::new();
219 let mut current_line: Vec<&str> = Vec::new();
220 let mut current_width = 0.0;
221
222 for word in words {
224 let word_width = measure_text_with(
225 word,
226 &self.current_font,
227 self.font_size,
228 self.font_metrics_store.as_ref(),
229 );
230
231 if !current_line.is_empty() && current_width + word_width > available_width {
233 lines.push(current_line);
234 current_line = vec![word];
235 current_width = word_width;
236 } else {
237 current_line.push(word);
238 current_width += word_width;
239 }
240 }
241
242 if !current_line.is_empty() {
243 lines.push(current_line);
244 }
245
246 for (i, line) in lines.iter().enumerate() {
248 let line_text = line.join("");
249 let line_width = measure_text_with(
250 &line_text,
251 &self.current_font,
252 self.font_size,
253 self.font_metrics_store.as_ref(),
254 );
255
256 let x = match self.alignment {
261 TextAlign::Left => start_x,
262 TextAlign::Right => self.page_width - self.margins.right - line_width,
263 TextAlign::Center => start_x + (available_width - line_width) / 2.0,
264 TextAlign::Justified => start_x,
265 };
266
267 use crate::graphics::ops::Op;
268
269 self.operations.push(Op::BeginText);
270
271 self.operations.push(Op::SetFont {
273 name: self.current_font.pdf_name(),
274 size: self.font_size,
275 });
276
277 if let Some(spacing) = self.character_spacing {
285 self.operations.push(Op::SetCharSpacing(spacing));
286 }
287 if let Some(spacing) = self.word_spacing {
288 self.operations.push(Op::SetWordSpacing(spacing));
289 }
290 if let Some(scale) = self.horizontal_scaling {
291 self.operations
295 .push(Op::SetHorizontalScaling(scale * 100.0));
296 }
297 if let Some(leading) = self.leading {
298 self.operations.push(Op::SetLeading(leading));
299 }
300 if let Some(rise) = self.text_rise {
301 self.operations.push(Op::SetTextRise(rise));
302 }
303 if let Some(mode) = self.rendering_mode {
304 self.operations.push(Op::SetRenderingMode(mode));
305 }
306
307 if let Some(color) = self.fill_color {
314 self.operations.push(Op::SetFillColor(color));
315 }
316 if let Some(color) = self.stroke_color {
317 self.operations.push(Op::SetStrokeColor(color));
318 }
319
320 self.operations.push(Op::SetTextPosition {
321 x,
322 y: self.cursor_y,
323 });
324
325 if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
328 let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
329 if spaces_count > 0 {
330 let extra_space = available_width - line_width;
331 let space_adjustment = extra_space / spaces_count as f64;
332 self.operations.push(Op::SetWordSpacing(space_adjustment));
333 }
334 }
335
336 let mut buf = Vec::with_capacity(line_text.len());
338 for ch in line_text.chars() {
339 match ch {
340 '(' => buf.extend_from_slice(b"\\("),
341 ')' => buf.extend_from_slice(b"\\)"),
342 '\\' => buf.extend_from_slice(b"\\\\"),
343 '\n' => buf.extend_from_slice(b"\\n"),
344 '\r' => buf.extend_from_slice(b"\\r"),
345 '\t' => buf.extend_from_slice(b"\\t"),
346 _ => {
347 let mut tmp = [0u8; 4];
348 buf.extend_from_slice(ch.encode_utf8(&mut tmp).as_bytes());
349 }
350 }
351 }
352 self.operations.push(Op::ShowText(buf));
353
354 self.used_characters_by_font
357 .entry(self.current_font.pdf_name())
358 .or_default()
359 .extend(line_text.chars());
360
361 if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
364 self.operations.push(Op::SetWordSpacing(0.0));
365 }
366
367 self.operations.push(Op::EndText);
368
369 self.cursor_y -= self.font_size * self.line_height;
371 }
372
373 Ok(self)
374 }
375
376 pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
377 self.write_wrapped(text)?;
378 self.cursor_y -= self.font_size * self.line_height * 0.5;
380 Ok(self)
381 }
382
383 pub fn newline(&mut self) -> &mut Self {
384 self.cursor_y -= self.font_size * self.line_height;
385 self.cursor_x = self.margins.left;
386 self
387 }
388
389 pub fn cursor_position(&self) -> (f64, f64) {
390 (self.cursor_x, self.cursor_y)
391 }
392
393 pub fn generate_operations(&self) -> Vec<u8> {
394 let mut buf = Vec::new();
395 crate::graphics::ops::serialize_ops(&mut buf, &self.operations);
396 buf
397 }
398
399 pub fn alignment(&self) -> TextAlign {
401 self.alignment
402 }
403
404 pub fn page_dimensions(&self) -> (f64, f64) {
406 (self.page_width, self.page_height)
407 }
408
409 pub fn margins(&self) -> &Margins {
411 &self.margins
412 }
413
414 pub fn line_height(&self) -> f64 {
416 self.line_height
417 }
418
419 pub fn operations(&self) -> String {
426 crate::graphics::ops::ops_to_string(&self.operations)
427 }
428
429 pub fn clear(&mut self) {
431 self.operations.clear();
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::page::Margins;
439
440 fn create_test_margins() -> Margins {
441 Margins {
442 left: 50.0,
443 right: 50.0,
444 top: 50.0,
445 bottom: 50.0,
446 }
447 }
448
449 #[test]
450 fn test_text_flow_context_new() {
451 let margins = create_test_margins();
452 let context = TextFlowContext::new(400.0, 600.0, margins);
453
454 assert_eq!(context.current_font, Font::Helvetica);
455 assert_eq!(context.font_size, 12.0);
456 assert_eq!(context.line_height, 1.2);
457 assert_eq!(context.alignment, TextAlign::Left);
458 assert_eq!(context.page_width, 400.0);
459 assert_eq!(context.page_height, 600.0);
460 assert_eq!(context.cursor_x, 50.0); assert_eq!(context.cursor_y, 550.0); }
463
464 #[test]
465 fn test_set_font() {
466 let margins = create_test_margins();
467 let mut context = TextFlowContext::new(400.0, 600.0, margins);
468
469 context.set_font(Font::TimesBold, 16.0);
470 assert_eq!(context.current_font, Font::TimesBold);
471 assert_eq!(context.font_size, 16.0);
472 }
473
474 #[test]
475 fn test_set_line_height() {
476 let margins = create_test_margins();
477 let mut context = TextFlowContext::new(400.0, 600.0, margins);
478
479 context.set_line_height(1.5);
480 assert_eq!(context.line_height(), 1.5);
481 }
482
483 #[test]
484 fn test_set_alignment() {
485 let margins = create_test_margins();
486 let mut context = TextFlowContext::new(400.0, 600.0, margins);
487
488 context.set_alignment(TextAlign::Center);
489 assert_eq!(context.alignment(), TextAlign::Center);
490 }
491
492 #[test]
493 fn test_at_position() {
494 let margins = create_test_margins();
495 let mut context = TextFlowContext::new(400.0, 600.0, margins);
496
497 context.at(100.0, 200.0);
498 let (x, y) = context.cursor_position();
499 assert_eq!(x, 100.0);
500 assert_eq!(y, 200.0);
501 }
502
503 #[test]
504 fn test_content_width() {
505 let margins = create_test_margins();
506 let context = TextFlowContext::new(400.0, 600.0, margins);
507
508 let content_width = context.content_width();
509 assert_eq!(content_width, 300.0); }
511
512 #[test]
513 fn test_text_align_variants() {
514 assert_eq!(TextAlign::Left, TextAlign::Left);
515 assert_eq!(TextAlign::Right, TextAlign::Right);
516 assert_eq!(TextAlign::Center, TextAlign::Center);
517 assert_eq!(TextAlign::Justified, TextAlign::Justified);
518
519 assert_ne!(TextAlign::Left, TextAlign::Right);
520 }
521
522 #[test]
523 fn test_write_wrapped_simple() {
524 let margins = create_test_margins();
525 let mut context = TextFlowContext::new(400.0, 600.0, margins);
526
527 context.write_wrapped("Hello World").unwrap();
528
529 let ops = context.operations();
530 assert!(ops.contains("BT\n"));
531 assert!(ops.contains("ET\n"));
532 assert!(ops.contains("/Helvetica 12 Tf"));
533 assert!(ops.contains("(Hello World) Tj"));
534 }
535
536 #[test]
537 fn test_write_paragraph() {
538 let margins = create_test_margins();
539 let mut context = TextFlowContext::new(400.0, 600.0, margins);
540
541 let initial_y = context.cursor_y;
542 context.write_paragraph("Test paragraph").unwrap();
543
544 assert!(context.cursor_y < initial_y);
546 }
547
548 #[test]
549 fn test_newline() {
550 let margins = create_test_margins();
551 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
552
553 let initial_y = context.cursor_y;
554 context.newline();
555
556 assert_eq!(context.cursor_x, margins.left);
557 assert!(context.cursor_y < initial_y);
558 assert_eq!(
559 context.cursor_y,
560 initial_y - context.font_size * context.line_height
561 );
562 }
563
564 #[test]
565 fn test_cursor_position() {
566 let margins = create_test_margins();
567 let mut context = TextFlowContext::new(400.0, 600.0, margins);
568
569 context.at(75.0, 125.0);
570 let (x, y) = context.cursor_position();
571 assert_eq!(x, 75.0);
572 assert_eq!(y, 125.0);
573 }
574
575 #[test]
576 fn test_generate_operations() {
577 let margins = create_test_margins();
578 let mut context = TextFlowContext::new(400.0, 600.0, margins);
579
580 context.write_wrapped("Test").unwrap();
581 let ops_bytes = context.generate_operations();
582 let ops_string = String::from_utf8(ops_bytes).unwrap();
583
584 assert_eq!(ops_string, context.operations());
585 }
586
587 #[test]
588 fn test_clear_operations() {
589 let margins = create_test_margins();
590 let mut context = TextFlowContext::new(400.0, 600.0, margins);
591
592 context.write_wrapped("Test").unwrap();
593 assert!(!context.operations().is_empty());
594
595 context.clear();
596 assert!(context.operations().is_empty());
597 }
598
599 #[test]
600 fn test_page_dimensions() {
601 let margins = create_test_margins();
602 let context = TextFlowContext::new(400.0, 600.0, margins);
603
604 let (width, height) = context.page_dimensions();
605 assert_eq!(width, 400.0);
606 assert_eq!(height, 600.0);
607 }
608
609 #[test]
610 fn test_margins_access() {
611 let margins = create_test_margins();
612 let context = TextFlowContext::new(400.0, 600.0, margins);
613
614 let ctx_margins = context.margins();
615 assert_eq!(ctx_margins.left, 50.0);
616 assert_eq!(ctx_margins.right, 50.0);
617 assert_eq!(ctx_margins.top, 50.0);
618 assert_eq!(ctx_margins.bottom, 50.0);
619 }
620
621 #[test]
622 fn test_method_chaining() {
623 let margins = create_test_margins();
624 let mut context = TextFlowContext::new(400.0, 600.0, margins);
625
626 context
627 .set_font(Font::Courier, 10.0)
628 .set_line_height(1.5)
629 .set_alignment(TextAlign::Center)
630 .at(100.0, 200.0);
631
632 assert_eq!(context.current_font, Font::Courier);
633 assert_eq!(context.font_size, 10.0);
634 assert_eq!(context.line_height(), 1.5);
635 assert_eq!(context.alignment(), TextAlign::Center);
636 let (x, y) = context.cursor_position();
637 assert_eq!(x, 100.0);
638 assert_eq!(y, 200.0);
639 }
640
641 #[test]
642 fn test_text_align_debug() {
643 let align = TextAlign::Center;
644 let debug_str = format!("{align:?}");
645 assert_eq!(debug_str, "Center");
646 }
647
648 #[test]
649 fn test_text_align_clone() {
650 let align1 = TextAlign::Justified;
651 let align2 = align1;
652 assert_eq!(align1, align2);
653 }
654
655 #[test]
656 fn test_text_align_copy() {
657 let align1 = TextAlign::Right;
658 let align2 = align1; assert_eq!(align1, align2);
660
661 assert_eq!(align1, TextAlign::Right);
663 assert_eq!(align2, TextAlign::Right);
664 }
665
666 #[test]
667 fn test_write_wrapped_with_alignment_right() {
668 let margins = create_test_margins();
669 let mut context = TextFlowContext::new(400.0, 600.0, margins);
670
671 context.set_alignment(TextAlign::Right);
672 context.write_wrapped("Right aligned text").unwrap();
673
674 let ops = context.operations();
675 assert!(ops.contains("BT\n"));
676 assert!(ops.contains("ET\n"));
677 assert!(ops.contains("Td"));
679 }
680
681 #[test]
682 fn test_write_wrapped_with_alignment_center() {
683 let margins = create_test_margins();
684 let mut context = TextFlowContext::new(400.0, 600.0, margins);
685
686 context.set_alignment(TextAlign::Center);
687 context.write_wrapped("Centered text").unwrap();
688
689 let ops = context.operations();
690 assert!(ops.contains("BT\n"));
691 assert!(ops.contains("(Centered text) Tj"));
692 }
693
694 #[test]
695 fn test_write_wrapped_with_alignment_justified() {
696 let margins = create_test_margins();
697 let mut context = TextFlowContext::new(400.0, 600.0, margins);
698
699 context.set_alignment(TextAlign::Justified);
700 context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
702
703 let ops = context.operations();
704 assert!(ops.contains("BT\n"));
705 assert!(ops.contains("Tw") || ops.contains("0 Tw"));
707 }
708
709 #[test]
710 fn test_write_wrapped_empty_text() {
711 let margins = create_test_margins();
712 let mut context = TextFlowContext::new(400.0, 600.0, margins);
713
714 context.write_wrapped("").unwrap();
715
716 assert!(context.operations().is_empty());
718 }
719
720 #[test]
721 fn test_write_wrapped_whitespace_only() {
722 let margins = create_test_margins();
723 let mut context = TextFlowContext::new(400.0, 600.0, margins);
724
725 context.write_wrapped(" ").unwrap();
726
727 let ops = context.operations();
728 assert!(ops.contains("BT\n") || ops.is_empty());
730 }
731
732 #[test]
733 fn test_write_wrapped_special_characters() {
734 let margins = create_test_margins();
735 let mut context = TextFlowContext::new(400.0, 600.0, margins);
736
737 context
738 .write_wrapped("Text with (parentheses) and \\backslash\\")
739 .unwrap();
740
741 let ops = context.operations();
742 assert!(ops.contains("\\(parentheses\\)"));
744 assert!(ops.contains("\\\\backslash\\\\"));
745 }
746
747 #[test]
748 fn test_write_wrapped_newlines_tabs() {
749 let margins = create_test_margins();
750 let mut context = TextFlowContext::new(400.0, 600.0, margins);
751
752 context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
753
754 let ops = context.operations();
755 assert!(ops.contains("\\n") || ops.contains("\\t"));
757 }
758
759 #[test]
760 fn test_write_wrapped_very_long_word() {
761 let margins = create_test_margins();
762 let mut context = TextFlowContext::new(200.0, 600.0, margins); let long_word = "a".repeat(100);
765 context.write_wrapped(&long_word).unwrap();
766
767 let ops = context.operations();
768 assert!(ops.contains("BT\n"));
769 assert!(ops.contains(&long_word));
770 }
771
772 #[test]
773 fn test_write_wrapped_cursor_movement() {
774 let margins = create_test_margins();
775 let mut context = TextFlowContext::new(400.0, 600.0, margins);
776
777 let initial_y = context.cursor_y;
778
779 context.write_wrapped("Line 1").unwrap();
780 let y_after_line1 = context.cursor_y;
781
782 context.write_wrapped("Line 2").unwrap();
783 let y_after_line2 = context.cursor_y;
784
785 assert!(y_after_line1 < initial_y);
787 assert!(y_after_line2 < y_after_line1);
788 }
789
790 #[test]
791 fn test_write_paragraph_spacing() {
792 let margins = create_test_margins();
793 let mut context = TextFlowContext::new(400.0, 600.0, margins);
794
795 let initial_y = context.cursor_y;
796 context.write_paragraph("Paragraph 1").unwrap();
797 let y_after_p1 = context.cursor_y;
798
799 context.write_paragraph("Paragraph 2").unwrap();
800 let y_after_p2 = context.cursor_y;
801
802 let spacing1 = initial_y - y_after_p1;
804 let spacing2 = y_after_p1 - y_after_p2;
805
806 assert!(spacing1 > 0.0);
807 assert!(spacing2 > 0.0);
808 }
809
810 #[test]
811 fn test_multiple_newlines() {
812 let margins = create_test_margins();
813 let mut context = TextFlowContext::new(400.0, 600.0, margins);
814
815 let initial_y = context.cursor_y;
816
817 context.newline();
818 let y1 = context.cursor_y;
819
820 context.newline();
821 let y2 = context.cursor_y;
822
823 context.newline();
824 let y3 = context.cursor_y;
825
826 let spacing1 = initial_y - y1;
828 let spacing2 = y1 - y2;
829 let spacing3 = y2 - y3;
830
831 assert!((spacing1 - spacing2).abs() < 1e-10);
833 assert!((spacing2 - spacing3).abs() < 1e-10);
834 assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
835 }
836
837 #[test]
838 fn test_content_width_different_margins() {
839 let margins = Margins {
840 left: 30.0,
841 right: 70.0,
842 top: 40.0,
843 bottom: 60.0,
844 };
845 let context = TextFlowContext::new(500.0, 700.0, margins);
846
847 let content_width = context.content_width();
848 assert_eq!(content_width, 400.0); }
850
851 #[test]
852 fn test_custom_line_height() {
853 let margins = create_test_margins();
854 let mut context = TextFlowContext::new(400.0, 600.0, margins);
855
856 context.set_line_height(2.0);
857
858 let initial_y = context.cursor_y;
859 context.newline();
860 let y_after = context.cursor_y;
861
862 let spacing = initial_y - y_after;
863 assert_eq!(spacing, context.font_size * 2.0); }
865
866 #[test]
867 fn test_different_fonts() {
868 let margins = create_test_margins();
869 let mut context = TextFlowContext::new(400.0, 600.0, margins);
870
871 let fonts = vec![
872 Font::Helvetica,
873 Font::HelveticaBold,
874 Font::TimesRoman,
875 Font::TimesBold,
876 Font::Courier,
877 Font::CourierBold,
878 ];
879
880 for font in fonts {
881 context.clear();
882 let font_name = font.pdf_name();
883 context.set_font(font, 14.0);
884 context.write_wrapped("Test text").unwrap();
885
886 let ops = context.operations();
887 assert!(ops.contains(&format!("/{font_name} 14 Tf")));
888 }
889 }
890
891 #[test]
892 fn test_font_size_variations() {
893 let margins = create_test_margins();
894 let mut context = TextFlowContext::new(400.0, 600.0, margins);
895
896 let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
897
898 for size in sizes {
899 context.clear();
900 context.set_font(Font::Helvetica, size);
901 context.write_wrapped("Test").unwrap();
902
903 let ops = context.operations();
904 assert!(ops.contains(&format!("/Helvetica {size} Tf")));
905 }
906 }
907
908 #[test]
909 fn test_at_position_edge_cases() {
910 let margins = create_test_margins();
911 let mut context = TextFlowContext::new(400.0, 600.0, margins);
912
913 context.at(0.0, 0.0);
915 assert_eq!(context.cursor_position(), (0.0, 0.0));
916
917 context.at(-10.0, -20.0);
919 assert_eq!(context.cursor_position(), (-10.0, -20.0));
920
921 context.at(10000.0, 20000.0);
923 assert_eq!(context.cursor_position(), (10000.0, 20000.0));
924 }
925
926 #[test]
927 fn test_write_wrapped_with_narrow_content() {
928 let margins = Margins {
929 left: 190.0,
930 right: 190.0,
931 top: 50.0,
932 bottom: 50.0,
933 };
934 let mut context = TextFlowContext::new(400.0, 600.0, margins);
935
936 context
938 .write_wrapped("This text should wrap a lot")
939 .unwrap();
940
941 let ops = context.operations();
942 let bt_count = ops.matches("BT\n").count();
944 assert!(bt_count > 1);
945 }
946
947 #[test]
948 fn test_justified_text_single_word_line() {
949 let margins = create_test_margins();
950 let mut context = TextFlowContext::new(400.0, 600.0, margins);
951
952 context.set_alignment(TextAlign::Justified);
953 context.write_wrapped("SingleWord").unwrap();
954
955 let ops = context.operations();
956 assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
958 }
959
960 #[test]
961 fn test_justified_text_last_line() {
962 let margins = create_test_margins();
963 let mut context = TextFlowContext::new(400.0, 600.0, margins);
964
965 context.set_alignment(TextAlign::Justified);
966 context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
968
969 let ops = context.operations();
970 assert!(ops.contains("0 Tw"));
972 }
973
974 #[test]
975 fn test_generate_operations_encoding() {
976 let margins = create_test_margins();
977 let mut context = TextFlowContext::new(400.0, 600.0, margins);
978
979 context.write_wrapped("UTF-8 Text: Ñ").unwrap();
980
981 let ops_bytes = context.generate_operations();
982 let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
983
984 assert_eq!(ops_bytes, context.operations().as_bytes());
985 assert_eq!(ops_string, context.operations());
986 }
987
988 #[test]
989 fn test_clear_resets_operations_only() {
990 let margins = create_test_margins();
991 let mut context = TextFlowContext::new(400.0, 600.0, margins);
992
993 context.set_font(Font::TimesBold, 18.0);
994 context.set_alignment(TextAlign::Right);
995 context.at(100.0, 200.0);
996 context.write_wrapped("Text").unwrap();
997
998 context.clear();
999
1000 assert!(context.operations().is_empty());
1002
1003 assert_eq!(context.current_font, Font::TimesBold);
1005 assert_eq!(context.font_size, 18.0);
1006 assert_eq!(context.alignment(), TextAlign::Right);
1007 let (x, y) = context.cursor_position();
1009 assert_eq!(x, 100.0); assert!(y < 200.0); }
1012
1013 #[test]
1014 fn test_long_text_wrapping() {
1015 let margins = create_test_margins();
1016 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1017
1018 let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
1019 Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
1020 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
1021
1022 context.write_wrapped(long_text).unwrap();
1023
1024 let ops = context.operations();
1025 let tj_count = ops.matches(") Tj").count();
1027 assert!(tj_count > 1);
1028 }
1029
1030 #[test]
1031 fn test_empty_operations_initially() {
1032 let margins = create_test_margins();
1033 let context = TextFlowContext::new(400.0, 600.0, margins);
1034
1035 assert!(context.operations().is_empty());
1036 assert_eq!(context.generate_operations().len(), 0);
1037 }
1038
1039 #[test]
1040 fn test_write_paragraph_empty() {
1041 let margins = create_test_margins();
1042 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1043
1044 let initial_y = context.cursor_y;
1045 context.write_paragraph("").unwrap();
1046
1047 assert!(context.cursor_y < initial_y);
1049 }
1050
1051 #[test]
1052 fn test_extreme_line_height() {
1053 let margins = create_test_margins();
1054 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1055
1056 context.set_line_height(0.1);
1058 let initial_y = context.cursor_y;
1059 context.newline();
1060 assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
1061
1062 context.set_line_height(10.0);
1064 let initial_y2 = context.cursor_y;
1065 context.newline();
1066 assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
1067 }
1068
1069 #[test]
1070 fn test_zero_content_width() {
1071 let margins = Margins {
1072 left: 200.0,
1073 right: 200.0,
1074 top: 50.0,
1075 bottom: 50.0,
1076 };
1077 let context = TextFlowContext::new(400.0, 600.0, margins);
1078
1079 assert_eq!(context.content_width(), 0.0);
1080 }
1081
1082 #[test]
1083 fn test_cursor_x_reset_on_newline() {
1084 let margins = create_test_margins();
1085 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
1086
1087 context.at(250.0, 300.0); context.newline();
1089
1090 assert_eq!(context.cursor_x, margins.left);
1092 assert_eq!(
1094 context.cursor_y,
1095 300.0 - context.font_size * context.line_height
1096 );
1097 }
1098
1099 #[test]
1102 fn test_available_width_respects_cursor_x() {
1103 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
1106
1107 assert_eq!(context.available_width(), 300.0);
1109
1110 context.at(200.0, 500.0);
1112 assert_eq!(context.available_width(), 150.0);
1113 }
1114
1115 #[test]
1116 fn test_available_width_clamps_to_zero() {
1117 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
1120
1121 context.at(380.0, 500.0);
1123 assert_eq!(context.available_width(), 0.0);
1124 }
1125
1126 #[test]
1127 fn test_write_wrapped_at_x_limits_available_width() {
1128 let margins = create_test_margins();
1132 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1133
1134 context.set_font(Font::Helvetica, 12.0);
1135 context.at(250.0, 500.0);
1137 context.write_wrapped("Hello World Hello World").unwrap();
1138
1139 let ops = context.operations();
1140 let bt_count = ops.matches("BT\n").count();
1142 assert!(
1143 bt_count > 1,
1144 "Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_write_wrapped_respects_cursor_x_offset() {
1150 let margins = Margins {
1152 left: 50.0,
1153 right: 50.0,
1154 top: 50.0,
1155 bottom: 50.0,
1156 };
1157 let mut context = TextFlowContext::new(600.0, 800.0, margins);
1158
1159 context.set_font(Font::Helvetica, 12.0);
1160 context.at(300.0, 700.0);
1161 context
1162 .write_wrapped("Hello World Foo Bar Baz Qux")
1163 .unwrap();
1164
1165 let ops = context.operations();
1166 for line in ops.lines() {
1168 if line.ends_with(" Td") {
1169 let parts: Vec<&str> = line.split_whitespace().collect();
1170 if parts.len() >= 3 {
1171 let x: f64 = parts[0].parse().expect("Td x should be a number");
1172 assert!(
1173 x >= 300.0 - 1e-6,
1174 "Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
1175 );
1176 }
1177 }
1178 }
1179 }
1180
1181 #[test]
1182 fn test_text_flow_context_threads_metrics_store() {
1183 use crate::text::metrics::{FontMetrics, FontMetricsStore};
1184 let unique = format!("FlowThreadTask6_{}", std::process::id());
1185 let store = FontMetricsStore::new();
1186 store.register(
1191 unique.clone(),
1192 FontMetrics::new(500).with_widths(&[('A', 1000)]),
1193 );
1194
1195 let mut ctx = TextFlowContext::with_metrics_store(
1196 595.0, 842.0, Margins::default(),
1199 Some(store),
1200 );
1201 ctx.set_font(Font::Custom(unique), 12.0);
1202 ctx.set_alignment(TextAlign::Center);
1203 ctx.write_wrapped("AA").unwrap();
1204
1205 let margins = Margins::default();
1216 let available_width = 595.0_f64 - margins.left - margins.right; let expected_line_width = 24.0_f64; let expected_td_x = margins.left + (available_width - expected_line_width) / 2.0;
1219
1220 let ops_bytes = ctx.generate_operations();
1221 let ops_str =
1222 String::from_utf8(ops_bytes).expect("generated operations must be valid UTF-8");
1223
1224 let td_x: f64 = ops_str
1226 .lines()
1227 .find(|l| l.ends_with(" Td"))
1228 .and_then(|l| l.split_whitespace().next())
1229 .and_then(|tok| tok.parse().ok())
1230 .expect("operations must contain a Td operator");
1231
1232 assert!(
1233 (td_x - expected_td_x).abs() < 0.01,
1234 "Td x must reflect per-store line width 24.0 pts \
1235 (expected {:.2}, got {:.2}); if the store was dropped the \
1236 fallback width produces x ≈ 289.50",
1237 expected_td_x,
1238 td_x
1239 );
1240 }
1241
1242 #[test]
1249 fn nan_cursor_position_in_flow_is_sanitised_at_emission() {
1250 let mut ctx = TextFlowContext::new(595.0, 842.0, Margins::default());
1251 ctx.at(f64::NAN, f64::NAN);
1252 ctx.write_wrapped("hello").unwrap();
1253 let ops = String::from_utf8(ctx.generate_operations())
1254 .expect("operations bytes must be valid UTF-8");
1255 assert!(
1256 !ops.contains("NaN") && !ops.contains("inf"),
1257 "non-finite tokens must not appear in flow content stream, got: {ops:?}"
1258 );
1259 assert!(
1260 ops.contains(" Td\n"),
1261 "Td operator must still be emitted, got: {ops:?}"
1262 );
1263 }
1264}