Skip to main content

oxidize_pdf/layout/
flow.rs

1use crate::error::Result;
2use crate::graphics::Image;
3use crate::layout::image_utils::fit_image_dimensions;
4use crate::layout::RichText;
5use crate::page::Margins;
6use crate::page_tables::PageTables;
7use crate::text::text_block::measure_text_block;
8use crate::text::{Font, Table, TextAlign, TextFlowContext};
9use crate::{Document, Page};
10use std::sync::Arc;
11
12/// Page dimensions and margins for FlowLayout.
13#[derive(Debug, Clone)]
14pub struct PageConfig {
15    pub width: f64,
16    pub height: f64,
17    pub margin_left: f64,
18    pub margin_right: f64,
19    pub margin_top: f64,
20    pub margin_bottom: f64,
21}
22
23impl PageConfig {
24    /// Create a config with explicit dimensions and margins.
25    pub fn new(
26        width: f64,
27        height: f64,
28        margin_left: f64,
29        margin_right: f64,
30        margin_top: f64,
31        margin_bottom: f64,
32    ) -> Self {
33        let config = Self {
34            width,
35            height,
36            margin_left,
37            margin_right,
38            margin_top,
39            margin_bottom,
40        };
41        debug_assert!(
42            config.content_width() > 0.0,
43            "margins ({} + {}) exceed page width ({})",
44            margin_left,
45            margin_right,
46            width
47        );
48        debug_assert!(
49            config.usable_height() > 0.0,
50            "margins ({} + {}) exceed page height ({})",
51            margin_top,
52            margin_bottom,
53            height
54        );
55        config
56    }
57
58    /// A4 page (595×842 pts) with default 72pt margins.
59    pub fn a4() -> Self {
60        Self::new(595.0, 842.0, 72.0, 72.0, 72.0, 72.0)
61    }
62
63    /// A4 page with custom uniform margins on all sides.
64    pub fn a4_with_margins(left: f64, right: f64, top: f64, bottom: f64) -> Self {
65        Self::new(595.0, 842.0, left, right, top, bottom)
66    }
67
68    /// Available width for content (page width minus left and right margins).
69    pub fn content_width(&self) -> f64 {
70        self.width - self.margin_left - self.margin_right
71    }
72
73    /// Available height for content (page height minus top and bottom margins).
74    pub fn usable_height(&self) -> f64 {
75        self.height - self.margin_top - self.margin_bottom
76    }
77
78    fn start_y(&self) -> f64 {
79        self.height - self.margin_top
80    }
81
82    fn create_page(&self) -> Page {
83        let mut page = Page::new(self.width, self.height);
84        page.set_margins(
85            self.margin_left,
86            self.margin_right,
87            self.margin_top,
88            self.margin_bottom,
89        );
90        page
91    }
92
93    fn to_margins(&self) -> Margins {
94        Margins {
95            left: self.margin_left,
96            right: self.margin_right,
97            top: self.margin_top,
98            bottom: self.margin_bottom,
99        }
100    }
101}
102
103/// An element that can be placed in a FlowLayout.
104#[derive(Debug)]
105pub enum FlowElement {
106    /// A block of word-wrapped text.
107    Text {
108        text: String,
109        font: Font,
110        font_size: f64,
111        line_height: f64,
112    },
113    /// Vertical space in points.
114    Spacer(f64),
115    /// A simple table.
116    Table(Table),
117    /// A single line of mixed-style text.
118    RichText { rich: RichText, line_height: f64 },
119    /// An image scaled to fit within max dimensions, preserving aspect ratio.
120    /// Uses `Arc<Image>` to avoid cloning the pixel buffer when building.
121    Image {
122        name: String,
123        image: Arc<Image>,
124        max_width: f64,
125        max_height: f64,
126        center: bool,
127    },
128}
129
130impl FlowElement {
131    /// Calculate the height this element will occupy.
132    fn measure_height(&self, content_width: f64) -> f64 {
133        match self {
134            FlowElement::Text {
135                text,
136                font,
137                font_size,
138                line_height,
139            } => {
140                let metrics =
141                    measure_text_block(text, font, *font_size, *line_height, content_width);
142                metrics.height
143            }
144            FlowElement::Spacer(h) => *h,
145            FlowElement::Table(table) => table.get_height(),
146            FlowElement::RichText { rich, line_height } => rich.max_font_size() * line_height,
147            FlowElement::Image {
148                image,
149                max_width,
150                max_height,
151                ..
152            } => {
153                let (_, h) =
154                    fit_image_dimensions(image.width(), image.height(), *max_width, *max_height);
155                h
156            }
157        }
158    }
159}
160
161/// Automatic flow layout engine with page break support.
162///
163/// Manages a vertical cursor and a list of elements. When an element
164/// would overflow the current page's bottom margin, a new page is
165/// created automatically.
166///
167/// # Example
168///
169/// ```rust,no_run
170/// use oxidize_pdf::{Document, Font};
171/// use oxidize_pdf::layout::{FlowLayout, PageConfig};
172///
173/// let config = PageConfig::a4_with_margins(50.0, 50.0, 50.0, 50.0);
174/// let mut layout = FlowLayout::new(config);
175/// layout.add_text("Hello World", Font::Helvetica, 12.0);
176/// layout.add_spacer(20.0);
177/// layout.add_text("Second paragraph", Font::Helvetica, 12.0);
178///
179/// let mut doc = Document::new();
180/// layout.build_into(&mut doc).unwrap();
181/// ```
182pub struct FlowLayout {
183    config: PageConfig,
184    elements: Vec<FlowElement>,
185}
186
187impl FlowLayout {
188    /// Create a new FlowLayout with the given page configuration.
189    pub fn new(config: PageConfig) -> Self {
190        Self {
191            config,
192            elements: Vec::new(),
193        }
194    }
195
196    /// Add a text block. Uses default line_height of 1.2.
197    pub fn add_text(&mut self, text: &str, font: Font, font_size: f64) -> &mut Self {
198        self.elements.push(FlowElement::Text {
199            text: text.to_string(),
200            font,
201            font_size,
202            line_height: 1.2,
203        });
204        self
205    }
206
207    /// Add a text block with custom line height.
208    pub fn add_text_with_line_height(
209        &mut self,
210        text: &str,
211        font: Font,
212        font_size: f64,
213        line_height: f64,
214    ) -> &mut Self {
215        self.elements.push(FlowElement::Text {
216            text: text.to_string(),
217            font,
218            font_size,
219            line_height,
220        });
221        self
222    }
223
224    /// Add vertical spacing in points.
225    pub fn add_spacer(&mut self, points: f64) -> &mut Self {
226        self.elements.push(FlowElement::Spacer(points));
227        self
228    }
229
230    /// Add a table.
231    pub fn add_table(&mut self, table: Table) -> &mut Self {
232        self.elements.push(FlowElement::Table(table));
233        self
234    }
235
236    /// Add an image scaled to fit within max dimensions, left-aligned.
237    /// Wraps the image in `Arc` internally to avoid expensive buffer clones.
238    pub fn add_image(
239        &mut self,
240        name: &str,
241        image: Arc<Image>,
242        max_width: f64,
243        max_height: f64,
244    ) -> &mut Self {
245        self.elements.push(FlowElement::Image {
246            name: name.to_string(),
247            image,
248            max_width,
249            max_height,
250            center: false,
251        });
252        self
253    }
254
255    /// Add an image scaled to fit within max dimensions, centered horizontally.
256    /// Wraps the image in `Arc` internally to avoid expensive buffer clones.
257    pub fn add_image_centered(
258        &mut self,
259        name: &str,
260        image: Arc<Image>,
261        max_width: f64,
262        max_height: f64,
263    ) -> &mut Self {
264        self.elements.push(FlowElement::Image {
265            name: name.to_string(),
266            image,
267            max_width,
268            max_height,
269            center: true,
270        });
271        self
272    }
273
274    /// Add a single line of mixed-style text.
275    pub fn add_rich_text(&mut self, rich: RichText) -> &mut Self {
276        self.elements.push(FlowElement::RichText {
277            rich,
278            line_height: 1.2,
279        });
280        self
281    }
282
283    /// Build all elements into the document, creating pages as needed.
284    ///
285    /// **Limitation**: Elements taller than `PageConfig::usable_height()` (e.g., a very
286    /// large table) will overflow past the bottom margin on a single page. They are not
287    /// split across pages.
288    pub fn build_into(&self, doc: &mut Document) -> Result<()> {
289        let content_width = self.config.content_width();
290        let mut current_page = self.config.create_page();
291        let mut cursor_y = self.config.start_y();
292
293        for element in &self.elements {
294            let needed_height = element.measure_height(content_width);
295
296            // Page break: if element doesn't fit and we've already placed something
297            if cursor_y - needed_height < self.config.margin_bottom
298                && cursor_y < self.config.start_y()
299            {
300                doc.add_page(current_page);
301                current_page = self.config.create_page();
302                cursor_y = self.config.start_y();
303            }
304
305            match element {
306                FlowElement::Text {
307                    text,
308                    font,
309                    font_size,
310                    line_height,
311                } => {
312                    let mut text_flow = TextFlowContext::new(
313                        self.config.width,
314                        self.config.height,
315                        self.config.to_margins(),
316                    );
317                    text_flow
318                        .set_font(font.clone(), *font_size)
319                        .set_line_height(*line_height)
320                        .set_alignment(TextAlign::Left)
321                        .at(self.config.margin_left, cursor_y - font_size * line_height);
322                    text_flow.write_wrapped(text)?;
323                    current_page.add_text_flow(&text_flow);
324                }
325                FlowElement::Spacer(_) => {
326                    // Spacers only consume vertical space, no rendering needed
327                }
328                FlowElement::Table(table) => {
329                    current_page.add_simple_table(
330                        table,
331                        self.config.margin_left,
332                        cursor_y - needed_height,
333                    )?;
334                }
335                FlowElement::RichText { rich, line_height } => {
336                    let ops = rich.render_operations(
337                        self.config.margin_left,
338                        cursor_y - rich.max_font_size() * line_height,
339                    );
340                    current_page.append_raw_content(ops.as_bytes());
341                }
342                FlowElement::Image {
343                    name,
344                    image,
345                    max_width,
346                    max_height,
347                    center,
348                } => {
349                    let (w, h) = fit_image_dimensions(
350                        image.width(),
351                        image.height(),
352                        *max_width,
353                        *max_height,
354                    );
355                    let x = if *center {
356                        crate::layout::image_utils::centered_image_x(
357                            self.config.margin_left,
358                            content_width,
359                            w,
360                        )
361                    } else {
362                        self.config.margin_left
363                    };
364                    current_page.add_image(name.clone(), Image::clone(image));
365                    current_page.draw_image(name, x, cursor_y - h, w, h)?;
366                }
367            }
368
369            cursor_y -= needed_height;
370        }
371
372        doc.add_page(current_page);
373        Ok(())
374    }
375}