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_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 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 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}