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::{Constraints, Rect};
8use crate::layoutable::{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 = el.measure(
71        ctx,
72        Constraints {
73            max_width: area.width,
74            max_height: f32::INFINITY,
75        },
76    );
77    if natural.height > area.height + EPS {
78        warnings.push(LayoutWarning {
79            kind: LayoutWarningKind::HeaderFooterOverflow,
80            page,
81            element_hint: "Header/Footer content taller than reserved band".to_string(),
82        });
83    }
84    match el.layout(ctx, area, warnings, page) {
85        LayoutResult::Fit(node) => node,
86        // Header/Footer never spans pages: keep whatever fit, the overflow
87        // warning above already flagged the clipped remainder.
88        LayoutResult::Split { current, .. } => current,
89    }
90}
91
92pub fn paginate(doc: &Document, ctx: &LayoutCtx) -> PaginatedDocument {
93    let (page_w, page_h) = doc.page_format.size();
94    let header_h = doc.header.as_ref().map(|h| h.height).unwrap_or(0.0);
95    let footer_h = doc.footer.as_ref().map(|h| h.height).unwrap_or(0.0);
96    let body_w = (page_w - doc.margin.left - doc.margin.right).max(0.0);
97    let body_h = (page_h - doc.margin.top - doc.margin.bottom - header_h - footer_h).max(0.0);
98    let body_area = Rect {
99        x: doc.margin.left,
100        y: doc.margin.top + header_h,
101        width: body_w,
102        height: body_h,
103    };
104
105    // Pass 1: layout without `total_pages` — only used to determine the
106    // page count.
107    let mut pass1_warnings = Vec::new();
108    let pass1_pages = paginate_body(&doc.children, body_area, ctx, &mut pass1_warnings);
109    let total_pages = pass1_pages.len().max(1);
110
111    // Pass 2: independent re-run, now with `total_pages` available to
112    // Header/Footer closures. Same measure/layout code path as pass 1; the
113    // body box is identical, so this reproduces the exact same split
114    // points (verified by a dedicated test).
115    let mut warnings = Vec::new();
116    let pass2_pages = paginate_body(&doc.children, body_area, ctx, &mut warnings);
117
118    let mut pages = Vec::with_capacity(total_pages);
119    for (i, body_node) in pass2_pages.into_iter().enumerate() {
120        let page_number = i + 1;
121        let pc = PageContext {
122            page: page_number,
123            total_pages,
124        };
125
126        let header = if page_number >= doc.header_visible_from {
127            doc.header.as_ref().map(|h| {
128                let el = (h.content)(&pc);
129                let area = Rect {
130                    x: doc.margin.left,
131                    y: doc.margin.top,
132                    width: body_w,
133                    height: h.height,
134                };
135                layout_band(&el, area, ctx, &mut warnings, page_number)
136            })
137        } else {
138            None
139        };
140
141        let footer = if page_number >= doc.footer_visible_from {
142            doc.footer.as_ref().map(|f| {
143                let el = (f.content)(&pc);
144                let area = Rect {
145                    x: doc.margin.left,
146                    y: page_h - doc.margin.bottom - footer_h,
147                    width: body_w,
148                    height: f.height,
149                };
150                layout_band(&el, area, ctx, &mut warnings, page_number)
151            })
152        } else {
153            None
154        };
155
156        pages.push(PageRender {
157            page_number,
158            header,
159            footer,
160            body: body_node,
161        });
162    }
163
164    PaginatedDocument {
165        page_width: page_w,
166        page_height: page_h,
167        body_area,
168        pages,
169        warnings,
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    struct FixedMetrics;
178    impl crate::font_resolver::FontMetrics for FixedMetrics {
179        fn advance(&self, _ch: char) -> f32 {
180            600.0
181        }
182        fn ascent(&self) -> f32 {
183            800.0
184        }
185        fn descent(&self) -> f32 {
186            -200.0
187        }
188    }
189    struct FixedResolver;
190    impl crate::font_resolver::FontResolver for FixedResolver {
191        fn metrics(&self, _key: lightweight_pdf_core::FontKey) -> &dyn crate::font_resolver::FontMetrics {
192            &FixedMetrics
193        }
194    }
195
196    #[test]
197    fn pass1_and_pass2_page_counts_match() {
198        let ctx = LayoutCtx { resolver: &FixedResolver };
199        let children: Vec<Element> = (0..40)
200            .map(|i| Element::Text(lightweight_pdf_core::Text::new(format!("Zeile {i} mit etwas Text drumherum."))))
201            .collect();
202        let body_area = Rect {
203            x: 0.0,
204            y: 0.0,
205            width: 400.0,
206            height: 200.0,
207        };
208        let mut w1 = Vec::new();
209        let mut w2 = Vec::new();
210        let p1 = paginate_body(&children, body_area, &ctx, &mut w1);
211        let p2 = paginate_body(&children, body_area, &ctx, &mut w2);
212        assert_eq!(p1.len(), p2.len());
213        assert!(p1.len() > 1, "expected the long body to span multiple pages");
214    }
215}