A tiny, deterministic XML → PDF engine
Print without
a browser.
A small layout engine that typesets PDFs from XML — no headless Chrome, no 300 MB image. Describe the document; get the same bytes on every run, from any language.
Render from Ruby Node Python Rails the browser
This invoice isn't laid out by hand — it's computed. The line items, the tax, and the total all fall out of the data block at the top, looped into markup. And it's typeset by the real engine: the same Rust that writes your PDFs, compiled to WebAssembly and running in this tab. Change a number and the whole page re-typesets — open it in the playground to fork and share a link.
The engine ships as a package for your language — the native binary is bundled in, so there's nothing to install at runtime — and as inlined WebAssembly for the browser. Feed it your XML, get PDF bytes back (or the engine's diagnostics if something's off). Same engine, same bytes, everywhere.
-
Ruby gem · pdg
# Gemfile: gem "pdg" require "pdg" pdf = PDG.render(xml)
Ruby guide -
Node.js npm · pdg
// npm install pdg import { render } from "pdg"; const pdf = await render(xml);
Node.js guide -
Python pip · pdg
# pip install pdg import pdg data = pdg.render(xml)
Python guide -
Rails gem · pdg-rails
# a show.pdf.erb view, then: respond_to { |f| f.pdf } # → GET /invoices/1.pdf
Rails guide -
Browser npm · pdg-browser
// wasm, inlined — no config const { render } = await import("pdg-browser"); const pdf = await render(xml);
Browser guide -
CLI
Or drive the binary directly — feed a file or stream over stdio:
$ pdg build in.xml out.pdfRead the docs
-
01
No headless browser
No Chromium, no 300 MB image, no
waitForSelector. A single binary that cold-starts in milliseconds — at home in a lambda, a sidecar, or a CI job. -
02
Deterministic by construction
Same input, same bytes — every run, every machine. Diff a PDF in review, snapshot one in CI. No font-load races, no off-by-a-pixel.
-
03
One engine, client and server
The preview in this tab is the Rust you deploy, compiled to WebAssembly. Prototype in the browser and ship the identical layout — what you see is what renders.
The whole engine,
in one question.
“Given this much space —
how much of you fits?”
Every element answers it. Ask recursively and text wrapping, column balancing, tables, and clean page breaks all fall out of the single rule — no special cases, no escape hatches. Three honest stages: parse→layout→render.
-
Fits
The element lays itself out and reports the height it used.
-
Partly fits
It takes what it can, then hands the remainder back to continue on the next page.
-
Doesn't fit
It defers; the parent moves it whole to the next page. Nothing is clipped, ever.
A dozen elements, learnable in an afternoon — block structure, inline markup, page furniture, and a style: box model that rides on any of them.
No cascade to fight, no specificity wars: what you write is what lands on the page. Open the full reference