1use crate::error::Result;
2use crate::graphics::Color;
3use crate::page::Margins;
4use crate::text::{measure_text, split_into_words, Font};
5use std::collections::{HashMap, HashSet};
6use std::fmt::Write;
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: String,
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 used_characters_by_font: HashMap<String, HashSet<char>>,
39}
40
41impl TextFlowContext {
42 pub fn new(page_width: f64, page_height: f64, margins: Margins) -> Self {
43 Self {
44 operations: String::new(),
45 current_font: Font::Helvetica,
46 font_size: 12.0,
47 line_height: 1.2,
48 cursor_x: margins.left,
49 cursor_y: page_height - margins.top,
50 alignment: TextAlign::Left,
51 page_width,
52 page_height,
53 margins,
54 fill_color: None,
55 used_characters_by_font: HashMap::new(),
56 }
57 }
58
59 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
64 &self.used_characters_by_font
65 }
66
67 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
68 self.current_font = font;
69 self.font_size = size;
70 self
71 }
72
73 pub fn set_line_height(&mut self, multiplier: f64) -> &mut Self {
74 self.line_height = multiplier;
75 self
76 }
77
78 pub fn set_alignment(&mut self, alignment: TextAlign) -> &mut Self {
79 self.alignment = alignment;
80 self
81 }
82
83 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
89 self.fill_color = Some(color);
90 self
91 }
92
93 pub fn current_font(&self) -> &Font {
95 &self.current_font
96 }
97
98 pub fn font_size(&self) -> f64 {
100 self.font_size
101 }
102
103 pub fn fill_color(&self) -> Option<Color> {
105 self.fill_color
106 }
107
108 pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
109 self.cursor_x = x;
110 self.cursor_y = y;
111 self
112 }
113
114 pub fn content_width(&self) -> f64 {
115 self.page_width - self.margins.left - self.margins.right
116 }
117
118 pub fn available_width(&self) -> f64 {
124 (self.page_width - self.margins.right - self.cursor_x).max(0.0)
125 }
126
127 pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
128 let start_x = self.cursor_x;
129 let available_width = self.available_width();
130
131 let words = split_into_words(text);
133 let mut lines: Vec<Vec<&str>> = Vec::new();
134 let mut current_line: Vec<&str> = Vec::new();
135 let mut current_width = 0.0;
136
137 for word in words {
139 let word_width = measure_text(word, &self.current_font, self.font_size);
140
141 if !current_line.is_empty() && current_width + word_width > available_width {
143 lines.push(current_line);
144 current_line = vec![word];
145 current_width = word_width;
146 } else {
147 current_line.push(word);
148 current_width += word_width;
149 }
150 }
151
152 if !current_line.is_empty() {
153 lines.push(current_line);
154 }
155
156 for (i, line) in lines.iter().enumerate() {
158 let line_text = line.join("");
159 let line_width = measure_text(&line_text, &self.current_font, self.font_size);
160
161 let x = match self.alignment {
166 TextAlign::Left => start_x,
167 TextAlign::Right => self.page_width - self.margins.right - line_width,
168 TextAlign::Center => start_x + (available_width - line_width) / 2.0,
169 TextAlign::Justified => start_x,
170 };
171
172 self.operations.push_str("BT\n");
174
175 writeln!(
177 &mut self.operations,
178 "/{} {} Tf",
179 self.current_font.pdf_name(),
180 self.font_size
181 )
182 .expect("Writing to String should never fail");
183
184 if let Some(color) = self.fill_color {
194 crate::graphics::color::write_fill_color(&mut self.operations, color);
195 }
196
197 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, self.cursor_y)
199 .expect("Writing to String should never fail");
200
201 if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
203 let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
205 if spaces_count > 0 {
206 let extra_space = available_width - line_width;
207 let space_adjustment = extra_space / spaces_count as f64;
208
209 writeln!(&mut self.operations, "{space_adjustment:.2} Tw")
211 .expect("Writing to String should never fail");
212 }
213 }
214
215 self.operations.push('(');
217 for ch in line_text.chars() {
218 match ch {
219 '(' => self.operations.push_str("\\("),
220 ')' => self.operations.push_str("\\)"),
221 '\\' => self.operations.push_str("\\\\"),
222 '\n' => self.operations.push_str("\\n"),
223 '\r' => self.operations.push_str("\\r"),
224 '\t' => self.operations.push_str("\\t"),
225 _ => self.operations.push(ch),
226 }
227 }
228 self.operations.push_str(") Tj\n");
229
230 self.used_characters_by_font
236 .entry(self.current_font.pdf_name())
237 .or_default()
238 .extend(line_text.chars());
239
240 if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
242 self.operations.push_str("0 Tw\n");
243 }
244
245 self.operations.push_str("ET\n");
247
248 self.cursor_y -= self.font_size * self.line_height;
250 }
251
252 Ok(self)
253 }
254
255 pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
256 self.write_wrapped(text)?;
257 self.cursor_y -= self.font_size * self.line_height * 0.5;
259 Ok(self)
260 }
261
262 pub fn newline(&mut self) -> &mut Self {
263 self.cursor_y -= self.font_size * self.line_height;
264 self.cursor_x = self.margins.left;
265 self
266 }
267
268 pub fn cursor_position(&self) -> (f64, f64) {
269 (self.cursor_x, self.cursor_y)
270 }
271
272 pub fn generate_operations(&self) -> Vec<u8> {
273 self.operations.as_bytes().to_vec()
274 }
275
276 pub fn alignment(&self) -> TextAlign {
278 self.alignment
279 }
280
281 pub fn page_dimensions(&self) -> (f64, f64) {
283 (self.page_width, self.page_height)
284 }
285
286 pub fn margins(&self) -> &Margins {
288 &self.margins
289 }
290
291 pub fn line_height(&self) -> f64 {
293 self.line_height
294 }
295
296 pub fn operations(&self) -> &str {
298 &self.operations
299 }
300
301 pub fn clear(&mut self) {
303 self.operations.clear();
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::page::Margins;
311
312 fn create_test_margins() -> Margins {
313 Margins {
314 left: 50.0,
315 right: 50.0,
316 top: 50.0,
317 bottom: 50.0,
318 }
319 }
320
321 #[test]
322 fn test_text_flow_context_new() {
323 let margins = create_test_margins();
324 let context = TextFlowContext::new(400.0, 600.0, margins);
325
326 assert_eq!(context.current_font, Font::Helvetica);
327 assert_eq!(context.font_size, 12.0);
328 assert_eq!(context.line_height, 1.2);
329 assert_eq!(context.alignment, TextAlign::Left);
330 assert_eq!(context.page_width, 400.0);
331 assert_eq!(context.page_height, 600.0);
332 assert_eq!(context.cursor_x, 50.0); assert_eq!(context.cursor_y, 550.0); }
335
336 #[test]
337 fn test_set_font() {
338 let margins = create_test_margins();
339 let mut context = TextFlowContext::new(400.0, 600.0, margins);
340
341 context.set_font(Font::TimesBold, 16.0);
342 assert_eq!(context.current_font, Font::TimesBold);
343 assert_eq!(context.font_size, 16.0);
344 }
345
346 #[test]
347 fn test_set_line_height() {
348 let margins = create_test_margins();
349 let mut context = TextFlowContext::new(400.0, 600.0, margins);
350
351 context.set_line_height(1.5);
352 assert_eq!(context.line_height(), 1.5);
353 }
354
355 #[test]
356 fn test_set_alignment() {
357 let margins = create_test_margins();
358 let mut context = TextFlowContext::new(400.0, 600.0, margins);
359
360 context.set_alignment(TextAlign::Center);
361 assert_eq!(context.alignment(), TextAlign::Center);
362 }
363
364 #[test]
365 fn test_at_position() {
366 let margins = create_test_margins();
367 let mut context = TextFlowContext::new(400.0, 600.0, margins);
368
369 context.at(100.0, 200.0);
370 let (x, y) = context.cursor_position();
371 assert_eq!(x, 100.0);
372 assert_eq!(y, 200.0);
373 }
374
375 #[test]
376 fn test_content_width() {
377 let margins = create_test_margins();
378 let context = TextFlowContext::new(400.0, 600.0, margins);
379
380 let content_width = context.content_width();
381 assert_eq!(content_width, 300.0); }
383
384 #[test]
385 fn test_text_align_variants() {
386 assert_eq!(TextAlign::Left, TextAlign::Left);
387 assert_eq!(TextAlign::Right, TextAlign::Right);
388 assert_eq!(TextAlign::Center, TextAlign::Center);
389 assert_eq!(TextAlign::Justified, TextAlign::Justified);
390
391 assert_ne!(TextAlign::Left, TextAlign::Right);
392 }
393
394 #[test]
395 fn test_write_wrapped_simple() {
396 let margins = create_test_margins();
397 let mut context = TextFlowContext::new(400.0, 600.0, margins);
398
399 context.write_wrapped("Hello World").unwrap();
400
401 let ops = context.operations();
402 assert!(ops.contains("BT\n"));
403 assert!(ops.contains("ET\n"));
404 assert!(ops.contains("/Helvetica 12 Tf"));
405 assert!(ops.contains("(Hello World) Tj"));
406 }
407
408 #[test]
409 fn test_write_paragraph() {
410 let margins = create_test_margins();
411 let mut context = TextFlowContext::new(400.0, 600.0, margins);
412
413 let initial_y = context.cursor_y;
414 context.write_paragraph("Test paragraph").unwrap();
415
416 assert!(context.cursor_y < initial_y);
418 }
419
420 #[test]
421 fn test_newline() {
422 let margins = create_test_margins();
423 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
424
425 let initial_y = context.cursor_y;
426 context.newline();
427
428 assert_eq!(context.cursor_x, margins.left);
429 assert!(context.cursor_y < initial_y);
430 assert_eq!(
431 context.cursor_y,
432 initial_y - context.font_size * context.line_height
433 );
434 }
435
436 #[test]
437 fn test_cursor_position() {
438 let margins = create_test_margins();
439 let mut context = TextFlowContext::new(400.0, 600.0, margins);
440
441 context.at(75.0, 125.0);
442 let (x, y) = context.cursor_position();
443 assert_eq!(x, 75.0);
444 assert_eq!(y, 125.0);
445 }
446
447 #[test]
448 fn test_generate_operations() {
449 let margins = create_test_margins();
450 let mut context = TextFlowContext::new(400.0, 600.0, margins);
451
452 context.write_wrapped("Test").unwrap();
453 let ops_bytes = context.generate_operations();
454 let ops_string = String::from_utf8(ops_bytes).unwrap();
455
456 assert_eq!(ops_string, context.operations());
457 }
458
459 #[test]
460 fn test_clear_operations() {
461 let margins = create_test_margins();
462 let mut context = TextFlowContext::new(400.0, 600.0, margins);
463
464 context.write_wrapped("Test").unwrap();
465 assert!(!context.operations().is_empty());
466
467 context.clear();
468 assert!(context.operations().is_empty());
469 }
470
471 #[test]
472 fn test_page_dimensions() {
473 let margins = create_test_margins();
474 let context = TextFlowContext::new(400.0, 600.0, margins);
475
476 let (width, height) = context.page_dimensions();
477 assert_eq!(width, 400.0);
478 assert_eq!(height, 600.0);
479 }
480
481 #[test]
482 fn test_margins_access() {
483 let margins = create_test_margins();
484 let context = TextFlowContext::new(400.0, 600.0, margins);
485
486 let ctx_margins = context.margins();
487 assert_eq!(ctx_margins.left, 50.0);
488 assert_eq!(ctx_margins.right, 50.0);
489 assert_eq!(ctx_margins.top, 50.0);
490 assert_eq!(ctx_margins.bottom, 50.0);
491 }
492
493 #[test]
494 fn test_method_chaining() {
495 let margins = create_test_margins();
496 let mut context = TextFlowContext::new(400.0, 600.0, margins);
497
498 context
499 .set_font(Font::Courier, 10.0)
500 .set_line_height(1.5)
501 .set_alignment(TextAlign::Center)
502 .at(100.0, 200.0);
503
504 assert_eq!(context.current_font, Font::Courier);
505 assert_eq!(context.font_size, 10.0);
506 assert_eq!(context.line_height(), 1.5);
507 assert_eq!(context.alignment(), TextAlign::Center);
508 let (x, y) = context.cursor_position();
509 assert_eq!(x, 100.0);
510 assert_eq!(y, 200.0);
511 }
512
513 #[test]
514 fn test_text_align_debug() {
515 let align = TextAlign::Center;
516 let debug_str = format!("{align:?}");
517 assert_eq!(debug_str, "Center");
518 }
519
520 #[test]
521 fn test_text_align_clone() {
522 let align1 = TextAlign::Justified;
523 let align2 = align1;
524 assert_eq!(align1, align2);
525 }
526
527 #[test]
528 fn test_text_align_copy() {
529 let align1 = TextAlign::Right;
530 let align2 = align1; assert_eq!(align1, align2);
532
533 assert_eq!(align1, TextAlign::Right);
535 assert_eq!(align2, TextAlign::Right);
536 }
537
538 #[test]
539 fn test_write_wrapped_with_alignment_right() {
540 let margins = create_test_margins();
541 let mut context = TextFlowContext::new(400.0, 600.0, margins);
542
543 context.set_alignment(TextAlign::Right);
544 context.write_wrapped("Right aligned text").unwrap();
545
546 let ops = context.operations();
547 assert!(ops.contains("BT\n"));
548 assert!(ops.contains("ET\n"));
549 assert!(ops.contains("Td"));
551 }
552
553 #[test]
554 fn test_write_wrapped_with_alignment_center() {
555 let margins = create_test_margins();
556 let mut context = TextFlowContext::new(400.0, 600.0, margins);
557
558 context.set_alignment(TextAlign::Center);
559 context.write_wrapped("Centered text").unwrap();
560
561 let ops = context.operations();
562 assert!(ops.contains("BT\n"));
563 assert!(ops.contains("(Centered text) Tj"));
564 }
565
566 #[test]
567 fn test_write_wrapped_with_alignment_justified() {
568 let margins = create_test_margins();
569 let mut context = TextFlowContext::new(400.0, 600.0, margins);
570
571 context.set_alignment(TextAlign::Justified);
572 context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
574
575 let ops = context.operations();
576 assert!(ops.contains("BT\n"));
577 assert!(ops.contains("Tw") || ops.contains("0 Tw"));
579 }
580
581 #[test]
582 fn test_write_wrapped_empty_text() {
583 let margins = create_test_margins();
584 let mut context = TextFlowContext::new(400.0, 600.0, margins);
585
586 context.write_wrapped("").unwrap();
587
588 assert!(context.operations().is_empty());
590 }
591
592 #[test]
593 fn test_write_wrapped_whitespace_only() {
594 let margins = create_test_margins();
595 let mut context = TextFlowContext::new(400.0, 600.0, margins);
596
597 context.write_wrapped(" ").unwrap();
598
599 let ops = context.operations();
600 assert!(ops.contains("BT\n") || ops.is_empty());
602 }
603
604 #[test]
605 fn test_write_wrapped_special_characters() {
606 let margins = create_test_margins();
607 let mut context = TextFlowContext::new(400.0, 600.0, margins);
608
609 context
610 .write_wrapped("Text with (parentheses) and \\backslash\\")
611 .unwrap();
612
613 let ops = context.operations();
614 assert!(ops.contains("\\(parentheses\\)"));
616 assert!(ops.contains("\\\\backslash\\\\"));
617 }
618
619 #[test]
620 fn test_write_wrapped_newlines_tabs() {
621 let margins = create_test_margins();
622 let mut context = TextFlowContext::new(400.0, 600.0, margins);
623
624 context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
625
626 let ops = context.operations();
627 assert!(ops.contains("\\n") || ops.contains("\\t"));
629 }
630
631 #[test]
632 fn test_write_wrapped_very_long_word() {
633 let margins = create_test_margins();
634 let mut context = TextFlowContext::new(200.0, 600.0, margins); let long_word = "a".repeat(100);
637 context.write_wrapped(&long_word).unwrap();
638
639 let ops = context.operations();
640 assert!(ops.contains("BT\n"));
641 assert!(ops.contains(&long_word));
642 }
643
644 #[test]
645 fn test_write_wrapped_cursor_movement() {
646 let margins = create_test_margins();
647 let mut context = TextFlowContext::new(400.0, 600.0, margins);
648
649 let initial_y = context.cursor_y;
650
651 context.write_wrapped("Line 1").unwrap();
652 let y_after_line1 = context.cursor_y;
653
654 context.write_wrapped("Line 2").unwrap();
655 let y_after_line2 = context.cursor_y;
656
657 assert!(y_after_line1 < initial_y);
659 assert!(y_after_line2 < y_after_line1);
660 }
661
662 #[test]
663 fn test_write_paragraph_spacing() {
664 let margins = create_test_margins();
665 let mut context = TextFlowContext::new(400.0, 600.0, margins);
666
667 let initial_y = context.cursor_y;
668 context.write_paragraph("Paragraph 1").unwrap();
669 let y_after_p1 = context.cursor_y;
670
671 context.write_paragraph("Paragraph 2").unwrap();
672 let y_after_p2 = context.cursor_y;
673
674 let spacing1 = initial_y - y_after_p1;
676 let spacing2 = y_after_p1 - y_after_p2;
677
678 assert!(spacing1 > 0.0);
679 assert!(spacing2 > 0.0);
680 }
681
682 #[test]
683 fn test_multiple_newlines() {
684 let margins = create_test_margins();
685 let mut context = TextFlowContext::new(400.0, 600.0, margins);
686
687 let initial_y = context.cursor_y;
688
689 context.newline();
690 let y1 = context.cursor_y;
691
692 context.newline();
693 let y2 = context.cursor_y;
694
695 context.newline();
696 let y3 = context.cursor_y;
697
698 let spacing1 = initial_y - y1;
700 let spacing2 = y1 - y2;
701 let spacing3 = y2 - y3;
702
703 assert!((spacing1 - spacing2).abs() < 1e-10);
705 assert!((spacing2 - spacing3).abs() < 1e-10);
706 assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
707 }
708
709 #[test]
710 fn test_content_width_different_margins() {
711 let margins = Margins {
712 left: 30.0,
713 right: 70.0,
714 top: 40.0,
715 bottom: 60.0,
716 };
717 let context = TextFlowContext::new(500.0, 700.0, margins);
718
719 let content_width = context.content_width();
720 assert_eq!(content_width, 400.0); }
722
723 #[test]
724 fn test_custom_line_height() {
725 let margins = create_test_margins();
726 let mut context = TextFlowContext::new(400.0, 600.0, margins);
727
728 context.set_line_height(2.0);
729
730 let initial_y = context.cursor_y;
731 context.newline();
732 let y_after = context.cursor_y;
733
734 let spacing = initial_y - y_after;
735 assert_eq!(spacing, context.font_size * 2.0); }
737
738 #[test]
739 fn test_different_fonts() {
740 let margins = create_test_margins();
741 let mut context = TextFlowContext::new(400.0, 600.0, margins);
742
743 let fonts = vec![
744 Font::Helvetica,
745 Font::HelveticaBold,
746 Font::TimesRoman,
747 Font::TimesBold,
748 Font::Courier,
749 Font::CourierBold,
750 ];
751
752 for font in fonts {
753 context.clear();
754 let font_name = font.pdf_name();
755 context.set_font(font, 14.0);
756 context.write_wrapped("Test text").unwrap();
757
758 let ops = context.operations();
759 assert!(ops.contains(&format!("/{font_name} 14 Tf")));
760 }
761 }
762
763 #[test]
764 fn test_font_size_variations() {
765 let margins = create_test_margins();
766 let mut context = TextFlowContext::new(400.0, 600.0, margins);
767
768 let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
769
770 for size in sizes {
771 context.clear();
772 context.set_font(Font::Helvetica, size);
773 context.write_wrapped("Test").unwrap();
774
775 let ops = context.operations();
776 assert!(ops.contains(&format!("/Helvetica {size} Tf")));
777 }
778 }
779
780 #[test]
781 fn test_at_position_edge_cases() {
782 let margins = create_test_margins();
783 let mut context = TextFlowContext::new(400.0, 600.0, margins);
784
785 context.at(0.0, 0.0);
787 assert_eq!(context.cursor_position(), (0.0, 0.0));
788
789 context.at(-10.0, -20.0);
791 assert_eq!(context.cursor_position(), (-10.0, -20.0));
792
793 context.at(10000.0, 20000.0);
795 assert_eq!(context.cursor_position(), (10000.0, 20000.0));
796 }
797
798 #[test]
799 fn test_write_wrapped_with_narrow_content() {
800 let margins = Margins {
801 left: 190.0,
802 right: 190.0,
803 top: 50.0,
804 bottom: 50.0,
805 };
806 let mut context = TextFlowContext::new(400.0, 600.0, margins);
807
808 context
810 .write_wrapped("This text should wrap a lot")
811 .unwrap();
812
813 let ops = context.operations();
814 let bt_count = ops.matches("BT\n").count();
816 assert!(bt_count > 1);
817 }
818
819 #[test]
820 fn test_justified_text_single_word_line() {
821 let margins = create_test_margins();
822 let mut context = TextFlowContext::new(400.0, 600.0, margins);
823
824 context.set_alignment(TextAlign::Justified);
825 context.write_wrapped("SingleWord").unwrap();
826
827 let ops = context.operations();
828 assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
830 }
831
832 #[test]
833 fn test_justified_text_last_line() {
834 let margins = create_test_margins();
835 let mut context = TextFlowContext::new(400.0, 600.0, margins);
836
837 context.set_alignment(TextAlign::Justified);
838 context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
840
841 let ops = context.operations();
842 assert!(ops.contains("0 Tw"));
844 }
845
846 #[test]
847 fn test_generate_operations_encoding() {
848 let margins = create_test_margins();
849 let mut context = TextFlowContext::new(400.0, 600.0, margins);
850
851 context.write_wrapped("UTF-8 Text: Ñ").unwrap();
852
853 let ops_bytes = context.generate_operations();
854 let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
855
856 assert_eq!(ops_bytes, context.operations().as_bytes());
857 assert_eq!(ops_string, context.operations());
858 }
859
860 #[test]
861 fn test_clear_resets_operations_only() {
862 let margins = create_test_margins();
863 let mut context = TextFlowContext::new(400.0, 600.0, margins);
864
865 context.set_font(Font::TimesBold, 18.0);
866 context.set_alignment(TextAlign::Right);
867 context.at(100.0, 200.0);
868 context.write_wrapped("Text").unwrap();
869
870 context.clear();
871
872 assert!(context.operations().is_empty());
874
875 assert_eq!(context.current_font, Font::TimesBold);
877 assert_eq!(context.font_size, 18.0);
878 assert_eq!(context.alignment(), TextAlign::Right);
879 let (x, y) = context.cursor_position();
881 assert_eq!(x, 100.0); assert!(y < 200.0); }
884
885 #[test]
886 fn test_long_text_wrapping() {
887 let margins = create_test_margins();
888 let mut context = TextFlowContext::new(400.0, 600.0, margins);
889
890 let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
891 Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
892 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
893
894 context.write_wrapped(long_text).unwrap();
895
896 let ops = context.operations();
897 let tj_count = ops.matches(") Tj").count();
899 assert!(tj_count > 1);
900 }
901
902 #[test]
903 fn test_empty_operations_initially() {
904 let margins = create_test_margins();
905 let context = TextFlowContext::new(400.0, 600.0, margins);
906
907 assert!(context.operations().is_empty());
908 assert_eq!(context.generate_operations().len(), 0);
909 }
910
911 #[test]
912 fn test_write_paragraph_empty() {
913 let margins = create_test_margins();
914 let mut context = TextFlowContext::new(400.0, 600.0, margins);
915
916 let initial_y = context.cursor_y;
917 context.write_paragraph("").unwrap();
918
919 assert!(context.cursor_y < initial_y);
921 }
922
923 #[test]
924 fn test_extreme_line_height() {
925 let margins = create_test_margins();
926 let mut context = TextFlowContext::new(400.0, 600.0, margins);
927
928 context.set_line_height(0.1);
930 let initial_y = context.cursor_y;
931 context.newline();
932 assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
933
934 context.set_line_height(10.0);
936 let initial_y2 = context.cursor_y;
937 context.newline();
938 assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
939 }
940
941 #[test]
942 fn test_zero_content_width() {
943 let margins = Margins {
944 left: 200.0,
945 right: 200.0,
946 top: 50.0,
947 bottom: 50.0,
948 };
949 let context = TextFlowContext::new(400.0, 600.0, margins);
950
951 assert_eq!(context.content_width(), 0.0);
952 }
953
954 #[test]
955 fn test_cursor_x_reset_on_newline() {
956 let margins = create_test_margins();
957 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
958
959 context.at(250.0, 300.0); context.newline();
961
962 assert_eq!(context.cursor_x, margins.left);
964 assert_eq!(
966 context.cursor_y,
967 300.0 - context.font_size * context.line_height
968 );
969 }
970
971 #[test]
974 fn test_available_width_respects_cursor_x() {
975 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
978
979 assert_eq!(context.available_width(), 300.0);
981
982 context.at(200.0, 500.0);
984 assert_eq!(context.available_width(), 150.0);
985 }
986
987 #[test]
988 fn test_available_width_clamps_to_zero() {
989 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
992
993 context.at(380.0, 500.0);
995 assert_eq!(context.available_width(), 0.0);
996 }
997
998 #[test]
999 fn test_write_wrapped_at_x_limits_available_width() {
1000 let margins = create_test_margins();
1004 let mut context = TextFlowContext::new(400.0, 600.0, margins);
1005
1006 context.set_font(Font::Helvetica, 12.0);
1007 context.at(250.0, 500.0);
1009 context.write_wrapped("Hello World Hello World").unwrap();
1010
1011 let ops = context.operations();
1012 let bt_count = ops.matches("BT\n").count();
1014 assert!(
1015 bt_count > 1,
1016 "Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
1017 );
1018 }
1019
1020 #[test]
1021 fn test_write_wrapped_respects_cursor_x_offset() {
1022 let margins = Margins {
1024 left: 50.0,
1025 right: 50.0,
1026 top: 50.0,
1027 bottom: 50.0,
1028 };
1029 let mut context = TextFlowContext::new(600.0, 800.0, margins);
1030
1031 context.set_font(Font::Helvetica, 12.0);
1032 context.at(300.0, 700.0);
1033 context
1034 .write_wrapped("Hello World Foo Bar Baz Qux")
1035 .unwrap();
1036
1037 let ops = context.operations();
1038 for line in ops.lines() {
1040 if line.ends_with(" Td") {
1041 let parts: Vec<&str> = line.split_whitespace().collect();
1042 if parts.len() >= 3 {
1043 let x: f64 = parts[0].parse().expect("Td x should be a number");
1044 assert!(
1045 x >= 300.0 - 1e-6,
1046 "Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
1047 );
1048 }
1049 }
1050 }
1051 }
1052}