Merge pull request #399 from christianlouis/copilot/fix-ocr-language-data-issue
fix(ocr): ensure Tesseract language data is available for embed_text_layer regardless of active OCR provider
This commit is contained in:
@@ -279,6 +279,12 @@ def ensure_ocr_languages_from_settings() -> dict[str, list[str]]:
|
||||
present. Missing Tesseract tessdata files are downloaded automatically;
|
||||
missing EasyOCR models are downloaded via the library's built-in mechanism.
|
||||
|
||||
Tesseract language data is always ensured when ``ocrmypdf`` is available on
|
||||
the system, regardless of which OCR providers are active. This is required
|
||||
because :func:`~app.utils.ocr_provider.embed_text_layer` uses ``ocrmypdf``
|
||||
(and therefore Tesseract) as a post-processing fallback for **all** OCR
|
||||
providers – not only the ``tesseract`` provider.
|
||||
|
||||
This function is idempotent – calling it multiple times is safe.
|
||||
|
||||
Returns:
|
||||
@@ -297,7 +303,12 @@ def ensure_ocr_languages_from_settings() -> dict[str, list[str]]:
|
||||
providers_raw = getattr(settings, "ocr_providers", None) or "azure"
|
||||
active_providers = {p.strip().lower() for p in providers_raw.split(",") if p.strip()}
|
||||
|
||||
if "tesseract" in active_providers:
|
||||
# Always ensure Tesseract language data when ocrmypdf is on the system PATH.
|
||||
# embed_text_layer() calls ocrmypdf as a text-layer post-processor for every
|
||||
# OCR provider that does not natively produce a searchable PDF (azure,
|
||||
# easyocr, mistral, google_docai, aws_textract). If the tessdata files are
|
||||
# absent, ocrmypdf exits with code 3 and the text layer is silently skipped.
|
||||
if "tesseract" in active_providers or shutil.which("ocrmypdf") is not None:
|
||||
lang_str = getattr(settings, "tesseract_language", None) or "eng"
|
||||
logger.info("Ensuring Tesseract language data for configured languages: %s", lang_str)
|
||||
result["tesseract_missing"] = ensure_tesseract_languages(lang_str)
|
||||
|
||||
@@ -93,6 +93,19 @@ def embed_text_layer(input_pdf_path: str, output_pdf_path: str, *, language: str
|
||||
)
|
||||
return False
|
||||
|
||||
# Ensure Tesseract language data is present before invoking ocrmypdf.
|
||||
# ocrmypdf uses Tesseract internally regardless of which OCR provider is
|
||||
# active, so we must guarantee the tessdata files exist here.
|
||||
from app.utils.ocr_language_manager import ensure_tesseract_languages # noqa: PLC0415
|
||||
|
||||
missing_langs = ensure_tesseract_languages(language)
|
||||
if missing_langs:
|
||||
logger.warning(
|
||||
"[embed_text_layer] Missing Tesseract language data for: %s – "
|
||||
"text-layer embedding may fail or produce degraded results.",
|
||||
", ".join(missing_langs),
|
||||
)
|
||||
|
||||
in_place = input_pdf_path == output_pdf_path
|
||||
if in_place:
|
||||
import tempfile
|
||||
|
||||
@@ -344,7 +344,10 @@ class TestEnsureOCRLanguagesFromSettings:
|
||||
assert result == {"tesseract_missing": [], "easyocr_failed": []}
|
||||
|
||||
def test_runs_easyocr_check_when_provider_active(self):
|
||||
"""Calls ensure_easyocr_models when 'easyocr' is in ocr_providers."""
|
||||
"""Calls ensure_easyocr_models when 'easyocr' is in ocr_providers.
|
||||
|
||||
When ocrmypdf is absent, only easyocr language checks run.
|
||||
"""
|
||||
from app.utils.ocr_language_manager import ensure_ocr_languages_from_settings
|
||||
|
||||
mock_settings = self._mock_settings(providers="easyocr", easyocr_langs="en,de")
|
||||
@@ -352,6 +355,7 @@ class TestEnsureOCRLanguagesFromSettings:
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]) as mock_tess,
|
||||
patch("app.utils.ocr_language_manager.ensure_easyocr_models", return_value=[]) as mock_easy,
|
||||
patch("app.config.settings", mock_settings),
|
||||
patch("shutil.which", return_value=None),
|
||||
):
|
||||
result = ensure_ocr_languages_from_settings()
|
||||
|
||||
@@ -359,8 +363,8 @@ class TestEnsureOCRLanguagesFromSettings:
|
||||
mock_easy.assert_called_once_with(["en", "de"])
|
||||
assert result["easyocr_failed"] == []
|
||||
|
||||
def test_skips_both_when_only_azure(self):
|
||||
"""Neither Tesseract nor EasyOCR checks run when only Azure is configured."""
|
||||
def test_skips_both_when_only_azure_and_no_ocrmypdf(self):
|
||||
"""Neither Tesseract nor EasyOCR checks run when only Azure is configured and ocrmypdf is absent."""
|
||||
from app.utils.ocr_language_manager import ensure_ocr_languages_from_settings
|
||||
|
||||
mock_settings = self._mock_settings(providers="azure")
|
||||
@@ -368,6 +372,7 @@ class TestEnsureOCRLanguagesFromSettings:
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages") as mock_tess,
|
||||
patch("app.utils.ocr_language_manager.ensure_easyocr_models") as mock_easy,
|
||||
patch("app.config.settings", mock_settings),
|
||||
patch("shutil.which", return_value=None),
|
||||
):
|
||||
result = ensure_ocr_languages_from_settings()
|
||||
|
||||
@@ -375,6 +380,27 @@ class TestEnsureOCRLanguagesFromSettings:
|
||||
mock_easy.assert_not_called()
|
||||
assert result == {"tesseract_missing": [], "easyocr_failed": []}
|
||||
|
||||
def test_runs_tesseract_when_ocrmypdf_available_with_non_tesseract_provider(self):
|
||||
"""Tesseract language check runs when ocrmypdf is on PATH even without the tesseract provider.
|
||||
|
||||
embed_text_layer() uses ocrmypdf internally regardless of the active OCR
|
||||
provider. Pre-downloading language data on startup prevents code-3 failures.
|
||||
"""
|
||||
from app.utils.ocr_language_manager import ensure_ocr_languages_from_settings
|
||||
|
||||
mock_settings = self._mock_settings(providers="azure", tesseract_lang="eng+deu+fra")
|
||||
with (
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]) as mock_tess,
|
||||
patch("app.utils.ocr_language_manager.ensure_easyocr_models") as mock_easy,
|
||||
patch("app.config.settings", mock_settings),
|
||||
patch("shutil.which", return_value="/usr/local/bin/ocrmypdf"),
|
||||
):
|
||||
result = ensure_ocr_languages_from_settings()
|
||||
|
||||
mock_tess.assert_called_once_with("eng+deu+fra")
|
||||
mock_easy.assert_not_called()
|
||||
assert result == {"tesseract_missing": [], "easyocr_failed": []}
|
||||
|
||||
def test_runs_both_when_both_providers_active(self):
|
||||
"""Both checks run when both 'tesseract' and 'easyocr' are in ocr_providers."""
|
||||
from app.utils.ocr_language_manager import ensure_ocr_languages_from_settings
|
||||
|
||||
@@ -1075,6 +1075,7 @@ class TestEmbedTextLayer:
|
||||
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/ocrmypdf"),
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]),
|
||||
patch("subprocess.run", return_value=mock_proc) as mock_run,
|
||||
):
|
||||
result = embed_text_layer(pdf, output, language="eng")
|
||||
@@ -1097,6 +1098,7 @@ class TestEmbedTextLayer:
|
||||
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/ocrmypdf"),
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]),
|
||||
patch("subprocess.run", return_value=mock_proc),
|
||||
):
|
||||
result = embed_text_layer(pdf, str(tmp_path / "out.pdf"))
|
||||
@@ -1113,6 +1115,7 @@ class TestEmbedTextLayer:
|
||||
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/ocrmypdf"),
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]),
|
||||
patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="ocrmypdf", timeout=600)),
|
||||
):
|
||||
result = embed_text_layer(pdf, str(tmp_path / "out.pdf"))
|
||||
@@ -1140,6 +1143,7 @@ class TestEmbedTextLayer:
|
||||
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/ocrmypdf"),
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]),
|
||||
patch("subprocess.run", side_effect=fake_run),
|
||||
):
|
||||
result = embed_text_layer(pdf, pdf)
|
||||
@@ -1172,6 +1176,7 @@ class TestEmbedTextLayer:
|
||||
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/ocrmypdf"),
|
||||
patch("app.utils.ocr_language_manager.ensure_tesseract_languages", return_value=[]),
|
||||
patch("subprocess.run", return_value=mock_proc),
|
||||
patch("tempfile.mkstemp", side_effect=fake_mkstemp),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user