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>
19 KiB
Installation
Erstinstallation und Anlage von Hotfolder-Instanzen mit install.sh.
Verwandte Dokumente: README · Update · Debian-Major-Upgrade
Voraussetzungen
| Punkt | Anforderung |
|---|---|
| Betriebssystem | Debian 12 (bookworm) oder Debian 13 — systemd wird vorausgesetzt |
| Python | 3.11+ (wegen tomllib aus der stdlib); kommt aus der Distribution |
| Rechte | root (sudo ./install.sh) |
| Netz | apt-Zugriff für die System-Pakete, PyPI-Zugriff für die venv |
| Repo | muss dauerhaft liegen bleiben — update.sh kopiert daraus (s. UPDATE.md) |
Die System-Pakete installiert der Installer selbst. Die Liste steht als
einzige Quelle in install.sh (Funktion pdf_ocr_apt_packages(), zwischen den
Marken # --- BEGIN apt-packages / # --- END apt-packages) und wird von
update.sh von dort ausgelesen:
python3 python3-venv python3-pip
tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng
ghostscript qpdf unpaper pngquant icc-profiles-free
ca-certificates curl
Weitere Tesseract-Sprachpakete installiert der Installer bei Bedarf pro Instanz nach (siehe OCR-Sprachen).
Die Python-Abhängigkeiten stehen fest gepinnt in requirements.txt
(ocrmypdf, watchdog, requests, paramiko). Warum das so ist und wie man die Pins
anhebt: OS-UPGRADE.md.
Installation
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh
install.sh ist Installer und Instanz-Manager in einem und idempotent —
jeder weitere Aufruf überspringt, was schon steht.
Basis-Install vs. Instanz-Anlage
Der Installer unterscheidet zwei Ebenen:
| Ebene | Wann | Was passiert |
|---|---|---|
| Basis-Install | einmalig; erkannt an venv + Template-Unit |
System-Pakete, Ghostscript-Check, Container-Erkennung, Default-User pdfocr, Code nach /opt/pdf-ocr-hotfolder/, venv, systemd-Template-Unit |
| Instanz-Anlage | bei jedem Lauf, beliebig oft | Abfragen pro Instanz, Datenverzeichnisse, <instanz>.toml, optionales User-Drop-in, enable --now |
Ist die Basis-Installation vorhanden, aber die venv passt nicht mehr zum
System-Python (typisch nach einem Distributions-Upgrade), läuft der Basis-Install
zur Reparatur erneut: die alte venv wird nach venv.old-<timestamp>
weggesichert und neu gebaut. Für den geplanten Weg über ein Debian-Major-Upgrade
ist aber update.sh --rebuild-venv gedacht, siehe OS-UPGRADE.md.
Beim Erstlauf ist mindestens eine Instanz Pflicht. Danach fragt der
Installer in der Schleife Weitere Instanz anlegen? [j/N]:.
Die Abfragen pro Instanz
create_instance() stellt fünf Fragen. Alle Antworten gelten nur für diese
Instanz — nichts davon ist global.
1. Instanz-Name
Instanz-Name (nur a-z, 0-9, -):
Muster ^[a-z0-9][a-z0-9-]*$. Der Name wird zum Unit-Suffix
(pdf-ocr-hotfolder@<name>.service) und zum Config-Dateinamen
(/etc/pdf-ocr-hotfolder/<name>.toml). Existiert die Config schon, bricht die
Anlage ab — ein versehentliches Überschreiben gibt es nicht.
2. Basis-Pfad für die Daten
Basis-Pfad für Daten [/var/lib/pdf-ocr-hotfolder/<name>]:
Darunter entstehen incoming/, outgoing/, working/, error/ und werden auf
den Service-User gechownt.
3. Service-User
Service-User [pdfocr]:
- Existiert der User (lokal oder als AD-User via SSSD/Winbind), wird er
übernommen; die primäre Gruppe ermittelt der Installer per
id -gn. - Existiert er nicht, bietet der Installer an, ihn lokal als System-User anzulegen. Wird das abgelehnt, bricht die Instanz-Anlage ab — der User muss dann erst über AD/SSSD bereitstehen.
- Ist der gewählte User nicht
pdfocr, legt der Installer das Drop-in/etc/systemd/system/pdf-ocr-hotfolder@<instanz>.service.d/user.confmitUser=/Group=an.
Bei AD-Usern mit lokaler UID werden die Datei-Berechtigungen über die UID gesetzt — das läuft transparent.
4. OCR-Sprachen
Tesseract-Sprachen [deu+eng]:
Die Sprachen werden bewusst je Instanz abgefragt: ein Hotfolder
buchhaltung sieht nur deutsche Belege, ein Hotfolder export
internationale Korrespondenz.
# /etc/pdf-ocr-hotfolder/buchhaltung.toml
languages = "deu"
# /etc/pdf-ocr-hotfolder/export.toml
languages = "deu+eng+fra"
Die Liste so eng wie möglich halten. Jede zusätzliche Sprache kostet
Laufzeit und Erkennungsqualität: Tesseract wägt mehr Modelle gegeneinander ab
und verwechselt dabei Wörter, die in einer Sprache eindeutig wären.
deu+eng+fra auf reinen Deutsch-Scans ist kein Sicherheitsnetz, sondern ein
Rückschritt.
Was der Installer damit macht:
- Format prüfen — Sprachcodes mit
+verbunden (^[a-z]{3}(_[A-Za-z]+)?(\+…)*$), alsodeu,deu+eng,chi_sim+eng. Bei Unsinn wird erneut gefragt, nicht abgebrochen. - Jeden Code gegen
tesseract --list-langsprüfen. Fehlt eine Sprachdatei, bietet er das passende apt-Paket an:tesseract-ocr-<code>, Unterstrich wird zum Bindestrich (chi_sim→tesseract-ocr-chi-sim). - Lehnt man ab oder scheitert die Installation, warnt er, dass OCR mit dieser Sprache bei jeder Datei scheitern würde, und fragt die Sprachen erneut ab — die fehlende Sprache kann man dann einfach weglassen.
- Ist
tesseractgar nicht aufrufbar, wird die Prüfung übersprungen und die Eingabe unverändert übernommen.
5. Original archivieren?
Original nach erfolgreichem OCR archivieren? [j/N]:
- Nein (Default) →
original_on_success = "delete", das Original wird nach erfolgreichem OCR gelöscht. - Ja →
original_on_success = "archive", danach:
Archiv-Verzeichnis [<basis>/archive]:
Der Pfad muss absolut sein und darf nicht incoming/, outgoing/,
working/ oder error/ der Instanz sein — im Eingang würde das Original sonst
endlos neu aufgegriffen, in den übrigen kollidiert es mit der Verarbeitung. Das
Verzeichnis wird angelegt und auf den Service-User gechownt; innerhalb des
Basis-Pfads erledigt das bestehende chown -R das mit, ein Archiv außerhalb
bekommt ein eigenes.
Was danach passiert
Die Instanz-Config entsteht per sed aus config.example.toml. Substituiert
werden die vier [paths]-Zeilen sowie [ocr].languages,
[output].original_on_success und [output].archive_dir. Anschließend liest
der Installer diese drei Keys aus der erzeugten Datei zurück und vergleicht sie
mit der Eingabe; erst wenn das passt, nennt die Zusammenfassung Sprachen und
Archiv-Verzeichnis. Sonst gibt es eine Warnung mit der Bitte, die Config von
Hand nachzuziehen.
Die Config bekommt chmod 640 und chown root:<service-gruppe>, das
Verzeichnis /etc/pdf-ocr-hotfolder selbst 750 root:pdfocr — in den
Instanz-Configs stehen Klartext-Passwörter für SMTP, Nextcloud und SFTP.
Zum Schluss: daemon-reload und systemctl enable --now pdf-ocr-hotfolder@<instanz>.service.
Test
cp irgendein-scan.pdf /var/lib/pdf-ocr-hotfolder/<instanz>/incoming/
journalctl -u pdf-ocr-hotfolder@<instanz> -f
Nach wenigen Sekunden liegt das OCR-PDF im outgoing/ der Instanz.
Multi-Instanz-Betrieb
Das Tool arbeitet komplett instanzbasiert über die systemd-Template-Unit
pdf-ocr-hotfolder@<name>.service. Jede Instanz hat eigene Config, eigene
Datenverzeichnisse, eigene Unit, optional eigenen Service-User — und eigene
OCR-Sprachen und Original-Behandlung.
sudo ./install.sh # legt z.B. kunde-a, kunde-b, buchhaltung an
systemctl status 'pdf-ocr-hotfolder@*'
journalctl -u pdf-ocr-hotfolder@kunde-a -f
Der Code unter /opt/pdf-ocr-hotfolder/ (inkl. venv) ist für alle Instanzen
gemeinsam. Ein Update trifft damit immer alle Instanzen auf einmal — siehe
UPDATE.md.
Das vollständige Verzeichnis-Layout steht im README.
LXC/Container: Error 226/NAMESPACE
In LXC-Containern schlagen die systemd-Hardening-Optionen der Unit
(PrivateTmp, ProtectSystem, ProtectKernelTunables, …) fehl; systemd
quittiert das mit Error 226/NAMESPACE.
Der Installer erkennt Container über systemd-detect-virt --container und
bietet das Drop-in automatisch an. Manuell:
sudo mkdir -p /etc/systemd/system/pdf-ocr-hotfolder@.service.d/
sudo cp /opt/pdf-ocr-hotfolder/systemd/lxc-compat.conf \
/etc/systemd/system/pdf-ocr-hotfolder@.service.d/
sudo systemctl daemon-reload
sudo systemctl restart 'pdf-ocr-hotfolder@*'
Das Drop-in setzt alle betroffenen Hardening-Schalter auf false. Es liegt auf
Template-Ebene (pdf-ocr-hotfolder@.service.d/) und gilt damit für alle
Instanzen. Ist es installiert, zieht update.sh es bei jedem Update aus dem
Repo nach, damit ein neu ergänzter Hardening-Schalter nicht alle
Container-Instanzen reißt.
Ghostscript-Bug auf Debian 12
Ghostscript 10.0.0 bis einschließlich 10.02.0 — der Debian-12-Default — enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf verweigert deshalb den Dienst, statt ein kaputtes Ergebnis zu liefern.
Wann genau ocrmypdf abbricht
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 aufocrmypdf==16.13.0, und auf Debian 12 landete daraufhin jede PDF inerror/— bei grünemsystemctl statusund „Preflight ok".requirements.txtpinnt deshalb 17.x.
Was das Tool dagegen tut
pdfa_level = ""ist der Default (kein PDF/A-Output).requirements.txtpinnt ocrmypdf 17.x. Ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar;update.shweist 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_levelund 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-configmeldet denselben Zustand als Fehler (Exit 2) und zeigt ocrmypdf- und Ghostscript-Version an (siehe UPDATE.md).- Der Rauchtest in
update.shschiebt nach dem Update eine Test-PDF durch die echte Pipeline — er hätte den Ausfall sofort gezeigt.
Abhilfe
Weg 1 — Ghostscript anheben (empfohlen). Der Installer erkennt betroffene Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \
sudo tee /etc/apt/sources.list.d/bookworm-backports.list
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
Der Installer legt Instanzen an, löscht aber keine. Von Hand:
sudo systemctl disable --now pdf-ocr-hotfolder@<name>
sudo rm /etc/pdf-ocr-hotfolder/<name>.toml
sudo rm -rf /etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d
sudo systemctl daemon-reload
# Datenverzeichnis /var/lib/pdf-ocr-hotfolder/<name> manuell aufräumen
Das Datenverzeichnis bleibt bewusst liegen: dort können noch unverarbeitete
PDFs in incoming/, Fehlerfälle in error/ oder Originale im Archiv liegen.
Erst hineinsehen, dann löschen.
Solange die Config unter /etc/pdf-ocr-hotfolder/ liegt, zählt update.sh die
Instanz weiter mit — auch wenn sie gestoppt ist.
Konfigurationsreferenz
Vollständiges, kommentiertes Beispiel: config.example.toml.
Jede Instanz hat ihre eigene Kopie unter /etc/pdf-ocr-hotfolder/<instanz>.toml.
Unbekannte Keys werden beim Laden ignoriert, aber gemeldet — beim
Dienststart im Log und von --check-config. Ein Tippfehler wie
[ocr].langauges fällt damit auf.
[paths] — Pflicht
| Key | Bedeutung |
|---|---|
incoming |
Eingang, hier schreibt der Scanner hinein |
outgoing |
Ausgang, fertige OCR-PDFs |
working |
Arbeitsverzeichnis während der Verarbeitung |
error |
fehlgeschlagene PDFs |
Fehlt die Sektion oder einer der vier Einträge, gibt es eine deutsche
ConfigError-Meldung mit Datei- und Key-Nennung und Exit 2 — der Dienst
startet nicht.
[ocr]
| Key | Default | Bedeutung |
|---|---|---|
languages |
"deu+eng" |
Tesseract-Sprachen; der Installer fragt sie pro Instanz ab |
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. 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 |
timeout |
300 |
max. Sekunden, die Tesseract pro Seite laufen darf; 0 = kein eigenes Limit |
timeout ist ein Seiten-Timeout, kein Gesamt-Timeout. Der Wert geht als
tesseract_timeout an ocrmypdf; ein Dokument-Timeout kennt ocrmypdf nicht. Wer
noch den alten Default 1800 aus einer Config vor 0.4.0 stehen hat, gibt
Tesseract 30 Minuten je Seite — Richtwert ist 300. Läuft eine Seite in den
Timeout, landet sie ohne Textebene im Ergebnis, die übrigen Seiten laufen
weiter. Ein durchgereichtes 0 würde ocrmypdf dazu bringen, OCR still zu
überspringen, deshalb wird 0 (oder negativ) gar nicht erst übergeben und der
ocrmypdf-Default greift. Siehe auch
Config-Drift.
[output]
| Key | Default | Bedeutung |
|---|---|---|
name_mode |
"prefix" |
prefix → OCR_scan.pdf, suffix → scan_OCR.pdf (vor der Extension), none → unverändert |
name_tag |
"OCR_" |
verbatim eingefügter String; leer wirkt wie none |
original_on_success |
"delete" |
delete oder archive — Installer fragt das ab |
archive_dir |
"" |
absoluter Pfad, Pflicht bei archive; Namenskollision → Zeitstempel-Suffix |
Ein Tippfehler in name_mode oder original_on_success führt beim Start zum
Abbruch mit Exit 2, nicht erst bei der ersten Datei.
[verapdf]
| Key | Default | Bedeutung |
|---|---|---|
enabled |
false |
PDF/A-Validierung per veraPDF-CLI |
binary |
/opt/verapdf/verapdf |
Pfad zum veraPDF-Binary |
flavour |
"1b" |
PDF/A-Flavour |
veraPDF startet eine JVM und ist entsprechend teuer — nur einschalten, wenn die
Validierung wirklich gebraucht wird. Bei FAIL wandert das OCR-Ergebnis nach
error/; das Original folgt original_on_success (bei archive bleibt es also
erhalten).
[upload.folder] / [upload.nextcloud] / [upload.sftp]
Beliebig viele Ziele gleichzeitig aktivierbar. Sind alle aus, bleibt das fertige
PDF einfach in outgoing/ liegen.
| Sektion | Keys |
|---|---|
[upload.folder] |
enabled, target — leer heißt [paths].outgoing, dann No-op |
[upload.nextcloud] |
enabled, url, username, password, remote_path, verify_ssl |
[upload.sftp] |
enabled, host, port, username, key_file, password, remote_path |
Schlägt mindestens ein Ziel fehl, zählt das als Fehler und die Mail geht als
FEHLER raus — das PDF bleibt aber bewusst in outgoing/ liegen, das OCR
selbst war ja erfolgreich.
[notify.email]
| Key | Default | Bedeutung |
|---|---|---|
enabled |
false |
E-Mail-Benachrichtigung an/aus |
smtp_host, smtp_port, smtp_user, smtp_password, use_starttls |
— | SMTP-Zugang |
from_addr |
— | Absender |
to_addrs |
[] |
Empfängerliste |
on |
"errors" |
always | errors | never |
[logging]
| Key | Default | Bedeutung |
|---|---|---|
level |
"INFO" |
DEBUG | INFO | WARNING | ERROR |
Der Dienst schreibt kein eigenes Logfile — alles geht nach stdout und damit ins journal.
Troubleshooting
Tesseract findet die Sprache nicht
sudo apt install tesseract-ocr-deu tesseract-ocr-eng
Danach [ocr].languages prüfen. Der Installer nimmt einem das beim Anlegen
einer Instanz ab, ein nachträglich in die Config geschriebener Sprachcode wird
aber nicht geprüft — --check-config zeigt die eingestellten Sprachen an.
"PriorOcrFoundError"
ocrmypdf erkennt bereits vorhandenen OCR-Text. skip_text = true in der Config
setzen (Default).
Berechtigungsprobleme bei AD-User
Der Service-User braucht rw auf alle vier Verzeichnisse der Instanz (und auf das Archiv, falls konfiguriert):
sudo chown -R DOMAIN\\scanuser:DOMAIN\\scangroup /var/lib/pdf-ocr-hotfolder/<instanz>
veraPDF-Validierung schlägt immer fehl
[verapdf].binary prüfen. Wenn die Validierung nicht zwingend gebraucht wird:
enabled = false.
Dienst startet nicht (Exit 2)
Exit 2 heißt immer: Config oder Preflight. Die Ursache steht im journal und ausführlicher in:
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
Dienst startet nicht (203/EXEC)
Der Interpreter der venv ist weg — fast immer nach einem Distributions-Upgrade. Siehe OS-UPGRADE.md.
Manueller Lauf (One-Shot)
Bestehende PDFs einer Instanz einmalig verarbeiten und beenden — greift auch
Dateien auf, die in working/ liegen geblieben sind:
sudo -u pdfocr /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
--config /etc/pdf-ocr-hotfolder/kunde-a.toml --once
Exit-Code: 0 = alles verarbeitet (auch "nichts da"), 1 = mindestens eine
Datei fehlgeschlagen, 2 = Config- oder Preflight-Fehler.