Ratgeber · Werkzeuge & Code
HTML zu PDF im Code: Puppeteer, WeasyPrint, wkhtmltopdf und Co
Wer PDFs aus HTML im Code erzeugen will, hat die Wahl zwischen Headless-Browsern, Python-Renderern und PHP-Bibliotheken. Dieser Überblick zeigt, welches Werkzeug zu welchem Anwendungsfall passt.
Warum die Wahl des Werkzeugs entscheidend ist
Ein Browser-Tool reicht, um schnell eine einzelne Seite als PDF zu speichern. Sobald PDFs aber automatisiert entstehen sollen, etwa Rechnungen, Reports oder Versandetiketten aus einer Anwendung heraus, braucht es eine Bibliothek oder ein CLI-Werkzeug. Die Kandidaten unterscheiden sich nicht nur in der Programmiersprache, sondern vor allem darin, wie gut sie CSS interpretieren, wie viel Speicher sie brauchen und wie aufwändig sie im Deployment sind.
Die Kernfrage lautet immer: Wie komplex ist das Layout? Eine schlichte Textseite mit Tabelle stellt andere Anforderungen als ein mehrspaltiges Magazin-Layout mit Kopf- und Fußzeilen, Seitenumbruch-Steuerung und Webfonts. Genau an dieser Achse trennen sich die Werkzeuge.
Puppeteer und Playwright: Headless Chrome
Puppeteer und Playwright steuern einen echten, im Hintergrund laufenden Chromium-Browser. Das bedeutet: Was im Chrome funktioniert, funktioniert auch im PDF. Flexbox, CSS Grid, moderne Selektoren, Webfonts, SVG und sogar JavaScript-gerendertes HTML werden vollständig unterstützt. Für komplexe, designlastige Dokumente gibt es derzeit keine zuverlässigere Lösung.
Der Ablauf ist einfach: Browser starten, Seite laden oder HTML setzen, auf das Rendern warten, dann page.pdf() aufrufen.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.setContent("<h1>Rechnung 2026-0042</h1><p>Betrag: 199,00 Euro</p>", {
waitUntil: "networkidle0",
});
await page.pdf({
path: "rechnung.pdf",
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
displayHeaderFooter: true,
headerTemplate: "<div></div>",
footerTemplate:
'<div style="font-size:9px; width:100%; text-align:center;">Seite <span class="pageNumber"></span> von <span class="totalPages"></span></div>',
});
await browser.close();
Wichtig ist printBackground: true, sonst fehlen Hintergrundfarben und -bilder. Header- und Footer-Templates sind eigene HTML-Schnipsel mit den Platzhaltern pageNumber und totalPages. CSS-Regeln aus @page werden teilweise berücksichtigt, lassen sich aber bei Puppeteer am sichersten über die Optionen im Code steuern.
Playwright funktioniert nahezu identisch, bringt aber von Haus aus mehr Komfort für mehrere Browser-Engines mit. Für PDF reicht Chromium, da nur die Chromium-Engine PDF-Export unterstützt.
Der Preis dieser Mächtigkeit ist der Ressourcenverbrauch. Jeder Browser-Prozess belegt schnell mehrere hundert Megabyte RAM. In Serverless-Umgebungen wie AWS Lambda ist das volle Chromium-Binary zudem zu groß für das Deployment-Limit. Pakete wie @sparticuz/chromium liefern eine abgespeckte Variante, der Cold Start bleibt aber träge.
wkhtmltopdf: das ältere CLI
wkhtmltopdf ist ein Kommandozeilen-Werkzeug, das HTML über eine ältere WebKit-Engine (Qt) in PDF umwandelt. Über viele Jahre war es der Standard für serverseitige PDF-Erzeugung, weil es ohne Programmiersprache auskommt und sich leicht in Skripte einbinden lässt.
wkhtmltopdf \
--enable-local-file-access \
--margin-top 20mm --margin-bottom 20mm \
--footer-center "Seite [page] von [topage]" \
rechnung.html rechnung.pdf
Der Aufruf ist denkbar knapp, und für einfache Dokumente liefert es brauchbare Ergebnisse. Das Problem: Die zugrunde liegende WebKit-Version ist alt, Flexbox und Grid werden nicht oder fehlerhaft unterstützt, und seit 2023 wird das Projekt nicht mehr aktiv weiterentwickelt. Für Neuentwicklungen ist es deshalb nicht mehr empfehlenswert. Wer ein bestehendes wkhtmltopdf-Setup hat und nur schlichte Dokumente erzeugt, kann es übergangsweise weiterbetreiben, sollte aber eine Migration einplanen.
WeasyPrint: Python mit starker Paged-Media-Unterstützung
WeasyPrint ist eine reine Python-Bibliothek, die ohne Browser auskommt. Statt eine Browser-Engine zu kapseln, implementiert sie ein eigenes Rendering, das stark auf die CSS-Paged-Media-Spezifikation ausgerichtet ist. Genau dort spielt WeasyPrint seine Stärke aus: Seitenränder, benannte Seiten, Kopf- und Fußzeilen über @page-Regeln, Seitenzähler und kontrollierte Umbrüche funktionieren sauber und standardkonform.
from weasyprint import HTML, CSS
html = """
<h1>Quartalsbericht Q2 2026</h1>
<p>Umsatz, Kosten und Marge im Überblick.</p>
"""
stylesheet = CSS(string="""
@page {
size: A4;
margin: 20mm 15mm;
@bottom-center {
content: "Seite " counter(page) " von " counter(pages);
font-size: 9px;
}
}
h1 { color: #1a56db; }
""")
HTML(string=html).write_pdf("bericht.pdf", stylesheets=[stylesheet])
Die Footer-Definition steckt hier direkt im CSS, nicht im Anwendungscode. Das ist sauberer als die Template-Strings bei Puppeteer und lässt sich mit dem restlichen Stylesheet versionieren. WeasyPrint kommt ohne JavaScript-Ausführung aus, was bei statischen Dokumenten ein Vorteil ist, bei dynamisch im Browser gerendertem Inhalt aber eine Grenze setzt. Auch sehr moderne CSS-Features sind nur teilweise abgedeckt. Für strukturierte Dokumente wie Rechnungen, Berichte und Briefe ist WeasyPrint dennoch oft die beste Wahl, weil es leichtgewichtig ist, sich gut in Python-Backends einfügt und in schlanken Containern ohne Chromium läuft.
Dompdf und mPDF: die PHP-Welt
Im PHP-Umfeld dominieren zwei Bibliotheken. Dompdf ist die etabliertere und einfachere von beiden. Sie deckt grundlegendes CSS ab, hat aber bei komplexen Layouts und modernen CSS-Features deutliche Lücken. Für Rechnungen aus einem Laravel- oder Symfony-Backend reicht sie in vielen Fällen.
use Dompdf\Dompdf;
$dompdf = new Dompdf();
$dompdf->loadHtml('<h1>Rechnung</h1><p>Betrag: 199,00 Euro</p>');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
file_put_contents('rechnung.pdf', $dompdf->output());
mPDF ist mächtiger und insbesondere bei Tabellen, Kopf- und Fußzeilen sowie Unicode- und RTL-Sprachen stärker. Es unterstützt mehr CSS und bringt eine eigene Syntax für Seitenelemente mit. Der Preis ist ein höherer Speicherverbrauch und eine komplexere API. Beide Bibliotheken haben gemeinsam, dass sie kein echtes Browser-Rendering bieten. Sobald das Layout auf Flexbox, Grid oder JavaScript angewiesen ist, stoßen sie an Grenzen. Ihr großer Vorteil ist die einfache Integration in PHP-Anwendungen, gerade auf Shared Hosting ohne Möglichkeit, einen Browser-Prozess zu starten.
Vergleich auf einen Blick
| Werkzeug | Sprache | CSS-Unterstützung | Ressourcen | Beste Eignung |
|---|---|---|---|---|
| Puppeteer / Playwright | Node.js | Vollständig (Chromium) | Hoch | Komplexe Layouts, Webfonts, JS-Inhalte |
| wkhtmltopdf | CLI | Veraltet (altes WebKit) | Mittel | Bestehende, einfache Setups |
| WeasyPrint | Python | Gut, starker Paged-Media-Fokus | Niedrig | Rechnungen, Berichte, Serverless ohne Browser |
| Dompdf | PHP | Grundlegend | Niedrig | Einfache PDFs in PHP-Apps |
| mPDF | PHP | Erweitert, gut bei Tabellen/Unicode | Mittel | Anspruchsvollere PDFs in PHP-Apps |
Entscheidungshilfe nach Anwendungsfall
Bei der Werkzeugwahl helfen drei Leitfragen. Erstens: Wie komplex ist das CSS? Für moderne, designlastige Dokumente mit Flexbox, Grid und Webfonts führt kaum ein Weg an Puppeteer oder Playwright vorbei. Wer auf strukturierten Dokumenten mit klaren Seitenrändern und Fußzeilen arbeitet, ist mit WeasyPrint oft besser bedient, weil es leichter und standardnäher ist.
Zweitens: In welcher Sprache läuft das Backend? Ein Node-Stack legt Puppeteer nahe, ein Python-Backend WeasyPrint, eine PHP-Anwendung Dompdf oder mPDF. Den Stack für das PDF-Werkzeug zu wechseln, lohnt sich nur bei sehr hohen Layout-Anforderungen.
Drittens: Wie ist die Laufzeitumgebung aufgebaut? Auf Shared Hosting ohne eigene Prozesse scheiden Browser-basierte Lösungen aus, dort bleiben die PHP-Bibliotheken oder WeasyPrint. In Containern mit ausreichend RAM ist Puppeteer problemlos. In Serverless-Funktionen ist es möglich, aber heikel, hier ist WeasyPrint wegen des fehlenden Browser-Overheads oft die robustere Wahl.
Ein praktischer Hinweis zum Schluss: Webfonts und Seitenumbrüche sind die häufigsten Stolperstellen. Bei Puppeteer muss man auf das vollständige Laden der Fonts warten, etwa mit waitUntil: "networkidle0" oder einem expliziten document.fonts.ready. Bei WeasyPrint und den PHP-Bibliotheken müssen Fonts oft lokal eingebunden und registriert werden. Umbrüche steuert man werkzeugübergreifend am besten über break-inside: avoid und break-before: page im CSS, statt sich auf Standardverhalten zu verlassen. Wer diese Details früh testet, spart sich später viel Ärger mit abgeschnittenen Tabellen und fehlenden Schriften.
FAQ
Häufige Fragen
Welches Werkzeug liefert die beste CSS-Unterstützung?
Puppeteer und Playwright steuern echtes Chromium und rendern damit modernes CSS, Flexbox, Grid, Webfonts und JavaScript identisch zum Browser. Für komplexe Layouts ist das die zuverlässigste Wahl, kostet aber mehr Ressourcen als reine Rendering-Bibliotheken.
Brauche ich für serverseitiges PDF zwingend einen Browser?
Nein. WeasyPrint (Python) und die PHP-Bibliotheken Dompdf und mPDF rendern ohne Browser-Prozess. Sie eignen sich für Umgebungen ohne Chromium-Binary, etwa schlanke Container oder Shared Hosting, decken aber nur eine Teilmenge moderner CSS-Features ab.
Funktioniert Puppeteer in einer Serverless-Umgebung wie AWS Lambda?
Ja, aber mit Einschränkungen. Das volle Chromium-Binary sprengt oft das Deployment-Limit. Mit chromium-min oder einer Browser-Layer und passenden Launch-Flags läuft es, der Cold Start dauert jedoch spürbar länger. Für reine Serverless-Setups ohne Browser ist WeasyPrint oft praktischer.
Warum gilt wkhtmltopdf als veraltet?
wkhtmltopdf basiert auf einer alten WebKit-Version (Qt) und wird seit 2023 nicht mehr aktiv weiterentwickelt. Moderne CSS-Features fehlen, und es gibt offene Sicherheits- und Wartungsthemen. Für Neuentwicklungen sind Puppeteer oder WeasyPrint die bessere Wahl.
Quellen