Skip to main content

forme/
lib.rs

1//! # Forme
2//!
3//! A page-native PDF rendering engine.
4//!
5//! Most PDF renderers treat a document as an infinite vertical canvas and then
6//! slice it into pages after layout. This produces broken tables, orphaned
7//! headers, collapsed flex layouts on page boundaries, and years of GitHub
8//! issues begging for fixes.
9//!
10//! Forme does the opposite: **the page is the fundamental unit of layout.**
11//! Every layout decision—every flex calculation, every line break, every table
12//! row placement—is made with the page boundary as a hard constraint. Content
13//! doesn't get "sliced" after the fact. It flows *into* pages.
14//!
15//! ## Architecture
16//!
17//! ```text
18//! Input (JSON/API)
19//!       ↓
20//!   [model]    — Document tree: nodes, styles, content
21//!       ↓
22//!   [style]    — Resolve cascade, inheritance, defaults
23//!       ↓
24//!   [layout]   — Page-aware layout engine
25//!       ↓
26//!   [pdf]      — Serialize to PDF bytes
27//! ```
28
29pub 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
63/// Certify PDF bytes with an X.509 certificate.
64///
65/// Takes arbitrary PDF bytes and a certification configuration, and returns
66/// new PDF bytes with a valid digital signature. Uses incremental update
67/// to preserve the original PDF content.
68pub 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
75/// Redact regions of a PDF by overlaying opaque rectangles.
76///
77/// Takes arbitrary PDF bytes and a list of redaction regions (page, x, y,
78/// width, height in top-origin coordinates). Returns new PDF bytes with
79/// the redaction rectangles drawn on top via incremental update.
80pub 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
87/// Find text regions matching patterns in a PDF.
88///
89/// Searches PDF content streams for literal or regex patterns and returns
90/// redaction regions (in web top-origin coordinates) for each match.
91pub 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
98/// Redact text matching patterns from a PDF.
99///
100/// Convenience wrapper: finds text regions matching the patterns, then
101/// applies coordinate-based redaction to all matches.
102pub 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
109/// Merge multiple PDFs into a single document.
110///
111/// Takes a slice of PDF byte slices and returns merged PDF bytes containing
112/// all pages in order. Requires at least 2 input PDFs.
113pub fn merge_pdfs(pdfs: &[&[u8]]) -> Result<Vec<u8>, FormeError> {
114    pdf::merge::merge_pdfs(pdfs)
115}
116
117/// Render a document to PDF bytes.
118///
119/// This is the primary entry point. Takes a document tree and returns
120/// the raw bytes of a valid PDF file. If the document has a `certification`
121/// configuration, the output PDF is digitally signed.
122pub fn render(document: &Document) -> Result<Vec<u8>, FormeError> {
123    let mut font_context = FontContext::new();
124    register_document_fonts(&mut font_context, &document.fonts);
125    let engine = LayoutEngine::new();
126    let mut pages = engine.layout(document, &font_context);
127
128    // Re-layout if sentinel digit count was wrong (up to 3 total passes)
129    for _ in 0..2 {
130        let needed = digits_for_count(pages.len());
131        if needed == font_context.sentinel_digit_count() {
132            break;
133        }
134        font_context.set_sentinel_digit_count(needed);
135        pages = engine.layout(document, &font_context);
136    }
137
138    let writer = PdfWriter::new();
139    let tagged = document.tagged
140        || document.pdf_ua
141        || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
142    let pdf = writer.write(
143        &pages,
144        &document.metadata,
145        &font_context,
146        tagged,
147        document.pdfa.as_ref(),
148        document.pdf_ua,
149        document.embedded_data.as_deref(),
150        document.flatten_forms,
151    )?;
152    let pdf = if let Some(ref sig_config) = document.certification {
153        pdf::certify::certify_pdf(&pdf, sig_config)?
154    } else {
155        pdf
156    };
157    Ok(pdf)
158}
159
160/// Render a document to PDF bytes along with layout metadata.
161///
162/// Same as `render()` but also returns `LayoutInfo` describing the
163/// position and dimensions of every element on every page.
164/// If the document has a `certification` configuration, the output PDF
165/// is digitally signed.
166pub fn render_with_layout(document: &Document) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
167    let mut font_context = FontContext::new();
168    register_document_fonts(&mut font_context, &document.fonts);
169    let engine = LayoutEngine::new();
170    let mut pages = engine.layout(document, &font_context);
171
172    // Re-layout if sentinel digit count was wrong (up to 3 total passes)
173    for _ in 0..2 {
174        let needed = digits_for_count(pages.len());
175        if needed == font_context.sentinel_digit_count() {
176            break;
177        }
178        font_context.set_sentinel_digit_count(needed);
179        pages = engine.layout(document, &font_context);
180    }
181
182    let layout_info = LayoutInfo::from_pages(&pages);
183    let writer = PdfWriter::new();
184    let tagged = document.tagged
185        || document.pdf_ua
186        || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
187    let pdf = writer.write(
188        &pages,
189        &document.metadata,
190        &font_context,
191        tagged,
192        document.pdfa.as_ref(),
193        document.pdf_ua,
194        document.embedded_data.as_deref(),
195        document.flatten_forms,
196    )?;
197    let pdf = if let Some(ref sig_config) = document.certification {
198        pdf::certify::certify_pdf(&pdf, sig_config)?
199    } else {
200        pdf
201    };
202    Ok((pdf, layout_info))
203}
204
205/// Return the number of digits needed to display `n` as a decimal string.
206fn digits_for_count(n: usize) -> u32 {
207    if n < 10 {
208        1
209    } else if n < 100 {
210        2
211    } else if n < 1000 {
212        3
213    } else {
214        4
215    }
216}
217
218/// Register custom fonts from the document's `fonts` array.
219fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
220    use base64::Engine as _;
221    let b64 = base64::engine::general_purpose::STANDARD;
222
223    for entry in fonts {
224        let bytes = if let Some(comma_pos) = entry.src.find(',') {
225            // data URI: "data:font/ttf;base64,AAAA..."
226            b64.decode(&entry.src[comma_pos + 1..]).ok()
227        } else {
228            // raw base64 string
229            b64.decode(&entry.src).ok()
230        };
231
232        if let Some(data) = bytes {
233            font_context
234                .registry_mut()
235                .register(&entry.family, entry.weight, entry.italic, data);
236        }
237    }
238}
239
240/// Render a document described as JSON to PDF bytes.
241pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
242    let document: Document = serde_json::from_str(json)?;
243    render(&document)
244}
245
246/// Render a document described as JSON to PDF bytes along with layout metadata.
247pub fn render_json_with_layout(json: &str) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
248    let document: Document = serde_json::from_str(json)?;
249    render_with_layout(&document)
250}
251
252/// Render a template with data to PDF bytes.
253///
254/// Takes a template JSON tree (with `$ref`, `$each`, `$if`, operators) and
255/// a data JSON object. Evaluates all expressions, then renders the resulting
256/// document to PDF.
257pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
258    let template: serde_json::Value = serde_json::from_str(template_json)?;
259    let data: serde_json::Value = serde_json::from_str(data_json)?;
260    let resolved = template::evaluate_template(&template, &data)?;
261    let document: Document = serde_json::from_value(resolved)?;
262    render(&document)
263}
264
265/// Render a template with data to PDF bytes along with layout metadata.
266pub fn render_template_with_layout(
267    template_json: &str,
268    data_json: &str,
269) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
270    let template: serde_json::Value = serde_json::from_str(template_json)?;
271    let data: serde_json::Value = serde_json::from_str(data_json)?;
272    let resolved = template::evaluate_template(&template, &data)?;
273    let document: Document = serde_json::from_value(resolved)?;
274    render_with_layout(&document)
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_digits_for_count() {
283        assert_eq!(digits_for_count(0), 1);
284        assert_eq!(digits_for_count(1), 1);
285        assert_eq!(digits_for_count(9), 1);
286        assert_eq!(digits_for_count(10), 2);
287        assert_eq!(digits_for_count(99), 2);
288        assert_eq!(digits_for_count(100), 3);
289        assert_eq!(digits_for_count(999), 3);
290        assert_eq!(digits_for_count(1000), 4);
291    }
292}