1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 backend::{FontRef, PdfBackend},
7 backend::pdf_writer_backend::PdfWriterBackend,
8 compliance::ua::{AccessibilityConfig, StructTag, StructureTree, UaValidator},
9 elements::{
10 footnote::FootnoteAccumulator,
11 footer::{PageFooter, SectionedFooter},
12 header::SectionedHeader,
13 toc::TocEntry,
14 Element, LayoutMode, RenderContext,
15 },
16 layout::{PageFlow, TextLayoutEngine},
17 page::PageLayout,
18 styles::{SecurityClassification, TraceabilityMetadata, Watermark},
19 NormaxisPdfError, Result,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum PdfStandard {
26 #[default]
28 Pdf17,
29 PdfA1b,
31 PdfA2b,
33 PdfUa2,
36}
37
38impl PdfStandard {
39 pub fn is_pdfa(self) -> bool {
40 matches!(self, Self::PdfA1b | Self::PdfA2b)
41 }
42
43 pub fn is_pdfu2(self) -> bool {
44 matches!(self, Self::PdfUa2)
45 }
46
47 pub fn xmp_part(self) -> u8 {
48 match self {
49 Self::PdfA1b => 1,
50 Self::PdfA2b => 2,
51 Self::PdfUa2 | Self::Pdf17 => 0,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum CompressionLevel {
60 None,
61 Fast,
62 #[default]
63 Default,
64 Best,
65}
66
67impl CompressionLevel {
68 pub fn to_zlib_level(self) -> u32 {
69 match self {
70 Self::None => 0,
71 Self::Fast => 1,
72 Self::Default => 6,
73 Self::Best => 9,
74 }
75 }
76}
77
78pub struct Document {
80 pub(crate) title: String,
81 pub(crate) style: crate::styles::DocumentStyle,
82 pub(crate) fonts: crate::fonts::FontRegistry,
83 pub(crate) header: Option<Box<dyn Element>>,
84 pub(crate) sectioned_header: Option<SectionedHeader>,
85 pub(crate) footer: Option<Box<dyn Element>>,
86 pub(crate) sectioned_footer: Option<SectionedFooter>,
87 pub(crate) watermark: Option<Watermark>,
88 pub(crate) elements: Vec<Box<dyn Element>>,
89 #[allow(dead_code)]
90 pub(crate) footnotes: Vec<(u32, Vec<String>)>,
91 #[allow(dead_code)]
92 pub(crate) toc_entries: Option<Vec<TocEntry>>,
93 pub(crate) compression: CompressionLevel,
94 pub(crate) standard: PdfStandard,
95 pub(crate) signature: Option<crate::signing::SignatureOptions>,
96 pub(crate) traceability: Option<TraceabilityMetadata>,
97 pub(crate) accessibility: AccessibilityConfig,
98}
99
100impl Document {
101 fn collect_toc_entries_pass(&self) -> Vec<TocEntry> {
103 let layout = PageLayout::from_style(&self.style);
104 let hdr_h = if let Some(ref h) = self.header { h.estimated_height_mm() } else { 0.0 };
105 let mut cursor_y = layout.page_height_mm - layout.margin_top_mm - hdr_h;
106 let mut page = 1u32;
107 let mut entries = Vec::new();
108
109 for element in &self.elements {
110 if let LayoutMode::Flow = element.layout_mode() {
111 let h = element.estimated_height_mm();
112 if cursor_y - h < layout.margin_bottom_mm {
113 page += 1;
114 cursor_y = layout.page_height_mm - layout.margin_top_mm - hdr_h;
115 }
116 cursor_y -= h;
117 }
118 if let Some((level, title)) = element.as_section_info() {
119 entries.push(TocEntry { level, title: title.to_string(), page_number: page });
120 }
121 }
122 entries
123 }
124
125 pub fn render_to_bytes(mut self) -> Result<Vec<u8>> {
126 let toc_data = self.collect_toc_entries_pass();
128 if !toc_data.is_empty() {
129 for element in &mut self.elements {
130 element.inject_toc_entries(&toc_data);
131 }
132 }
133
134 if self.watermark.is_none() {
136 if let Some(ref trace) = self.traceability {
137 if trace.classification != SecurityClassification::Public {
138 self.watermark = Some(
139 Watermark::new(trace.classification.label_pt())
140 .opacity(0.08)
141 .color(trace.classification.watermark_color()),
142 );
143 }
144 }
145 }
146
147 let Document {
148 title,
149 style,
150 fonts,
151 header,
152 sectioned_header,
153 footer,
154 sectioned_footer,
155 watermark,
156 elements,
157 footnotes: _,
158 toc_entries: _,
159 compression,
160 standard,
161 signature,
162 traceability: _,
163 accessibility,
164 } = self;
165
166 let total_pages = {
168 let mut flow = PageFlow::new(&style);
169 let mut pages = 1u32;
170 let hdr_h = header_height_mm(&header, §ioned_header, 1);
171 flow.advance(hdr_h);
172 for element in &elements {
173 if let LayoutMode::Flow = element.layout_mode() {
174 let h = element.estimated_height_mm();
175 if flow.would_overflow(h) {
176 flow.new_page();
177 pages += 1;
178 flow.advance(header_height_mm(&header, §ioned_header, flow.page_number));
179 }
180 flow.advance(h);
181 }
182 }
183 pages
184 };
185
186 let (pw, ph) = style.page_size.dimensions_mm();
187 let layout = PageLayout::from_style(&style);
188
189 let accessibility = if standard == PdfStandard::PdfUa2 && !accessibility.enabled {
191 AccessibilityConfig { enabled: true, ..accessibility }
192 } else {
193 accessibility
194 };
195
196 let mut backend = PdfWriterBackend::new(&title, compression.to_zlib_level());
198 if standard.is_pdfa() {
199 backend.set_pdfa(standard.xmp_part());
200 }
201 if standard.is_pdfu2() {
202 backend.set_pdfu2();
203 }
204 if let Some(ref sig) = signature {
205 backend.set_signature(&sig.reason, &sig.location, sig.reserved_bytes);
206 }
207
208 let mut font_map: HashMap<String, FontRef> = HashMap::new();
210 for (family_name, family) in fonts.families() {
211 if let Ok(fr) = backend.embed_font(&family.regular.bytes, &family_name, false, false) {
212 font_map.insert(format!("{family_name}::regular"), fr);
213 }
214 if let Some(ref v) = family.bold {
215 if let Ok(fr) = backend.embed_font(&v.bytes, &family_name, true, false) {
216 font_map.insert(format!("{family_name}::bold"), fr);
217 }
218 }
219 if let Some(ref v) = family.italic {
220 if let Ok(fr) = backend.embed_font(&v.bytes, &family_name, false, true) {
221 font_map.insert(format!("{family_name}::italic"), fr);
222 }
223 }
224 if let Some(ref v) = family.bold_italic {
225 if let Ok(fr) = backend.embed_font(&v.bytes, &family_name, true, true) {
226 font_map.insert(format!("{family_name}::bold_italic"), fr);
227 }
228 }
229 }
230
231 backend.new_page(pw, ph)?;
233
234 let default_font_family = fonts.default_family_name().to_string();
235 let layout_engine = TextLayoutEngine::new(&fonts, &style);
236 let flow = PageFlow::new(&style);
237
238 let ua_enabled = accessibility.enabled;
239 let ua_lang = accessibility.lang.clone();
240
241 let mut ctx = RenderContext {
242 backend: Box::new(backend),
243 font_map,
244 flow,
245 layout,
246 layout_engine,
247 style,
248 fonts,
249 force_page_break: false,
250 default_font_family,
251 page_number: 1,
252 total_pages,
253 resume_index: 0,
254 glyph_tracker: crate::layout::GlyphUsageTracker::new(),
255 reserved_footnotes_mm: 0.0,
256 ua_config: accessibility,
257 ua_events: StructureTree::new(),
258 mcid_counter: 0,
259 last_heading_level: None,
260 };
261
262 let mut fixed_pending: Vec<(i32, &dyn Element)> = Vec::new();
264 let mut footnote_acc = FootnoteAccumulator::new();
265
266 if ua_enabled {
268 ctx.ua_events.begin_group(StructTag::Document, None);
269 }
270
271 render_watermark_if_any(&watermark, &mut ctx, pw, ph);
273 render_header_for_page(&header, §ioned_header, &mut ctx);
274
275 for element in &elements {
276 match element.layout_mode() {
277 LayoutMode::Flow => {
278 if ctx.force_page_break {
279 ctx.force_page_break = false;
280 flush_page(
281 &mut ctx, &mut fixed_pending, &mut footnote_acc,
282 &footer, §ioned_footer,
283 &watermark, &header, §ioned_header,
284 pw, ph,
285 )?;
286 }
287
288 ctx.reset_resume();
289 loop {
290 let result = element.render(&mut ctx)?;
291 if !result.has_more {
292 break;
293 }
294 flush_page(
295 &mut ctx, &mut fixed_pending, &mut footnote_acc,
296 &footer, §ioned_footer,
297 &watermark, &header, §ioned_header,
298 pw, ph,
299 )?;
300 }
301 }
302 LayoutMode::Fixed(ref fb) => {
303 fixed_pending.push((fb.z_index, element.as_ref()));
304 }
305 }
306 }
307
308 fixed_pending.sort_by_key(|(z, _)| *z);
310 for (_, elem) in &fixed_pending {
311 let _ = elem.render(&mut ctx);
312 }
313 fixed_pending.clear();
314
315 footnote_acc.render_pending(&mut ctx)?;
316 render_footer_for_page(&footer, §ioned_footer, &mut ctx);
317
318 if ua_enabled {
320 ctx.ua_events.end_group(); let events = std::mem::take(&mut ctx.ua_events.events);
322 let tree_for_validation = StructureTree { events: events.clone() };
323 let validator = UaValidator::validate(Some(&tree_for_validation), &ua_lang);
324 validator.report();
325 ctx.backend.write_structure_tree(&events, &ua_lang);
326 }
327
328 ctx.backend.finish()
330 }
331
332 pub fn render_to_file(self, path: impl AsRef<std::path::Path>) -> Result<()> {
333 let bytes = self.render_to_bytes()?;
334 std::fs::write(path, bytes).map_err(NormaxisPdfError::IoError)
335 }
336
337 pub fn render_prepared_for_signing(
340 self,
341 opts: crate::signing::SignatureOptions,
342 ) -> Result<crate::signing::PreparedPdf> {
343 let reserved = opts.reserved_bytes;
344 let bytes = Document { signature: Some(opts), ..self }.render_to_bytes()?;
345 crate::signing::extract_prepared(bytes, reserved)
346 }
347}
348
349fn header_height_mm(
352 header: &Option<Box<dyn Element>>,
353 sectioned: &Option<SectionedHeader>,
354 page: u32,
355) -> f64 {
356 if let Some(sh) = sectioned {
357 sh.resolve(page).map(|h| h.estimated_height_mm()).unwrap_or(0.0)
358 } else if let Some(h) = header {
359 h.estimated_height_mm()
360 } else {
361 0.0
362 }
363}
364
365#[allow(clippy::too_many_arguments)]
366fn flush_page<'e>(
367 ctx: &mut RenderContext,
368 fixed_pending: &mut Vec<(i32, &'e dyn Element)>,
369 footnote_acc: &mut FootnoteAccumulator,
370 footer: &Option<Box<dyn Element>>,
371 sectioned_footer: &Option<SectionedFooter>,
372 watermark: &Option<Watermark>,
373 header: &Option<Box<dyn Element>>,
374 sectioned_header: &Option<SectionedHeader>,
375 pw: f64,
376 ph: f64,
377) -> Result<()> {
378 footnote_acc.render_pending(ctx)?;
380 ctx.reserved_footnotes_mm = 0.0;
381
382 fixed_pending.sort_by_key(|(z, _)| *z);
384 for (_, elem) in fixed_pending.iter() {
385 let _ = elem.render(ctx);
386 }
387 fixed_pending.clear();
388
389 render_footer_for_page(footer, sectioned_footer, ctx);
390
391 ctx.backend.new_page(pw, ph)?;
393 ctx.flow.new_page();
394 ctx.page_number = ctx.flow.page_number;
395 ctx.mcid_counter = 0;
396
397 render_watermark_if_any(watermark, ctx, pw, ph);
398 render_header_for_page(header, sectioned_header, ctx);
399 Ok(())
400}
401
402fn render_header_for_page(
403 header: &Option<Box<dyn Element>>,
404 sectioned: &Option<SectionedHeader>,
405 ctx: &mut RenderContext,
406) {
407 let page = ctx.page_number;
408 if let Some(sh) = sectioned {
409 if let Some(hdr) = sh.resolve(page) {
410 let _ = hdr.render(ctx);
411 }
412 } else if let Some(hdr) = header {
413 let _ = hdr.render(ctx);
414 }
415}
416
417fn render_footer_for_page(
418 footer: &Option<Box<dyn Element>>,
419 sectioned: &Option<SectionedFooter>,
420 ctx: &mut RenderContext,
421) {
422 let page = ctx.page_number;
423 let footer_ref: Option<&PageFooter> = if let Some(sf) = sectioned {
424 sf.resolve(page)
425 } else {
426 None
427 };
428
429 if let Some(f) = footer_ref {
430 let saved = ctx.flow.cursor_y_mm;
431 ctx.flow.cursor_y_mm = ctx.style.margin_bottom_mm + f.estimated_height_mm();
432 let _ = f.render(ctx);
433 ctx.flow.cursor_y_mm = saved;
434 return;
435 }
436
437 if let Some(f) = footer {
438 let h = f.estimated_height_mm();
439 let saved = ctx.flow.cursor_y_mm;
440 ctx.flow.cursor_y_mm = ctx.style.margin_bottom_mm + h;
441 let _ = f.render(ctx);
442 ctx.flow.cursor_y_mm = saved;
443 }
444}
445
446fn render_watermark_if_any(
447 watermark: &Option<Watermark>,
448 ctx: &mut RenderContext,
449 page_width_mm: f64,
450 page_height_mm: f64,
451) {
452 let Some(wm) = watermark else { return };
453 let Some(font_ref) = ctx.get_font_ref(false, false) else { return };
454
455 let cx_mm = page_width_mm / 2.0;
456 let cy_mm = page_height_mm / 2.0;
457 let half_w = ctx.fonts.get_default()
458 .measure_text_mm(&wm.text, wm.font_size, false, false) / 2.0;
459
460 if ctx.ua_config.enabled {
461 ctx.backend.begin_artifact_content();
462 }
463
464 let _ = ctx.backend.set_opacity(wm.opacity);
466 let _ = ctx.backend.draw_text_rotated(
467 &wm.text,
468 cx_mm, cy_mm,
469 wm.font_size,
470 font_ref,
471 &wm.color,
472 wm.angle_deg,
473 half_w,
474 );
475 ctx.backend.reset_opacity();
476
477 if ctx.ua_config.enabled {
478 ctx.backend.end_tagged_content();
479 }
480}