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!(
352            context.cursor_y,
353            initial_y - context.font_size * context.line_height
354        );
355    }
356
357    #[test]
358    fn test_cursor_position() {
359        let margins = create_test_margins();
360        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
361
362        context.at(75.0, 125.0);
363        let (x, y) = context.cursor_position();
364        assert_eq!(x, 75.0);
365        assert_eq!(y, 125.0);
366    }
367
368    #[test]
369    fn test_generate_operations() {
370        let margins = create_test_margins();
371        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
372
373        context.write_wrapped("Test").unwrap();
374        let ops_bytes = context.generate_operations();
375        let ops_string = String::from_utf8(ops_bytes).unwrap();
376
377        assert_eq!(ops_string, context.operations());
378    }
379
380    #[test]
381    fn test_clear_operations() {
382        let margins = create_test_margins();
383        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
384
385        context.write_wrapped("Test").unwrap();
386        assert!(!context.operations().is_empty());
387
388        context.clear();
389        assert!(context.operations().is_empty());
390    }
391
392    #[test]
393    fn test_page_dimensions() {
394        let margins = create_test_margins();
395        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
396
397        let (width, height) = context.page_dimensions();
398        assert_eq!(width, 400.0);
399        assert_eq!(height, 600.0);
400    }
401
402    #[test]
403    fn test_margins_access() {
404        let margins = create_test_margins();
405        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
406
407        let ctx_margins = context.margins();
408        assert_eq!(ctx_margins.left, 50.0);
409        assert_eq!(ctx_margins.right, 50.0);
410        assert_eq!(ctx_margins.top, 50.0);
411        assert_eq!(ctx_margins.bottom, 50.0);
412    }
413
414    #[test]
415    fn test_method_chaining() {
416        let margins = create_test_margins();
417        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
418
419        context
420            .set_font(Font::Courier, 10.0)
421            .set_line_height(1.5)
422            .set_alignment(TextAlign::Center)
423            .at(100.0, 200.0);
424
425        assert_eq!(context.current_font, Font::Courier);
426        assert_eq!(context.font_size, 10.0);
427        assert_eq!(context.line_height(), 1.5);
428        assert_eq!(context.alignment(), TextAlign::Center);
429        let (x, y) = context.cursor_position();
430        assert_eq!(x, 100.0);
431        assert_eq!(y, 200.0);
432    }
433
434    #[test]
435    fn test_text_align_debug() {
436        let align = TextAlign::Center;
437        let debug_str = format!("{:?}", align);
438        assert_eq!(debug_str, "Center");
439    }
440
441    #[test]
442    fn test_text_align_clone() {
443        let align1 = TextAlign::Justified;
444        let align2 = align1;
445        assert_eq!(align1, align2);
446    }
447
448    #[test]
449    fn test_text_align_copy() {
450        let align1 = TextAlign::Right;
451        let align2 = align1; // Copy semantics
452        assert_eq!(align1, align2);
453
454        // Both variables should still be usable
455        assert_eq!(align1, TextAlign::Right);
456        assert_eq!(align2, TextAlign::Right);
457    }
458}