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:
@@ -1,3 +1,3 @@
|
||||
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.6.1"
|
||||
|
||||
@@ -14,6 +14,8 @@ from .service import (
|
||||
PreflightError,
|
||||
check_output_config,
|
||||
check_preflight,
|
||||
detect_ghostscript_version,
|
||||
detect_ocrmypdf_version,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -70,11 +72,15 @@ def check_config(cfg_path: Path) -> int:
|
||||
print(f" OCR-Sprachen = {cfg.ocr.languages}")
|
||||
print(f" Seiten-Timeout= {cfg.ocr.timeout} s")
|
||||
print(f" PDF/A-Level = {cfg.ocr.pdfa_level or '(aus)'}")
|
||||
print(f" skip_text = {'an' if cfg.ocr.skip_text else 'aus'}")
|
||||
print(f" ocrmypdf = {detect_ocrmypdf_version() or '(nicht installiert)'}")
|
||||
print(f" Ghostscript = {detect_ghostscript_version() or '(nicht gefunden)'}")
|
||||
|
||||
errors: list[str] = []
|
||||
try:
|
||||
check_preflight(cfg.ocr.pdfa_level)
|
||||
print(" Preflight ok (tesseract, gs vorhanden).")
|
||||
check_preflight(cfg.ocr.pdfa_level, cfg.ocr.skip_text)
|
||||
print(" Preflight ok (tesseract, gs vorhanden, Ghostscript-Version "
|
||||
"passt zu ocrmypdf + [ocr]-Einstellungen).")
|
||||
except PreflightError as e:
|
||||
errors.append(str(e))
|
||||
try:
|
||||
|
||||
@@ -25,8 +25,13 @@ class OcrConfig:
|
||||
jobs: int = 4
|
||||
skip_text: bool = True
|
||||
oversample: int = 300
|
||||
# Default bewusst leer: pdfa_level + skip_text zerschießt OCR mit
|
||||
# Ghostscript 10.0.0-10.02.0 (Debian-12-Default), siehe Issue #3
|
||||
# Default bewusst leer: mit Ghostscript 10.0.0-10.02.0 (Debian-12-Default)
|
||||
# lehnt ocrmypdf die Kombination pdfa_level + skip_text ab (Issue #3).
|
||||
# ACHTUNG, kein Freibrief: pdfa_level = "" allein schützt nur zusammen mit
|
||||
# ocrmypdf >= 17. Bis 16.x läuft dieselbe Prüfung auch ohne PDF/A und
|
||||
# blockiert dann JEDE Datei — deshalb pinnt requirements.txt 17.x und der
|
||||
# Preflight prüft die installierte ocrmypdf-Version mit (siehe
|
||||
# service._gs_block_reason).
|
||||
pdfa_level: str = ""
|
||||
deskew: bool = True
|
||||
clean: bool = False
|
||||
@@ -251,9 +256,10 @@ def legacy_warnings(cfg: Config) -> list[str]:
|
||||
out.append(
|
||||
f"[ocr].pdfa_level = {cfg.ocr.pdfa_level!r}: PDF/A-Ausgabe ist "
|
||||
"aktiv. Ghostscript 10.0.0-10.02.0 (Debian-12-Default) hat einen "
|
||||
"Bug, der zusammen mit skip_text das OCR blockiert (Issue #3). Der "
|
||||
"Preflight bricht ab, falls die installierte Ghostscript-Version "
|
||||
"betroffen ist; ab 10.02.1 ist alles in Ordnung."
|
||||
"Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt "
|
||||
"(Issue #3). Der Preflight bricht ab, falls die installierte "
|
||||
"Ghostscript-Version betroffen ist; ab 10.02.1 ist alles in "
|
||||
"Ordnung."
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
+113
-15
@@ -35,11 +35,20 @@ class PreflightError(RuntimeError):
|
||||
# Pflicht-Binaries für ocrmypdf
|
||||
_REQUIRED_BINARIES = ("tesseract", "gs")
|
||||
|
||||
# Ghostscript-Versionen mit bekanntem PDF/A+skip_text Bug (Issue #3):
|
||||
# Ghostscript-Versionen mit bekanntem Bug (Issue #3):
|
||||
# 10.0.0 .. 10.02.0 (inklusive). Ab 10.02.1 wieder nutzbar.
|
||||
_GS_BROKEN_MIN = (10, 0, 0)
|
||||
_GS_BROKEN_MAX = (10, 2, 0)
|
||||
|
||||
# Ab dieser ocrmypdf-Major steht die Ghostscript-Pruefung hinter
|
||||
# `if options.output_type.startswith('pdfa')` — mit pdfa_level = "" wird
|
||||
# Ghostscript gar nicht angefasst und die Pruefung greift nicht.
|
||||
# Darunter (16.x und aelter) laeuft sie BEDINGUNGSLOS, also auch bei
|
||||
# output_type="pdf": dort reicht skip_text=true, um auf Debian 12 jede
|
||||
# einzelne PDF scheitern zu lassen. Quelle jeweils
|
||||
# ocrmypdf/builtin_plugins/ghostscript.py::check_options().
|
||||
_OCRMYPDF_GS_GUARD_MAJOR = 17
|
||||
|
||||
|
||||
def _parse_version(text: str) -> tuple[int, ...] | None:
|
||||
"""Extrahiert die erste X.Y[.Z] Version aus einem String."""
|
||||
@@ -50,7 +59,7 @@ def _parse_version(text: str) -> tuple[int, ...] | None:
|
||||
|
||||
|
||||
def is_ghostscript_broken(version: str | None) -> bool:
|
||||
"""Prüft, ob eine Ghostscript-Version vom PDF/A+skip_text Bug betroffen ist.
|
||||
"""Prüft, ob eine Ghostscript-Version vom bekannten Bug betroffen ist.
|
||||
|
||||
Betrifft 10.0.0 bis einschließlich 10.02.0. Ab 10.02.1 wieder sicher.
|
||||
"""
|
||||
@@ -79,6 +88,36 @@ def detect_ghostscript_version() -> str | None:
|
||||
return result.stdout.strip() or None
|
||||
|
||||
|
||||
def detect_ocrmypdf_version() -> str | None:
|
||||
"""Liest die installierte ocrmypdf-Version aus den Paket-Metadaten.
|
||||
|
||||
Bewusst über `importlib.metadata` statt über einen Import: das ist
|
||||
billiger und funktioniert auch in den Tests, in denen ocrmypdf gar nicht
|
||||
installiert ist (dann None).
|
||||
"""
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
return version("ocrmypdf")
|
||||
except Exception: # noqa: BLE001 - fehlende Metadaten dürfen nichts umwerfen
|
||||
return None
|
||||
|
||||
|
||||
def ocrmypdf_checks_gs_always(version: str | None) -> bool:
|
||||
"""True, wenn ocrmypdf die Ghostscript-Pruefung unabhaengig vom output_type fährt.
|
||||
|
||||
Das ist bei 16.x und aelter der Fall (siehe `_OCRMYPDF_GS_GUARD_MAJOR`).
|
||||
Ist die Version unbekannt, wird `False` angenommen: der Pin in
|
||||
requirements.txt steht auf 17.x, und ein Fehlalarm, der den Dienst nicht
|
||||
starten laesst, waere schlimmer als die fehlende Warnung.
|
||||
"""
|
||||
if not version:
|
||||
return False
|
||||
parsed = _parse_version(version)
|
||||
if parsed is None:
|
||||
return False
|
||||
return parsed[0] < _OCRMYPDF_GS_GUARD_MAJOR
|
||||
|
||||
|
||||
def check_output_config(mode: str, archive_dir: str,
|
||||
name_mode: str = "prefix") -> None:
|
||||
"""Validiert die [output]-Section. Wirft PreflightError bei Problemen."""
|
||||
@@ -101,12 +140,13 @@ def check_output_config(mode: str, archive_dir: str,
|
||||
)
|
||||
|
||||
|
||||
def check_preflight(pdfa_level: str = "") -> None:
|
||||
def check_preflight(pdfa_level: str = "", skip_text: bool = False) -> None:
|
||||
"""Prüft externe Abhängigkeiten.
|
||||
|
||||
- Tesseract und Ghostscript müssen im PATH sein
|
||||
- Bei gesetztem pdfa_level wird die Ghostscript-Version gegen den
|
||||
bekannten 10.0.0–10.02.0 Bug geprüft
|
||||
- Die Ghostscript-Version wird gegen den bekannten 10.0.0–10.02.0 Bug
|
||||
geprüft, und zwar genau unter der Bedingung, unter der ocrmypdf selbst
|
||||
abbricht (siehe `_gs_block_reason`).
|
||||
|
||||
Wirft PreflightError bei fehlenden Binaries oder unsicherem Ghostscript.
|
||||
"""
|
||||
@@ -117,15 +157,73 @@ def check_preflight(pdfa_level: str = "") -> None:
|
||||
+ ". Bitte installieren: sudo apt install tesseract-ocr ghostscript"
|
||||
)
|
||||
|
||||
reason = _gs_block_reason(pdfa_level, skip_text)
|
||||
if reason:
|
||||
raise PreflightError(reason)
|
||||
|
||||
|
||||
def _gs_block_reason(pdfa_level: str, skip_text: bool) -> str | None:
|
||||
"""Liefert die Fehlermeldung, wenn ocrmypdf mit diesem Ghostscript abbricht.
|
||||
|
||||
Abgebildet wird die reale Bedingung aus
|
||||
`ocrmypdf/builtin_plugins/ghostscript.py::check_options()`:
|
||||
|
||||
betroffene GS-Version UND (skip_text ODER redo_ocr)
|
||||
UND (PDF/A-Ausgabe ODER ocrmypdf < 17)
|
||||
|
||||
Die letzte Klammer ist der Teil, der v0.6.0 durchrutschen ließ: bis
|
||||
einschließlich ocrmypdf 16.x steht die Pruefung ohne jeden Guard in
|
||||
`check_options()` und schlaegt deshalb auch bei `output_type="pdf"` zu.
|
||||
Ab 17.0.0 umschliesst sie ein
|
||||
`if options.output_type.startswith('pdfa'):` — ohne PDF/A wird
|
||||
Ghostscript nicht angefasst.
|
||||
|
||||
`redo_ocr` kennt unsere Config nicht (es gibt keinen entsprechenden Key in
|
||||
`OcrConfig`), deshalb steht es hier bewusst nicht in der Bedingung.
|
||||
|
||||
Returns:
|
||||
Fehlermeldung oder None, wenn die Kombination unkritisch ist.
|
||||
"""
|
||||
if not skip_text:
|
||||
# Weder skip_text noch redo_ocr — ocrmypdf fasst den Pfad nicht an.
|
||||
return None
|
||||
|
||||
gs_version = detect_ghostscript_version()
|
||||
if not is_ghostscript_broken(gs_version):
|
||||
return None
|
||||
|
||||
ocrmypdf_version = detect_ocrmypdf_version()
|
||||
always = ocrmypdf_checks_gs_always(ocrmypdf_version)
|
||||
if not pdfa_level and not always:
|
||||
return None
|
||||
|
||||
if pdfa_level:
|
||||
gs_version = detect_ghostscript_version()
|
||||
if is_ghostscript_broken(gs_version):
|
||||
raise PreflightError(
|
||||
f"Ghostscript {gs_version} ist mit pdfa_level='{pdfa_level}' nicht "
|
||||
"kompatibel (bekannter Bug in 10.0.0–10.02.0). "
|
||||
"Entweder ghostscript auf >=10.02.1 upgraden (z.B. via bookworm-backports) "
|
||||
"oder in der Config [ocr].pdfa_level = \"\" setzen."
|
||||
)
|
||||
ursache = (
|
||||
f"[ocr].pdfa_level = {pdfa_level!r} (PDF/A-Ausgabe) zusammen mit "
|
||||
"[ocr].skip_text = true"
|
||||
)
|
||||
else:
|
||||
ursache = (
|
||||
f"[ocr].skip_text = true und ocrmypdf {ocrmypdf_version} — bis "
|
||||
f"einschließlich {_OCRMYPDF_GS_GUARD_MAJOR - 1}.x prüft ocrmypdf "
|
||||
"Ghostscript auch dann, wenn gar kein PDF/A erzeugt wird. Jede "
|
||||
"einzelne PDF würde in error/ landen"
|
||||
)
|
||||
|
||||
return (
|
||||
f"Ghostscript {gs_version} ist von einem bekannten Fehler betroffen "
|
||||
"(10.0.0–10.02.0, der Debian-12-Standard) und wird von ocrmypdf "
|
||||
f"abgelehnt: {ursache}. "
|
||||
"Abhilfe — eines von beidem: "
|
||||
"(1) Ghostscript >= 10.02.1 aus bookworm-backports installieren "
|
||||
"(install.sh bietet das an): "
|
||||
"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 — "
|
||||
"oder (2) in der Config [ocr].skip_text = false setzen "
|
||||
"(dann wird vorhandener Text neu erkannt statt übersprungen)"
|
||||
+ (" bzw. [ocr].pdfa_level = \"\"." if pdfa_level else ".")
|
||||
)
|
||||
|
||||
|
||||
def _is_pdf(path: Path) -> bool:
|
||||
@@ -201,7 +299,7 @@ class HotfolderService:
|
||||
# ---- Lifecycle ----
|
||||
|
||||
def run(self) -> None:
|
||||
check_preflight(self.cfg.ocr.pdfa_level)
|
||||
check_preflight(self.cfg.ocr.pdfa_level, self.cfg.ocr.skip_text)
|
||||
check_output_config(self.cfg.output.original_on_success,
|
||||
self.cfg.output.archive_dir,
|
||||
self.cfg.output.name_mode)
|
||||
@@ -228,7 +326,7 @@ class HotfolderService:
|
||||
Returns:
|
||||
Anzahl fehlgeschlagener PDFs (0 = alles ok).
|
||||
"""
|
||||
check_preflight(self.cfg.ocr.pdfa_level)
|
||||
check_preflight(self.cfg.ocr.pdfa_level, self.cfg.ocr.skip_text)
|
||||
check_output_config(self.cfg.output.original_on_success,
|
||||
self.cfg.output.archive_dir,
|
||||
self.cfg.output.name_mode)
|
||||
|
||||
Reference in New Issue
Block a user