1use crate::fonts::FontRegistry;
20use crate::images::{self, ImageEmbedError};
21use lightweight_pdf_core::{Align, Color, Document, FontKey, Watermark};
22use lightweight_pdf_layout::{paginate, LayoutCtx, LayoutWarning, PageRender, Rect, RenderNode};
23use lightweight_pdf_writer::{CidFont, ContentBuilder, PdfDocument, PdfPage, Rgb};
24use std::collections::{BTreeMap, BTreeSet, HashMap};
25
26#[derive(Debug)]
27pub enum RenderError {
28 Font(lightweight_pdf_fonts::FontError),
29 Image(ImageEmbedError),
30}
31
32impl core::fmt::Display for RenderError {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 match self {
35 RenderError::Font(e) => write!(f, "font error: {e}"),
36 RenderError::Image(e) => write!(f, "image error: {e}"),
37 }
38 }
39}
40
41impl From<lightweight_pdf_fonts::FontError> for RenderError {
42 fn from(e: lightweight_pdf_fonts::FontError) -> Self {
43 RenderError::Font(e)
44 }
45}
46
47impl From<ImageEmbedError> for RenderError {
48 fn from(e: ImageEmbedError) -> Self {
49 RenderError::Image(e)
50 }
51}
52
53fn to_rgb(c: Color) -> Rgb {
54 Rgb(c.0, c.1, c.2)
55}
56
57fn align_offset(align: Align, available: f32, used: f32) -> f32 {
58 match align {
59 Align::Start => 0.0,
60 Align::Center => ((available - used) / 2.0).max(0.0),
61 Align::End => (available - used).max(0.0),
62 }
63}
64
65fn pdf_rect_y(page_height: f32, y_top: f32, height: f32) -> f32 {
68 page_height - y_top - height
69}
70
71fn collect_chars_in_node(node: &RenderNode, used: &mut HashMap<FontKey, BTreeSet<char>>) {
72 match node {
73 RenderNode::Empty | RenderNode::Rect { .. } | RenderNode::Line { .. } | RenderNode::Image { .. } => {}
74 RenderNode::Group { children, .. } => {
75 for child in children {
76 collect_chars_in_node(child, used);
77 }
78 }
79 RenderNode::TextLines { style, lines, .. } => {
80 let set = used.entry(style.font).or_default();
81 for line in lines {
82 set.extend(line.chars());
83 }
84 }
85 }
86}
87
88fn collect_chars_in_page(page: &PageRender, used: &mut HashMap<FontKey, BTreeSet<char>>) {
89 if let Some(header) = &page.header {
90 collect_chars_in_node(header, used);
91 }
92 collect_chars_in_node(&page.body, used);
93 if let Some(footer) = &page.footer {
94 collect_chars_in_node(footer, used);
95 }
96}
97
98struct EmbeddedFont {
104 index: usize,
105 char_to_gid: BTreeMap<char, u16>,
106 ascent_1000: f32,
107 widths_1000_by_gid: Vec<f32>,
108}
109
110fn encode_cid(line: &str, char_to_gid: &BTreeMap<char, u16>) -> Vec<u8> {
111 let mut bytes = Vec::with_capacity(line.chars().count() * 2);
112 for ch in line.chars() {
113 let gid = char_to_gid.get(&ch).copied().unwrap_or(0); bytes.extend_from_slice(&gid.to_be_bytes());
115 }
116 bytes
117}
118
119fn line_width_pt(font: &EmbeddedFont, size: f32, line: &str) -> f32 {
120 let sum_1000: f32 = line
121 .chars()
122 .map(|ch| {
123 let gid = font.char_to_gid.get(&ch).copied().unwrap_or(0);
124 font.widths_1000_by_gid.get(gid as usize).copied().unwrap_or(0.0)
125 })
126 .sum();
127 sum_1000 / 1000.0 * size
128}
129
130fn render_node(
131 node: &RenderNode,
132 page_height: f32,
133 embedded: &HashMap<FontKey, EmbeddedFont>,
134 pdf: &mut PdfDocument,
135 cb: &mut ContentBuilder,
136) -> Result<(), RenderError> {
137 match node {
138 RenderNode::Empty => {}
139 RenderNode::Group {
140 area,
141 clip,
142 background,
143 border,
144 children,
145 } => {
146 cb.save();
147 if *clip {
148 cb.clip_rect(area.x, pdf_rect_y(page_height, area.y, area.height), area.width, area.height);
149 }
150 if let Some(bg) = background {
151 cb.fill_rect(
152 area.x,
153 pdf_rect_y(page_height, area.y, area.height),
154 area.width,
155 area.height,
156 to_rgb(*bg),
157 );
158 }
159 for child in children {
160 render_node(child, page_height, embedded, pdf, cb)?;
161 }
162 if let Some(b) = border {
163 cb.stroke_rect(
164 area.x,
165 pdf_rect_y(page_height, area.y, area.height),
166 area.width,
167 area.height,
168 b.width,
169 to_rgb(b.color),
170 );
171 }
172 cb.restore();
173 }
174 RenderNode::Rect { area, background, border } => {
175 if let Some(bg) = background {
176 cb.fill_rect(
177 area.x,
178 pdf_rect_y(page_height, area.y, area.height),
179 area.width,
180 area.height,
181 to_rgb(*bg),
182 );
183 }
184 if let Some(b) = border {
185 cb.stroke_rect(
186 area.x,
187 pdf_rect_y(page_height, area.y, area.height),
188 area.width,
189 area.height,
190 b.width,
191 to_rgb(b.color),
192 );
193 }
194 }
195 RenderNode::Line {
196 x1,
197 y1,
198 x2,
199 y2,
200 thickness,
201 color,
202 } => {
203 cb.line(*x1, page_height - *y1, *x2, page_height - *y2, *thickness, to_rgb(*color));
204 }
205 RenderNode::TextLines {
206 area,
207 style,
208 lines,
209 line_height_pt,
210 } => {
211 let Some(font) = embedded.get(&style.font) else {
212 return Ok(()); };
214 let resource = PdfDocument::font_resource_name(font.index);
215 let ascent_pt = font.ascent_1000 / 1000.0 * style.size;
216 for (i, line) in lines.iter().enumerate() {
217 if line.is_empty() {
218 continue;
219 }
220 let line_top = area.y + i as f32 * line_height_pt;
221 let baseline_pdf_y = page_height - (line_top + ascent_pt);
222 let line_width = line_width_pt(font, style.size, line);
223 let x = area.x + align_offset(style.align, area.width, line_width);
224 let bytes = encode_cid(line, &font.char_to_gid);
225 cb.text(&resource, style.size, x, baseline_pdf_y, to_rgb(style.color), &bytes);
226 }
227 }
228 RenderNode::Image {
229 area,
230 bytes,
231 format,
232 width_px,
233 height_px,
234 components,
235 } => {
236 let mut pdf_image = images::build_pdf_image(bytes, *format, *components)?;
237 pdf_image.width_px = *width_px;
238 pdf_image.height_px = *height_px;
239 let index = pdf.add_image(pdf_image);
240 let resource = PdfDocument::image_resource_name(index);
241 cb.draw_image(
242 &resource,
243 area.x,
244 pdf_rect_y(page_height, area.y, area.height),
245 area.width,
246 area.height,
247 );
248 }
249 }
250 Ok(())
251}
252
253fn draw_watermark(
260 watermark: &Watermark,
261 body_area: Rect,
262 page_height: f32,
263 embedded: &HashMap<FontKey, EmbeddedFont>,
264 cb: &mut ContentBuilder,
265) {
266 let Some(font) = embedded.get(&watermark.font) else {
267 return;
268 };
269 let resource = PdfDocument::font_resource_name(font.index);
270 let bytes = encode_cid(&watermark.text, &font.char_to_gid);
271 let half_width = line_width_pt(font, watermark.size, &watermark.text) / 2.0;
272 let cx = body_area.x + body_area.width / 2.0;
273 let cy_layout = body_area.y + body_area.height / 2.0;
274 let cy_pdf = page_height - cy_layout;
275
276 cb.save();
277 cb.clip_rect(
278 body_area.x,
279 pdf_rect_y(page_height, body_area.y, body_area.height),
280 body_area.width,
281 body_area.height,
282 );
283 cb.text_rotated(
284 &resource,
285 watermark.size,
286 cx,
287 cy_pdf,
288 watermark.rotation_deg,
289 half_width,
290 to_rgb(watermark.color),
291 &bytes,
292 );
293 cb.restore();
294}
295
296fn render_document(doc: &Document, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
297 let ctx = LayoutCtx { resolver: fonts };
298 let paginated = paginate(doc, &ctx);
299
300 let mut used_chars: HashMap<FontKey, BTreeSet<char>> = HashMap::new();
301 for page in &paginated.pages {
302 collect_chars_in_page(page, &mut used_chars);
303 }
304 if let Some(watermark) = &doc.watermark {
305 used_chars.entry(watermark.font).or_default().extend(watermark.text.chars());
306 }
307
308 let mut pdf = PdfDocument::new();
309 let mut embedded: HashMap<FontKey, EmbeddedFont> = HashMap::new();
310 for (key, entry) in fonts.font_entries() {
311 let Some(chars) = used_chars.get(&key) else {
312 continue; };
314 let subset = lightweight_pdf_fonts::subset_font(&entry.data, chars)?;
315 let metrics = entry.metrics();
316 let char_to_gid = subset.char_to_gid.clone();
317 let widths_1000_by_gid = subset.widths_1000.clone();
318 let index = pdf.add_font(CidFont {
319 base_font: entry.base_font_name.to_string(),
320 subset_bytes: subset.font_data,
321 widths: subset.widths_1000,
322 ascent: metrics.ascent,
323 descent: metrics.descent,
324 cap_height: metrics.cap_height,
325 italic_angle: metrics.italic_angle,
326 bbox: metrics.bbox,
327 is_italic: metrics.is_italic,
328 is_bold: metrics.is_bold,
329 to_unicode: subset.char_to_gid.iter().map(|(&ch, &gid)| (gid, ch)).collect(),
330 });
331 embedded.insert(
332 key,
333 EmbeddedFont {
334 index,
335 char_to_gid,
336 ascent_1000: metrics.ascent,
337 widths_1000_by_gid,
338 },
339 );
340 }
341
342 for page in &paginated.pages {
343 let mut cb = ContentBuilder::new();
344 cb.save();
345 cb.clip_rect(0.0, 0.0, paginated.page_width, paginated.page_height);
346 if let Some(watermark) = &doc.watermark {
350 draw_watermark(watermark, paginated.body_area, paginated.page_height, &embedded, &mut cb);
351 }
352 if let Some(header) = &page.header {
353 render_node(header, paginated.page_height, &embedded, &mut pdf, &mut cb)?;
354 }
355 render_node(&page.body, paginated.page_height, &embedded, &mut pdf, &mut cb)?;
356 if let Some(footer) = &page.footer {
357 render_node(footer, paginated.page_height, &embedded, &mut pdf, &mut cb)?;
358 }
359 cb.restore();
360 pdf.add_page(PdfPage {
361 width: paginated.page_width,
362 height: paginated.page_height,
363 content: cb.into_bytes(),
364 });
365 }
366
367 Ok((pdf.write(), paginated.warnings))
368}
369
370pub trait DocumentExt {
378 #[cfg(feature = "default-fonts")]
381 fn render(&self) -> Result<Vec<u8>, RenderError>;
382 #[cfg(feature = "default-fonts")]
383 fn render_with_diagnostics(&self) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError>;
384
385 fn render_with_fonts(&self, fonts: &FontRegistry) -> Result<Vec<u8>, RenderError>;
386 fn render_with_fonts_and_diagnostics(&self, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError>;
387}
388
389impl DocumentExt for Document {
390 #[cfg(feature = "default-fonts")]
391 fn render(&self) -> Result<Vec<u8>, RenderError> {
392 let (bytes, _warnings) = self.render_with_diagnostics()?;
393 Ok(bytes)
394 }
395
396 #[cfg(feature = "default-fonts")]
397 fn render_with_diagnostics(&self) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
398 let fonts = FontRegistry::with_defaults()?;
399 render_document(self, &fonts)
400 }
401
402 fn render_with_fonts(&self, fonts: &FontRegistry) -> Result<Vec<u8>, RenderError> {
403 let (bytes, _warnings) = self.render_with_fonts_and_diagnostics(fonts)?;
404 Ok(bytes)
405 }
406
407 fn render_with_fonts_and_diagnostics(&self, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
408 render_document(self, fonts)
409 }
410}