Skip to main content

oxidize_pdf/text/
flow.rs

1use crate::error::Result;
2use crate::page::Margins;
3use crate::text::{measure_text, split_into_words, Font};
4use std::fmt::Write;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum TextAlign {
8    Left,
9    Right,
10    Center,
11    Justified,
12}
13
14pub struct TextFlowContext {
15    operations: String,
16    current_font: Font,
17    font_size: f64,
18    line_height: f64,
19    cursor_x: f64,
20    cursor_y: f64,
21    alignment: TextAlign,
22    page_width: f64,
23    #[allow(dead_code)]
24    page_height: f64,
25    margins: Margins,
26}
27
28impl TextFlowContext {
29    pub fn new(page_width: f64, page_height: f64, margins: Margins) -> Self {
30        Self {
31            operations: String::new(),
32            current_font: Font::Helvetica,
33            font_size: 12.0,
34            line_height: 1.2,
35            cursor_x: margins.left,
36            cursor_y: page_height - margins.top,
37            alignment: TextAlign::Left,
38            page_width,
39            page_height,
40            margins,
41        }
42    }
43
44    pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
45        self.current_font = font;
46        self.font_size = size;
47        self
48    }
49
50    pub fn set_line_height(&mut self, multiplier: f64) -> &mut Self {
51        self.line_height = multiplier;
52        self
53    }
54
55    pub fn set_alignment(&mut self, alignment: TextAlign) -> &mut Self {
56        self.alignment = alignment;
57        self
58    }
59
60    pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
61        self.cursor_x = x;
62        self.cursor_y = y;
63        self
64    }
65
66    pub fn content_width(&self) -> f64 {
67        self.page_width - self.margins.left - self.margins.right
68    }
69
70    /// Returns the width available for text starting at the current cursor_x position.
71    ///
72    /// Unlike `content_width()` which always uses `margins.left` as the origin,
73    /// `available_width()` accounts for the actual cursor position so that text
74    /// placed via `.at(x, y)` does not overflow the right margin.
75    pub fn available_width(&self) -> f64 {
76        (self.page_width - self.margins.right - self.cursor_x).max(0.0)
77    }
78
79    pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
80        let start_x = self.cursor_x;
81        let available_width = self.available_width();
82
83        // Split text into words
84        let words = split_into_words(text);
85        let mut lines: Vec<Vec<&str>> = Vec::new();
86        let mut current_line: Vec<&str> = Vec::new();
87        let mut current_width = 0.0;
88
89        // Build lines based on available width (respects cursor_x offset)
90        for word in words {
91            let word_width = measure_text(word, &self.current_font, self.font_size);
92
93            // Check if we need to start a new line
94            if !current_line.is_empty() && current_width + word_width > available_width {
95                lines.push(current_line);
96                current_line = vec![word];
97                current_width = word_width;
98            } else {
99                current_line.push(word);
100                current_width += word_width;
101            }
102        }
103
104        if !current_line.is_empty() {
105            lines.push(current_line);
106        }
107
108        // Render each line
109        for (i, line) in lines.iter().enumerate() {
110            let line_text = line.join("");
111            let line_width = measure_text(&line_text, &self.current_font, self.font_size);
112
113            // Calculate x position based on alignment.
114            // start_x is the column where this block of text begins (set via .at()).
115            // Left/Justified start at start_x; Center is relative to start_x;
116            // Right stays anchored to the right margin.
117            let x = match self.alignment {
118                TextAlign::Left => start_x,
119                TextAlign::Right => self.page_width - self.margins.right - line_width,
120                TextAlign::Center => start_x + (available_width - line_width) / 2.0,
121                TextAlign::Justified => start_x,
122            };
123
124            // Begin text object
125            self.operations.push_str("BT\n");
126
127            // Set font
128            writeln!(
129                &mut self.operations,
130                "/{} {} Tf",
131                self.current_font.pdf_name(),
132                self.font_size
133            )
134            .expect("Writing to String should never fail");
135
136            // Set text position
137            writeln!(&mut self.operations, "{:.2} {:.2} Td", x, self.cursor_y)
138                .expect("Writing to String should never fail");
139
140            // Handle justification
141            if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
142                // Calculate extra space to distribute
143                let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
144                if spaces_count > 0 {
145                    let extra_space = available_width - line_width;
146                    let space_adjustment = extra_space / spaces_count as f64;
147
148                    // Set word spacing
149                    writeln!(&mut self.operations, "{space_adjustment:.2} Tw")
150                        .expect("Writing to String should never fail");
151                }
152            }
153
154            // Show text
155            self.operations.push('(');
156            for ch in line_text.chars() {
157                match ch {
158                    '(' => self.operations.push_str("\\("),
159                    ')' => self.operations.push_str("\\)"),
160                    '\\' => self.operations.push_str("\\\\"),
161                    '\n' => self.operations.push_str("\\n"),
162                    '\r' => self.operations.push_str("\\r"),
163                    '\t' => self.operations.push_str("\\t"),
164                    _ => self.operations.push(ch),
165                }
166            }
167            self.operations.push_str(") Tj\n");
168
169            // Reset word spacing if it was set
170            if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
171                self.operations.push_str("0 Tw\n");
172            }
173
174            // End text object
175            self.operations.push_str("ET\n");
176
177            // Move cursor down for next line
178            self.cursor_y -= self.font_size * self.line_height;
179        }
180
181        Ok(self)
182    }
183
184    pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
185        self.write_wrapped(text)?;
186        // Add extra space after paragraph
187        self.cursor_y -= self.font_size * self.line_height * 0.5;
188        Ok(self)
189    }
190
191    pub fn newline(&mut self) -> &mut Self {
192        self.cursor_y -= self.font_size * self.line_height;
193        self.cursor_x = self.margins.left;
194        self
195    }
196
197    pub fn cursor_position(&self) -> (f64, f64) {
198        (self.cursor_x, self.cursor_y)
199    }
200
201    pub fn generate_operations(&self) -> Vec<u8> {
202        self.operations.as_bytes().to_vec()
203    }
204
205    /// Get the current alignment
206    pub fn alignment(&self) -> TextAlign {
207        self.alignment
208    }
209
210    /// Get the page dimensions
211    pub fn page_dimensions(&self) -> (f64, f64) {
212        (self.page_width, self.page_height)
213    }
214
215    /// Get the margins
216    pub fn margins(&self) -> &Margins {
217        &self.margins
218    }
219
220    /// Get current line height multiplier
221    pub fn line_height(&self) -> f64 {
222        self.line_height
223    }
224
225    /// Get the operations string
226    pub fn operations(&self) -> &str {
227        &self.operations
228    }
229
230    /// Clear all operations
231    pub fn clear(&mut self) {
232        self.operations.clear();
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::page::Margins;
240
241    fn create_test_margins() -> Margins {
242        Margins {
243            left: 50.0,
244            right: 50.0,
245            top: 50.0,
246            bottom: 50.0,
247        }
248    }
249
250    #[test]
251    fn test_text_flow_context_new() {
252        let margins = create_test_margins();
253        let context = TextFlowContext::new(400.0, 600.0, margins);
254
255        assert_eq!(context.current_font, Font::Helvetica);
256        assert_eq!(context.font_size, 12.0);
257        assert_eq!(context.line_height, 1.2);
258        assert_eq!(context.alignment, TextAlign::Left);
259        assert_eq!(context.page_width, 400.0);
260        assert_eq!(context.page_height, 600.0);
261        assert_eq!(context.cursor_x, 50.0); // margins.left
262        assert_eq!(context.cursor_y, 550.0); // page_height - margins.top
263    }
264
265    #[test]
266    fn test_set_font() {
267        let margins = create_test_margins();
268        let mut context = TextFlowContext::new(400.0, 600.0, margins);
269
270        context.set_font(Font::TimesBold, 16.0);
271        assert_eq!(context.current_font, Font::TimesBold);
272        assert_eq!(context.font_size, 16.0);
273    }
274
275    #[test]
276    fn test_set_line_height() {
277        let margins = create_test_margins();
278        let mut context = TextFlowContext::new(400.0, 600.0, margins);
279
280        context.set_line_height(1.5);
281        assert_eq!(context.line_height(), 1.5);
282    }
283
284    #[test]
285    fn test_set_alignment() {
286        let margins = create_test_margins();
287        let mut context = TextFlowContext::new(400.0, 600.0, margins);
288
289        context.set_alignment(TextAlign::Center);
290        assert_eq!(context.alignment(), TextAlign::Center);
291    }
292
293    #[test]
294    fn test_at_position() {
295        let margins = create_test_margins();
296        let mut context = TextFlowContext::new(400.0, 600.0, margins);
297
298        context.at(100.0, 200.0);
299        let (x, y) = context.cursor_position();
300        assert_eq!(x, 100.0);
301        assert_eq!(y, 200.0);
302    }
303
304    #[test]
305    fn test_content_width() {
306        let margins = create_test_margins();
307        let context = TextFlowContext::new(400.0, 600.0, margins);
308
309        let content_width = context.content_width();
310        assert_eq!(content_width, 300.0); // 400 - 50 - 50
311    }
312
313    #[test]
314    fn test_text_align_variants() {
315        assert_eq!(TextAlign::Left, TextAlign::Left);
316        assert_eq!(TextAlign::Right, TextAlign::Right);
317        assert_eq!(TextAlign::Center, TextAlign::Center);
318        assert_eq!(TextAlign::Justified, TextAlign::Justified);
319
320        assert_ne!(TextAlign::Left, TextAlign::Right);
321    }
322
323    #[test]
324    fn test_write_wrapped_simple() {
325        let margins = create_test_margins();
326        let mut context = TextFlowContext::new(400.0, 600.0, margins);
327
328        context.write_wrapped("Hello World").unwrap();
329
330        let ops = context.operations();
331        assert!(ops.contains("BT\n"));
332        assert!(ops.contains("ET\n"));
333        assert!(ops.contains("/Helvetica 12 Tf"));
334        assert!(ops.contains("(Hello World) Tj"));
335    }
336
337    #[test]
338    fn test_write_paragraph() {
339        let margins = create_test_margins();
340        let mut context = TextFlowContext::new(400.0, 600.0, margins);
341
342        let initial_y = context.cursor_y;
343        context.write_paragraph("Test paragraph").unwrap();
344
345        // Y position should have moved down more than just line height
346        assert!(context.cursor_y < initial_y);
347    }
348
349    #[test]
350    fn test_newline() {
351        let margins = create_test_margins();
352        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
353
354        let initial_y = context.cursor_y;
355        context.newline();
356
357        assert_eq!(context.cursor_x, margins.left);
358        assert!(context.cursor_y < initial_y);
359        assert_eq!(
360            context.cursor_y,
361            initial_y - context.font_size * context.line_height
362        );
363    }
364
365    #[test]
366    fn test_cursor_position() {
367        let margins = create_test_margins();
368        let mut context = TextFlowContext::new(400.0, 600.0, margins);
369
370        context.at(75.0, 125.0);
371        let (x, y) = context.cursor_position();
372        assert_eq!(x, 75.0);
373        assert_eq!(y, 125.0);
374    }
375
376    #[test]
377    fn test_generate_operations() {
378        let margins = create_test_margins();
379        let mut context = TextFlowContext::new(400.0, 600.0, margins);
380
381        context.write_wrapped("Test").unwrap();
382        let ops_bytes = context.generate_operations();
383        let ops_string = String::from_utf8(ops_bytes).unwrap();
384
385        assert_eq!(ops_string, context.operations());
386    }
387
388    #[test]
389    fn test_clear_operations() {
390        let margins = create_test_margins();
391        let mut context = TextFlowContext::new(400.0, 600.0, margins);
392
393        context.write_wrapped("Test").unwrap();
394        assert!(!context.operations().is_empty());
395
396        context.clear();
397        assert!(context.operations().is_empty());
398    }
399
400    #[test]
401    fn test_page_dimensions() {
402        let margins = create_test_margins();
403        let context = TextFlowContext::new(400.0, 600.0, margins);
404
405        let (width, height) = context.page_dimensions();
406        assert_eq!(width, 400.0);
407        assert_eq!(height, 600.0);
408    }
409
410    #[test]
411    fn test_margins_access() {
412        let margins = create_test_margins();
413        let context = TextFlowContext::new(400.0, 600.0, margins);
414
415        let ctx_margins = context.margins();
416        assert_eq!(ctx_margins.left, 50.0);
417        assert_eq!(ctx_margins.right, 50.0);
418        assert_eq!(ctx_margins.top, 50.0);
419        assert_eq!(ctx_margins.bottom, 50.0);
420    }
421
422    #[test]
423    fn test_method_chaining() {
424        let margins = create_test_margins();
425        let mut context = TextFlowContext::new(400.0, 600.0, margins);
426
427        context
428            .set_font(Font::Courier, 10.0)
429            .set_line_height(1.5)
430            .set_alignment(TextAlign::Center)
431            .at(100.0, 200.0);
432
433        assert_eq!(context.current_font, Font::Courier);
434        assert_eq!(context.font_size, 10.0);
435        assert_eq!(context.line_height(), 1.5);
436        assert_eq!(context.alignment(), TextAlign::Center);
437        let (x, y) = context.cursor_position();
438        assert_eq!(x, 100.0);
439        assert_eq!(y, 200.0);
440    }
441
442    #[test]
443    fn test_text_align_debug() {
444        let align = TextAlign::Center;
445        let debug_str = format!("{align:?}");
446        assert_eq!(debug_str, "Center");
447    }
448
449    #[test]
450    fn test_text_align_clone() {
451        let align1 = TextAlign::Justified;
452        let align2 = align1;
453        assert_eq!(align1, align2);
454    }
455
456    #[test]
457    fn test_text_align_copy() {
458        let align1 = TextAlign::Right;
459        let align2 = align1; // Copy semantics
460        assert_eq!(align1, align2);
461
462        // Both variables should still be usable
463        assert_eq!(align1, TextAlign::Right);
464        assert_eq!(align2, TextAlign::Right);
465    }
466
467    #[test]
468    fn test_write_wrapped_with_alignment_right() {
469        let margins = create_test_margins();
470        let mut context = TextFlowContext::new(400.0, 600.0, margins);
471
472        context.set_alignment(TextAlign::Right);
473        context.write_wrapped("Right aligned text").unwrap();
474
475        let ops = context.operations();
476        assert!(ops.contains("BT\n"));
477        assert!(ops.contains("ET\n"));
478        // Right alignment should position text differently
479        assert!(ops.contains("Td"));
480    }
481
482    #[test]
483    fn test_write_wrapped_with_alignment_center() {
484        let margins = create_test_margins();
485        let mut context = TextFlowContext::new(400.0, 600.0, margins);
486
487        context.set_alignment(TextAlign::Center);
488        context.write_wrapped("Centered text").unwrap();
489
490        let ops = context.operations();
491        assert!(ops.contains("BT\n"));
492        assert!(ops.contains("(Centered text) Tj"));
493    }
494
495    #[test]
496    fn test_write_wrapped_with_alignment_justified() {
497        let margins = create_test_margins();
498        let mut context = TextFlowContext::new(400.0, 600.0, margins);
499
500        context.set_alignment(TextAlign::Justified);
501        // Long text that will wrap and justify
502        context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
503
504        let ops = context.operations();
505        assert!(ops.contains("BT\n"));
506        // Justified text may have word spacing adjustments
507        assert!(ops.contains("Tw") || ops.contains("0 Tw"));
508    }
509
510    #[test]
511    fn test_write_wrapped_empty_text() {
512        let margins = create_test_margins();
513        let mut context = TextFlowContext::new(400.0, 600.0, margins);
514
515        context.write_wrapped("").unwrap();
516
517        // Empty text should not generate operations
518        assert!(context.operations().is_empty());
519    }
520
521    #[test]
522    fn test_write_wrapped_whitespace_only() {
523        let margins = create_test_margins();
524        let mut context = TextFlowContext::new(400.0, 600.0, margins);
525
526        context.write_wrapped("   ").unwrap();
527
528        let ops = context.operations();
529        // Should handle whitespace-only text
530        assert!(ops.contains("BT\n") || ops.is_empty());
531    }
532
533    #[test]
534    fn test_write_wrapped_special_characters() {
535        let margins = create_test_margins();
536        let mut context = TextFlowContext::new(400.0, 600.0, margins);
537
538        context
539            .write_wrapped("Text with (parentheses) and \\backslash\\")
540            .unwrap();
541
542        let ops = context.operations();
543        // Special characters should be escaped
544        assert!(ops.contains("\\(parentheses\\)"));
545        assert!(ops.contains("\\\\backslash\\\\"));
546    }
547
548    #[test]
549    fn test_write_wrapped_newlines_tabs() {
550        let margins = create_test_margins();
551        let mut context = TextFlowContext::new(400.0, 600.0, margins);
552
553        context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
554
555        let ops = context.operations();
556        // Newlines and tabs should be escaped
557        assert!(ops.contains("\\n") || ops.contains("\\t"));
558    }
559
560    #[test]
561    fn test_write_wrapped_very_long_word() {
562        let margins = create_test_margins();
563        let mut context = TextFlowContext::new(200.0, 600.0, margins); // Narrow page
564
565        let long_word = "a".repeat(100);
566        context.write_wrapped(&long_word).unwrap();
567
568        let ops = context.operations();
569        assert!(ops.contains("BT\n"));
570        assert!(ops.contains(&long_word));
571    }
572
573    #[test]
574    fn test_write_wrapped_cursor_movement() {
575        let margins = create_test_margins();
576        let mut context = TextFlowContext::new(400.0, 600.0, margins);
577
578        let initial_y = context.cursor_y;
579
580        context.write_wrapped("Line 1").unwrap();
581        let y_after_line1 = context.cursor_y;
582
583        context.write_wrapped("Line 2").unwrap();
584        let y_after_line2 = context.cursor_y;
585
586        // Cursor should move down after each line
587        assert!(y_after_line1 < initial_y);
588        assert!(y_after_line2 < y_after_line1);
589    }
590
591    #[test]
592    fn test_write_paragraph_spacing() {
593        let margins = create_test_margins();
594        let mut context = TextFlowContext::new(400.0, 600.0, margins);
595
596        let initial_y = context.cursor_y;
597        context.write_paragraph("Paragraph 1").unwrap();
598        let y_after_p1 = context.cursor_y;
599
600        context.write_paragraph("Paragraph 2").unwrap();
601        let y_after_p2 = context.cursor_y;
602
603        // Paragraphs should have extra spacing
604        let spacing1 = initial_y - y_after_p1;
605        let spacing2 = y_after_p1 - y_after_p2;
606
607        assert!(spacing1 > 0.0);
608        assert!(spacing2 > 0.0);
609    }
610
611    #[test]
612    fn test_multiple_newlines() {
613        let margins = create_test_margins();
614        let mut context = TextFlowContext::new(400.0, 600.0, margins);
615
616        let initial_y = context.cursor_y;
617
618        context.newline();
619        let y1 = context.cursor_y;
620
621        context.newline();
622        let y2 = context.cursor_y;
623
624        context.newline();
625        let y3 = context.cursor_y;
626
627        // Each newline should move cursor down by same amount
628        let spacing1 = initial_y - y1;
629        let spacing2 = y1 - y2;
630        let spacing3 = y2 - y3;
631
632        // Use approximate equality for floating point comparisons
633        assert!((spacing1 - spacing2).abs() < 1e-10);
634        assert!((spacing2 - spacing3).abs() < 1e-10);
635        assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
636    }
637
638    #[test]
639    fn test_content_width_different_margins() {
640        let margins = Margins {
641            left: 30.0,
642            right: 70.0,
643            top: 40.0,
644            bottom: 60.0,
645        };
646        let context = TextFlowContext::new(500.0, 700.0, margins);
647
648        let content_width = context.content_width();
649        assert_eq!(content_width, 400.0); // 500 - 30 - 70
650    }
651
652    #[test]
653    fn test_custom_line_height() {
654        let margins = create_test_margins();
655        let mut context = TextFlowContext::new(400.0, 600.0, margins);
656
657        context.set_line_height(2.0);
658
659        let initial_y = context.cursor_y;
660        context.newline();
661        let y_after = context.cursor_y;
662
663        let spacing = initial_y - y_after;
664        assert_eq!(spacing, context.font_size * 2.0); // line_height = 2.0
665    }
666
667    #[test]
668    fn test_different_fonts() {
669        let margins = create_test_margins();
670        let mut context = TextFlowContext::new(400.0, 600.0, margins);
671
672        let fonts = vec![
673            Font::Helvetica,
674            Font::HelveticaBold,
675            Font::TimesRoman,
676            Font::TimesBold,
677            Font::Courier,
678            Font::CourierBold,
679        ];
680
681        for font in fonts {
682            context.clear();
683            let font_name = font.pdf_name();
684            context.set_font(font, 14.0);
685            context.write_wrapped("Test text").unwrap();
686
687            let ops = context.operations();
688            assert!(ops.contains(&format!("/{font_name} 14 Tf")));
689        }
690    }
691
692    #[test]
693    fn test_font_size_variations() {
694        let margins = create_test_margins();
695        let mut context = TextFlowContext::new(400.0, 600.0, margins);
696
697        let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
698
699        for size in sizes {
700            context.clear();
701            context.set_font(Font::Helvetica, size);
702            context.write_wrapped("Test").unwrap();
703
704            let ops = context.operations();
705            assert!(ops.contains(&format!("/Helvetica {size} Tf")));
706        }
707    }
708
709    #[test]
710    fn test_at_position_edge_cases() {
711        let margins = create_test_margins();
712        let mut context = TextFlowContext::new(400.0, 600.0, margins);
713
714        // Test zero position
715        context.at(0.0, 0.0);
716        assert_eq!(context.cursor_position(), (0.0, 0.0));
717
718        // Test negative position
719        context.at(-10.0, -20.0);
720        assert_eq!(context.cursor_position(), (-10.0, -20.0));
721
722        // Test large position
723        context.at(10000.0, 20000.0);
724        assert_eq!(context.cursor_position(), (10000.0, 20000.0));
725    }
726
727    #[test]
728    fn test_write_wrapped_with_narrow_content() {
729        let margins = Margins {
730            left: 190.0,
731            right: 190.0,
732            top: 50.0,
733            bottom: 50.0,
734        };
735        let mut context = TextFlowContext::new(400.0, 600.0, margins);
736
737        // Content width is only 20.0 units
738        context
739            .write_wrapped("This text should wrap a lot")
740            .unwrap();
741
742        let ops = context.operations();
743        // Should have multiple text objects for wrapped lines
744        let bt_count = ops.matches("BT\n").count();
745        assert!(bt_count > 1);
746    }
747
748    #[test]
749    fn test_justified_text_single_word_line() {
750        let margins = create_test_margins();
751        let mut context = TextFlowContext::new(400.0, 600.0, margins);
752
753        context.set_alignment(TextAlign::Justified);
754        context.write_wrapped("SingleWord").unwrap();
755
756        let ops = context.operations();
757        // Single word lines should not have word spacing
758        assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
759    }
760
761    #[test]
762    fn test_justified_text_last_line() {
763        let margins = create_test_margins();
764        let mut context = TextFlowContext::new(400.0, 600.0, margins);
765
766        context.set_alignment(TextAlign::Justified);
767        // Text that will create multiple lines
768        context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
769
770        let ops = context.operations();
771        // Should reset word spacing (0 Tw) for last line
772        assert!(ops.contains("0 Tw"));
773    }
774
775    #[test]
776    fn test_generate_operations_encoding() {
777        let margins = create_test_margins();
778        let mut context = TextFlowContext::new(400.0, 600.0, margins);
779
780        context.write_wrapped("UTF-8 Text: Ñ").unwrap();
781
782        let ops_bytes = context.generate_operations();
783        let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
784
785        assert_eq!(ops_bytes, context.operations().as_bytes());
786        assert_eq!(ops_string, context.operations());
787    }
788
789    #[test]
790    fn test_clear_resets_operations_only() {
791        let margins = create_test_margins();
792        let mut context = TextFlowContext::new(400.0, 600.0, margins);
793
794        context.set_font(Font::TimesBold, 18.0);
795        context.set_alignment(TextAlign::Right);
796        context.at(100.0, 200.0);
797        context.write_wrapped("Text").unwrap();
798
799        context.clear();
800
801        // Operations should be cleared
802        assert!(context.operations().is_empty());
803
804        // But other settings should remain
805        assert_eq!(context.current_font, Font::TimesBold);
806        assert_eq!(context.font_size, 18.0);
807        assert_eq!(context.alignment(), TextAlign::Right);
808        // Cursor position should reflect where we are after writing text (moved down by line height)
809        let (x, y) = context.cursor_position();
810        assert_eq!(x, 100.0); // X position should be unchanged
811        assert!(y < 200.0); // Y position should have moved down after writing text
812    }
813
814    #[test]
815    fn test_long_text_wrapping() {
816        let margins = create_test_margins();
817        let mut context = TextFlowContext::new(400.0, 600.0, margins);
818
819        let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
820                        Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
821                        Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
822
823        context.write_wrapped(long_text).unwrap();
824
825        let ops = context.operations();
826        // Should have multiple lines
827        let tj_count = ops.matches(") Tj").count();
828        assert!(tj_count > 1);
829    }
830
831    #[test]
832    fn test_empty_operations_initially() {
833        let margins = create_test_margins();
834        let context = TextFlowContext::new(400.0, 600.0, margins);
835
836        assert!(context.operations().is_empty());
837        assert_eq!(context.generate_operations().len(), 0);
838    }
839
840    #[test]
841    fn test_write_paragraph_empty() {
842        let margins = create_test_margins();
843        let mut context = TextFlowContext::new(400.0, 600.0, margins);
844
845        let initial_y = context.cursor_y;
846        context.write_paragraph("").unwrap();
847
848        // Empty paragraph should still add spacing
849        assert!(context.cursor_y < initial_y);
850    }
851
852    #[test]
853    fn test_extreme_line_height() {
854        let margins = create_test_margins();
855        let mut context = TextFlowContext::new(400.0, 600.0, margins);
856
857        // Very small line height
858        context.set_line_height(0.1);
859        let initial_y = context.cursor_y;
860        context.newline();
861        assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
862
863        // Very large line height
864        context.set_line_height(10.0);
865        let initial_y2 = context.cursor_y;
866        context.newline();
867        assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
868    }
869
870    #[test]
871    fn test_zero_content_width() {
872        let margins = Margins {
873            left: 200.0,
874            right: 200.0,
875            top: 50.0,
876            bottom: 50.0,
877        };
878        let context = TextFlowContext::new(400.0, 600.0, margins);
879
880        assert_eq!(context.content_width(), 0.0);
881    }
882
883    #[test]
884    fn test_cursor_x_reset_on_newline() {
885        let margins = create_test_margins();
886        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
887
888        context.at(250.0, 300.0); // Move cursor to custom position
889        context.newline();
890
891        // X should reset to left margin
892        assert_eq!(context.cursor_x, margins.left);
893        // Y should decrease by line height
894        assert_eq!(
895            context.cursor_y,
896            300.0 - context.font_size * context.line_height
897        );
898    }
899
900    // --- Issue #167: available_width respects cursor_x ---
901
902    #[test]
903    fn test_available_width_respects_cursor_x() {
904        // Page: 400pt wide, 50pt margins each side → content_width = 300pt
905        let margins = create_test_margins(); // left=50, right=50
906        let mut context = TextFlowContext::new(400.0, 600.0, margins);
907
908        // Default: cursor_x == margins.left == 50, available_width == 300
909        assert_eq!(context.available_width(), 300.0);
910
911        // After .at(200, 500): cursor_x = 200, available_width = 400 - 50 - 200 = 150
912        context.at(200.0, 500.0);
913        assert_eq!(context.available_width(), 150.0);
914    }
915
916    #[test]
917    fn test_available_width_clamps_to_zero() {
918        // cursor_x past the right margin → available_width = 0 (not negative)
919        let margins = create_test_margins(); // right = 50
920        let mut context = TextFlowContext::new(400.0, 600.0, margins);
921
922        // cursor_x = 380, right margin = 50 → would be 400-50-380 = -30 → clamp to 0
923        context.at(380.0, 500.0);
924        assert_eq!(context.available_width(), 0.0);
925    }
926
927    #[test]
928    fn test_write_wrapped_at_x_limits_available_width() {
929        // Page 400pt, margins 50pt each → content_width = 300pt
930        // Place cursor at x=250: available_width = 400-50-250 = 100pt
931        // Use text wider than 100pt but narrower than 300pt → must wrap at x=250
932        let margins = create_test_margins();
933        let mut context = TextFlowContext::new(400.0, 600.0, margins);
934
935        context.set_font(Font::Helvetica, 12.0);
936        // "Hello World Hello World" at 12pt Helvetica exceeds 100pt easily
937        context.at(250.0, 500.0);
938        context.write_wrapped("Hello World Hello World").unwrap();
939
940        let ops = context.operations();
941        // Multiple BT blocks → wrapping occurred
942        let bt_count = ops.matches("BT\n").count();
943        assert!(
944            bt_count > 1,
945            "Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
946        );
947    }
948
949    #[test]
950    fn test_write_wrapped_respects_cursor_x_offset() {
951        // Cursor at x=300, page 600pt wide, margins 50pt each → available_width = 250pt
952        let margins = Margins {
953            left: 50.0,
954            right: 50.0,
955            top: 50.0,
956            bottom: 50.0,
957        };
958        let mut context = TextFlowContext::new(600.0, 800.0, margins);
959
960        context.set_font(Font::Helvetica, 12.0);
961        context.at(300.0, 700.0);
962        context
963            .write_wrapped("Hello World Foo Bar Baz Qux")
964            .unwrap();
965
966        let ops = context.operations();
967        // Every Td x-coordinate should be >= 300.0
968        for line in ops.lines() {
969            if line.ends_with(" Td") {
970                let parts: Vec<&str> = line.split_whitespace().collect();
971                if parts.len() >= 3 {
972                    let x: f64 = parts[0].parse().expect("Td x should be a number");
973                    assert!(
974                        x >= 300.0 - 1e-6,
975                        "Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
976                    );
977                }
978            }
979        }
980    }
981}