1use 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;
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 = 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 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 let (prepared_children, toc_headings) = crate::toc::prepare_toc(&doc.children);
102
103 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 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}