Office-Konverter-Service — Konzept
Das Problem
Mehrere ungelöste Punkte hängen am gleichen Haken — alle brauchen einen Office-zu-PDF-Konverter auf dem Server:
- Drucken — Klick auf "Drucken" im DMS soll einen sauberen Druck-Dialog öffnen. Geht heute nicht für
.docx/.xlsx/.pptx, weil der Browser sie nicht rendern kann. - Vorschau — Office-Files lassen sich im DMS-Detail-Pane heute nur runterladen. Schule will sie inline sehen wie ein PDF.
- Download als PDF — "Schick mir den Brief als PDF" ist ein häufiger Wunsch. Heute keine Funktion.
.docxöffnen und bearbeiten — Schule lädt einen Word-Brief hoch, will ihn redigieren. Heute: Download → Word lokal → Upload.- Archiv/Retention — Bei Auto-Wipe sollen alte Office-Dokumente vorher in PDF/A archiviert werden, damit sie auch in 10 Jahren lesbar sind.
- eIDAS-Signaturen — Funktioniert nur auf PDF. Roundtrip
.docx → PDF → signierenbraucht den Konverter. - Mein Fach Mail-Versand — Anhänge sollen optional als PDF normalisiert verschickt werden (kein Word-Versions-Drama beim Empfänger).
Ein Service löst alle sieben Probleme. Ohne Service brauchen wir für jedes ein Sub-Hack.
Lösung: Ein gemeinsamer Konverter-Container
Architektur
┌─────────────────────┐ ┌─────────────────────┐
│ Backend-API │ HTTP │ prilog-converter │
│ (Fastify) │ ─────▶ │ (Container) │
│ │ │ │
│ /documents/:id/ │ │ POST /convert │
│ convert?to=pdf │ │ body: file bytes │
│ │ │ ?from=docx&to=pdf │
│ /documents/:id/ │ ◀──── │ │
│ pdf (cached) │ PDF │ internally: │
└─────────────────────┘ │ soffice --headless │
│ --convert-to pdf │
└─────────────────────┘- Eigener Container
prilog-converter— nur LibreOffice + ein dünner HTTP-Server (Python Flask oder Node Fastify) dersoffice --headless --convert-to <format>aufruft. - Stateless — kein DB-Zugriff, nimmt Bytes rein, gibt Bytes raus.
- Job-Queue über Redis (haben wir) — Konvertierung kann 2–10 Sekunden dauern, Backend blockiert nicht.
- Caching — konvertiertes PDF wird in MinIO unter
documents/{docId}/converted/{hash}.pdfabgelegt. Bei nächster Anfrage: direkt aus Cache. - Per-Tenant Rate-Limit — verhindert dass ein Bulk-Convert (50 Klassenarbeiten gleichzeitig) andere Tenants blockiert.
Container-Spec
prilog-converter:
image: prilog-converter:latest
build:
context: ./services/converter
dockerfile: Dockerfile
environment:
- PORT=4000
- MAX_CONCURRENT=4
resources:
memory: 1.5G # LibreOffice braucht das
cpus: 1Dockerfile-Skelett:
FROM debian:12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice-core libreoffice-writer libreoffice-calc \
libreoffice-impress fonts-dejavu fonts-liberation \
nodejs npm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json server.js ./
RUN npm install
EXPOSE 4000
CMD ["node", "server.js"]HTTP-Endpoint im Service
POST /convert?from=docx&to=pdf
Content-Type: application/octet-stream
Body: <file bytes>
→ 200 OK, application/pdf, body = converted PDF
→ 422, body = { error: "Konversion fehlgeschlagen" }Unterstützte Format-Paare (Phase 1): jedes von [docx, doc, odt, rtf, txt, md, html, xlsx, ods, csv, pptx, odp] → pdf. Roundtrip pdf → docx ist out-of-scope (LibreOffice macht das, aber Layout zerschießt → braucht's nicht).
Backend-Endpunkte (Platform-API)
| Methode | Pfad | Wirkung |
|---|---|---|
GET /platform/v1/documents/:id/pdf | Konvertiertes PDF (cached) | |
POST /platform/v1/documents/:id/convert | Konvertierung anstoßen, Job-ID zurück | |
GET /platform/v1/documents/:id/print-url | Liefert kurzlebigen PDF-Link für window.print() | |
POST /platform/v1/documents/:id/save-as-pdf | Konvertiert + legt das PDF als neues Document im DMS an |
Caching-Logik: PDF wird einmal pro (docId, sourceVersion) erzeugt und in S3 abgelegt. Bei Update des Quell-Documents wird das Cache-PDF invalidiert (neu auf Anfrage).
Use-Case-Mapping
1. Drucken (Stufe 1)
UI: "Drucken"-Button im DMS-Detail-Pane + im Doppelklick-Menü.
Flow:
Klick "Drucken"
→ GET /print-url
→ URL = /documents/{id}/pdf (signed, 5min TTL)
→ öffne hidden iframe mit dieser URL
→ iframe.onload: iframe.contentWindow.print()
→ Browser zeigt Druck-DialogPDF wird im Browser nativ gerendert. Funktioniert für alle Formate die der Service unterstützt + nativ-PDFs (kein Convert nötig) + Bilder + Markdown (Tiptap-PDF-Export).
2. Vorschau
DMS-Detail-Pane: heute funktioniert Vorschau für PDF/Image/Text. Mit dem Service:
.docx/.xlsx/.pptx→ on-demand convert → PDF in iframe einbetten- Pre-Cache bei Upload: optional, sonst lazy bei erstem Klick
3. Download als PDF
Action im DMS: "Als PDF herunterladen". Triggert /save-as-pdf (legt es als neues Document im selben Folder an) oder /pdf (direkt-download ohne neues Document).
4. .docx bearbeiten — Variante A (Tiptap-Roundtrip)
Upload .docx
→ Backend: mammoth.js docx-zu-HTML (im Backend)
→ HTML → Tiptap-JSON
→ CollabDocument anlegen mit sourceDocumentId = das .docx-Document
→ User editiert in Tiptap
→ Speichern: Tiptap-JSON → HTML → LibreOffice html-zu-docx
→ schreibt zurück in das Quell-DocumentLossy für komplexe Layouts (Tabellen mit Merge, Bilder mit Wrap, Header/Footer). Für 90% der Schul-Briefe gut genug. Bei Format-Verlust kann der User immer "Original behalten" wählen → die .docx-Datei bleibt unverändert.
UI: Doppelklick auf .docx → wenn unter 50 KB pure Text → öffne Tiptap-Editor mit Hinweis "Bearbeitung in Prilog. Komplexe Layouts können verloren gehen." + Button "Lieber in Word lokal öffnen".
5. Archiv-PDF/A (Phase 2 / DMS-Retention)
Cron-Job vor Auto-Delete: Konvertiere alle Office-Dokumente eines Tenants zu PDF/A-2b (gesetzlicher Aufbewahrungsstandard). Hänge an das Original-Document an als Version. Auto-Delete dann auf das Original, PDF/A bleibt.
LibreOffice unterstützt PDF/A direkt: --convert-to "pdf:writer_pdf_Export:SelectPdfVersion=2".
6. eIDAS-Signaturen (separater Refactor, hier nur Anker)
Signatur-Workflow funktioniert nur auf PDF. Dieser Service ist der Vorstufen-Schritt: jedes signaturwürdige Document wird vor der Signatur durchgesetzt → PDF → signieren.
7. Mein-Fach-Mail-Anhänge (Phase 2)
Beim Brief-Versand aus Mein Fach: Toggle "Anhänge als PDF normalisieren". Konverter wandelt vor dem SMTP-Send.
Phasenplan
| Phase | Inhalt | Aufwand |
|---|---|---|
| A | Container prilog-converter + HTTP-Service + Docker-Compose-Eintrag + Health-Check | ~3h |
| B | Backend-Endpunkte /pdf + /print-url + Caching in MinIO + Job-Queue | ~3h |
| C | Frontend "Drucken"-Button (Stufe 1 vollständig) + DMS-Detail-Pane Vorschau-Erweiterung für Office | ~2h |
| D | "Als PDF speichern" + "Als PDF herunterladen" Actions im DMS | ~1h |
| E | .docx → Tiptap → .docx Roundtrip via mammoth.js + LibreOffice (HTML→DOCX) | ~5h |
| F | Archiv-Cron PDF/A-Konvertierung vor Auto-Wipe | ~2h |
| G | Mail-Anhang-Normalisierung im Mein-Fach-Versand | ~2h |
MVP = A+B+C+D (~9h, ein Tag) — löst Drucken, Vorschau, Download-as-PDF, "Als PDF speichern". Die häufigsten Probleme der Lehrer-Workflows.
Status 2026-05-06 22:00 UTC: Phasen A+B+C+D LIVE.
- LibreOffice 24.2 als Host-Service (pm2
converter, Port 4000) auf api.prilog.chat - Backend-Endpunkte
/documents/:id/pdf,/print-url,/save-as-pdfmit S3-Caching - Frontend
PrintButtonmit Browser-Dialog-Modus - Phasen E (.docx-Roundtrip), F (PDF/A-Archiv), G (Mail-Anhang) offen.
E ist die .docx-Edit-Funktion, die alleine ~5h dauert weil mammoth → Tiptap-JSON nicht trivial ist. Lohnt sich erst wenn die Schule sagt "wir editieren ständig Word in Prilog".
F+G sind Compliance/Komfort-Phasen.
Sicherheits-Aspekte
- LibreOffice-Sandbox — Container läuft als unprivilegierter User, Network egress nur zu unserer API.
- Input-Validation —
Content-LengthBegrenzung 100 MB, MIME-Sniff, kein Auto-Open von Macros (--norestore --nologo --nofirststartwizard --disable-extension-update). - Macro-Files (.docm, .xlsm) — werden ohne Macro-Ausführung konvertiert. Default LibreOffice-Verhalten.
- Rate-Limit pro Tenant — Redis-Counter, max 30 Conversions/Minute. Ueberlauf landet in Queue, User sieht "wird verarbeitet…".
- Cache-Isolation — Per-Tenant S3-Pfad, kein Cross-Tenant-Leak möglich.
Offene Fragen
- Wo läuft der Container? Auf jedem Tenant-Box-Stack mit (eigener
soffice+ 1.5 GB RAM Overhead pro Tenant), oder zentral als geteilter Service (geringerer RAM-Footprint, aber Tenant-Daten verlassen kurzzeitig die eigene Box)? — Empfehlung: zentral, weil Konvertierung stateless ist und die Datei nach dem Convert sofort gelöscht wird (kein Daten-Residency-Problem). Bei DSGVO-strikten Schulen optional Per-Tenant-Mode. - Tiptap-zu-DOCX-Qualität — Acceptable-Threshold? "Brief sieht zu 95% wie Original aus" oder "Pixel-perfekt"? — Empfehlung: 95% für simple Texte, klare Warnung bei Verlust-Risiko in der UI.
- Welche Office-Formate priorisieren? —
.docx,.xlsxTop-Prio..pptxnice-to-have..pages/.numbers/.key(Apple): LibreOffice kann's, aber selten relevant für Schulen. - Univer für
.xlsxediting — Wir haben bereits Univer-Sheets. Soll Doppelklick auf.xlsxein Sheet daraus machen (wie heute schon.usheet)? Müsste analog zur.docx → Tiptap-Logik laufen. Empfehlung: ja, in Phase E gleich mit.
Warum so?
- Ein Service löst sieben Probleme — kein Sub-Hack pro Feature, alle Verbraucher rufen die gleichen Endpunkte.
- Eigener Container — LibreOffice ist 700 MB Image. Backend bleibt schlank, klare Trennung, einzeln skalierbar.
- Caching macht's flüssig — beim zweiten Aufruf eines Documents-Drucks ist die PDF in <50ms da statt 5s LibreOffice-Startup.
- Variante A (Tiptap-Roundtrip) — kein Collabora-RAM-Wahnsinn, kein OnlyOffice-Lizenz-Stress. 90% der Use-Cases gedeckt, 10% kriegen klare Notice "Bearbeite lokal in Word".
- PDF/A-Archiv — DSGVO-konformer Aufbewahrungsweg, der heute Hand-Arbeit ist.