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, 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/// Render a document to PDF bytes.
61///
62/// This is the primary entry point. Takes a document tree and returns
63/// the raw bytes of a valid PDF file.
64pub fn render(document: &Document) -> Result<Vec<u8>, FormeError> {
65    let mut font_context = FontContext::new();
66    register_document_fonts(&mut font_context, &document.fonts);
67    let engine = LayoutEngine::new();
68    let pages = engine.layout(document, &font_context);
69    let writer = PdfWriter::new();
70    let tagged = document.tagged || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
71    writer.write(
72        &pages,
73        &document.metadata,
74        &font_context,
75        tagged,
76        document.pdfa.as_ref(),
77        document.embedded_data.as_deref(),
78    )
79}
80
81/// Render a document to PDF bytes along with layout metadata.
82///
83/// Same as `render()` but also returns `LayoutInfo` describing the
84/// position and dimensions of every element on every page.
85pub fn render_with_layout(document: &Document) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
86    let mut font_context = FontContext::new();
87    register_document_fonts(&mut font_context, &document.fonts);
88    let engine = LayoutEngine::new();
89    let pages = engine.layout(document, &font_context);
90    let layout_info = LayoutInfo::from_pages(&pages);
91    let writer = PdfWriter::new();
92    let tagged = document.tagged || matches!(document.pdfa, Some(model::PdfAConformance::A2a));
93    let pdf = writer.write(
94        &pages,
95        &document.metadata,
96        &font_context,
97        tagged,
98        document.pdfa.as_ref(),
99        document.embedded_data.as_deref(),
100    )?;
101    Ok((pdf, layout_info))
102}
103
104/// Register custom fonts from the document's `fonts` array.
105fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
106    use base64::Engine as _;
107    let b64 = base64::engine::general_purpose::STANDARD;
108
109    for entry in fonts {
110        let bytes = if let Some(comma_pos) = entry.src.find(',') {
111            // data URI: "data:font/ttf;base64,AAAA..."
112            b64.decode(&entry.src[comma_pos + 1..]).ok()
113        } else {
114            // raw base64 string
115            b64.decode(&entry.src).ok()
116        };
117
118        if let Some(data) = bytes {
119            font_context
120                .registry_mut()
121                .register(&entry.family, entry.weight, entry.italic, data);
122        }
123    }
124}
125
126/// Render a document described as JSON to PDF bytes.
127pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
128    let document: Document = serde_json::from_str(json)?;
129    render(&document)
130}
131
132/// Render a document described as JSON to PDF bytes along with layout metadata.
133pub fn render_json_with_layout(json: &str) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
134    let document: Document = serde_json::from_str(json)?;
135    render_with_layout(&document)
136}
137
138/// Render a template with data to PDF bytes.
139///
140/// Takes a template JSON tree (with `$ref`, `$each`, `$if`, operators) and
141/// a data JSON object. Evaluates all expressions, then renders the resulting
142/// document to PDF.
143pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
144    let template: serde_json::Value = serde_json::from_str(template_json)?;
145    let data: serde_json::Value = serde_json::from_str(data_json)?;
146    let resolved = template::evaluate_template(&template, &data)?;
147    let document: Document = serde_json::from_value(resolved)?;
148    render(&document)
149}
150
151/// Render a template with data to PDF bytes along with layout metadata.
152pub fn render_template_with_layout(
153    template_json: &str,
154    data_json: &str,
155) -> Result<(Vec<u8>, LayoutInfo), FormeError> {
156    let template: serde_json::Value = serde_json::from_str(template_json)?;
157    let data: serde_json::Value = serde_json::from_str(data_json)?;
158    let resolved = template::evaluate_template(&template, &data)?;
159    let document: Document = serde_json::from_value(resolved)?;
160    render_with_layout(&document)
161}