From 08e40f96e8050530b5585160b949816159574271 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 25 May 2026 01:45:45 -0700 Subject: [PATCH] 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. --- src/ocrmypdf/_exec/tesseract.py | 29 +++++++++++++++++++++++++++++ tests/test_tesseract.py | 8 +++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/ocrmypdf/_exec/tesseract.py b/src/ocrmypdf/_exec/tesseract.py index 246ca4eb..861f2d50 100644 --- a/src/ocrmypdf/_exec/tesseract.py +++ b/src/ocrmypdf/_exec/tesseract.py @@ -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 " 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) diff --git a/tests/test_tesseract.py b/tests/test_tesseract.py index 81c42c3f..c7cc2bb3 100644 --- a/tests/test_tesseract.py +++ b/tests/test_tesseract.py @@ -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']: