1#[cfg(feature = "tagged-pdf")]
30mod struct_tree;
31mod text;
32mod tree;
33
34use crate::fonts::FontRegistry;
35use crate::images::ImageEmbedError;
36use lightweight_pdf_core::{Color, Document, FontKey, Watermark};
37use lightweight_pdf_layout::{paginate, LayoutCtx, LayoutWarning, PageRender, Rect};
38use lightweight_pdf_writer::{ContentBuilder, PdfDocument, PdfPage, Rgb};
39use std::collections::{BTreeSet, HashMap};
40use text::EmbeddedFont;
41
42#[derive(Debug)]
43pub enum RenderError {
44 Font(lightweight_pdf_fonts::FontError),
45 Image(ImageEmbedError),
46 MissingFont(FontKey),
51 PdfAFeatureDisabled,
55 ZugferdFeatureDisabled,
59 TaggedPdfFeatureDisabled,
63 NoFontsRegistered,
69}
70
71impl core::fmt::Display for RenderError {
72 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
73 match self {
74 RenderError::Font(e) => write!(f, "font error: {e}"),
75 RenderError::Image(e) => write!(f, "image error: {e}"),
76 RenderError::MissingFont(key) => write!(f, "no font registered for key {key:?}"),
77 RenderError::PdfAFeatureDisabled => {
78 write!(
79 f,
80 "Document::pdf_a3b() was set but this crate wasn't built with the `pdf-a` feature"
81 )
82 }
83 RenderError::ZugferdFeatureDisabled => {
84 write!(
85 f,
86 "Document::zugferd_xml() was set but this crate wasn't built with the `zugferd` feature"
87 )
88 }
89 RenderError::TaggedPdfFeatureDisabled => {
90 write!(
91 f,
92 "Document::pdf_ua() was set but this crate wasn't built with the `tagged-pdf` feature"
93 )
94 }
95 RenderError::NoFontsRegistered => {
96 write!(
97 f,
98 "FontRegistry has no fonts registered — call register()/register_named() (or with_defaults()/with_fonts()) first"
99 )
100 }
101 }
102 }
103}
104
105impl From<lightweight_pdf_fonts::FontError> for RenderError {
106 fn from(e: lightweight_pdf_fonts::FontError) -> Self {
107 RenderError::Font(e)
108 }
109}
110
111impl From<ImageEmbedError> for RenderError {
112 fn from(e: ImageEmbedError) -> Self {
113 RenderError::Image(e)
114 }
115}
116
117fn to_rgb(c: Color) -> Rgb {
118 Rgb(c.0, c.1, c.2)
119}
120
121fn pdf_rect_y(page_height: f32, y_top: f32, height: f32) -> f32 {
124 page_height - y_top - height
125}
126
127struct RenderCtx<'a> {
135 page_height: f32,
136 embedded: &'a HashMap<FontKey, EmbeddedFont>,
137 anchors: &'a HashMap<String, (usize, f32)>,
138 pdf: &'a mut PdfDocument,
139 cb: &'a mut ContentBuilder,
140 annotations: &'a mut Vec<lightweight_pdf_writer::PdfLinkAnnotation>,
141 #[cfg(feature = "tagged-pdf")]
143 page_index: usize,
144 #[cfg(feature = "tagged-pdf")]
150 struct_tree: Option<&'a mut struct_tree::StructTreeBuilder>,
151 #[cfg(feature = "tagged-pdf")]
152 warnings: &'a mut Vec<LayoutWarning>,
153 force_artifact: bool,
163}
164
165struct DocumentLookups<'a> {
170 embedded: &'a HashMap<FontKey, EmbeddedFont>,
171 anchors: &'a HashMap<String, (usize, f32)>,
172}
173
174#[allow(clippy::too_many_arguments)]
177#[cfg_attr(not(feature = "tagged-pdf"), allow(unused_variables))]
178fn render_page(
179 page: &PageRender,
180 watermark: Option<&Watermark>,
181 body_area: Rect,
182 page_width: f32,
183 page_height: f32,
184 page_index: usize,
185 lookups: &DocumentLookups,
186 pdf: &mut PdfDocument,
187 #[cfg(feature = "tagged-pdf")] mut struct_tree: Option<&mut struct_tree::StructTreeBuilder>,
188 #[cfg(feature = "tagged-pdf")] warnings: &mut Vec<LayoutWarning>,
189) -> Result<PdfPage, RenderError> {
190 let mut cb = ContentBuilder::new();
191 let mut annotations = Vec::new();
192 cb.save();
193 cb.clip_rect(0.0, 0.0, page_width, page_height);
194 #[cfg(feature = "tagged-pdf")]
195 if let Some(st) = struct_tree.as_deref_mut() {
196 st.start_page();
197 }
198 if let Some(watermark) = watermark {
204 #[cfg(feature = "tagged-pdf")]
205 let is_tagged = struct_tree.is_some();
206 #[cfg(not(feature = "tagged-pdf"))]
207 let is_tagged = false;
208 if is_tagged {
209 cb.begin_artifact();
210 }
211 text::draw_watermark(watermark, body_area, page_height, lookups.embedded, &mut cb);
212 if is_tagged {
213 cb.end_marked_content();
214 }
215 }
216 let mut ctx = RenderCtx {
217 page_height,
218 embedded: lookups.embedded,
219 anchors: lookups.anchors,
220 pdf,
221 cb: &mut cb,
222 annotations: &mut annotations,
223 #[cfg(feature = "tagged-pdf")]
224 page_index,
225 #[cfg(feature = "tagged-pdf")]
226 struct_tree,
227 #[cfg(feature = "tagged-pdf")]
228 warnings,
229 force_artifact: false,
230 };
231 if let Some(header) = &page.header {
232 ctx.force_artifact = true;
233 tree::render_node(header, &mut ctx)?;
234 ctx.force_artifact = false;
235 }
236 tree::render_node(&page.body, &mut ctx)?;
237 if let Some(footer) = &page.footer {
238 ctx.force_artifact = true;
239 tree::render_node(footer, &mut ctx)?;
240 }
241 cb.restore();
242 Ok(PdfPage {
243 width: page_width,
244 height: page_height,
245 content: cb.into_bytes(),
246 annotations,
247 })
248}
249
250fn render_document(doc: &Document, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
251 #[cfg(not(feature = "pdf-a"))]
252 if doc.pdf_a3b {
253 return Err(RenderError::PdfAFeatureDisabled);
254 }
255 #[cfg(not(feature = "zugferd"))]
256 if doc.zugferd_xml.is_some() {
257 return Err(RenderError::ZugferdFeatureDisabled);
258 }
259 #[cfg(not(feature = "tagged-pdf"))]
260 if doc.pdf_ua {
261 return Err(RenderError::TaggedPdfFeatureDisabled);
262 }
263 if fonts.is_empty() {
264 return Err(RenderError::NoFontsRegistered);
265 }
266
267 let ctx = LayoutCtx::new(fonts);
268 #[cfg_attr(not(feature = "tagged-pdf"), allow(unused_mut))]
269 let mut paginated = paginate(doc, &ctx);
270
271 let mut used_chars: HashMap<FontKey, BTreeSet<char>> = HashMap::new();
272 for page in &paginated.pages {
273 text::collect_chars_in_page(page, &mut used_chars);
274 }
275 if let Some(watermark) = &doc.watermark {
276 used_chars.entry(watermark.font).or_default().extend(watermark.text.chars());
277 }
278
279 let mut anchors: HashMap<String, (usize, f32)> = HashMap::new();
284 for (page_index, page) in paginated.pages.iter().enumerate() {
285 text::collect_anchors_in_page(page, page_index, paginated.page_height, &mut anchors);
286 }
287
288 let mut pdf = PdfDocument::new();
289 pdf.outline = text::build_outline(&paginated.pages, paginated.page_height);
290 pdf.metadata.title = doc.metadata.title.clone();
291 pdf.metadata.author = doc.metadata.author.clone();
292 pdf.metadata.subject = doc.metadata.subject.clone();
293 pdf.metadata.keywords = doc.metadata.keywords.clone();
294 pdf.metadata.creator = doc.metadata.creator.clone();
295 pdf.metadata.creation_date = doc.metadata.creation_date.map(|d| d.to_pdf_string());
296 pdf.metadata.mod_date = doc.metadata.mod_date.map(|d| d.to_pdf_string());
297 #[cfg(feature = "pdf-a")]
298 {
299 pdf.metadata.xmp_creation_date = doc.metadata.creation_date.map(|d| d.to_xmp_string());
300 pdf.metadata.xmp_mod_date = doc.metadata.mod_date.map(|d| d.to_xmp_string());
301 pdf.pdf_a3b = doc.pdf_a3b;
302 }
303 #[cfg(feature = "zugferd")]
304 {
305 pdf.zugferd_xml = doc.zugferd_xml.clone();
306 }
307 pdf.lang = doc.lang.clone();
308 #[cfg(feature = "tagged-pdf")]
309 {
310 pdf.pdf_ua = doc.pdf_ua;
311 }
312
313 let embedded = text::embed_fonts(&mut pdf, fonts, &used_chars)?;
314 let lookups = DocumentLookups {
315 embedded: &embedded,
316 anchors: &anchors,
317 };
318
319 #[cfg(feature = "tagged-pdf")]
320 let mut struct_builder = doc.pdf_ua.then(struct_tree::StructTreeBuilder::new);
321
322 for (page_index, page) in paginated.pages.iter().enumerate() {
323 let pdf_page = render_page(
324 page,
325 doc.watermark.as_ref(),
326 paginated.body_area,
327 paginated.page_width,
328 paginated.page_height,
329 page_index,
330 &lookups,
331 &mut pdf,
332 #[cfg(feature = "tagged-pdf")]
333 struct_builder.as_mut(),
334 #[cfg(feature = "tagged-pdf")]
335 &mut paginated.warnings,
336 )?;
337 pdf.add_page(pdf_page);
338 }
339
340 #[cfg(feature = "tagged-pdf")]
341 if let Some(builder) = struct_builder {
342 pdf.struct_tree = Some(builder.finish());
343 }
344
345 Ok((pdf.write(), paginated.warnings))
346}
347
348pub trait DocumentExt {
356 #[cfg(feature = "default-fonts")]
359 fn render(&self) -> Result<Vec<u8>, RenderError>;
360 #[cfg(feature = "default-fonts")]
361 fn render_with_diagnostics(&self) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError>;
362
363 fn render_with_fonts(&self, fonts: &FontRegistry) -> Result<Vec<u8>, RenderError>;
364 fn render_with_fonts_and_diagnostics(&self, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError>;
365}
366
367impl DocumentExt for Document {
368 #[cfg(feature = "default-fonts")]
369 fn render(&self) -> Result<Vec<u8>, RenderError> {
370 let (bytes, _warnings) = self.render_with_diagnostics()?;
371 Ok(bytes)
372 }
373
374 #[cfg(feature = "default-fonts")]
375 fn render_with_diagnostics(&self) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
376 let fonts = FontRegistry::with_defaults()?;
377 render_document(self, &fonts)
378 }
379
380 fn render_with_fonts(&self, fonts: &FontRegistry) -> Result<Vec<u8>, RenderError> {
381 let (bytes, _warnings) = self.render_with_fonts_and_diagnostics(fonts)?;
382 Ok(bytes)
383 }
384
385 fn render_with_fonts_and_diagnostics(&self, fonts: &FontRegistry) -> Result<(Vec<u8>, Vec<LayoutWarning>), RenderError> {
386 render_document(self, fonts)
387 }
388}