Skip to content
lightweight-pdf
MITLive demo
Rust · WebAssembly · MIT

Build PDFs,
don’t typeset them

Invoices, quotes, delivery notes, reports, certificates. A fixed set of building blocks instead of a markup language – small enough to run inside a Cloudflare Worker.

0.3.0
crates.io
1
required dependency
590 KiB
WASM, gzip
demo_*.rs
$ cargo run --example demo_invoice
wrote examples/demo_invoice.pdf (23977 bytes)

$ cargo run --example demo_offer
wrote examples/demo_offer.pdf (24876 bytes)

$ cargo run --example demo_report
wrote examples/demo_report.pdf (26348 bytes)
three demo documents, one rendererrun from 2026-09-16
Building blocks
  • Text
  • Tables
  • Rows & columns
  • Lists
  • Images
  • Headers & footers
  • Table of contents
  • Bookmarks
  • Watermarks
  • Themes
  • Links
1
required dependency

skrifa parses font files. miniz_oxide is optional, behind the default-on compress feature. The PDF writer and the TrueType subsetter are part of the project.

590 KiB
WASM module, gzip

Measured with wrangler deploy --dry-run for the build from wasm, default-fonts and compress; 1252.95 KiB raw.

≈ 4 ms
render time per document

Release build, average of five runs for the invoice, offer and report demos. Page count, table size and font weights move that number.

01 — Overview

A library for documents that always look the same.

lightweight-pdf is deliberately not a typesetting system. There is no markup language of its own and no parser for one – just a builder API over a fixed set of layout primitives. That keeps the feature set small, and the binary small enough for runtimes with a size limit.

github.com/casoon/lightweight-pdf →
  1. 01Own PDF writer — objects, cross-reference table and streams are written by the library itself
  2. 02Own subsetter — TrueType fonts are reduced to the glyphs in use and embedded
  3. 03Two-pass layout — so that “page 2 of 3” in the footer is correct
  4. 04Warnings instead of silent failures — clipped text and missing glyphs are reported
02 — Demo

One running generator and three rendered documents.

pdf.casoon.dev: start page asking “Was willst du drucken?” with tiles for word search, sudoku and further templates
pdf.casoon.dev – pick a template, fill in the form, print the PDF. Captured on 2026-09-16; the demo is in German.
First page of the rendered invoice demo with sender, line-item table and totals
demo_invoice.pdf, page 1 – produced by `cargo run --example demo_invoice`.
First page of the rendered quote demo with line items and a total
demo_offer.pdf, page 1 – produced by `cargo run --example demo_offer`.
First page of the rendered report demo with headings, body text and a table
demo_report.pdf, page 1 – produced by `cargo run --example demo_report`.

All names, addresses and amounts in the demo documents are fictional. The showcase in the documentation re-renders them on every site build, source next to PDF.

03 — Get started

Two routes, one result.

  1. 1As a library

    Add the crate

    The lightweight-pdf facade carries the public API; the default fonts (Source Sans 3) are bundled by default.

    cargo add lightweight-pdf
  2. 2Build a document

    Compose the primitives

    Text, tables, rows and columns, plus header and footer. render() returns the PDF bytes.

    Document::new(PageFormat::A4)
  3. 3Without Rust

    CLI with JSON

    The CLI is its own crate, so the library never depends on clap. Input is a JSON document, or a template plus data.

    cargo install lightweight-pdf-cli
04 — Features

What comes in the box.

Pagination that counts

Layout runs in two passes. The total page count is only known after the first one, so page 2 of 3 stays correct even for content that breaks across pages.

Tables with a column model

Fixed and flexible column widths, header repetition across pages, row striping, vertical alignment and cell backgrounds.

Fonts embedded, not linked

The built-in TrueType subsetter keeps only the glyphs actually used. Custom fonts can be added with registerFont.

JSON and templates

A document can be described as JSON. Templates with {{placeholders}} and $each are resolved against a data file; lwpdf schema prints the JSON Schema.

PDF/A-3b, ZUGFeRD, PDF/UA

Behind their own features: PDF/A-3b, embedded ZUGFeRD/Factur-X invoice XML and Tagged PDF per PDF/UA-1 – verified with veraPDF, ZUGFeRD additionally with Mustang.

Snapshot tests for PDFs

lightweight-pdf-testing compares rendered pages pixel by pixel against stored references. The crate also works for PDFs from other tools.

05 — Architecture

Eight crates, one direction.

  1. Model

    Document and elements

    The document tree and the builder API. It knows nothing about PDF or layout – only primitives and their properties.

    lightweight-pdf-core
  2. Layout

    Wrapping and pagination

    The Layoutable trait, text wrapping, hyphenation and the two-pass page break, including the table of contents.

    lightweight-pdf-layout
  3. Output

    Writer and fonts

    PDF objects, the cross-reference table and streams, plus font metrics via skrifa and the subsetting. Both crates are leaves with no path dependency on model or layout.

    lightweight-pdf-writerlightweight-pdf-fontsskrifa
06 — Integration

Rust or JSON.

src/main.rsRust
use lightweight_pdf::*;

let mut doc = Document::new(PageFormat::A4)
    .margin(Margin::all(20.0))
    .footer(Footer::new(20.0, |ctx| {
        Text::new(format!("Seite {} von {}", ctx.page, ctx.total_pages)).into()
    }));

doc.add(Text::new("Rechnung").heading1());
doc.add(
    Table::new()
        .columns([TableColumn::flex(1.0), TableColumn::fixed(60.0).align(Align::End)])
        .header(["Position", "Betrag"])
        .rows(vec![vec![Element::from("Beratung"), Element::from("1.200,00 EUR")]]),
);

let bytes = doc.render().expect("render should succeed");
std::fs::write("rechnung.pdf", bytes).unwrap();
invoice-template.jsonTemplate
{
  "schema_version": 1,
  "document": {
    "page_format": "A4",
    "margin": { "top": 40.0, "right": 40.0, "bottom": 40.0, "left": 40.0 },
    "metadata": { "title": "{{invoice.number}}" },
    "children": [
      {
        "type": "text",
        "content": "Rechnung {{invoice.number}}",
        "style": { "size": 22.0, "font": "sans-bold" }
      },
      {
        "type": "table",
        "columns": [
          { "width": { "flex": 1.0 } },
          { "width": { "fixed": 80.0 }, "align": "end" }
        ],
        "header": [
          { "element": { "type": "text", "content": "Beschreibung" } },
          { "element": { "type": "text", "content": "Betrag" } }
        ]
      }
    ]
  }
}
TerminalCLI
$ lwpdf validate examples/invoice-template.json \
    --data examples/invoice-data.json
ok: examples/invoice-template.json is valid

$ lwpdf render examples/invoice-template.json \
    --data examples/invoice-data.json -o invoice.pdf
wrote invoice.pdf (10435 bytes)
Runs on
  • Rust (native)
  • wasm32-unknown-unknown
  • Cloudflare Workers
  • Node.js
  • Browser
  • CLI (lwpdf)
CI builds the whole workspace for the wasm32 target on every run. The JavaScript package is built from bindings/js – it is not on npm.
07 — Comparison

Where the boundary runs.

Criterionlightweight-pdfprintpdfTypstHeadless Chrome
Inputbuilder API or JSONAPI with basic layoutown markup languageHTML/CSS
WASM, compressed≈ 590 KiB (measured)not publishedtens of MiB reportedno WASM
Direct dependencies1 (skrifa)24a large compilera whole browser
LicenceMITMITApache-2.0Chromium: BSD-style
Text shapingnoyes (allsorts)yesyes

As of August 2026, checked against each project’s own README and crates.io page, not from memory. More rows and details are in the comparison in the documentation.

08 — In use

pdf.casoon.dev runs on this library.

The demo’s technical page →
pdf.casoon.devCloudflare Worker · as of 2026-09-16
templates online
9
wasm module, gzip
970 KiB
worker startup
26 ms
puzzle PDF, 3 pages
≈ 28 KB
What the generator produces
  • Puzzles (word search, sudoku, maze)3 %
  • Worksheets (arithmetic, cloze test)2 %
  • Organisation (timetable, place cards)2 %
  • Business (certificate, invoice)2 %
Figures from the demo’s technical page. Its module additionally contains four puzzle generators and their layouts.

The PDFs are produced either in the browser or in the worker. For AI assistants, the same generator is available as an MCP server. The demo site is in German.

09 — Limits

When it is the wrong tool.

The narrow feature set is what pays for the size. Four points worth knowing before you decide.

No HTML, no CSS, no macro language

The input is the document tree – as Rust code or as JSON. If you need to print existing HTML, you need a browser-based route.

No text shaping

There is no shaping stack such as allsorts or rustybuzz. The library is not suitable for Arabic, Indic or other complex scripts.

Few conformance levels

PDF/A-3b and PDF/UA-1 are covered, plus ZUGFeRD embedding. Other PDF/A levels are not; krilla covers more there.

No npm package

The JavaScript/WASM bindings live in the repository under bindings/js and are built there. @casoon/lightweight-pdf is not published on the npm registry.

LicenceThe code of all workspace crates is under the MIT licence. The bundled default fonts (Source Sans 3) are under the SIL Open Font License 1.1; the OFL permits embedding and subsetting in generated documents, so the PDFs carry no obligation from it.Licence text in the repository →
10 — FAQ

Frequently asked questions.

Can I use the library from JavaScript?

Yes, through the wasm-bindgen bindings under bindings/js. They are not published on npm, though: the package is built from the repository (npm install, npm run build), which needs the Rust toolchain with the wasm32-unknown-unknown target plus wasm-pack and wasm-opt.

Does it really run in a Cloudflare Worker?

Yes. examples/worker is a starter that turns a POST with a JSON document into a PDF response. The module was measured at 1252.95 KiB raw and 590.37 KiB gzip; in a local wrangler dev, a cold start including the first render took roughly 34 to 47 ms and warm renders roughly 11 to 25 ms including HTTP overhead. That is the workerd runtime, not the actual edge.

Do I need Rust to generate PDFs?

No. cargo install lightweight-pdf-cli provides the lwpdf binary with the commands render, validate, fonts and schema. Input is a JSON document, or a template plus a data file. Exit code 0 means success, 1 a render problem, 2 an input problem.

How are electronic invoices covered?

Document::zugferd_xml() embeds ZUGFeRD/Factur-X XML behind the zugferd feature, which implies PDF/A-3b. The output was verified with veraPDF and Mustang. Whether the XML meets a given recipient’s business requirements remains the calling application’s responsibility.

What dependencies does the crate pull in?

Only skrifa is required, for parsing font files. miniz_oxide for flate compression sits behind the default-on compress feature. Further features such as png, hyphenation, serde or wasm bring their own dependencies and can be turned off.

How large are the generated PDFs?

The three demo documents come to about 24 to 26 KB each with compression on, fonts embedded. Without the compress feature the same documents get noticeably larger; a difference of 40 to 60 percent is typical.

Try it first, then build it in.

The demo produces print-ready PDFs without an account. The code behind it is open under MIT.