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::{ChartDataPoint, ChartSeries, DotPlotGroup};
52pub use model::{ColumnDef, ColumnWidth, FontEntry, SignatureConfig, TextRun};
53pub use model::{Document, Metadata, Node, NodeKind, PageConfig, PageSize};
54pub use style::Style;
55
56use font::FontContext;
57use layout::LayoutEngine;
58use pdf::PdfWriter;
59
60/// Sign PDF bytes with an X.509 certificate.
61///
62/// Takes arbitrary PDF bytes and a signature configuration, and returns
63/// new PDF bytes with a valid digital signature. Uses incremental update
64/// to preserve the original PDF content.
65pub fn sign_pdf(pdf_bytes: &[u8], config: &model::SignatureConfig) -> Result<Vec<u8>, FormeError> {
66    pdf::signing::sign_pdf(pdf_bytes, config)
67}
68
69/// Render a document to PDF bytes.
70///
71/// This is the primary entry point. Takes a document tree and returns
72/// the raw bytes of a valid PDF file. If the document has a `signature`
73/// configuration, the output PDF is digitally signed.
74pub fn render(document: &Document) -> Result<Vec<u8>, FormeError> {
75    let mut font_context = FontContext::new();
76    register_document_fonts(&mut font_context, &document.fonts);
77    let engine = LayoutEngine::new();
78    let mut pages = engine.layout(document, &font_context);
79
80    // Re-layout if sentinel digit count was wrong (up to 3 total passes)
81    for _ in 0..2 {
82        let needed = digits_for_count(pages.len());
83        if needed == font_context.sentinel_digit_count() {
84            break;
85        }
86        font_context.set_sentinel_digit_count(needed);
87        pages = engine.layout(document, &font_context);
88    }
89
90    let writer = PdfWriter::new();
91    let tagged = document.tagged
92        || document.pdf_ua
93        || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
94    let pdf = writer.write(
95        &pages,
96        &document.metadata,
97        &font_context,
98        tagged,
99        document.pdfa.as_ref(),
100        document.pdf_ua,
101        document.embedded_data.as_deref(),
102        document.flatten_forms,
103    )?;
104    let pdf = if let Some(ref sig_config) = document.signature {
105        pdf::signing::sign_pdf(&pdf, sig_config)?
106    } else {
107        pdf
108    };
109    Ok(pdf)
110}
111
112/// Render a document to PDF bytes along with layout metadata.
113///
114/// Same as `render()` but also returns `LayoutInfo` describing the
115/// position and dimensions of every element on every page.
116/// If the document has a `signature` configuration, the output PDF
117/// is digitally signed.
118pub fn render_with_layout(document: &Document) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
119    let mut font_context = FontContext::new();
120    register_document_fonts(&mut font_context, &document.fonts);
121    let engine = LayoutEngine::new();
122    let mut pages = engine.layout(document, &font_context);
123
124    // Re-layout if sentinel digit count was wrong (up to 3 total passes)
125    for _ in 0..2 {
126        let needed = digits_for_count(pages.len());
127        if needed == font_context.sentinel_digit_count() {
128            break;
129        }
130        font_context.set_sentinel_digit_count(needed);
131        pages = engine.layout(document, &font_context);
132    }
133
134    let layout_info = LayoutInfo::from_pages(&pages);
135    let writer = PdfWriter::new();
136    let tagged = document.tagged
137        || document.pdf_ua
138        || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
139    let pdf = writer.write(
140        &pages,
141        &document.metadata,
142        &font_context,
143        tagged,
144        document.pdfa.as_ref(),
145        document.pdf_ua,
146        document.embedded_data.as_deref(),
147        document.flatten_forms,
148    )?;
149    let pdf = if let Some(ref sig_config) = document.signature {
150        pdf::signing::sign_pdf(&pdf, sig_config)?
151    } else {
152        pdf
153    };
154    Ok((pdf, layout_info))
155}
156
157/// Return the number of digits needed to display `n` as a decimal string.
158fn digits_for_count(n: usize) -> u32 {
159    if n < 10 {
160        1
161    } else if n < 100 {
162        2
163    } else if n < 1000 {
164        3
165    } else {
166        4
167    }
168}
169
170/// Register custom fonts from the document's `fonts` array.
171fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
172    use base64::Engine as _;
173    let b64 = base64::engine::general_purpose::STANDARD;
174
175    for entry in fonts {
176        let bytes = if let Some(comma_pos) = entry.src.find(',') {
177            // data URI: "data:font/ttf;base64,AAAA..."
178            b64.decode(&entry.src[comma_pos + 1..]).ok()
179        } else {
180            // raw base64 string
181            b64.decode(&entry.src).ok()
182        };
183
184        if let Some(data) = bytes {
185            font_context
186                .registry_mut()
187                .register(&entry.family, entry.weight, entry.italic, data);
188        }
189    }
190}
191
192/// Render a document described as JSON to PDF bytes.
193pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
194    let document: Document = serde_json::from_str(json)?;
195    render(&document)
196}
197
198/// Render a document described as JSON to PDF bytes along with layout metadata.
199pub fn render_json_with_layout(json: &str) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
200    let document: Document = serde_json::from_str(json)?;
201    render_with_layout(&document)
202}
203
204/// Render a template with data to PDF bytes.
205///
206/// Takes a template JSON tree (with `$ref`, `$each`, `$if`, operators) and
207/// a data JSON object. Evaluates all expressions, then renders the resulting
208/// document to PDF.
209pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
210    let template: serde_json::Value = serde_json::from_str(template_json)?;
211    let data: serde_json::Value = serde_json::from_str(data_json)?;
212    let resolved = template::evaluate_template(&template, &data)?;
213    let document: Document = serde_json::from_value(resolved)?;
214    render(&document)
215}
216
217/// Render a template with data to PDF bytes along with layout metadata.
218pub fn render_template_with_layout(
219    template_json: &str,
220    data_json: &str,
221) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
222    let template: serde_json::Value = serde_json::from_str(template_json)?;
223    let data: serde_json::Value = serde_json::from_str(data_json)?;
224    let resolved = template::evaluate_template(&template, &data)?;
225    let document: Document = serde_json::from_value(resolved)?;
226    render_with_layout(&document)
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_digits_for_count() {
235        assert_eq!(digits_for_count(0), 1);
236        assert_eq!(digits_for_count(1), 1);
237        assert_eq!(digits_for_count(9), 1);
238        assert_eq!(digits_for_count(10), 2);
239        assert_eq!(digits_for_count(99), 2);
240        assert_eq!(digits_for_count(100), 3);
241        assert_eq!(digits_for_count(999), 3);
242        assert_eq!(digits_for_count(1000), 4);
243    }
244}