Surface Tesseract config errors instead of FileNotFoundError

When Tesseract cannot find its 'hocr' or 'txt' config files in the
tessdata configs/ directory, it prints "read_params_file: Can't open"
warnings, exits 0, and produces no output. OCRmyPDF then crashed with a
confusing FileNotFoundError on the missing hOCR file (issue #1687).

Promote read_params_file warnings to TesseractConfigError with guidance
on the likely cause, and verify the expected output file exists after
Tesseract claims success as defense-in-depth for other silent-failure
modes.
This commit is contained in:
James R. Barlow
2026-05-25 01:45:45 -07:00
parent 3f6feb1dcc
commit 08e40f96e8
2 changed files with 36 additions and 1 deletions
+29
View File
@@ -313,6 +313,23 @@ def tesseract_log_output(stream: bytes) -> None:
tlog.warning(line.strip())
elif 'read_params_file' in line.lower():
tlog.error(line.strip())
# Tesseract emits "read_params_file: Can't open <name>" when it
# cannot locate a config file (e.g. 'hocr', 'txt') in its
# tessdata configs/ directory, then exits 0 without producing
# the requested output. Promote to a hard error so the user
# sees the root cause instead of a downstream FileNotFoundError.
if "Can't open" in line:
missing = line.split("Can't open", 1)[1].strip()
else:
missing = line.strip()
raise TesseractConfigError(
f"Tesseract cannot open its config file '{missing}'. "
"This usually means Tesseract is installed but its config "
"files are missing from the tessdata configs/ directory. "
"On Debian/Ubuntu, ensure the 'tesseract-ocr' package is "
"fully installed. If you set TESSDATA_PREFIX, verify its "
"configs/ subdirectory contains the required files."
)
else:
tlog.info(line.strip())
@@ -393,6 +410,12 @@ def generate_hocr(
raise SubprocessOutputError() from e
else:
tesseract_log_output(stdout)
if not output_hocr.exists():
raise SubprocessOutputError(
"Tesseract exited successfully but did not produce the "
f"expected hOCR output at {output_hocr}. Tesseract output:\n"
+ (stdout.decode(errors='replace') if stdout else '(empty)')
)
# The sidecar text file will get the suffix .txt; rename it to
# whatever caller wants it named
with suppress(FileNotFoundError):
@@ -461,6 +484,12 @@ def generate_pdf(
stdout = p.stdout
with suppress(FileNotFoundError):
prefix.with_suffix('.txt').replace(output_text)
if not output_pdf.exists():
raise SubprocessOutputError(
"Tesseract exited successfully but did not produce the "
f"expected PDF output at {output_pdf}. Tesseract output:\n"
+ (stdout.decode(errors='replace') if stdout else '(empty)')
)
except TimeoutExpired:
page_timedout(timeout)
use_skip_page(output_pdf, output_text)
+7 -1
View File
@@ -128,7 +128,6 @@ def test_timeout(caplog):
(b'Error in boxClipToRectangle', ''),
(b'an unexpected error', 'an unexpected error'),
(b'a dire warning', 'a dire warning'),
(b'read_params_file something', 'read_params_file'),
(b'an innocent message', 'innocent'),
(b'\x7f\x7f\x80innocent unicode failure', 'innocent'),
],
@@ -148,6 +147,13 @@ def test_tesseract_log_output_raises(caplog):
assert 'not found' in caplog.text
def test_tesseract_log_output_raises_on_missing_config(caplog):
with pytest.raises(tesseract.TesseractConfigError) as excinfo:
tesseract.tesseract_log_output(b"read_params_file: Can't open hocr")
assert 'hocr' in excinfo.value.args[0]
assert 'read_params_file' in caplog.text
def test_blocked_language(resources, no_outpdf):
infile = resources / 'masks.pdf'
for bad_lang in ['osd', 'equ']: