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 render_with_layout_and_passes(document)
265 .map(|(pdf, layout, warnings, _passes)| (pdf, layout, warnings))
266}
267
268pub fn render_with_layout_and_passes(
274 document: &Document,
275) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
276 let (pages, font_context, passes, layout_warnings) = layout_with_sentinel_passes(document);
277 let layout_info = LayoutInfo::from_pages(&pages);
278 let writer = PdfWriter::new();
279 let tagged = document.tagged
280 || document.pdf_ua
281 || matches!(
282 document.pdfa,
283 Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
284 );
285 let (pdf, warnings) = writer.write(
286 &pages,
287 &document.metadata,
288 &font_context,
289 tagged,
290 document.pdfa.as_ref(),
291 document.pdf_ua,
292 document.embedded_data.as_deref(),
293 &document.attachments,
294 document.zugferd.as_ref(),
295 document.flatten_forms,
296 )?;
297 let pdf = if let Some(ref sig_config) = document.certification {
298 pdf::certify::certify_pdf(&pdf, sig_config)?
299 } else {
300 pdf
301 };
302 let warnings = {
303 let mut all = layout_warnings;
304 all.extend(warnings);
305 all
306 };
307 Ok((pdf, layout_info, warnings, passes))
308}
309
310fn digits_for_count(n: usize) -> u32 {
312 if n < 10 {
313 1
314 } else if n < 100 {
315 2
316 } else if n < 1000 {
317 3
318 } else {
319 4
320 }
321}
322
323fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
325 use base64::Engine as _;
326 let b64 = base64::engine::general_purpose::STANDARD;
327
328 for entry in fonts {
329 let bytes = if let Some(comma_pos) = entry.src.find(',') {
330 b64.decode(&entry.src[comma_pos + 1..]).ok()
332 } else {
333 b64.decode(&entry.src).ok()
335 };
336
337 if let Some(data) = bytes {
338 font_context
339 .registry_mut()
340 .register(&entry.family, entry.weight, entry.italic, data);
341 }
342 }
343}
344
345pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
347 let document: Document = serde_json::from_str(json)?;
348 render(&document)
349}
350
351pub fn render_json_with_layout(
353 json: &str,
354) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
355 let document: Document = serde_json::from_str(json)?;
356 render_with_layout(&document)
357}
358
359pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, 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(&document)
370}
371
372pub fn render_template_with_layout(
374 template_json: &str,
375 data_json: &str,
376) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
377 let template: serde_json::Value = serde_json::from_str(template_json)?;
378 let data: serde_json::Value = serde_json::from_str(data_json)?;
379 let resolved = template::evaluate_template(&template, &data)?;
380 let document: Document = serde_json::from_value(resolved)?;
381 render_with_layout(&document).map(|(pdf, layout, _warnings)| (pdf, layout))
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_digits_for_count() {
390 assert_eq!(digits_for_count(0), 1);
391 assert_eq!(digits_for_count(1), 1);
392 assert_eq!(digits_for_count(9), 1);
393 assert_eq!(digits_for_count(10), 2);
394 assert_eq!(digits_for_count(99), 2);
395 assert_eq!(digits_for_count(100), 3);
396 assert_eq!(digits_for_count(999), 3);
397 assert_eq!(digits_for_count(1000), 4);
398 }
399}