Dokumente programmatisch erstellen klingt simpel — ein paar Felder befüllen, als PDF exportieren, fertig. In der Praxis landet man schnell bei XML-Namespaces, kaputten Styles und Bibliotheken, die seit Jahren kein Update mehr gesehen haben.

Ich habe beides ausprobiert: Den klassischen Weg mit Apache POI in Java und den modernen Ansatz mit Puppeteer. Hier ein Vergleich — was funktioniert, was nervt, und warum ich heute nicht mehr zurückgehen würde.

Der klassische Weg: Apache POI und die Office-XML-Struktur

Was ist Apache POI?

Apache POI ist eine Java-Bibliothek zum Lesen und Schreiben von Microsoft-Office-Dateien — Word, Excel, PowerPoint. Das Projekt existiert seit über 20 Jahren und ist der De-facto-Standard in der Java-Welt.

Technisch solide, aber nicht unbedingt angenehm in der Nutzung.

Der Standard dahinter: Office Open XML (OOXML)

Um zu verstehen, warum POI so mühsam ist, lohnt sich ein Blick darauf, was eine .docx-Datei eigentlich ist.

Eine .docx-Datei ist kein einzelnes Dokument, sondern ein ZIP-Archiv mit einer ganzen Ordnerstruktur aus XML-Dateien. Der Text liegt in word/document.xml, Styles in word/styles.xml, Beziehungen zwischen den Teilen in word/_rels/document.xml.rels.

Das Format heißt Office Open XML (OOXML) und ist ein ECMA/ISO-Standard. Auf dem Papier ein offener, standardisierter Weg, Office-Dokumente darzustellen. In der Praxis sieht das so aus:

<w:p w:rsidR="00A77427" w:rsidRDefault="007F1D13">
  <w:pPr>
    <w:pStyle w:val="Heading1"/>
  </w:pPr>
  <w:r>
    <w:rPr>
      <w:b/>
      <w:sz w:val="28"/>
    </w:rPr>
    <w:t>Hallo Welt</w:t>
  </w:r>
</w:p>

Ein fetter “Hallo Welt”-Text als Heading. Dafür braucht es einen Paragraph (w:p), Paragraph-Properties (w:pPr), einen Run (w:r), Run-Properties (w:rPr) und dann erst den eigentlichen Text (w:t). Wer “Hallo” fett und “Welt” kursiv will, braucht zwei separate Runs.

Das ist kein Bug — so ist der Standard. Genau das macht die programmatische Arbeit damit so unintuitiv.

Warum das beim Entwickeln wehtut

  • Verschachtelte Abstraktionen: Man denkt in Absätzen und Formatierungen. OOXML denkt in Paragraphs, Runs, Properties, Numbering-Definitions und Relationship-Parts. Der Abstand zwischen “ich will eine Tabelle mit Rahmen” und dem, was man dafür im Code schreiben muss, ist enorm.

  • Inkonsistentes Verhalten: Word interpretiert OOXML großzügig. Zwei optisch identische Dokumente können völlig unterschiedliches XML haben — je nachdem, wie sie erstellt wurden.

  • Style-Vererbung: Styles erben voneinander, werden von Theme-Defaults überschrieben und verhalten sich je nach Dokumentenversion unterschiedlich. Debugging wird zum Detektivspiel.

  • Tabellen: Zellen mergen, Rahmen setzen, Spaltenbreiten definieren — jeder Schritt erfordert ein eigenes XML-Konstrukt mit eigenem Namespace. Eine einfache Tabelle mit zusammengeführten Zellen kann schnell 100 Zeilen XML werden.

  • Die API spiegelt die Komplexität: Apache POI abstrahiert das XML nur bedingt. Man arbeitet mit XWPFParagraph, XWPFRun, CTTblPr und ähnlichen Klassen, die die XML-Struktur direkt abbilden.

Hat es funktioniert? Ja. War es wartbar? Bedingt.

Der moderne Weg: Puppeteer

Die Idee

Warum mit XML-Strukturen kämpfen, wenn man HTML und CSS schon kann?

Der Ansatz:

  1. Dokument als HTML/CSS bauen (oder mit einer Template-Engine wie Handlebars)
  2. Puppeteer starten (headless Chrome)
  3. HTML rendern lassen
  4. Als PDF exportieren

Kein XML, keine Namespaces, keine Run-Properties.

In der Praxis

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.setContent(`
  <html>
    <style>
      body { font-family: Arial, sans-serif; margin: 40px; }
      h1 { color: #333; border-bottom: 2px solid #666; }
      table { width: 100%; border-collapse: collapse; }
      td, th { border: 1px solid #ccc; padding: 8px; }
    </style>
    <body>
      <h1>Angebot Nr. ${offerNumber}</h1>
      <p>Sehr geehrte/r ${customerName},</p>
      <table>
        <tr><th>Position</th><th>Beschreibung</th><th>Preis</th></tr>
        ${items.map(i =>
          `<tr><td>${i.pos}</td><td>${i.desc}</td><td>${i.price}</td></tr>`
        ).join('')}
      </table>
    </body>
  </html>
`);

await page.pdf({ path: 'angebot.pdf', format: 'A4' });
await browser.close();

Das ist im Wesentlichen der gesamte Code. Der gleiche Output hätte mit Apache POI ein Vielfaches an Zeilen gebraucht.

Vorteile

  • HTML und CSS — Technologien, die jeder Webentwickler kennt. Kein neues Format, keine kryptischen APIs.
  • Vorschau im Browser — Template öffnen, F12 drücken, fertig. Kein XML-Debugging.
  • CSS-Layout — Grid, Flexbox, Media Queries. Komplexe Layouts, die in OOXML extrem aufwändig wären, sind mit CSS einfach umsetzbar.
  • Template-Engines — Handlebars, Nunjucks oder einfach Template-Literals. Daten einsetzen wie bei einer Website.
  • Wartbarkeit — Neue Teammitglieder können sofort loslegen. HTML/CSS-Kenntnisse sind weit verbreitet, OOXML-Expertise nicht.

Einschränkungen

Auch der Puppeteer-Ansatz hat Schwächen:

  • Seitenumbrüche — CSS bietet page-break-before/after und break-inside, aber das Verhalten ist bei dynamischen Inhalten nicht immer vorhersehbar.
  • Header und Footer — Puppeteer unterstützt sie in page.pdf(), aber nur als separates HTML-Template. Seitenzahlen funktionieren, komplexe Header mit Bildern werden aufwändig.
  • Kein .docx-Output — Puppeteer erzeugt PDFs. Wer ein editierbares Word-Dokument braucht, muss auf andere Bibliotheken zurückgreifen oder beide Ansätze kombinieren.
  • Ressourcenverbrauch — Ein Headless Chrome ist nicht leichtgewichtig. Bei Batch-Verarbeitung braucht man Pooling und Ressourcen-Management.
  • Print-CSS@media print und @page haben eigene Regeln. Nicht alles, was am Bildschirm funktioniert, sieht im PDF genauso aus.

Der entscheidende Unterschied: Diese Probleme sind lösbar und gut dokumentiert. Bei OOXML-Problemen landet man dagegen oft in jahrzehntealten JIRA-Tickets mit dem Status “Won’t Fix”.

Der Vergleich auf einen Blick

AspektApache POI (OOXML)Puppeteer (HTML/CSS)
LernkurveSteil — OOXML-Verständnis nötigFlach — HTML/CSS reicht
Output-Format.docx, .xlsx nativPDF nativ, .docx nur über Umwege
Layout-KontrolleMühsam, viel BoilerplateCSS — flexibel und mächtig
DebuggingXML durchforstenBrowser-DevTools
WartbarkeitGering, spezialisiertes Wissen nötigHoch, Standard-Webskills
PerformanceLeichtgewichtig, schnellHeadless Browser = mehr Ressourcen
Editierbare DokumenteJaNein (nur PDF)

Einsatz in der Praxis

Im Kontext von Dokumentenmanagementsystemen (DMS) wird der Ansatz besonders nützlich:

  • Angebote und Rechnungen — Daten aus CRM oder ERP, festes Template, variable Werte.
  • Verträge und Formulare — Standardisierte Dokumente mit individuellen Feldern. Einmal bauen, tausendfach nutzen.
  • Berichte — KPI-Reports, Statusberichte, Audit-Protokolle. Daten ziehen, Template befüllen, PDF ablegen.
  • Onboarding-Dokumente — Arbeitsverträge, Checklisten, Zugangsdaten. Gleiche Struktur, individuelle Inhalte.
  • Archivierung — Automatisch generierte PDFs sind langzeitstabil (PDF/A) und durchsuchbar.

Ein typischer Workflow: Ein Event triggert die Generierung, Daten werden aus der Quelle gezogen, das Template befüllt und das PDF automatisch im DMS abgelegt — mit Metadaten, Versionierung und Zugriffsrechten.

Fazit

OOXML und Apache POI funktionieren, aber der Aufwand steht oft in keinem Verhältnis zum Ergebnis. Puppeteer ist keine Universallösung — aber es nutzt Technologien, die Webentwickler bereits beherrschen, und macht die Dokumentengenerierung zu einem lösbaren Problem.

Solange der Output PDF sein darf, würde ich heute immer den HTML-zu-PDF-Weg gehen.

Glossar

Office Open XML (OOXML)
Ein ECMA/ISO-Standard (ECMA-376) für Office-Dokumente. Eine .docx-Datei ist ein ZIP-Archiv mit XML-Dateien, die Inhalt, Styles und Beziehungen in einer tief verschachtelten Struktur aus Paragraphs, Runs und Properties beschreiben.
Headless Browser
Ein Browser ohne grafische Oberfläche, der programmatisch gesteuert wird. Puppeteer steuert einen headless Chrome per API — nützlich für Tests, Scraping und die Erzeugung von PDFs aus HTML/CSS.
Apache POI
Eine Java-Bibliothek zum Lesen und Schreiben von Microsoft-Office-Dateien (Word, Excel, PowerPoint). Arbeitet direkt auf der OOXML-Struktur und bildet deren Komplexität in der API ab.
Template-Engine
Eine Software-Komponente, die Platzhalter in Vorlagen durch dynamische Daten ersetzt. Beispiele sind Handlebars, Nunjucks oder EJS — weit verbreitet in der Webentwicklung für serverseitiges Rendering und E-Mail-Templates.
Dokumentenmanagementsystem (DMS)
Ein System zur digitalen Verwaltung, Versionierung und Archivierung von Dokumenten. Unterstützt typischerweise Metadaten, Zugriffsrechte und Volltextsuche — zentral für compliance-relevante Ablage in Unternehmen.