1pub mod barcode;
30pub mod chart;
31pub mod error;
32pub mod font;
33pub mod image_loader;
34pub mod layout;
35pub mod model;
36pub mod pdf;
37pub mod qrcode;
38pub mod style;
39pub mod svg;
40pub mod template;
41pub mod text;
42
43#[cfg(feature = "wasm")]
44pub mod wasm;
45
46#[cfg(feature = "wasm-raw")]
47pub mod wasm_raw;
48
49pub use error::FormeError;
50pub use layout::LayoutInfo;
51pub use model::{
52 CertificationConfig, ColumnDef, ColumnWidth, FontEntry, PatternType, RedactionPattern,
53 RedactionRegion, TextRun,
54};
55pub use model::{ChartDataPoint, ChartSeries, DotPlotGroup};
56pub use model::{Document, Metadata, Node, NodeKind, PageConfig, PageSize};
57pub use style::Style;
58
59use font::FontContext;
60use layout::LayoutEngine;
61use pdf::PdfWriter;
62
63pub fn certify_pdf(
69 pdf_bytes: &[u8],
70 config: &model::CertificationConfig,
71) -> Result<Vec<u8>, FormeError> {
72 pdf::certify::certify_pdf(pdf_bytes, config)
73}
74
75pub fn redact_pdf(
81 pdf_bytes: &[u8],
82 regions: &[model::RedactionRegion],
83) -> Result<Vec<u8>, FormeError> {
84 pdf::redaction::redact_pdf(pdf_bytes, regions)
85}
86
87pub fn find_text_regions(
92 pdf_bytes: &[u8],
93 patterns: &[model::RedactionPattern],
94) -> Result<Vec<RedactionRegion>, FormeError> {
95 pdf::redaction::find_text_regions(pdf_bytes, patterns)
96}
97
98pub fn redact_text(
103 pdf_bytes: &[u8],
104 patterns: &[model::RedactionPattern],
105) -> Result<Vec<u8>, FormeError> {
106 pdf::redaction::redact_text(pdf_bytes, patterns)
107}
108
109pub fn merge_pdfs(pdfs: &[&[u8]]) -> Result<Vec<u8>, FormeError> {
114 pdf::merge::merge_pdfs(pdfs)
115}
116
117pub fn render(document: &Document) -> Result<Vec<u8>, FormeError> {
123 render_with_warnings(document).map(|(pdf, _warnings)| pdf)
124}
125
126fn layout_with_sentinel_passes(
151 document: &Document,
152) -> (
153 Vec<crate::layout::LayoutPage>,
154 FontContext,
155 u32,
156 Vec<String>,
157) {
158 layout_with_sentinel_passes_audited(document, false)
159}
160
161fn layout_with_sentinel_passes_audited(
162 document: &Document,
163 audit: bool,
164) -> (
165 Vec<crate::layout::LayoutPage>,
166 FontContext,
167 u32,
168 Vec<String>,
169) {
170 let mut font_context = FontContext::new();
171 register_document_fonts(&mut font_context, &document.fonts);
172 let engine = LayoutEngine::new();
173 font_context.reset_page_sentinel();
174 let mut pages = engine.layout(document, &font_context);
175 let mut passes = 1u32;
176
177 if font_context.saw_page_sentinel() {
178 for _ in 0..2 {
179 let needed = digits_for_count(pages.len());
180 if needed == font_context.sentinel_digit_count() {
181 break;
182 }
183 font_context.set_sentinel_digit_count(needed);
184 pages = engine.layout(document, &font_context);
185 passes += 1;
186 }
187 }
188 let mut layout_warnings = engine.take_warnings();
189 if audit {
190 crate::layout::audit::audit_content(document, &pages, &mut layout_warnings);
191 }
192 (pages, font_context, passes, layout_warnings)
193}
194
195pub fn count_layout_passes(document: &Document) -> u32 {
199 layout_with_sentinel_passes(document).2
200}
201
202pub fn render_with_warnings(document: &Document) -> Result<(Vec<u8>, Vec<String>), FormeError> {
206 render_with_warnings_and_passes(document).map(|(pdf, warnings, _passes)| (pdf, warnings))
207}
208
209#[derive(Debug, Clone, Copy, Default, serde::Deserialize)]
215#[serde(default, rename_all = "camelCase")]
216pub struct RenderOptions {
217 pub audit_content: bool,
224}
225
226pub fn render_with_warnings_and_passes(
229 document: &Document,
230) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
231 render_with_options(document, RenderOptions::default())
232}
233
234fn effective_pdf_version(document: &Document) -> crate::model::PdfVersion {
243 use crate::model::{PdfAConformance, PdfVersion};
244 if document.pdf_ua2 {
245 return PdfVersion::V2_0;
246 }
247 match &document.pdfa {
248 Some(PdfAConformance::A4 | PdfAConformance::A4f) => PdfVersion::V2_0,
249 _ => document.pdf_version,
250 }
251}
252
253fn validate_pdf_version(document: &Document) -> Result<(), FormeError> {
254 use crate::model::{PdfAConformance, PdfVersion};
255 if document.pdf_version != PdfVersion::V2_0 {
256 return Ok(());
257 }
258 if let Some(level) = &document.pdfa {
259 let family = match level {
260 PdfAConformance::A2a | PdfAConformance::A2b | PdfAConformance::A2u => "PDF/A-2",
261 PdfAConformance::A3a | PdfAConformance::A3b | PdfAConformance::A3u => "PDF/A-3",
262 PdfAConformance::A4 | PdfAConformance::A4f => return Ok(()),
265 };
266 return Err(FormeError::RenderError(format!(
267 "pdfVersion \"2.0\" contradicts pdfa: {family} is defined over ISO 32000-1 (PDF 1.7). Drop the pdfa claim, or keep pdfVersion \"1.7\" (the default). PDF/A-4 is the 2.0-based archival standard."
268 )));
269 }
270 if document.pdf_ua {
271 return Err(FormeError::RenderError(
272 "pdfVersion \"2.0\" contradicts pdfUa: PDF/UA-1 is defined over ISO 32000-1 (PDF 1.7). Drop the pdfUa claim, or keep pdfVersion \"1.7\" (the default). PDF/UA-2 is the 2.0-based accessibility standard (pdfUa2)."
273 .to_string(),
274 ));
275 }
276 Ok(())
277}
278
279fn validate_pdf_ua2(document: &Document) -> Result<(), FormeError> {
283 use crate::model::PdfAConformance;
284 if !document.pdf_ua2 {
285 return Ok(());
286 }
287 if document.pdf_ua {
288 return Err(FormeError::RenderError(
289 "pdfUa2 contradicts pdfUa: a file claims PDF/UA-1 (ISO 32000-1) or PDF/UA-2 \
290 (PDF 2.0), not both. Keep exactly one."
291 .to_string(),
292 ));
293 }
294 if let Some(
295 PdfAConformance::A2a
296 | PdfAConformance::A2b
297 | PdfAConformance::A2u
298 | PdfAConformance::A3a
299 | PdfAConformance::A3b
300 | PdfAConformance::A3u,
301 ) = document.pdfa
302 {
303 return Err(FormeError::RenderError(
304 "pdfUa2 contradicts a 1.7-based pdfa level (2x/3x are ISO 32000-1). \
305 Compose PDF/UA-2 with pdfa: \"4\" or \"4f\", or drop one claim."
306 .to_string(),
307 ));
308 }
309 Ok(())
310}
311
312pub fn render_with_options(
313 document: &Document,
314 options: RenderOptions,
315) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
316 validate_pdf_version(document)?;
317 validate_pdf_ua2(document)?;
318 let profile = std::env::var("FORME_PROFILE").is_ok();
322 let t_layout = if profile {
323 Some(std::time::Instant::now())
324 } else {
325 None
326 };
327 let (pages, font_context, passes, layout_warnings) =
328 layout_with_sentinel_passes_audited(document, options.audit_content);
329 let layout_ms = t_layout.map(|t| t.elapsed().as_secs_f64() * 1000.0);
330
331 let writer = PdfWriter::new();
332 let tagged = document.tagged
333 || document.pdf_ua
334 || document.pdf_ua2
335 || matches!(
336 document.pdfa,
337 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
338 );
339 let t_ser = if profile {
340 Some(std::time::Instant::now())
341 } else {
342 None
343 };
344 let (pdf, warnings) = writer.write(
345 &pages,
346 &document.metadata,
347 &font_context,
348 tagged,
349 document.pdfa.as_ref(),
350 document.pdf_ua,
351 document.embedded_data.as_deref(),
352 &document.attachments,
353 document.zugferd.as_ref(),
354 document.flatten_forms,
355 effective_pdf_version(document),
356 document.pdf_ua2,
357 )?;
358 let warnings = {
359 let mut all = layout_warnings;
360 all.extend(warnings);
361 all
362 };
363 let serialize_ms = t_ser.map(|t| t.elapsed().as_secs_f64() * 1000.0);
364 let pdf = if let Some(ref sig_config) = document.certification {
365 pdf::certify::certify_pdf(&pdf, sig_config)?
366 } else {
367 pdf
368 };
369 if let (Some(l), Some(s)) = (layout_ms, serialize_ms) {
370 eprintln!(
371 "FORME_PROFILE pages={} passes={passes} layout_ms={l:.1} serialize_ms={s:.1}",
372 pages.len()
373 );
374 }
375 Ok((pdf, warnings, passes))
376}
377
378pub fn render_with_layout(
385 document: &Document,
386) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
387 render_with_layout_and_passes(document)
388 .map(|(pdf, layout, warnings, _passes)| (pdf, layout, warnings))
389}
390
391pub fn render_with_layout_and_passes(
397 document: &Document,
398) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
399 render_with_layout_and_options(document, RenderOptions::default())
400}
401
402pub fn render_with_layout_and_options(
404 document: &Document,
405 options: RenderOptions,
406) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
407 validate_pdf_version(document)?;
408 validate_pdf_ua2(document)?;
409 let (pages, font_context, passes, layout_warnings) =
410 layout_with_sentinel_passes_audited(document, options.audit_content);
411 let layout_info = LayoutInfo::from_pages(&pages);
412 let writer = PdfWriter::new();
413 let tagged = document.tagged
414 || document.pdf_ua
415 || document.pdf_ua2
416 || matches!(
417 document.pdfa,
418 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
419 );
420 let (pdf, warnings) = writer.write(
421 &pages,
422 &document.metadata,
423 &font_context,
424 tagged,
425 document.pdfa.as_ref(),
426 document.pdf_ua,
427 document.embedded_data.as_deref(),
428 &document.attachments,
429 document.zugferd.as_ref(),
430 document.flatten_forms,
431 effective_pdf_version(document),
432 document.pdf_ua2,
433 )?;
434 let pdf = if let Some(ref sig_config) = document.certification {
435 pdf::certify::certify_pdf(&pdf, sig_config)?
436 } else {
437 pdf
438 };
439 let warnings = {
440 let mut all = layout_warnings;
441 all.extend(warnings);
442 all
443 };
444 Ok((pdf, layout_info, warnings, passes))
445}
446
447fn digits_for_count(n: usize) -> u32 {
449 if n < 10 {
450 1
451 } else if n < 100 {
452 2
453 } else if n < 1000 {
454 3
455 } else {
456 4
457 }
458}
459
460fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
462 use base64::Engine as _;
463 let b64 = base64::engine::general_purpose::STANDARD;
464
465 for entry in fonts {
466 let bytes = if let Some(comma_pos) = entry.src.find(',') {
467 b64.decode(&entry.src[comma_pos + 1..]).ok()
469 } else {
470 b64.decode(&entry.src).ok()
472 };
473
474 if let Some(data) = bytes {
475 font_context
476 .registry_mut()
477 .register(&entry.family, entry.weight, entry.italic, data);
478 }
479 }
480}
481
482pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
484 let document: Document = serde_json::from_str(json)?;
485 render(&document)
486}
487
488pub fn render_json_with_layout(
490 json: &str,
491) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
492 let document: Document = serde_json::from_str(json)?;
493 render_with_layout(&document)
494}
495
496pub fn render_json_with_layout_and_options(
501 json: &str,
502 options: RenderOptions,
503) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
504 let document: Document = serde_json::from_str(json)?;
505 render_with_layout_and_options(&document, options)
506}
507
508pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
514 let template: serde_json::Value = serde_json::from_str(template_json)?;
515 let data: serde_json::Value = serde_json::from_str(data_json)?;
516 let resolved = template::evaluate_template(&template, &data)?;
517 let document: Document = serde_json::from_value(resolved)?;
518 render(&document)
519}
520
521pub fn render_template_with_layout(
530 template_json: &str,
531 data_json: &str,
532) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
533 let template: serde_json::Value = serde_json::from_str(template_json)?;
534 let data: serde_json::Value = serde_json::from_str(data_json)?;
535 let resolved = template::evaluate_template(&template, &data)?;
536 let document: Document = serde_json::from_value(resolved)?;
537 render_with_layout(&document)
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn test_digits_for_count() {
546 assert_eq!(digits_for_count(0), 1);
547 assert_eq!(digits_for_count(1), 1);
548 assert_eq!(digits_for_count(9), 1);
549 assert_eq!(digits_for_count(10), 2);
550 assert_eq!(digits_for_count(99), 2);
551 assert_eq!(digits_for_count(100), 3);
552 assert_eq!(digits_for_count(999), 3);
553 assert_eq!(digits_for_count(1000), 4);
554 }
555}