1use 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;
14const 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 pub body_area: Rect,
34 pub pages: Vec<PageRender>,
35 pub warnings: Vec<LayoutWarning>,
36}
37
38pub 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 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 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 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}