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_format.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    // Pass 1: layout without `total_pages` — only used to determine the
98    // page count.
99    let mut pass1_warnings = Vec::new();
100    let pass1_pages = paginate_body(&doc.children, body_area, ctx, &mut pass1_warnings);
101    let total_pages = pass1_pages.len().max(1);
102
103    // Pass 2: independent re-run, now with `total_pages` available to
104    // Header/Footer closures. Same measure/layout code path as pass 1; the
105    // body box is identical, so this reproduces the exact same split
106    // points (verified by a dedicated test).
107    let mut warnings = Vec::new();
108    let pass2_pages = paginate_body(&doc.children, body_area, ctx, &mut warnings);
109
110    let mut pages = Vec::with_capacity(total_pages);
111    for (i, body_node) in pass2_pages.into_iter().enumerate() {
112        let page_number = i + 1;
113        let pc = PageContext {
114            page: page_number,
115            total_pages,
116        };
117
118        let header = if page_number >= doc.header_visible_from {
119            doc.header.as_ref().map(|h| {
120                let el = (h.content)(&pc);
121                let area = Rect {
122                    x: doc.margin.left,
123                    y: doc.margin.top,
124                    width: body_w,
125                    height: h.height,
126                };
127                layout_band(&el, area, ctx, &mut warnings, page_number)
128            })
129        } else {
130            None
131        };
132
133        let footer = if page_number >= doc.footer_visible_from {
134            doc.footer.as_ref().map(|f| {
135                let el = (f.content)(&pc);
136                let area = Rect {
137                    x: doc.margin.left,
138                    y: page_h - doc.margin.bottom - footer_h,
139                    width: body_w,
140                    height: f.height,
141                };
142                layout_band(&el, area, ctx, &mut warnings, page_number)
143            })
144        } else {
145            None
146        };
147
148        pages.push(PageRender {
149            page_number,
150            header,
151            footer,
152            body: body_node,
153        });
154    }
155
156    PaginatedDocument {
157        page_width: page_w,
158        page_height: page_h,
159        body_area,
160        pages,
161        warnings,
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    struct FixedMetrics;
170    impl crate::font_resolver::FontMetrics for FixedMetrics {
171        fn advance(&self, _ch: char) -> f32 {
172            600.0
173        }
174        fn ascent(&self) -> f32 {
175            800.0
176        }
177        fn descent(&self) -> f32 {
178            -200.0
179        }
180    }
181    struct FixedResolver;
182    impl crate::font_resolver::FontResolver for FixedResolver {
183        fn metrics(&self, _key: lightweight_pdf_core::FontKey) -> &dyn crate::font_resolver::FontMetrics {
184            &FixedMetrics
185        }
186    }
187
188    #[test]
189    fn pass1_and_pass2_page_counts_match() {
190        let ctx = LayoutCtx { resolver: &FixedResolver };
191        let children: Vec<Element> = (0..40)
192            .map(|i| Element::Text(lightweight_pdf_core::Text::new(format!("Zeile {i} mit etwas Text drumherum."))))
193            .collect();
194        let body_area = Rect {
195            x: 0.0,
196            y: 0.0,
197            width: 400.0,
198            height: 200.0,
199        };
200        let mut w1 = Vec::new();
201        let mut w2 = Vec::new();
202        let p1 = paginate_body(&children, body_area, &ctx, &mut w1);
203        let p2 = paginate_body(&children, body_area, &ctx, &mut w2);
204        assert_eq!(p1.len(), p2.len());
205        assert!(p1.len() > 1, "expected the long body to span multiple pages");
206    }
207}