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 self.operations.push(crate::text::build_show_text_op(
343 &line_text,
344 &self.current_font,
345 ));
346
347 self.used_characters_by_font
350 .entry(self.current_font.pdf_name())
351 .or_default()
352 .extend(line_text.chars());
353
354 if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
357 self.operations.push(Op::SetWordSpacing(0.0));
358 }
359
360 self.operations.push(Op::EndText);
361
362 self.cursor_y -= self.font_size * self.line_height;
364 }
365
366 Ok(self)
367 }
368
369 pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
370 self.write_wrapped(text)?;
371 self.cursor_y -= self.font_size * self.line_height * 0.5;
373 Ok(self)
374 }
375
376 pub fn newline(&mut self) -> &mut Self {
377 self.cursor_y -= self.font_size * self.line_height;
378 self.cursor_x = self.margins.left;
379 self
380 }
381
382 pub fn cursor_position(&self) -> (f64, f64) {
383 (self.cursor_x, self.cursor_y)
384 }
385
386 pub fn generate_operations(&self) -> Vec<u8> {
387 let mut buf = Vec::new();
388 crate::graphics::ops::serialize_ops(&mut buf, &self.operations);
389 buf
390 }
391
392 pub fn alignment(&self) -> TextAlign {
394 self.alignment
395 }
396
397 pub fn page_dimensions(&self) -> (f64, f64) {
399 (self.page_width, self.page_height)
400 }
401
402 pub fn margins(&self) -> &Margins {
404 &self.margins
405 }
406
407 pub fn line_height(&self) -> f64 {
409 self.line_height
410 }
411
412 pub fn operations(&self) -> String {
419 crate::graphics::ops::ops_to_string(&self.operations)
420 }
421
422 pub fn clear(&mut self) {
424 self.operations.clear();
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::page::Margins;
432
433 fn create_test_margins() -> Margins {
434 Margins {
435 left: 50.0,
436 right: 50.0,
437 top: 50.0,
438 bottom: 50.0,
439 }
440 }
441
442 #[test]
443 fn test_text_flow_context_new() {
444 let margins = create_test_margins();
445 let context = TextFlowContext::new(400.0, 600.0, margins);
446
447 assert_eq!(context.current_font, Font::Helvetica);
448 assert_eq!(context.font_size, 12.0);
449 assert_eq!(context.line_height, 1.2);
450 assert_eq!(context.alignment, TextAlign::Left);
451 assert_eq!(context.page_width, 400.0);
452 assert_eq!(context.page_height, 600.0);
453 assert_eq!(context.cursor_x, 50.0); assert_eq!(context.cursor_y, 550.0); }
456
457 #[test]
458 fn test_set_font() {
459 let margins = create_test_margins();
460 let mut context = TextFlowContext::new(400.0, 600.0, margins);
461
462 context.set_font(Font::TimesBold, 16.0);
463 assert_eq!(context.current_font, Font::TimesBold);
464 assert_eq!(context.font_size, 16.0);
465 }
466
467 #[test]
468 fn test_set_line_height() {
469 let margins = create_test_margins();
470 let mut context = TextFlowContext::new(400.0, 600.0, margins);
471
472 context.set_line_height(1.5);
473 assert_eq!(context.line_height(), 1.5);
474 }
475
476 #[test]
477 fn test_set_alignment() {
478 let margins = create_test_margins();
479 let mut context = TextFlowContext::new(400.0, 600.0, margins);
480
481 context.set_alignment(TextAlign::Center);
482 assert_eq!(context.alignment(), TextAlign::Center);
483 }
484
485 #[test]
486 fn test_at_position() {
487 let margins = create_test_margins();
488 let mut context = TextFlowContext::new(400.0, 600.0, margins);
489
490 context.at(100.0, 200.0);
491 let (x, y) = context.cursor_position();
492 assert_eq!(x, 100.0);
493 assert_eq!(y, 200.0);
494 }
495
496 #[test]
497 fn test_content_width() {
498 let margins = create_test_margins();
499 let context = TextFlowContext::new(400.0, 600.0, margins);
500
501 let content_width = context.content_width();
502 assert_eq!(content_width, 300.0); }
504
505 #[test]
506 fn test_text_align_variants() {
507 assert_eq!(TextAlign::Left, TextAlign::Left);
508 assert_eq!(TextAlign::Right, TextAlign::Right);
509 assert_eq!(TextAlign::Center, TextAlign::Center);
510 assert_eq!(TextAlign::Justified, TextAlign::Justified);
511
512 assert_ne!(TextAlign::Left, TextAlign::Right);
513 }
514
515 #[test]
516 fn test_write_wrapped_simple() {
517 let margins = create_test_margins();
518 let mut context = TextFlowContext::new(400.0, 600.0, margins);
519
520 context.write_wrapped("Hello World").unwrap();
521
522 let ops = context.operations();
523 assert!(ops.contains("BT\n"));
524 assert!(ops.contains("ET\n"));
525 assert!(ops.contains("/Helvetica 12 Tf"));
526 assert!(ops.contains("(Hello World) Tj"));
527 }
528
529 #[test]
530 fn test_write_paragraph() {
531 let margins = create_test_margins();
532 let mut context = TextFlowContext::new(400.0, 600.0, margins);
533
534 let initial_y = context.cursor_y;
535 context.write_paragraph("Test paragraph").unwrap();
536
537 assert!(context.cursor_y < initial_y);
539 }
540
541 #[test]
542 fn test_newline() {
543 let margins = create_test_margins();
544 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
545
546 let initial_y = context.cursor_y;
547 context.newline();
548
549 assert_eq!(context.cursor_x, margins.left);
550 assert!(context.cursor_y < initial_y);
551 assert_eq!(
552 context.cursor_y,
553 initial_y - context.font_size * context.line_height
554 );
555 }
556
557 #[test]
558 fn test_cursor_position() {
559 let margins = create_test_margins();
560 let mut context = TextFlowContext::new(400.0, 600.0, margins);
561
562 context.at(75.0, 125.0);
563 let (x, y) = context.cursor_position();
564 assert_eq!(x, 75.0);
565 assert_eq!(y, 125.0);
566 }
567
568 #[test]
569 fn test_generate_operations() {
570 let margins = create_test_margins();
571 let mut context = TextFlowContext::new(400.0, 600.0, margins);
572
573 context.write_wrapped("Test").unwrap();
574 let ops_bytes = context.generate_operations();
575 let ops_string = String::from_utf8(ops_bytes).unwrap();
576
577 assert_eq!(ops_string, context.operations());
578 }
579
580 #[test]
581 fn test_clear_operations() {
582 let margins = create_test_margins();
583 let mut context = TextFlowContext::new(400.0, 600.0, margins);
584
585 context.write_wrapped("Test").unwrap();
586 assert!(!context.operations().is_empty());
587
588 context.clear();
589 assert!(context.operations().is_empty());
590 }
591
592 #[test]
593 fn test_page_dimensions() {
594 let margins = create_test_margins();
595 let context = TextFlowContext::new(400.0, 600.0, margins);
596
597 let (width, height) = context.page_dimensions();
598 assert_eq!(width, 400.0);
599 assert_eq!(height, 600.0);
600 }
601
602 #[test]
603 fn test_margins_access() {
604 let margins = create_test_margins();
605 let context = TextFlowContext::new(400.0, 600.0, margins);
606
607 let ctx_margins = context.margins();
608 assert_eq!(ctx_margins.left, 50.0);
609 assert_eq!(ctx_margins.right, 50.0);
610 assert_eq!(ctx_margins.top, 50.0);
611 assert_eq!(ctx_margins.bottom, 50.0);
612 }
613
614 #[test]
615 fn test_method_chaining() {
616 let margins = create_test_margins();
617 let mut context = TextFlowContext::new(400.0, 600.0, margins);
618
619 context
620 .set_font(Font::Courier, 10.0)
621 .set_line_height(1.5)
622 .set_alignment(TextAlign::Center)
623 .at(100.0, 200.0);
624
625 assert_eq!(context.current_font, Font::Courier);
626 assert_eq!(context.font_size, 10.0);
627 assert_eq!(context.line_height(), 1.5);
628 assert_eq!(context.alignment(), TextAlign::Center);
629 let (x, y) = context.cursor_position();
630 assert_eq!(x, 100.0);
631 assert_eq!(y, 200.0);
632 }
633
634 #[test]
635 fn test_text_align_debug() {
636 let align = TextAlign::Center;
637 let debug_str = format!("{align:?}");
638 assert_eq!(debug_str, "Center");
639 }
640
641 #[test]
642 fn test_text_align_clone() {
643 let align1 = TextAlign::Justified;
644 let align2 = align1;
645 assert_eq!(align1, align2);
646 }
647
648 #[test]
649 fn test_text_align_copy() {
650 let align1 = TextAlign::Right;
651 let align2 = align1; assert_eq!(align1, align2);
653
654 assert_eq!(align1, TextAlign::Right);
656 assert_eq!(align2, TextAlign::Right);
657 }
658
659 #[test]
660 fn test_write_wrapped_with_alignment_right() {
661 let margins = create_test_margins();
662 let mut context = TextFlowContext::new(400.0, 600.0, margins);
663
664 context.set_alignment(TextAlign::Right);
665 context.write_wrapped("Right aligned text").unwrap();
666
667 let ops = context.operations();
668 assert!(ops.contains("BT\n"));
669 assert!(ops.contains("ET\n"));
670 assert!(ops.contains("Td"));
672 }
673
674 #[test]
675 fn test_write_wrapped_with_alignment_center() {
676 let margins = create_test_margins();
677 let mut context = TextFlowContext::new(400.0, 600.0, margins);
678
679 context.set_alignment(TextAlign::Center);
680 context.write_wrapped("Centered text").unwrap();
681
682 let ops = context.operations();
683 assert!(ops.contains("BT\n"));
684 assert!(ops.contains("(Centered text) Tj"));
685 }
686
687 #[test]
688 fn test_write_wrapped_with_alignment_justified() {
689 let margins = create_test_margins();
690 let mut context = TextFlowContext::new(400.0, 600.0, margins);
691
692 context.set_alignment(TextAlign::Justified);
693 context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
695
696 let ops = context.operations();
697 assert!(ops.contains("BT\n"));
698 assert!(ops.contains("Tw") || ops.contains("0 Tw"));
700 }
701
702 #[test]
703 fn test_write_wrapped_empty_text() {
704 let margins = create_test_margins();
705 let mut context = TextFlowContext::new(400.0, 600.0, margins);
706
707 context.write_wrapped("").unwrap();
708
709 assert!(context.operations().is_empty());
711 }
712
713 #[test]
714 fn test_write_wrapped_whitespace_only() {
715 let margins = create_test_margins();
716 let mut context = TextFlowContext::new(400.0, 600.0, margins);
717
718 context.write_wrapped(" ").unwrap();
719
720 let ops = context.operations();
721 assert!(ops.contains("BT\n") || ops.is_empty());
723 }
724
725 #[test]
726 fn test_write_wrapped_special_characters() {
727 let margins = create_test_margins();
728 let mut context = TextFlowContext::new(400.0, 600.0, margins);
729
730 context
731 .write_wrapped("Text with (parentheses) and \\backslash\\")
732 .unwrap();
733
734 let ops = context.operations();
735 assert!(ops.contains("\\(parentheses\\)"));
737 assert!(ops.contains("\\\\backslash\\\\"));
738 }
739
740 #[test]
741 fn test_write_wrapped_newlines_tabs() {
742 let margins = create_test_margins();
743 let mut context = TextFlowContext::new(400.0, 600.0, margins);
744
745 context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
746
747 let ops = context.operations();
748 assert!(ops.contains("\\n") || ops.contains("\\t"));
750 }
751
752 #[test]
753 fn test_write_wrapped_very_long_word() {
754 let margins = create_test_margins();
755 let mut context = TextFlowContext::new(200.0, 600.0, margins); let long_word = "a".repeat(100);
758 context.write_wrapped(&long_word).unwrap();
759
760 let ops = context.operations();
761 assert!(ops.contains("BT\n"));
762 assert!(ops.contains(&long_word));
763 }
764
765 #[test]
766 fn test_write_wrapped_cursor_movement() {
767 let margins = create_test_margins();
768 let mut context = TextFlowContext::new(400.0, 600.0, margins);
769
770 let initial_y = context.cursor_y;
771
772 context.write_wrapped("Line 1").unwrap();
773 let y_after_line1 = context.cursor_y;
774
775 context.write_wrapped("Line 2").unwrap();
776 let y_after_line2 = context.cursor_y;
777
778 assert!(y_after_line1 < initial_y);
780 assert!(y_after_line2 < y_after_line1);
781 }
782
783 #[test]
784 fn test_write_paragraph_spacing() {
785 let margins = create_test_margins();
786 let mut context = TextFlowContext::new(400.0, 600.0, margins);
787
788 let initial_y = context.cursor_y;
789 context.write_paragraph("Paragraph 1").unwrap();
790 let y_after_p1 = context.cursor_y;
791
792 context.write_paragraph("Paragraph 2").unwrap();
793 let y_after_p2 = context.cursor_y;
794
795 let spacing1 = initial_y - y_after_p1;
797 let spacing2 = y_after_p1 - y_after_p2;
798
799 assert!(spacing1 > 0.0);
800 assert!(spacing2 > 0.0);
801 }
802
803 #[test]
804 fn test_multiple_newlines() {
805 let margins = create_test_margins();
806 let mut context = TextFlowContext::new(400.0, 600.0, margins);
807
808 let initial_y = context.cursor_y;
809
810 context.newline();
811 let y1 = context.cursor_y;
812
813 context.newline();
814 let y2 = context.cursor_y;
815
816 context.newline();
817 let y3 = context.cursor_y;
818
819 let spacing1 = initial_y - y1;
821 let spacing2 = y1 - y2;
822 let spacing3 = y2 - y3;
823
824 assert!((spacing1 - spacing2).abs() < 1e-10);
826 assert!((spacing2 - spacing3).abs() < 1e-10);
827 assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
828 }
829
830 #[test]
831 fn test_content_width_different_margins() {
832 let margins = Margins {
833 left: 30.0,
834 right: 70.0,
835 top: 40.0,
836 bottom: 60.0,
837 };
838 let context = TextFlowContext::new(500.0, 700.0, margins);
839
840 let content_width = context.content_width();
841 assert_eq!(content_width, 400.0); }
843
844 #[test]
845 fn test_custom_line_height() {
846 let margins = create_test_margins();
847 let mut context = TextFlowContext::new(400.0, 600.0, margins);
848
849 context.set_line_height(2.0);
850
851 let initial_y = context.cursor_y;
852 context.newline();
853 let y_after = context.cursor_y;
854
855 let spacing = initial_y - y_after;
856 assert_eq!(spacing, context.font_size * 2.0); }
858
859 #[test]
860 fn test_different_fonts() {
861 let margins = create_test_margins();
862 let mut context = TextFlowContext::new(400.0, 600.0, margins);
863
864 let fonts = vec![
865 Font::Helvetica,
866 Font::HelveticaBold,
867 Font::TimesRoman,
868 Font::TimesBold,
869 Font::Courier,
870 Font::CourierBold,
871 ];
872
873 for font in fonts {
874 context.clear();
875 let font_name = font.pdf_name();
876 context.set_font(font, 14.0);
877 context.write_wrapped("Test text").unwrap();
878
879 let ops = context.operations();
880 assert!(ops.contains(&format!("/{font_name} 14 Tf")));
881 }
882 }
883
884 #[test]
885 fn test_font_size_variations() {
886 let margins = create_test_margins();
887 let mut context = TextFlowContext::new(400.0, 600.0, margins);
888
889 let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
890
891 for size in sizes {
892 context.clear();
893 context.set_font(Font::Helvetica, size);
894 context.write_wrapped("Test").unwrap();
895
896 let ops = context.operations();
897 assert!(ops.contains(&format!("/Helvetica {size} Tf")));
898 }
899 }
900
901 #[test]
902 fn test_at_position_edge_cases() {
903 let margins = create_test_margins();
904 let mut context = TextFlowContext::new(400.0, 600.0, margins);
905
906 context.at(0.0, 0.0);
908 assert_eq!(context.cursor_position(), (0.0, 0.0));
909
910 context.at(-10.0, -20.0);
912 assert_eq!(context.cursor_position(), (-10.0, -20.0));
913
914 context.at(10000.0, 20000.0);
916 assert_eq!(context.cursor_position(), (10000.0, 20000.0));
917 }
918
919 #[test]
920 fn test_write_wrapped_with_narrow_content() {
921 let margins = Margins {
922 left: 190.0,
923 right: 190.0,
924 top: 50.0,
925 bottom: 50.0,
926 };
927 let mut context = TextFlowContext::new(400.0, 600.0, margins);
928
929 context
931 .write_wrapped("This text should wrap a lot")
932 .unwrap();
933
934 let ops = context.operations();
935 let bt_count = ops.matches("BT\n").count();
937 assert!(bt_count > 1);
938 }
939
940 #[test]
941 fn test_justified_text_single_word_line() {
942 let margins = create_test_margins();
943 let mut context = TextFlowContext::new(400.0, 600.0, margins);
944
945 context.set_alignment(TextAlign::Justified);
946 context.write_wrapped("SingleWord").unwrap();
947
948 let ops = context.operations();
949 assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
951 }
952
953 #[test]
954 fn test_justified_text_last_line() {
955 let margins = create_test_margins();
956 let mut context = TextFlowContext::new(400.0, 600.0, margins);
957
958 context.set_alignment(TextAlign::Justified);
959 context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
961
962 let ops = context.operations();
963 assert!(ops.contains("0 Tw"));
965 }
966
967 #[test]
968 fn test_generate_operations_encoding() {
969 let margins = create_test_margins();
970 let mut context = TextFlowContext::new(400.0, 600.0, margins);
971
972 context.write_wrapped("UTF-8 Text: Ñ").unwrap();
973
974 let ops_bytes = context.generate_operations();
975 let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
976
977 assert_eq!(ops_bytes, context.operations().as_bytes());
978 assert_eq!(ops_string, context.operations());
979 }
980
981 #[test]
982 fn test_clear_resets_operations_only() {
983 let margins = create_test_margins();
984 let mut context = TextFlowContext::new(400.0, 600.0, margins);
985
986 context.set_font(Font::TimesBold, 18.0);
987 context.set_alignment(TextAlign::Right);
988 context.at(100.0, 200.0);
989 context.write_wrapped("Text").unwrap();
990
991 context.clear();
992
993 assert!(context.operations().is_empty());
995
996 assert_eq!(context.current_font, Font::TimesBold);
998 assert_eq!(context.font_size, 18.0);
999 assert_eq!(context.alignment(), TextAlign::Right);
1000 let (x, y) = context.cursor_position();
1002 assert_eq!(x, 100.0); assert!(y < 200.0); }
1005
1006 #[test]
1007 fn test_long_text_wrapping() {
1008 let margins = create_test_margins();
1009 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1010
1011 let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
1012 Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
1013 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
1014
1015 context.write_wrapped(long_text).unwrap();
1016
1017 let ops = context.operations();
1018 let tj_count = ops.matches(") Tj").count();
1020 assert!(tj_count > 1);
1021 }
1022
1023 #[test]
1024 fn test_empty_operations_initially() {
1025 let margins = create_test_margins();
1026 let context = TextFlowContext::new(400.0, 600.0, margins);
1027
1028 assert!(context.operations().is_empty());
1029 assert_eq!(context.generate_operations().len(), 0);
1030 }
1031
1032 #[test]
1033 fn test_write_paragraph_empty() {
1034 let margins = create_test_margins();
1035 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1036
1037 let initial_y = context.cursor_y;
1038 context.write_paragraph("").unwrap();
1039
1040 assert!(context.cursor_y < initial_y);
1042 }
1043
1044 #[test]
1045 fn test_extreme_line_height() {
1046 let margins = create_test_margins();
1047 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1048
1049 context.set_line_height(0.1);
1051 let initial_y = context.cursor_y;
1052 context.newline();
1053 assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
1054
1055 context.set_line_height(10.0);
1057 let initial_y2 = context.cursor_y;
1058 context.newline();
1059 assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
1060 }
1061
1062 #[test]
1063 fn test_zero_content_width() {
1064 let margins = Margins {
1065 left: 200.0,
1066 right: 200.0,
1067 top: 50.0,
1068 bottom: 50.0,
1069 };
1070 let context = TextFlowContext::new(400.0, 600.0, margins);
1071
1072 assert_eq!(context.content_width(), 0.0);
1073 }
1074
1075 #[test]
1076 fn test_cursor_x_reset_on_newline() {
1077 let margins = create_test_margins();
1078 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
1079
1080 context.at(250.0, 300.0); context.newline();
1082
1083 assert_eq!(context.cursor_x, margins.left);
1085 assert_eq!(
1087 context.cursor_y,
1088 300.0 - context.font_size * context.line_height
1089 );
1090 }
1091
1092 #[test]
1095 fn test_available_width_respects_cursor_x() {
1096 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
1099
1100 assert_eq!(context.available_width(), 300.0);
1102
1103 context.at(200.0, 500.0);
1105 assert_eq!(context.available_width(), 150.0);
1106 }
1107
1108 #[test]
1109 fn test_available_width_clamps_to_zero() {
1110 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
1113
1114 context.at(380.0, 500.0);
1116 assert_eq!(context.available_width(), 0.0);
1117 }
1118
1119 #[test]
1120 fn test_write_wrapped_at_x_limits_available_width() {
1121 let margins = create_test_margins();
1125 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1126
1127 context.set_font(Font::Helvetica, 12.0);
1128 context.at(250.0, 500.0);
1130 context.write_wrapped("Hello World Hello World").unwrap();
1131
1132 let ops = context.operations();
1133 let bt_count = ops.matches("BT\n").count();
1135 assert!(
1136 bt_count > 1,
1137 "Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
1138 );
1139 }
1140
1141 #[test]
1142 fn test_write_wrapped_respects_cursor_x_offset() {
1143 let margins = Margins {
1145 left: 50.0,
1146 right: 50.0,
1147 top: 50.0,
1148 bottom: 50.0,
1149 };
1150 let mut context = TextFlowContext::new(600.0, 800.0, margins);
1151
1152 context.set_font(Font::Helvetica, 12.0);
1153 context.at(300.0, 700.0);
1154 context
1155 .write_wrapped("Hello World Foo Bar Baz Qux")
1156 .unwrap();
1157
1158 let ops = context.operations();
1159 for line in ops.lines() {
1161 if line.ends_with(" Td") {
1162 let parts: Vec<&str> = line.split_whitespace().collect();
1163 if parts.len() >= 3 {
1164 let x: f64 = parts[0].parse().expect("Td x should be a number");
1165 assert!(
1166 x >= 300.0 - 1e-6,
1167 "Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
1168 );
1169 }
1170 }
1171 }
1172 }
1173
1174 #[test]
1175 fn test_text_flow_context_threads_metrics_store() {
1176 use crate::text::metrics::{FontMetrics, FontMetricsStore};
1177 let unique = format!("FlowThreadTask6_{}", std::process::id());
1178 let store = FontMetricsStore::new();
1179 store.register(
1184 unique.clone(),
1185 FontMetrics::new(500).with_widths(&[('A', 1000)]),
1186 );
1187
1188 let mut ctx = TextFlowContext::with_metrics_store(
1189 595.0, 842.0, Margins::default(),
1192 Some(store),
1193 );
1194 ctx.set_font(Font::Custom(unique), 12.0);
1195 ctx.set_alignment(TextAlign::Center);
1196 ctx.write_wrapped("AA").unwrap();
1197
1198 let margins = Margins::default();
1209 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;
1212
1213 let ops_bytes = ctx.generate_operations();
1214 let ops_str =
1215 String::from_utf8(ops_bytes).expect("generated operations must be valid UTF-8");
1216
1217 let td_x: f64 = ops_str
1219 .lines()
1220 .find(|l| l.ends_with(" Td"))
1221 .and_then(|l| l.split_whitespace().next())
1222 .and_then(|tok| tok.parse().ok())
1223 .expect("operations must contain a Td operator");
1224
1225 assert!(
1226 (td_x - expected_td_x).abs() < 0.01,
1227 "Td x must reflect per-store line width 24.0 pts \
1228 (expected {:.2}, got {:.2}); if the store was dropped the \
1229 fallback width produces x ≈ 289.50",
1230 expected_td_x,
1231 td_x
1232 );
1233 }
1234
1235 #[test]
1242 fn nan_cursor_position_in_flow_is_sanitised_at_emission() {
1243 let mut ctx = TextFlowContext::new(595.0, 842.0, Margins::default());
1244 ctx.at(f64::NAN, f64::NAN);
1245 ctx.write_wrapped("hello").unwrap();
1246 let ops = String::from_utf8(ctx.generate_operations())
1247 .expect("operations bytes must be valid UTF-8");
1248 assert!(
1249 !ops.contains("NaN") && !ops.contains("inf"),
1250 "non-finite tokens must not appear in flow content stream, got: {ops:?}"
1251 );
1252 assert!(
1253 ops.contains(" Td\n"),
1254 "Td operator must still be emitted, got: {ops:?}"
1255 );
1256 }
1257}