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}