Case study · pdfcn-rs
pdfcn-rs: PDFs as components, no browser
HAML-like templates, shadcn-style components and Tailwind-style utilities compiled to PDF in memory, in pure Rust and inside a serverless function.
The problem
Generating invoices, catalogues or reports as PDF usually ends in one of two places: drawing coordinates by hand with a low-level library, or starting a headless Chromium to print HTML. The first is slow to maintain; the second weighs hundreds of megabytes, is slow to start and sits badly in a serverless function.
I wanted to write a PDF the way you write an interface, with components and utility classes, and generate it inside a Vercel Function or a Lambda.
Decisions
No Chromium. HTML and CSS are laid out by a layout engine written in Rust, with no browser process and no dynamic system dependencies.
A pipeline of small pieces. An indentation-based HAML-like template parser, an expression engine for {{ }}, - for and - if, a component registry (%InvoiceTable, %Card, %Badge…) that expands to markup, and a Tailwind-style utility scanner with no Node. Each crate has one job.
Escape once, at the edge. Templates resolve data without escaping, and escaping happens when the final HTML is rendered. No double escaping, no gaps.
Made for print. @page, exact colour and break-inside: avoid on rows and cards are injected by default.
What is built
- A CLI with
new,add,buildanddev(live-reload preview). - Charts, barcodes and SVG rasterised at print resolution.
- Factur-X invoices: the same PDF carries the EN 16931 XML that the client’s accounting system reads.
- A Vercel function at
/api/generate-pdf, Node bindings through napi-rs and a demo.
What it does not do (yet)
The repository documents its limits in detail: gap in flex and grid has no effect, box-shadow is not painted, and the PDF library’s headers and footers are not drawn yet. I’d rather have them written down than discover them in production.
What I learned
The component-and-utility mental model works as well on paper as on screen, and taking the browser out of the way changes what is possible in serverless. Documenting the limits as precisely as the features saves more time than any feature.