fix: ocrmypdf-Pin auf 17.4.1, Preflight erkennt den GS-Fall, Rauchtest (v0.6.1)

v0.6.0 hat ocrmypdf auf 16.13.0 gepinnt, um einen ungewollten Major-Sprung
zu verhindern. Auf Bestandsinstallationen war das ein DOWNGRADE (dort lief
via ">=16.0" bereits 17.x) — und 16.13.0 bricht auf Debian 12 mit dem
Bord-Ghostscript 10.0.0 bei JEDER PDF ab, sobald skip_text gesetzt ist.
Im Test auf CT 200 lief das Update mit Exit 0 durch, der Dienst blieb
"active", --check-config meldete "Preflight ok" — und jede Datei landete
in error/. Stiller Totalausfall.

- requirements.txt: ocrmypdf==17.4.1 (real auf Debian 12 + gs 10.0.0
  verifiziert). Ab 17.0.0 steht die GS-Pruefung in ocrmypdf unter einem
  `if options.output_type.startswith('pdfa')`; bis 16.x lief sie ohne
  diesen Guard und schlug auch bei output_type="pdf" zu.
- check_preflight() prueft Ghostscript nicht mehr nur bei gesetztem
  pdfa_level, sondern bildet die reale Bedingung ab:
  betroffene GS-Version UND skip_text UND (PDF/A ODER ocrmypdf < 17).
  Der Dienst bricht damit beim Start ab statt bei der ersten Datei.
- update.sh zeigt Versionsspruenge der gepinnten Pakete; Downgrades als
  WARN, auch in der Abschluss-Zusammenfassung.
- update.sh faehrt nach dem Start einen Rauchtest (eingebettete Mini-PDF
  durch die echte Pipeline) und raeumt restlos auf. Uebersprungen, wenn
  Upload-Ziele oder E-Mail-Notify aktiv sind, damit kein Testmuell zum
  Kunden geht. Abschaltbar mit --no-smoke-test.
- Doku korrigiert: pdfa_level = "" allein ist keine Entwarnung, die haengt
  an der ocrmypdf-Version.

152 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 22:55:47 +02:00
parent 3e24aa2ecd
commit aa9918adba
15 changed files with 1044 additions and 86 deletions
+42 -10
View File
@@ -249,19 +249,47 @@ Container-Instanzen reißt.
## Ghostscript-Bug auf Debian 12
Ghostscript 10.0.0 bis einschließlich 10.02.0 — der **Debian-12-Default** —
zerschießt OCR in der Kombination `[ocr].pdfa_level` + `skip_text = true`:
ocrmypdf blockiert komplett.
enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf
verweigert deshalb den Dienst, statt ein kaputtes Ergebnis zu liefern.
Deshalb:
### Wann genau ocrmypdf abbricht
- `pdfa_level = ""` ist der sichere Default (kein PDF/A-Output).
- Der Preflight beim Dienststart bricht mit **Exit 2** ab, wenn `pdfa_level`
gesetzt **und** die installierte Ghostscript-Version betroffen ist.
- `--check-config` meldet ein gesetztes `pdfa_level` als Warnung (siehe
Die Bedingung steht in `ocrmypdf/builtin_plugins/ghostscript.py`
(`check_options()`) und hängt an **zwei** Dingen — an der Config *und* an der
ocrmypdf-Version:
| ocrmypdf | Die Prüfung greift bei | Heißt für uns |
|----------|------------------------|---------------|
| **≤ 16.x** | `skip_text` oder `redo_ocr` — **unabhängig vom `output_type`** | Auch ohne PDF/A scheitert **jede** Datei, denn `skip_text = true` ist unser Default |
| **≥ 17.0** | dasselbe, aber nur innerhalb von `if options.output_type.startswith('pdfa')` | Ohne PDF/A wird Ghostscript gar nicht angefasst — unkritisch |
> ⚠️ **`pdfa_level = ""` allein ist damit kein Schutz.** Die Entwarnung gilt nur
> zusammen mit **ocrmypdf ≥ 17**. Das war der Fehler in 0.6.0: der Pin stand auf
> `ocrmypdf==16.13.0`, und auf Debian 12 landete daraufhin jede PDF in `error/` —
> bei grünem `systemctl status` und „Preflight ok".
> `requirements.txt` pinnt deshalb 17.x.
### Was das Tool dagegen tut
- `pdfa_level = ""` ist der Default (kein PDF/A-Output).
- `requirements.txt` pinnt **ocrmypdf 17.x**. Ein Downgrade auf 16.x macht jede
Debian-12-Instanz unbrauchbar; `update.sh` weist Versionssprünge der
gepinnten Pakete deshalb ausdrücklich aus.
- Der Preflight bricht beim **Dienststart** mit **Exit 2** ab, wenn die
Ghostscript-Version betroffen ist **und** die Kombination aus `skip_text`,
`pdfa_level` und installierter ocrmypdf-Version tatsächlich zum Abbruch
führen würde. Der Dienst startet dann gar nicht erst, statt jede Datei
einzeln scheitern zu lassen.
- `--check-config` meldet denselben Zustand als **Fehler (Exit 2)** und zeigt
ocrmypdf- und Ghostscript-Version an (siehe
[UPDATE.md](UPDATE.md#config-prüfung-per---check-config)).
- Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh` schiebt nach dem Update
eine Test-PDF durch die echte Pipeline — er hätte den Ausfall sofort gezeigt.
Der Installer erkennt betroffene Versionen und bietet auf Debian 12
bookworm-backports an. Manuell:
### Abhilfe
**Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene
Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
```bash
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \
@@ -272,6 +300,10 @@ sudo apt update && sudo apt install -t bookworm-backports ghostscript
Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet
werden.
**Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt
statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei
PDFs, die bereits eine Textebene haben.
---
## Instanz manuell löschen
@@ -325,7 +357,7 @@ startet nicht.
| `jobs` | `4` | Threads, die ocrmypdf innerhalb **einer** PDF nutzt |
| `skip_text` | `true` | Seiten, die schon Text haben, nicht neu OCRen |
| `oversample` | `300` | Auflösung für gerasterte Seiten |
| `pdfa_level` | `""` | `"1"`, `"2"`, `"3"` oder leer für reines PDF — leer wegen des [Ghostscript-Bugs](#ghostscript-bug-auf-debian-12) |
| `pdfa_level` | `""` | `"1"`, `"2"`, `"3"` oder leer für reines PDF — leer wegen des [Ghostscript-Bugs](#ghostscript-bug-auf-debian-12). Achtung: leer allein schützt nur zusammen mit ocrmypdf ≥ 17 |
| `deskew` | `true` | schiefe Scans begradigen |
| `clean` | `false` | Hintergrund säubern (unpaper) |
| `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden |
+26 -9
View File
@@ -191,28 +191,45 @@ Upgrade entfernt. Hintergrund:
Die Python-Abhängigkeiten sind **bewusst fest gepinnt**:
```
ocrmypdf==16.13.0
ocrmypdf==17.4.1
watchdog==6.0.0
requests==2.33.1
paramiko==4.0.0
```
Ohne Pins würde ein `pip install --upgrade` bei jedem Update ungefragt eine neue
Major-Version ziehen — ein Sprung von ocrmypdf **16 auf 17** reißt sonst alle
Instanzen auf einmal, und zwar im Moment des Updates, nicht zu einem Zeitpunkt,
den man sich ausgesucht hat.
Major-Version ziehen — der nächste Sprung wäre ocrmypdf **17 auf 18**, und der
reißt sonst alle Instanzen auf einmal, und zwar im Moment des Updates, nicht zu
einem Zeitpunkt, den man sich ausgesucht hat.
> ⚠️ **ocrmypdf darf nicht unter 17 fallen.** Bis einschließlich 16.x prüft
> ocrmypdf die Ghostscript-Version auch dann, wenn gar kein PDF/A erzeugt wird —
> auf Debian 12 (Ghostscript 10.0.0) scheitert damit **jede** PDF, weil
> `skip_text = true` unser Default ist. Genau das war der Ausfall in 0.6.0.
> Hintergrund: [INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
Die aktuellen Pins sind gegen Python 3.11 (Debian 12) und 3.13 (Debian 13)
geprüft; für beide gibt es fertige Wheels, es wird nichts kompiliert.
geprüft; für beide gibt es fertige Wheels, es wird nichts kompiliert. Das gilt
auch für die Abhängigkeiten, die ocrmypdf 17 zusätzlich mitbringt (`pydantic`,
`pypdfium2`, `fpdf2`, `uharfbuzz`).
**Beim Anheben:**
1. **Testmaschine benutzen** — nie direkt auf dem produktiven Hotfolder.
2. Dort `update.sh --rebuild-venv` fahren, damit die Pakete wirklich frisch
2. Prüfen, dass es für die neue Version auf **beiden** Python-Versionen fertige
Wheels gibt, sonst wird auf dem Zielsystem kompiliert:
```bash
pip install --dry-run --only-binary=:all: --python-version 3.11 \
--target /tmp/wheelcheck ocrmypdf==<version>
pip install --dry-run --only-binary=:all: --python-version 3.13 \
--target /tmp/wheelcheck ocrmypdf==<version>
```
3. Dort `update.sh --rebuild-venv` fahren, damit die Pakete wirklich frisch
aufgelöst werden.
3. `pytest` muss grün bleiben (135 Tests).
4. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
fällt dort also nicht auf.
4. `pytest` muss grün bleiben (152 Tests).
5. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
fällt dort also nicht auf. Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh`
macht genau das automatisch.
5. Erst dann committen und auf die produktiven Systeme geben.
Der ocrmypdf-Sprung 16 → 17 ist ein **Major-Sprung** und gehört in einen eigenen
+109 -9
View File
@@ -19,8 +19,9 @@ sudo ./update.sh
```
```
sudo ./update.sh --help # Optionen anzeigen
sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade)
sudo ./update.sh --help # Optionen anzeigen
sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade)
sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
```
`update.sh` muss aus dem Repo laufen. Findet es sich nicht selbst im Repo, liest
@@ -37,12 +38,13 @@ muss also liegen bleiben**, das Tool kopiert daraus.
| 4 | **Instanzen stoppen** | nur die, die vorher liefen oder kaputt waren |
| 5 | **Backup** | [Inhalt und Ort](#backup) |
| 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, `requirements.txt`, `VERSION`, `config.example.toml`, `.repo_path` |
| 7 | **Dependencies** | `pip install --upgrade -r requirements.txt` — oder venv-Neubau, falls nötig |
| 7 | **Dependencies** | `pip install --upgrade -r requirements.txt` — oder venv-Neubau, falls nötig. Versionssprünge der gepinnten Pakete werden [benannt](#versionssprünge-der-kernabhängigkeiten) |
| 8 | **systemd-Units** | Template-Unit aus dem Repo, LXC-Drop-in nachziehen, `daemon-reload` |
| 9 | **Berechtigungen** | Code gehört dem primären User (i.d.R. `pdfocr`) |
| 10 | **Configs prüfen** | `--check-config` je Instanz, siehe [unten](#config-prüfung-per---check-config) |
| 11 | **Instanzen starten + verifizieren** | mit Wartezeit und Crash-Loop-Erkennung |
| 12 | **Zusammenfassung** | Soll gegen Ist |
| 12 | **Rauchtest** | eine Test-PDF durch die echte Pipeline, siehe [unten](#rauchtest) |
| 13 | **Zusammenfassung** | Soll gegen Ist |
Ab Schritt 2 gilt: **System-Pakete werden auch beim Update nachgezogen**, nicht
nur bei der Installation. Bereits installierte Tesseract-Sprachpakete bleiben
@@ -187,6 +189,95 @@ Ein abgebrochenes Update lässt also keinen Hotfolder stumm gestoppt zurück.
---
## Versionssprünge der Kernabhängigkeiten
`update.sh` misst die Versionen der in `requirements.txt` gepinnten Pakete
**vor** und **nach** `pip install` und benennt jede Änderung:
```
[WARN] DOWNGRADE: ocrmypdf: 17.4.1 -> 16.13.0
[INFO] Upgrade: watchdog: 5.0.0 -> 6.0.0
[INFO] Neu: requests 2.33.1
```
Beides steht auch noch einmal in der Abschluss-Zusammenfassung, weil es im
Fließtext zwischen den pip-Ausgaben untergeht.
**Downgrades sind der interessante Fall.** Sie entstehen, wenn ein Pin in
`requirements.txt` gesenkt wurde. Genau so ist der Totalausfall in 0.6.0
entstanden: `ocrmypdf` wurde von 17.4.1 auf 16.13.0 heruntergezogen, das Update
lief mit Exit 0 durch, der Dienst meldete `active` — und jede PDF landete in
`error/`. Sichtbar war davon nichts außer `[INFO] Dependencies ok ✓`.
War ein Downgrade nicht beabsichtigt: Pin korrigieren und
`sudo ./update.sh --rebuild-venv` erneut fahren.
---
## Rauchtest
Nach dem Start schiebt `update.sh` pro Instanz eine winzige Test-PDF durch die
**echte** Pipeline und prüft, ob sie in `outgoing/` ankommt.
Das ist der Schritt, den 0.6.0 gefehlt hat: `systemctl` sagt `active`,
`--check-config` sagt `Preflight ok` — und trotzdem scheitert jede einzelne
Datei. Ein laufender Dienst ist eben kein Beleg dafür, dass er etwas
verarbeitet.
- Die Test-PDF steckt als base64 **im Skript** (694 Bytes, eine Seite). Es
braucht also kein Pillow, kein `gs` und kein `convert` auf dem Zielsystem.
- Der Dateiname ist eindeutig (`__smoketest_update_<zeitstempel>_<pid>.pdf`) und
kann mit keiner Kundendatei kollidieren.
- Wartezeit: `SMOKE_TIMEOUT` Sekunden (Standard 90), danach gilt der Test als
durchgefallen. Das Skript hängt nicht.
### Aufräumen
Test-PDF **und** Ergebnis werden danach restlos entfernt — in jedem Ausgang,
auch bei Fehlschlag und Timeout. Angefasst werden dabei ausschließlich Dateien
mit dem Testnamen, in `incoming/`, `working/` (inkl. `__ocr_`-Zwischendatei),
`outgoing/`, `error/` und im Archivverzeichnis. In keinem dieser Verzeichnisse
bleibt etwas vom Test liegen.
### Wann der Rauchtest übersprungen wird
Hat eine Instanz ein aktives Ziel, würde die Testdatei **nach außen** gehen —
im Zweifel zum Kunden. Solche Instanzen werden mit klarer Meldung übersprungen:
| Übersprungen bei | Grund |
|------------------|-------|
| `[upload.nextcloud].enabled = true` | Testdatei landete in der Nextcloud |
| `[upload.sftp].enabled = true` | Testdatei landete auf dem SFTP-Ziel |
| `[upload.folder]` mit gesetztem `target` | Zielordner liegt außerhalb von `outgoing/`, oft eine Kundenfreigabe |
| `[notify.email].enabled = true` | löst eine Benachrichtigungs-Mail aus |
`[upload.folder]` **ohne** `target` schreibt nach `outgoing/` und ist damit
harmlos — dort läuft der Test normal.
Für diese Instanzen bleibt der manuelle Weg: eine eigene PDF in `incoming/`
legen und `journalctl -u pdf-ocr-hotfolder@<instanz> -f` mitlesen.
### Wenn der Rauchtest fehlschlägt
Der Rauchtest setzt den **Exit-Code** des Updates auf 1 und nennt den
Journal-Befehl:
```
[ERROR] RAUCHTEST FEHLGESCHLAGEN: kunde1
[ERROR] Diese Instanzen laufen, verarbeiten aber keine PDFs.
[ERROR] Es wurde NICHT zurueckgerollt. Journal ansehen:
[ERROR] journalctl -u pdf-ocr-hotfolder@kunde1.service -n 80 --no-pager
```
**Es wird nichts automatisch zurückgerollt.** Der Code ist getauscht, die
Instanzen laufen. Rollback nur von Hand und nur bewusst — siehe
[Rollback](#rollback).
Abschalten: `sudo ./update.sh --no-smoke-test`. Dann fällt ein Totalausfall
erst der ersten echten Kundendatei auf.
---
## Config-Prüfung per `--check-config`
Nach dem Code-Update und vor dem Start prüft `update.sh` jede Instanz-Config mit
@@ -199,8 +290,11 @@ dem neuen Code:
`--check-config` verarbeitet nichts, es hat sogar Vorrang vor `--once`. Es lädt
die Config, zeigt die vier Pfade (inkl. Hinweis, falls ein Verzeichnis noch
fehlt), Sprachen, Seiten-Timeout und PDF/A-Level, fährt den Preflight
(`tesseract`, `gs`, Ghostscript-Version bei gesetztem `pdfa_level`) und
fehlt), Sprachen, Seiten-Timeout, PDF/A-Level, `skip_text` sowie die
installierte ocrmypdf- und Ghostscript-Version, fährt den Preflight
(`tesseract`, `gs`, und die Ghostscript-Version gegen die tatsächliche
ocrmypdf-Bedingung — siehe [Rauchtest](#rauchtest) und
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12)) und
validiert die `[output]`-Sektion.
| Exit | Bedeutung | Was der Admin tun soll |
@@ -257,9 +351,15 @@ ocrmypdf `tesseract_timeout=0` als "OCR komplett überspringen" interpretiert.
`pdfa_level` gehört auf `""` (reines PDF, kein PDF/A). Ist es gesetzt, warnt
`--check-config`, weil Ghostscript 10.0.0–10.02.0 — der Debian-12-Default — in
Kombination mit `skip_text` das OCR blockiert. Der Preflight bricht in dem Fall
mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch. Hintergrund:
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
Kombination mit `skip_text` von ocrmypdf abgelehnt wird. Der Preflight bricht in
dem Fall mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch.
> **`pdfa_level = ""` allein ist kein Schutz gegen den Ghostscript-Bug.** Das
> gilt erst zusammen mit **ocrmypdf ≥ 17**. Bis ocrmypdf 16.x läuft dieselbe
> Prüfung auch ohne PDF/A, und dann scheitert mit `skip_text = true` jede
> einzelne Datei. Die Entwarnung hängt also an der ocrmypdf-Version.
> `requirements.txt` pinnt darum 17.x, und der Preflight prüft beides zusammen.
> Hintergrund: [INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
### Unbekannte Keys