Zum Inhalt springen
lightweight-pdf
MITLive-Demo
Rust · WebAssembly · MIT

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.

0.3.0
crates.io
1
Pflicht-Abhängigkeit
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)
drei Demo-Dokumente, ein RendererLauf vom 16.09.2026
Bausteine
  • Text
  • Tabellen
  • Zeilen & Spalten
  • Listen
  • Bilder
  • Kopf- & Fußzeilen
  • Inhaltsverzeichnis
  • Lesezeichen
  • Wasserzeichen
  • Themes
  • Links
1
Pflicht-Abhängigkeit

skrifa liest Schriftdateien. miniz_oxide ist optional hinter dem standardmäßig aktiven Feature compress. PDF-Writer und TrueType-Subsetter gehören zum Projekt.

590 KiB
WASM-Modul, gzip

Gemessen über wrangler deploy --dry-run für den Build aus wasm, default-fonts und compress; roh 1252,95 KiB.

≈ 4 ms
Renderzeit je Dokument

Release-Build, Mittel aus fünf Läufen für die Demos Rechnung, Angebot und Bericht. Seitenzahl, Tabellengröße und Schriftschnitte verschieben den Wert.

01 — Überblick

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 →
  1. 01Eigener PDF-Writer — Objekte, Querverweise und Streams schreibt die Bibliothek selbst
  2. 02Eigener Subsetter — TrueType-Schriften werden auf die benutzten Glyphen reduziert und eingebettet
  3. 03Zweistufiges Layout — damit „Seite 2 von 3“ in der Fußzeile stimmt
  4. 04Warnungen statt stiller Fehler — abgeschnittener Text und fehlende Zeichen werden gemeldet
02 — Demo

Ein laufender Generator und drei erzeugte Dokumente.

pdf.casoon.dev: Startseite mit der Frage „Was willst du drucken?“ und Kacheln für Wortsuchrätsel, Sudoku und weitere Vorlagen
pdf.casoon.dev – Vorlage wählen, Formular ausfüllen, PDF drucken. Aufnahme vom 16.09.2026.
Erste Seite der gerenderten Rechnungs-Demo mit Absender, Positionstabelle und Summen
demo_invoice.pdf, Seite 1 – erzeugt mit `cargo run --example demo_invoice`.
Erste Seite der gerenderten Angebots-Demo mit Positionen und Gesamtsumme
demo_offer.pdf, Seite 1 – erzeugt mit `cargo run --example demo_offer`.
Erste Seite der gerenderten Bericht-Demo mit Überschriften, Fließtext und Tabelle
demo_report.pdf, Seite 1 – erzeugt mit `cargo run --example demo_report`.

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.

03 — Einstieg

Zwei Wege, dasselbe Ergebnis.

  1. 1Als Bibliothek

    Crate hinzufügen

    Die Facade lightweight-pdf bringt die öffentliche API mit; Schriften (Source Sans 3) sind standardmäßig gebündelt.

    cargo add lightweight-pdf
  2. 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)
  3. 3Ohne Rust

    CLI mit JSON

    Das CLI liegt in einem eigenen Crate, damit die Bibliothek nie von clap abhängt. Eingabe ist ein JSON-Dokument oder eine Vorlage plus Daten.

    cargo install lightweight-pdf-cli
04 — Features

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.

05 — Architektur

Acht Crates, eine Richtung.

  1. Modell

    Dokument und Elemente

    Der Dokumentbaum und die Builder-API. Kennt kein PDF und kein Layout – nur Bausteine und ihre Eigenschaften.

    lightweight-pdf-core
  2. Layout

    Umbruch und Paginierung

    Das Layoutable-Trait, Textumbruch, Silbentrennung und der zweistufige Seitenumbruch samt Inhaltsverzeichnis.

    lightweight-pdf-layout
  3. Ausgabe

    Writer und Schriften

    PDF-Objekte, Querverweistabelle und Streams, dazu Schriftmetriken über skrifa und das Subsetting. Beide Crates sind Blätter ohne Pfad-Abhängigkeit auf Modell oder Layout.

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

Rust oder 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.jsonVorlage
{
  "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)
Läuft auf
  • Rust (nativ)
  • wasm32-unknown-unknown
  • Cloudflare Workers
  • Node.js
  • Browser
  • CLI (lwpdf)
Der wasm32-Build wird in CI bei jedem Lauf für den gesamten Workspace erzeugt. Das JavaScript-Paket wird aus bindings/js gebaut – es liegt nicht auf npm.
07 — Vergleich

Wo die Grenze verläuft.

Kriteriumlightweight-pdfprintpdfTypstHeadless Chrome
EingabeBuilder-API oder JSONAPI mit Basis-Layouteigene AuszeichnungsspracheHTML/CSS
WASM, komprimiert≈ 590 KiB (gemessen)nicht veröffentlichtzweistellige MiB berichtetkein WASM
Direkte Abhängigkeiten1 (skrifa)24großer Compilerein ganzer Browser
LizenzMITMITApache-2.0Chromium: BSD-artig
Text-Shapingneinja (allsorts)jaja

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.

08 — Im Einsatz

pdf.casoon.dev läuft auf dieser Bibliothek.

Technik-Seite der Demo →
pdf.casoon.devCloudflare Worker · Stand 16.09.2026
Vorlagen online
9
Wasm-Modul, gzip
970 KiB
Worker-Start
26 ms
Rätsel-PDF, 3 Seiten
≈ 28 KB
Was der Generator erzeugt
  • Rätsel (Wortsuche, Sudoku, Labyrinth)3 %
  • Arbeitsblätter (Rechnen, Lückentext)2 %
  • Organisation (Stundenplan, Tischkarten)2 %
  • Geschäftliches (Urkunde, Rechnung)2 %
Zahlen von der Technik-Seite der Demo. Das dortige Modul enthält zusätzlich vier Rätselgeneratoren und deren Layouts.

Die PDFs entstehen wahlweise im Browser oder im Worker. Für KI-Assistenten liegt derselbe Generator als MCP-Server bereit.

09 — Grenzen

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.

LizenzDer Code aller Workspace-Crates steht unter der MIT-Lizenz. Die mitgelieferten Standardschriften (Source Sans 3) stehen unter der SIL Open Font License 1.1; die OFL erlaubt Einbettung und Subsetting in erzeugten Dokumenten, für die PDFs entstehen daraus also keine Pflichten.Lizenztext im Repository →
10 — FAQ

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.