PDFs bauen,
nicht setzen
Rechnungen, Angebote, Lieferscheine, Berichte, Urkunden. Ein fester Satz Bausteine statt einer Auszeichnungssprache – klein genug, um in einem Cloudflare Worker zu laufen.
$ 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)
skrifa liest Schriftdateien. miniz_oxide ist optional hinter dem standardmäßig aktiven Feature compress. PDF-Writer und TrueType-Subsetter gehören zum Projekt.
Gemessen über wrangler deploy --dry-run für den Build aus wasm, default-fonts und compress; roh 1252,95 KiB.
Release-Build, Mittel aus fünf Läufen für die Demos Rechnung, Angebot und Bericht. Seitenzahl, Tabellengröße und Schriftschnitte verschieben den Wert.
Eine Bibliothek für Dokumente, die immer gleich aussehen.
lightweight-pdf ist bewusst kein Satzsystem. Es gibt keine eigene Auszeichnungssprache und keinen Parser dafür, sondern eine Builder-API über einem festen Satz Layout-Bausteine. Das macht den Funktionsumfang kleiner – und das Binary klein genug für Laufzeiten mit Größenlimit.
github.com/casoon/lightweight-pdf →- 01Eigener PDF-Writer — Objekte, Querverweise und Streams schreibt die Bibliothek selbst
- 02Eigener Subsetter — TrueType-Schriften werden auf die benutzten Glyphen reduziert und eingebettet
- 03Zweistufiges Layout — damit „Seite 2 von 3“ in der Fußzeile stimmt
- 04Warnungen statt stiller Fehler — abgeschnittener Text und fehlende Zeichen werden gemeldet
Ein laufender Generator und drei erzeugte Dokumente.




Alle Namen, Adressen und Beträge in den Demo-Dokumenten sind erfunden. Der Showcase in der Dokumentation rendert sie bei jedem Site-Build neu, Quelltext neben PDF.
Zwei Wege, dasselbe Ergebnis.
- 1Als Bibliothek
Crate hinzufügen
Die Facade
lightweight-pdfbringt die öffentliche API mit; Schriften (Source Sans 3) sind standardmäßig gebündelt.cargo add lightweight-pdf - 2Dokument bauen
Bausteine zusammensetzen
Text, Tabellen, Zeilen und Spalten, dazu Kopf- und Fußzeile.
render()gibt die PDF-Bytes zurück.Document::new(PageFormat::A4) - 3Ohne Rust
CLI mit JSON
Das CLI liegt in einem eigenen Crate, damit die Bibliothek nie von
clapabhängt. Eingabe ist ein JSON-Dokument oder eine Vorlage plus Daten.cargo install lightweight-pdf-cli
Was in der Standardausstattung steckt.
Seitenumbruch, der zählt
Das Layout läuft in zwei Durchgängen. Erst danach steht die Gesamtseitenzahl fest, also stimmt Seite 2 von 3 auch bei Inhalten, die selbst umbrechen.
Tabellen mit Spaltenmodell
Feste und flexible Spaltenbreiten, Kopfzeilenwiederholung über Seiten, Zeilenraster, vertikale Ausrichtung und Zellhintergründe.
Schriften einbetten statt verlinken
Der eigene TrueType-Subsetter behält nur die tatsächlich benutzten Glyphen. Eigene Schriften lassen sich per registerFont ergänzen.
JSON und Vorlagen
Ein Dokument lässt sich als JSON beschreiben. Vorlagen mit {{platzhalter}} und $each werden gegen eine Datendatei aufgelöst; lwpdf schema gibt das JSON Schema aus.
PDF/A-3b, ZUGFeRD, PDF/UA
Hinter eigenen Features: PDF/A-3b, eingebettete ZUGFeRD-/Factur-X-Rechnungs-XML und Tagged PDF nach PDF/UA-1 – geprüft mit veraPDF, ZUGFeRD zusätzlich mit Mustang.
Snapshot-Tests für PDFs
lightweight-pdf-testing vergleicht gerenderte Seiten pixelweise gegen abgelegte Referenzen. Das Crate funktioniert auch für PDFs aus anderen Werkzeugen.
Acht Crates, eine Richtung.
- Modell
Dokument und Elemente
Der Dokumentbaum und die Builder-API. Kennt kein PDF und kein Layout – nur Bausteine und ihre Eigenschaften.
- Layout
Umbruch und Paginierung
Das
Layoutable-Trait, Textumbruch, Silbentrennung und der zweistufige Seitenumbruch samt Inhaltsverzeichnis. - Ausgabe
Writer und Schriften
PDF-Objekte, Querverweistabelle und Streams, dazu Schriftmetriken über
skrifaund das Subsetting. Beide Crates sind Blätter ohne Pfad-Abhängigkeit auf Modell oder Layout.
Rust oder JSON.
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();{
"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" } }
]
}
]
}
}$ 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)- Rust (nativ)
- wasm32-unknown-unknown
- Cloudflare Workers
- Node.js
- Browser
- CLI (lwpdf)
Wo die Grenze verläuft.
| Kriterium | lightweight-pdf | printpdf | Typst | Headless Chrome |
|---|---|---|---|---|
| Eingabe | Builder-API oder JSON | API mit Basis-Layout | eigene Auszeichnungssprache | HTML/CSS |
| WASM, komprimiert | ≈ 590 KiB (gemessen) | nicht veröffentlicht | zweistellige MiB berichtet | kein WASM |
| Direkte Abhängigkeiten | 1 (skrifa) | 24 | großer Compiler | ein ganzer Browser |
| Lizenz | MIT | MIT | Apache-2.0 | Chromium: BSD-artig |
| Text-Shaping | nein | ja (allsorts) | ja | ja |
Stand: August 2026, geprüft gegen die READMEs und crates.io-Seiten der Projekte, nicht aus dem Gedächtnis. Details und weitere Zeilen stehen im Vergleich der Dokumentation.
pdf.casoon.dev läuft auf dieser Bibliothek.
- Rätsel (Wortsuche, Sudoku, Labyrinth)3 %
- Arbeitsblätter (Rechnen, Lückentext)2 %
- Organisation (Stundenplan, Tischkarten)2 %
- Geschäftliches (Urkunde, Rechnung)2 %
Die PDFs entstehen wahlweise im Browser oder im Worker. Für KI-Assistenten liegt derselbe Generator als MCP-Server bereit.
Wofür es das falsche Werkzeug ist.
Der enge Funktionsumfang ist die Gegenleistung für die Größe. Vier Punkte, die vor der Entscheidung wichtig sind.
Kein HTML, kein CSS, keine Makrosprache
Die Eingabe ist der Dokumentbaum – als Rust-Code oder als JSON. Wer bestehendes HTML drucken will, braucht einen Browser-basierten Weg.
Kein Text-Shaping
Es gibt keinen Shaping-Stack wie allsorts oder rustybuzz. Für arabische, indische oder andere komplexe Schriftsysteme ist die Bibliothek nicht geeignet.
Wenige Konformitätsstufen
Abgedeckt sind PDF/A-3b und PDF/UA-1, dazu ZUGFeRD-Einbettung. Andere PDF/A-Stufen gibt es nicht; krilla deckt dort mehr ab.
Kein npm-Paket
Die JavaScript-/WASM-Bindings liegen im Repository unter bindings/js und werden dort gebaut. @casoon/lightweight-pdf ist nicht auf der npm-Registry veröffentlicht.
Häufige Fragen.
Kann ich die Bibliothek aus JavaScript nutzen?
Ja, über die wasm-bindgen-Bindings unter bindings/js. Sie sind aber nicht auf npm veröffentlicht: das Paket wird aus dem Repository gebaut (npm install, npm run build), dafür braucht es die Rust-Toolchain mit dem Target wasm32-unknown-unknown sowie wasm-pack und wasm-opt.
Läuft das wirklich in einem Cloudflare Worker?
Ja. examples/worker ist ein Starter, der ein POST mit JSON-Dokument als PDF-Antwort zurückgibt. Gemessen wurden 1252,95 KiB roh und 590,37 KiB gzip für das Modul; in einem lokalen wrangler dev lagen Kaltstart samt erstem Render bei etwa 34 bis 47 ms und warme Renders bei etwa 11 bis 25 ms inklusive HTTP-Overhead. Das ist die workerd-Laufzeit, nicht die echte Edge.
Brauche ich Rust, um PDFs zu erzeugen?
Nein. cargo install lightweight-pdf-cli bringt das Binary lwpdf mit den Befehlen render, validate, fonts und schema. Eingabe ist ein JSON-Dokument oder eine Vorlage plus Datendatei. Exit-Code 0 bedeutet Erfolg, 1 ein Renderproblem, 2 ein Eingabeproblem.
Wie werden E-Rechnungen abgedeckt?
Document::zugferd_xml() bettet ZUGFeRD-/Factur-X-XML hinter dem Feature zugferd ein, das PDF/A-3b impliziert. Geprüft wurde die Ausgabe mit veraPDF und Mustang. Ob das erzeugte XML den fachlichen Anforderungen eines Empfängers genügt, bleibt Sache der aufrufenden Anwendung.
Welche Abhängigkeiten zieht das Crate?
Pflicht ist allein skrifa für das Lesen von Schriftdateien. miniz_oxide für die Flate-Komprimierung sitzt hinter dem standardmäßig aktiven Feature compress. Weitere Features wie png, hyphenation, serde oder wasm bringen eigene Abhängigkeiten mit und sind abschaltbar.
Wie groß werden die erzeugten PDFs?
Die drei Demo-Dokumente liegen mit aktiver Komprimierung bei rund 24 bis 26 KB, Schriften eingebettet. Ohne das Feature compress werden dieselben Dokumente deutlich größer; typisch sind 40 bis 60 Prozent Unterschied.
Erst ausprobieren, dann einbauen.
Die Demo erzeugt druckfertige PDFs ohne Anmeldung. Der Code dahinter liegt offen unter MIT.