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    pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
71        let content_width = self.content_width();
72
73        // Split text into words
74        let words = split_into_words(text);
75        let mut lines: Vec<Vec<&str>> = Vec::new();
76        let mut current_line: Vec<&str> = Vec::new();
77        let mut current_width = 0.0;
78
79        // Build lines based on width constraints
80        for word in words {
81            let word_width = measure_text(word, self.current_font, self.font_size);
82
83            // Check if we need to start a new line
84            if !current_line.is_empty() && current_width + word_width > content_width {
85                lines.push(current_line);
86                current_line = vec![word];
87                current_width = word_width;
88            } else {
89                current_line.push(word);
90                current_width += word_width;
91            }
92        }
93
94        if !current_line.is_empty() {
95            lines.push(current_line);
96        }
97
98        // Render each line
99        for (i, line) in lines.iter().enumerate() {
100            let line_text = line.join("");
101            let line_width = measure_text(&line_text, self.current_font, self.font_size);
102
103            // Calculate x position based on alignment
104            let x = match self.alignment {
105                TextAlign::Left => self.margins.left,
106                TextAlign::Right => self.page_width - self.margins.right - line_width,
107                TextAlign::Center => self.margins.left + (content_width - line_width) / 2.0,
108                TextAlign::Justified => {
109                    if i < lines.len() - 1 && line.len() > 1 {
110                        // We'll handle justification below
111                        self.margins.left
112                    } else {
113                        self.margins.left
114                    }
115                }
116            };
117
118            // Begin text object
119            self.operations.push_str("BT\n");
120
121            // Set font
122            writeln!(
123                &mut self.operations,
124                "/{} {} Tf",
125                self.current_font.pdf_name(),
126                self.font_size
127            )
128            .unwrap();
129
130            // Set text position
131            writeln!(&mut self.operations, "{:.2} {:.2} Td", x, self.cursor_y).unwrap();
132
133            // Handle justification
134            if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
135                // Calculate extra space to distribute
136                let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
137                if spaces_count > 0 {
138                    let extra_space = content_width - line_width;
139                    let space_adjustment = extra_space / spaces_count as f64;
140
141                    // Set word spacing
142                    writeln!(&mut self.operations, "{space_adjustment:.2} Tw").unwrap();
143                }
144            }
145
146            // Show text
147            self.operations.push('(');
148            for ch in line_text.chars() {
149                match ch {
150                    '(' => self.operations.push_str("\\("),
151                    ')' => self.operations.push_str("\\)"),
152                    '\\' => self.operations.push_str("\\\\"),
153                    '\n' => self.operations.push_str("\\n"),
154                    '\r' => self.operations.push_str("\\r"),
155                    '\t' => self.operations.push_str("\\t"),
156                    _ => self.operations.push(ch),
157                }
158            }
159            self.operations.push_str(") Tj\n");
160
161            // Reset word spacing if it was set
162            if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
163                self.operations.push_str("0 Tw\n");
164            }
165
166            // End text object
167            self.operations.push_str("ET\n");
168
169            // Move cursor down for next line
170            self.cursor_y -= self.font_size * self.line_height;
171        }
172
173        Ok(self)
174    }
175
176    pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
177        self.write_wrapped(text)?;
178        // Add extra space after paragraph
179        self.cursor_y -= self.font_size * self.line_height * 0.5;
180        Ok(self)
181    }
182
183    pub fn newline(&mut self) -> &mut Self {
184        self.cursor_y -= self.font_size * self.line_height;
185        self.cursor_x = self.margins.left;
186        self
187    }
188
189    pub fn cursor_position(&self) -> (f64, f64) {
190        (self.cursor_x, self.cursor_y)
191    }
192
193    pub fn generate_operations(&self) -> Vec<u8> {
194        self.operations.as_bytes().to_vec()
195    }
196    
197    /// Get the current alignment
198    pub fn alignment(&self) -> TextAlign {
199        self.alignment
200    }
201    
202    /// Get the page dimensions
203    pub fn page_dimensions(&self) -> (f64, f64) {
204        (self.page_width, self.page_height)
205    }
206    
207    /// Get the margins
208    pub fn margins(&self) -> &Margins {
209        &self.margins
210    }
211    
212    /// Get current line height multiplier
213    pub fn line_height(&self) -> f64 {
214        self.line_height
215    }
216    
217    /// Get the operations string
218    pub fn operations(&self) -> &str {
219        &self.operations
220    }
221    
222    /// Clear all operations
223    pub fn clear(&mut self) {
224        self.operations.clear();
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::page::Margins;
232    
233    fn create_test_margins() -> Margins {
234        Margins {
235            left: 50.0,
236            right: 50.0,
237            top: 50.0,
238            bottom: 50.0,
239        }
240    }
241    
242    #[test]
243    fn test_text_flow_context_new() {
244        let margins = create_test_margins();
245        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
246        
247        assert_eq!(context.current_font, Font::Helvetica);
248        assert_eq!(context.font_size, 12.0);
249        assert_eq!(context.line_height, 1.2);
250        assert_eq!(context.alignment, TextAlign::Left);
251        assert_eq!(context.page_width, 400.0);
252        assert_eq!(context.page_height, 600.0);
253        assert_eq!(context.cursor_x, 50.0);  // margins.left
254        assert_eq!(context.cursor_y, 550.0); // page_height - margins.top
255    }
256    
257    #[test]
258    fn test_set_font() {
259        let margins = create_test_margins();
260        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
261        
262        context.set_font(Font::TimesBold, 16.0);
263        assert_eq!(context.current_font, Font::TimesBold);
264        assert_eq!(context.font_size, 16.0);
265    }
266    
267    #[test]
268    fn test_set_line_height() {
269        let margins = create_test_margins();
270        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
271        
272        context.set_line_height(1.5);
273        assert_eq!(context.line_height(), 1.5);
274    }
275    
276    #[test]
277    fn test_set_alignment() {
278        let margins = create_test_margins();
279        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
280        
281        context.set_alignment(TextAlign::Center);
282        assert_eq!(context.alignment(), TextAlign::Center);
283    }
284    
285    #[test]
286    fn test_at_position() {
287        let margins = create_test_margins();
288        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
289        
290        context.at(100.0, 200.0);
291        let (x, y) = context.cursor_position();
292        assert_eq!(x, 100.0);
293        assert_eq!(y, 200.0);
294    }
295    
296    #[test]
297    fn test_content_width() {
298        let margins = create_test_margins();
299        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
300        
301        let content_width = context.content_width();
302        assert_eq!(content_width, 300.0); // 400 - 50 - 50
303    }
304    
305    #[test]
306    fn test_text_align_variants() {
307        assert_eq!(TextAlign::Left, TextAlign::Left);
308        assert_eq!(TextAlign::Right, TextAlign::Right);
309        assert_eq!(TextAlign::Center, TextAlign::Center);
310        assert_eq!(TextAlign::Justified, TextAlign::Justified);
311        
312        assert_ne!(TextAlign::Left, TextAlign::Right);
313    }
314    
315    #[test]
316    fn test_write_wrapped_simple() {
317        let margins = create_test_margins();
318        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
319        
320        context.write_wrapped("Hello World").unwrap();
321        
322        let ops = context.operations();
323        assert!(ops.contains("BT\n"));
324        assert!(ops.contains("ET\n"));
325        assert!(ops.contains("/Helvetica 12 Tf"));
326        assert!(ops.contains("(Hello World) Tj"));
327    }
328    
329    #[test]
330    fn test_write_paragraph() {
331        let margins = create_test_margins();
332        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
333        
334        let initial_y = context.cursor_y;
335        context.write_paragraph("Test paragraph").unwrap();
336        
337        // Y position should have moved down more than just line height
338        assert!(context.cursor_y < initial_y);
339    }
340    
341    #[test]
342    fn test_newline() {
343        let margins = create_test_margins();
344        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
345        
346        let initial_y = context.cursor_y;
347        context.newline();
348        
349        assert_eq!(context.cursor_x, margins.left);
350        assert!(context.cursor_y < initial_y);
351        assert_eq!(context.cursor_y, initial_y - context.font_size * context.line_height);
352    }
353    
354    #[test]
355    fn test_cursor_position() {
356        let margins = create_test_margins();
357        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
358        
359        context.at(75.0, 125.0);
360        let (x, y) = context.cursor_position();
361        assert_eq!(x, 75.0);
362        assert_eq!(y, 125.0);
363    }
364    
365    #[test]
366    fn test_generate_operations() {
367        let margins = create_test_margins();
368        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
369        
370        context.write_wrapped("Test").unwrap();
371        let ops_bytes = context.generate_operations();
372        let ops_string = String::from_utf8(ops_bytes).unwrap();
373        
374        assert_eq!(ops_string, context.operations());
375    }
376    
377    #[test]
378    fn test_clear_operations() {
379        let margins = create_test_margins();
380        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
381        
382        context.write_wrapped("Test").unwrap();
383        assert!(!context.operations().is_empty());
384        
385        context.clear();
386        assert!(context.operations().is_empty());
387    }
388    
389    #[test]
390    fn test_page_dimensions() {
391        let margins = create_test_margins();
392        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
393        
394        let (width, height) = context.page_dimensions();
395        assert_eq!(width, 400.0);
396        assert_eq!(height, 600.0);
397    }
398    
399    #[test]
400    fn test_margins_access() {
401        let margins = create_test_margins();
402        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
403        
404        let ctx_margins = context.margins();
405        assert_eq!(ctx_margins.left, 50.0);
406        assert_eq!(ctx_margins.right, 50.0);
407        assert_eq!(ctx_margins.top, 50.0);
408        assert_eq!(ctx_margins.bottom, 50.0);
409    }
410    
411    #[test]
412    fn test_method_chaining() {
413        let margins = create_test_margins();
414        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
415        
416        context
417            .set_font(Font::Courier, 10.0)
418            .set_line_height(1.5)
419            .set_alignment(TextAlign::Center)
420            .at(100.0, 200.0);
421        
422        assert_eq!(context.current_font, Font::Courier);
423        assert_eq!(context.font_size, 10.0);
424        assert_eq!(context.line_height(), 1.5);
425        assert_eq!(context.alignment(), TextAlign::Center);
426        let (x, y) = context.cursor_position();
427        assert_eq!(x, 100.0);
428        assert_eq!(y, 200.0);
429    }
430    
431    #[test]
432    fn test_text_align_debug() {
433        let align = TextAlign::Center;
434        let debug_str = format!("{:?}", align);
435        assert_eq!(debug_str, "Center");
436    }
437    
438    #[test]
439    fn test_text_align_clone() {
440        let align1 = TextAlign::Justified;
441        let align2 = align1;
442        assert_eq!(align1, align2);
443    }
444    
445    #[test]
446    fn test_text_align_copy() {
447        let align1 = TextAlign::Right;
448        let align2 = align1; // Copy semantics
449        assert_eq!(align1, align2);
450        
451        // Both variables should still be usable
452        assert_eq!(align1, TextAlign::Right);
453        assert_eq!(align2, TextAlign::Right);
454    }
455}