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) -> (Vec<crate::layout::LayoutPage>, FontContext, u32) {
153 let mut font_context = FontContext::new();
154 register_document_fonts(&mut font_context, &document.fonts);
155 let engine = LayoutEngine::new();
156 font_context.reset_page_sentinel();
157 let mut pages = engine.layout(document, &font_context);
158 let mut passes = 1u32;
159
160 if font_context.saw_page_sentinel() {
161 for _ in 0..2 {
162 let needed = digits_for_count(pages.len());
163 if needed == font_context.sentinel_digit_count() {
164 break;
165 }
166 font_context.set_sentinel_digit_count(needed);
167 pages = engine.layout(document, &font_context);
168 passes += 1;
169 }
170 }
171 (pages, font_context, passes)
172}
173
174pub fn count_layout_passes(document: &Document) -> u32 {
178 layout_with_sentinel_passes(document).2
179}
180
181pub fn render_with_warnings(document: &Document) -> Result<(Vec<u8>, Vec<String>), FormeError> {
185 render_with_warnings_and_passes(document).map(|(pdf, warnings, _passes)| (pdf, warnings))
186}
187
188pub fn render_with_warnings_and_passes(
191 document: &Document,
192) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
193 let profile = std::env::var("FORME_PROFILE").is_ok();
197 let t_layout = if profile {
198 Some(std::time::Instant::now())
199 } else {
200 None
201 };
202 let (pages, font_context, passes) = layout_with_sentinel_passes(document);
203 let layout_ms = t_layout.map(|t| t.elapsed().as_secs_f64() * 1000.0);
204
205 let writer = PdfWriter::new();
206 let tagged = document.tagged
207 || document.pdf_ua
208 || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
209 let t_ser = if profile {
210 Some(std::time::Instant::now())
211 } else {
212 None
213 };
214 let (pdf, warnings) = writer.write(
215 &pages,
216 &document.metadata,
217 &font_context,
218 tagged,
219 document.pdfa.as_ref(),
220 document.pdf_ua,
221 document.embedded_data.as_deref(),
222 document.flatten_forms,
223 )?;
224 let serialize_ms = t_ser.map(|t| t.elapsed().as_secs_f64() * 1000.0);
225 let pdf = if let Some(ref sig_config) = document.certification {
226 pdf::certify::certify_pdf(&pdf, sig_config)?
227 } else {
228 pdf
229 };
230 if let (Some(l), Some(s)) = (layout_ms, serialize_ms) {
231 eprintln!(
232 "FORME_PROFILE pages={} passes={passes} layout_ms={l:.1} serialize_ms={s:.1}",
233 pages.len()
234 );
235 }
236 Ok((pdf, warnings, passes))
237}
238
239pub fn render_with_layout(
246 document: &Document,
247) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
248 let (pages, font_context, _passes) = layout_with_sentinel_passes(document);
249 let layout_info = LayoutInfo::from_pages(&pages);
250 let writer = PdfWriter::new();
251 let tagged = document.tagged
252 || document.pdf_ua
253 || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
254 let (pdf, warnings) = writer.write(
255 &pages,
256 &document.metadata,
257 &font_context,
258 tagged,
259 document.pdfa.as_ref(),
260 document.pdf_ua,
261 document.embedded_data.as_deref(),
262 document.flatten_forms,
263 )?;
264 let pdf = if let Some(ref sig_config) = document.certification {
265 pdf::certify::certify_pdf(&pdf, sig_config)?
266 } else {
267 pdf
268 };
269 Ok((pdf, layout_info, warnings))
270}
271
272fn digits_for_count(n: usize) -> u32 {
274 if n < 10 {
275 1
276 } else if n < 100 {
277 2
278 } else if n < 1000 {
279 3
280 } else {
281 4
282 }
283}
284
285fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
287 use base64::Engine as _;
288 let b64 = base64::engine::general_purpose::STANDARD;
289
290 for entry in fonts {
291 let bytes = if let Some(comma_pos) = entry.src.find(',') {
292 b64.decode(&entry.src[comma_pos + 1..]).ok()
294 } else {
295 b64.decode(&entry.src).ok()
297 };
298
299 if let Some(data) = bytes {
300 font_context
301 .registry_mut()
302 .register(&entry.family, entry.weight, entry.italic, data);
303 }
304 }
305}
306
307pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
309 let document: Document = serde_json::from_str(json)?;
310 render(&document)
311}
312
313pub fn render_json_with_layout(
315 json: &str,
316) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
317 let document: Document = serde_json::from_str(json)?;
318 render_with_layout(&document)
319}
320
321pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
327 let template: serde_json::Value = serde_json::from_str(template_json)?;
328 let data: serde_json::Value = serde_json::from_str(data_json)?;
329 let resolved = template::evaluate_template(&template, &data)?;
330 let document: Document = serde_json::from_value(resolved)?;
331 render(&document)
332}
333
334pub fn render_template_with_layout(
336 template_json: &str,
337 data_json: &str,
338) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
339 let template: serde_json::Value = serde_json::from_str(template_json)?;
340 let data: serde_json::Value = serde_json::from_str(data_json)?;
341 let resolved = template::evaluate_template(&template, &data)?;
342 let document: Document = serde_json::from_value(resolved)?;
343 render_with_layout(&document).map(|(pdf, layout, _warnings)| (pdf, layout))
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn test_digits_for_count() {
352 assert_eq!(digits_for_count(0), 1);
353 assert_eq!(digits_for_count(1), 1);
354 assert_eq!(digits_for_count(9), 1);
355 assert_eq!(digits_for_count(10), 2);
356 assert_eq!(digits_for_count(99), 2);
357 assert_eq!(digits_for_count(100), 3);
358 assert_eq!(digits_for_count(999), 3);
359 assert_eq!(digits_for_count(1000), 4);
360 }
361}