Skip to content

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:

  1. 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.
  2. Vorschau — Office-Files lassen sich im DMS-Detail-Pane heute nur runterladen. Schule will sie inline sehen wie ein PDF.
  3. Download als PDF — "Schick mir den Brief als PDF" ist ein häufiger Wunsch. Heute keine Funktion.
  4. .docx öffnen und bearbeiten — Schule lädt einen Word-Brief hoch, will ihn redigieren. Heute: Download → Word lokal → Upload.
  5. Archiv/Retention — Bei Auto-Wipe sollen alte Office-Dokumente vorher in PDF/A archiviert werden, damit sie auch in 10 Jahren lesbar sind.
  6. eIDAS-Signaturen — Funktioniert nur auf PDF. Roundtrip .docx → PDF → signieren braucht den Konverter.
  7. 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) der soffice --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}.pdf abgelegt. Bei nächster Anfrage: direkt aus Cache.
  • Per-Tenant Rate-Limit — verhindert dass ein Bulk-Convert (50 Klassenarbeiten gleichzeitig) andere Tenants blockiert.

Container-Spec

yaml
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: 1

Dockerfile-Skelett:

dockerfile
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)

MethodePfadWirkung
GET /platform/v1/documents/:id/pdfKonvertiertes PDF (cached)
POST /platform/v1/documents/:id/convertKonvertierung anstoßen, Job-ID zurück
GET /platform/v1/documents/:id/print-urlLiefert kurzlebigen PDF-Link für window.print()
POST /platform/v1/documents/:id/save-as-pdfKonvertiert + 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-Dialog

PDF 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-Document

Lossy 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

PhaseInhaltAufwand
AContainer prilog-converter + HTTP-Service + Docker-Compose-Eintrag + Health-Check~3h
BBackend-Endpunkte /pdf + /print-url + Caching in MinIO + Job-Queue~3h
CFrontend "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
FArchiv-Cron PDF/A-Konvertierung vor Auto-Wipe~2h
GMail-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-pdf mit S3-Caching
  • Frontend PrintButton mit 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-ValidationContent-Length Begrenzung 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

  1. 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.
  2. 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.
  3. Welche Office-Formate priorisieren?.docx, .xlsx Top-Prio. .pptx nice-to-have. .pages/.numbers/.key (Apple): LibreOffice kann's, aber selten relevant für Schulen.
  4. Univer für .xlsx editing — Wir haben bereits Univer-Sheets. Soll Doppelklick auf .xlsx ein 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.