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
234pub fn render_with_options(
236 document: &Document,
237 options: RenderOptions,
238) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
239 let profile = std::env::var("FORME_PROFILE").is_ok();
243 let t_layout = if profile {
244 Some(std::time::Instant::now())
245 } else {
246 None
247 };
248 let (pages, font_context, passes, layout_warnings) =
249 layout_with_sentinel_passes_audited(document, options.audit_content);
250 let layout_ms = t_layout.map(|t| t.elapsed().as_secs_f64() * 1000.0);
251
252 let writer = PdfWriter::new();
253 let tagged = document.tagged
254 || document.pdf_ua
255 || matches!(
256 document.pdfa,
257 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
258 );
259 let t_ser = if profile {
260 Some(std::time::Instant::now())
261 } else {
262 None
263 };
264 let (pdf, warnings) = writer.write(
265 &pages,
266 &document.metadata,
267 &font_context,
268 tagged,
269 document.pdfa.as_ref(),
270 document.pdf_ua,
271 document.embedded_data.as_deref(),
272 &document.attachments,
273 document.zugferd.as_ref(),
274 document.flatten_forms,
275 )?;
276 let warnings = {
277 let mut all = layout_warnings;
278 all.extend(warnings);
279 all
280 };
281 let serialize_ms = t_ser.map(|t| t.elapsed().as_secs_f64() * 1000.0);
282 let pdf = if let Some(ref sig_config) = document.certification {
283 pdf::certify::certify_pdf(&pdf, sig_config)?
284 } else {
285 pdf
286 };
287 if let (Some(l), Some(s)) = (layout_ms, serialize_ms) {
288 eprintln!(
289 "FORME_PROFILE pages={} passes={passes} layout_ms={l:.1} serialize_ms={s:.1}",
290 pages.len()
291 );
292 }
293 Ok((pdf, warnings, passes))
294}
295
296pub fn render_with_layout(
303 document: &Document,
304) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
305 render_with_layout_and_passes(document)
306 .map(|(pdf, layout, warnings, _passes)| (pdf, layout, warnings))
307}
308
309pub fn render_with_layout_and_passes(
315 document: &Document,
316) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
317 render_with_layout_and_options(document, RenderOptions::default())
318}
319
320pub fn render_with_layout_and_options(
322 document: &Document,
323 options: RenderOptions,
324) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
325 let (pages, font_context, passes, layout_warnings) =
326 layout_with_sentinel_passes_audited(document, options.audit_content);
327 let layout_info = LayoutInfo::from_pages(&pages);
328 let writer = PdfWriter::new();
329 let tagged = document.tagged
330 || document.pdf_ua
331 || matches!(
332 document.pdfa,
333 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
334 );
335 let (pdf, warnings) = writer.write(
336 &pages,
337 &document.metadata,
338 &font_context,
339 tagged,
340 document.pdfa.as_ref(),
341 document.pdf_ua,
342 document.embedded_data.as_deref(),
343 &document.attachments,
344 document.zugferd.as_ref(),
345 document.flatten_forms,
346 )?;
347 let pdf = if let Some(ref sig_config) = document.certification {
348 pdf::certify::certify_pdf(&pdf, sig_config)?
349 } else {
350 pdf
351 };
352 let warnings = {
353 let mut all = layout_warnings;
354 all.extend(warnings);
355 all
356 };
357 Ok((pdf, layout_info, warnings, passes))
358}
359
360fn digits_for_count(n: usize) -> u32 {
362 if n < 10 {
363 1
364 } else if n < 100 {
365 2
366 } else if n < 1000 {
367 3
368 } else {
369 4
370 }
371}
372
373fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
375 use base64::Engine as _;
376 let b64 = base64::engine::general_purpose::STANDARD;
377
378 for entry in fonts {
379 let bytes = if let Some(comma_pos) = entry.src.find(',') {
380 b64.decode(&entry.src[comma_pos + 1..]).ok()
382 } else {
383 b64.decode(&entry.src).ok()
385 };
386
387 if let Some(data) = bytes {
388 font_context
389 .registry_mut()
390 .register(&entry.family, entry.weight, entry.italic, data);
391 }
392 }
393}
394
395pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
397 let document: Document = serde_json::from_str(json)?;
398 render(&document)
399}
400
401pub fn render_json_with_layout(
403 json: &str,
404) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
405 let document: Document = serde_json::from_str(json)?;
406 render_with_layout(&document)
407}
408
409pub fn render_json_with_layout_and_options(
414 json: &str,
415 options: RenderOptions,
416) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
417 let document: Document = serde_json::from_str(json)?;
418 render_with_layout_and_options(&document, options)
419}
420
421pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
427 let template: serde_json::Value = serde_json::from_str(template_json)?;
428 let data: serde_json::Value = serde_json::from_str(data_json)?;
429 let resolved = template::evaluate_template(&template, &data)?;
430 let document: Document = serde_json::from_value(resolved)?;
431 render(&document)
432}
433
434pub fn render_template_with_layout(
436 template_json: &str,
437 data_json: &str,
438) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
439 let template: serde_json::Value = serde_json::from_str(template_json)?;
440 let data: serde_json::Value = serde_json::from_str(data_json)?;
441 let resolved = template::evaluate_template(&template, &data)?;
442 let document: Document = serde_json::from_value(resolved)?;
443 render_with_layout(&document).map(|(pdf, layout, _warnings)| (pdf, layout))
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_digits_for_count() {
452 assert_eq!(digits_for_count(0), 1);
453 assert_eq!(digits_for_count(1), 1);
454 assert_eq!(digits_for_count(9), 1);
455 assert_eq!(digits_for_count(10), 2);
456 assert_eq!(digits_for_count(99), 2);
457 assert_eq!(digits_for_count(100), 3);
458 assert_eq!(digits_for_count(999), 3);
459 assert_eq!(digits_for_count(1000), 4);
460 }
461}