Skip to main content

lightweight_pdf_layout/
pagination.rs

1//! Two-pass pagination: pass 1 counts pages, pass 2
2//! runs again with `total_pages` known so `Header`/`Footer` closures see
3//! correct values. Header/Footer bands are fixed at document-creation time
4//! (ADR-011) — the body content-box is therefore identical across both
5//! passes and every page, which is what makes the page count invariant.
6
7use crate::geometry::Rect;
8use crate::layoutable::{coerce_to_fit, measure_at_width, push_warning, LayoutCtx, LayoutResult, Layoutable};
9use crate::render_node::RenderNode;
10use crate::warnings::{LayoutWarning, LayoutWarningKind};
11use lightweight_pdf_core::{Align, Column, Common, Document, Element, PageContext};
12
13const EPS: f32 = 0.01;
14/// Safety valve against a pathological layout bug spinning forever
15/// (Grundprinzip 7's "harte Obergrenze" principle applied to pagination
16/// itself, not just a single oversized element).
17const HARD_PAGE_LIMIT: usize = 10_000;
18
19pub struct PageRender {
20    pub page_number: usize,
21    pub header: Option<RenderNode>,
22    pub footer: Option<RenderNode>,
23    pub body: RenderNode,
24}
25
26pub struct PaginatedDocument {
27    pub page_width: f32,
28    pub page_height: f32,
29    /// The body content box, identical on every page (ADR-011: fixed
30    /// header/footer bands make it page-count-invariant). Exposed so the
31    /// facade can clip a document-level watermark to it (Phase 6) without
32    /// recomputing margins/band heights itself.
33    pub body_area: Rect,
34    pub pages: Vec<PageRender>,
35    pub warnings: Vec<LayoutWarning>,
36}
37
38/// Repeatedly lays the document body out into an identical, fixed-size box
39/// per page until every child has been placed. Returns one `RenderNode`
40/// per page.
41pub fn paginate_body(children: &[Element], body_area: Rect, ctx: &LayoutCtx, warnings: &mut Vec<LayoutWarning>) -> Vec<RenderNode> {
42    let mut remaining = Element::Column(Column {
43        children: children.to_vec(),
44        gap: 0.0,
45        align: Align::Start,
46        common: Common::default(),
47    });
48    let mut pages = Vec::new();
49    let mut page_num = 1usize;
50    loop {
51        match remaining.layout(ctx, body_area, warnings, page_num) {
52            LayoutResult::Fit(node) => {
53                pages.push(node);
54                break;
55            }
56            LayoutResult::Split { current, remainder } => {
57                pages.push(current);
58                remaining = remainder;
59                page_num += 1;
60                if page_num > HARD_PAGE_LIMIT {
61                    break;
62                }
63            }
64        }
65    }
66    pages
67}
68
69fn layout_band(el: &Element, area: Rect, ctx: &LayoutCtx, warnings: &mut Vec<LayoutWarning>, page: usize) -> RenderNode {
70    let natural = measure_at_width(ctx, el, area.width);
71    if natural.height > area.height + EPS {
72        push_warning(
73            warnings,
74            LayoutWarningKind::HeaderFooterOverflow,
75            page,
76            "Header/Footer content taller than reserved band",
77        );
78    }
79    // Header/Footer never spans pages: keep whatever fit, the overflow
80    // warning above already flagged the clipped remainder.
81    coerce_to_fit(el.layout(ctx, area, warnings, page))
82}
83
84pub fn paginate(doc: &Document, ctx: &LayoutCtx) -> PaginatedDocument {
85    let (page_w, page_h) = doc.page_size();
86    let header_h = doc.header.as_ref().map(|h| h.height).unwrap_or(0.0);
87    let footer_h = doc.footer.as_ref().map(|h| h.height).unwrap_or(0.0);
88    let body_w = (page_w - doc.margin.left - doc.margin.right).max(0.0);
89    let body_h = (page_h - doc.margin.top - doc.margin.bottom - header_h - footer_h).max(0.0);
90    let body_area = Rect {
91        x: doc.margin.left,
92        y: doc.margin.top + header_h,
93        width: body_w,
94        height: body_h,
95    };
96
97    // `TableOfContents` (issue #10): collected once, identical in both
98    // passes (see `toc.rs`'s module doc for why that's what keeps the
99    // page count stable), headings get an anchor if they don't already
100    // have one so an entry can always `link_to` them.
101    let (prepared_children, toc_headings) = crate::toc::prepare_toc(&doc.children);
102
103    // Pass 1: layout without `total_pages` — only used to determine the
104    // page count (and, for `TableOfContents`, which page every heading
105    // landed on).
106    let mut pass1_warnings = Vec::new();
107    let ctx1 = LayoutCtx {
108        resolver: ctx.resolver,
109        toc_headings: &toc_headings,
110        toc_heading_pages: None,
111    };
112    let pass1_pages = paginate_body(&prepared_children, body_area, &ctx1, &mut pass1_warnings);
113    let total_pages = pass1_pages.len().max(1);
114    let toc_heading_pages = crate::toc::collect_anchor_pages(&pass1_pages);
115
116    // Pass 2: independent re-run, now with `total_pages` available to
117    // Header/Footer closures (and `toc_heading_pages` to
118    // `TableOfContents`). Same measure/layout code path as pass 1; the
119    // body box is identical, so this reproduces the exact same split
120    // points (verified by a dedicated test).
121    let mut warnings = Vec::new();
122    let ctx = &LayoutCtx {
123        resolver: ctx.resolver,
124        toc_headings: &toc_headings,
125        toc_heading_pages: Some(&toc_heading_pages),
126    };
127    let pass2_pages = paginate_body(&prepared_children, body_area, ctx, &mut warnings);
128
129    let mut pages = Vec::with_capacity(total_pages);
130    for (i, body_node) in pass2_pages.into_iter().enumerate() {
131        let page_number = i + 1;
132        let pc = PageContext {
133            page: page_number,
134            total_pages,
135        };
136
137        let header = if page_number >= doc.header_visible_from {
138            doc.header.as_ref().map(|h| {
139                let el = (h.content)(&pc);
140                let area = Rect {
141                    x: doc.margin.left,
142                    y: doc.margin.top,
143                    width: body_w,
144                    height: h.height,
145                };
146                layout_band(&el, area, ctx, &mut warnings, page_number)
147            })
148        } else {
149            None
150        };
151
152        let footer = if page_number >= doc.footer_visible_from {
153            doc.footer.as_ref().map(|f| {
154                let el = (f.content)(&pc);
155                let area = Rect {
156                    x: doc.margin.left,
157                    y: page_h - doc.margin.bottom - footer_h,
158                    width: body_w,
159                    height: f.height,
160                };
161                layout_band(&el, area, ctx, &mut warnings, page_number)
162            })
163        } else {
164            None
165        };
166
167        pages.push(PageRender {
168            page_number,
169            header,
170            footer,
171            body: body_node,
172        });
173    }
174
175    PaginatedDocument {
176        page_width: page_w,
177        page_height: page_h,
178        body_area,
179        pages,
180        warnings,
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    struct FixedMetrics;
189    impl crate::font_resolver::FontMetrics for FixedMetrics {
190        fn advance(&self, _ch: char) -> f32 {
191            600.0
192        }
193        fn ascent(&self) -> f32 {
194            800.0
195        }
196        fn descent(&self) -> f32 {
197            -200.0
198        }
199    }
200    struct FixedResolver;
201    impl crate::font_resolver::FontResolver for FixedResolver {
202        fn metrics(&self, _key: lightweight_pdf_core::FontKey) -> &dyn crate::font_resolver::FontMetrics {
203            &FixedMetrics
204        }
205    }
206
207    #[test]
208    fn pass1_and_pass2_page_counts_match() {
209        let ctx = LayoutCtx::new(&FixedResolver);
210        let children: Vec<Element> = (0..40)
211            .map(|i| Element::Text(lightweight_pdf_core::Text::new(format!("Zeile {i} mit etwas Text drumherum."))))
212            .collect();
213        let body_area = Rect {
214            x: 0.0,
215            y: 0.0,
216            width: 400.0,
217            height: 200.0,
218        };
219        let mut w1 = Vec::new();
220        let mut w2 = Vec::new();
221        let p1 = paginate_body(&children, body_area, &ctx, &mut w1);
222        let p2 = paginate_body(&children, body_area, &ctx, &mut w2);
223        assert_eq!(p1.len(), p2.len());
224        assert!(p1.len() > 1, "expected the long body to span multiple pages");
225    }
226}