HAML-like templates + Shadcn-style components + Tailwind-style utility classes, compiled to PDF bytes in memory. Pure Rust, no headless browser, running as a Vercel Function.
curl -X POST https://pdfcn-rs.vercel.app/api/generate-pdf \
-H "Content-Type: application/json" \
-d '{
"template": "%h1 Invoice\n%p Total: {{ total }}",
"data": { "total": "$42.00" }
}' \
--output invoice.pdf
Source: github.com/pmaojo/pdfcn-rs
Edit the template and data, then generate — this calls
/api/generate-pdf on this deployment and renders the PDF
it returns.
Full body accepted by POST /api/generate-pdf. Only template is required.
| Field | Type | Default | Description |
|---|---|---|---|
template | string | — | HAML-like template source (see syntax below). |
data | object | {} | JSON context resolved against {{ ... }} interpolations. |
page.size | "a4" | "letter" | "WxH" | "a4" | Custom sizes take millimeters, e.g. "210x297". |
page.orientation | "portrait" | "landscape" | "portrait" | Page orientation. |
page.margin_mm | number | engine default | Uniform page margin in millimeters. |
filename | string | "document.pdf" | Suggested name via Content-Disposition. |
fonts | object | {} | Map of family name → base64 TTF/OTF bytes. Reference via font-family: "<name>". |
images | object | {} | Map of src key → base64 JPEG/PNG bytes. No network fetch happens at render time — embed bytes yourself. |
theme | object | light | Semantic token set applied before scanning: { "mode": "light" | "dark", "overrides": { "primary": "#2563eb" } }. One entry rebrands every derived utility (bg-primary, text-primary-foreground, …) and every built-in component built on them — no template changes. Malformed themes are a 400. |
svgs | object | {} | Map of id → SVG source text (plain UTF-8, no base64) for %Vector(id="...") placeholders. |
factur_x | object | — | Splices a Factur-X invoice attachment into the response: { "xml": "...", "profile"?: "en16931", "icc_base64"?: "..." }. See the Factur-X section below. |
Responses: 200 with application/pdf bytes on success,
400 for a missing/invalid JSON body, 422 with a plain-text render error: … message
when the template or data is invalid.
Render up to 50 documents per request. All documents share one auth check,
one font/image decode, one deduplicated remote-image fetch pass and — via the parse cache —
one parse of an identical template, while renders fan out across cores. A render failure
inside one document becomes an error entry; its siblings still ship.
curl -X POST https://pdfcn-rs.vercel.app/api/generate-pdf-batch \
-H "Content-Type: application/json" \
-d '{
"theme": { "overrides": { "primary": "#0f766e" } },
"data": { "company": "Acme GmbH" },
"documents": [
{ "template": "%h1 Statement {{ month }}\\n%p {{ company }}",
"data": { "month": "July" }, "filename": "statement-july.pdf" },
{ "template": "%h1 Statement {{ month }}\\n%p {{ company }}",
"data": { "month": "August" }, "filename": "statement-august.pdf" }
]
}'
Shared fields (applied to every document unless it overrides them):
data (per-document keys win, shallow merge), page, theme,
fonts, images, partials, filename.
Per-document fields: template (required), data, page,
filename (defaults to document-N.pdf by position).
{
"results": [
{ "filename": "statement-july.pdf", "pdf_base64": "JVBERi…" },
{ "filename": "statement-august.pdf", "pdf_base64": "JVBERi…" },
{ "filename": "statement-broken.pdf", "error": "render error: …" }
]
}
Problems with the request itself — bad JSON, bad base64,
malformed theme, more than 50 documents — are a 400
before any rendering starts.
| Syntax | Meaning |
|---|---|
%tag | Element, e.g. %h1, %p. Indentation nests children — no closing tags. |
.class-name | A bare div with that class; chain classes like .flex.gap-2 (Tailwind-style utilities). |
%tag.class(attr="value") | Tag plus classes plus attributes in parentheses. |
{{ path.to.value }} | Interpolates from data; also valid inside attribute values. |
- if cond / - else | Conditional branch on a truthy value from data. |
- for x in list | Repeats the indented block once per item, binding x. |
- include "name" | Inlines a caller-supplied partial (requires a Partials resolver on the embedding side; the HTTP API uses NoPartials). |
Shadcn-style, pre-styled, used the same way as any %tag:
DocumentLayout Header Card Table Grid
Badge Separator SignatureBlock InvoiceTable
Alert Avatar Input Textarea Select
Label Checkbox RadioItem Progress
Breadcrumb Pagination BarChart QRCode
PageFooter LineChart StackedBarChart PieChart
Sparkline Barcode Vector
Splices an EN 16931/CII invoice XML into the response as a Factur-X-compliant
attachment, via an extra top-level factur_x field. Try the "Factur-X invoice"
example above — the PDF it returns actually carries the attachment; a page-view can't show it, but
pdftk in.pdf dump_data or any PDF/A-3-aware viewer's attachments panel will.
| Field | Type | Default | Description |
|---|---|---|---|
factur_x.xml | string | — | EN 16931/CII invoice XML, plain UTF-8 (required if factur_x is present). |
factur_x.profile | string | "en16931" | minimum, basic-wl, basic, en16931, or extended. |
factur_x.icc_base64 | string | — | A genuine sRGB ICC profile, base64-encoded, embedded as the PDF/A /OutputIntent. Omitted rather than fabricated when absent — see docs/spikes/002-factur-x-embedding.md. |