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 let mut font_context = FontContext::new();
159 register_document_fonts(&mut font_context, &document.fonts);
160 let engine = LayoutEngine::new();
161 font_context.reset_page_sentinel();
162 let mut pages = engine.layout(document, &font_context);
163 let mut passes = 1u32;
164
165 if font_context.saw_page_sentinel() {
166 for _ in 0..2 {
167 let needed = digits_for_count(pages.len());
168 if needed == font_context.sentinel_digit_count() {
169 break;
170 }
171 font_context.set_sentinel_digit_count(needed);
172 pages = engine.layout(document, &font_context);
173 passes += 1;
174 }
175 }
176 let layout_warnings = engine.take_warnings();
177 (pages, font_context, passes, layout_warnings)
178}
179
180pub fn count_layout_passes(document: &Document) -> u32 {
184 layout_with_sentinel_passes(document).2
185}
186
187pub fn render_with_warnings(document: &Document) -> Result<(Vec<u8>, Vec<String>), FormeError> {
191 render_with_warnings_and_passes(document).map(|(pdf, warnings, _passes)| (pdf, warnings))
192}
193
194pub fn render_with_warnings_and_passes(
197 document: &Document,
198) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
199 let profile = std::env::var("FORME_PROFILE").is_ok();
203 let t_layout = if profile {
204 Some(std::time::Instant::now())
205 } else {
206 None
207 };
208 let (pages, font_context, passes, layout_warnings) = layout_with_sentinel_passes(document);
209 let layout_ms = t_layout.map(|t| t.elapsed().as_secs_f64() * 1000.0);
210
211 let writer = PdfWriter::new();
212 let tagged = document.tagged
213 || document.pdf_ua
214 || matches!(
215 document.pdfa,
216 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
217 );
218 let t_ser = if profile {
219 Some(std::time::Instant::now())
220 } else {
221 None
222 };
223 let (pdf, warnings) = writer.write(
224 &pages,
225 &document.metadata,
226 &font_context,
227 tagged,
228 document.pdfa.as_ref(),
229 document.pdf_ua,
230 document.embedded_data.as_deref(),
231 &document.attachments,
232 document.zugferd.as_ref(),
233 document.flatten_forms,
234 )?;
235 let warnings = {
236 let mut all = layout_warnings;
237 all.extend(warnings);
238 all
239 };
240 let serialize_ms = t_ser.map(|t| t.elapsed().as_secs_f64() * 1000.0);
241 let pdf = if let Some(ref sig_config) = document.certification {
242 pdf::certify::certify_pdf(&pdf, sig_config)?
243 } else {
244 pdf
245 };
246 if let (Some(l), Some(s)) = (layout_ms, serialize_ms) {
247 eprintln!(
248 "FORME_PROFILE pages={} passes={passes} layout_ms={l:.1} serialize_ms={s:.1}",
249 pages.len()
250 );
251 }
252 Ok((pdf, warnings, passes))
253}
254
255pub fn render_with_layout(
262 document: &Document,
263) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
264 let (pages, font_context, _passes, layout_warnings) = layout_with_sentinel_passes(document);
265 let layout_info = LayoutInfo::from_pages(&pages);
266 let writer = PdfWriter::new();
267 let tagged = document.tagged
268 || document.pdf_ua
269 || matches!(
270 document.pdfa,
271 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
272 );
273 let (pdf, warnings) = writer.write(
274 &pages,
275 &document.metadata,
276 &font_context,
277 tagged,
278 document.pdfa.as_ref(),
279 document.pdf_ua,
280 document.embedded_data.as_deref(),
281 &document.attachments,
282 document.zugferd.as_ref(),
283 document.flatten_forms,
284 )?;
285 let pdf = if let Some(ref sig_config) = document.certification {
286 pdf::certify::certify_pdf(&pdf, sig_config)?
287 } else {
288 pdf
289 };
290 let warnings = {
291 let mut all = layout_warnings;
292 all.extend(warnings);
293 all
294 };
295 Ok((pdf, layout_info, warnings))
296}
297
298fn digits_for_count(n: usize) -> u32 {
300 if n < 10 {
301 1
302 } else if n < 100 {
303 2
304 } else if n < 1000 {
305 3
306 } else {
307 4
308 }
309}
310
311fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
313 use base64::Engine as _;
314 let b64 = base64::engine::general_purpose::STANDARD;
315
316 for entry in fonts {
317 let bytes = if let Some(comma_pos) = entry.src.find(',') {
318 b64.decode(&entry.src[comma_pos + 1..]).ok()
320 } else {
321 b64.decode(&entry.src).ok()
323 };
324
325 if let Some(data) = bytes {
326 font_context
327 .registry_mut()
328 .register(&entry.family, entry.weight, entry.italic, data);
329 }
330 }
331}
332
333pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
335 let document: Document = serde_json::from_str(json)?;
336 render(&document)
337}
338
339pub fn render_json_with_layout(
341 json: &str,
342) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
343 let document: Document = serde_json::from_str(json)?;
344 render_with_layout(&document)
345}
346
347pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
353 let template: serde_json::Value = serde_json::from_str(template_json)?;
354 let data: serde_json::Value = serde_json::from_str(data_json)?;
355 let resolved = template::evaluate_template(&template, &data)?;
356 let document: Document = serde_json::from_value(resolved)?;
357 render(&document)
358}
359
360pub fn render_template_with_layout(
362 template_json: &str,
363 data_json: &str,
364) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
365 let template: serde_json::Value = serde_json::from_str(template_json)?;
366 let data: serde_json::Value = serde_json::from_str(data_json)?;
367 let resolved = template::evaluate_template(&template, &data)?;
368 let document: Document = serde_json::from_value(resolved)?;
369 render_with_layout(&document).map(|(pdf, layout, _warnings)| (pdf, layout))
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_digits_for_count() {
378 assert_eq!(digits_for_count(0), 1);
379 assert_eq!(digits_for_count(1), 1);
380 assert_eq!(digits_for_count(9), 1);
381 assert_eq!(digits_for_count(10), 2);
382 assert_eq!(digits_for_count(99), 2);
383 assert_eq!(digits_for_count(100), 3);
384 assert_eq!(digits_for_count(999), 3);
385 assert_eq!(digits_for_count(1000), 4);
386 }
387}