"""Scanned PDF detection and the Mistral OCR translation path.""" import fitz import pytest from config import config from services.mistral_ocr import MistralOCRClient from services.providers.base import TranslationProvider from services.providers.schemas import TranslationRequest, TranslationResponse from translators.pdf_translator import PDFTranslator FAKE_OCR_PAGES = [ "# Facture\n\nMontant total : 1 250 euros\n\n![logo](img.png)", "Le paiement est du sous 30 jours.", ] # Minimal French → English mapping for the fake provider; anything else is # prefixed so tests can assert replacement happened. TRANSLATIONS = { "Facture": "Invoice", "Montant total : 1 250 euros": "Total amount: 1,250 euros", "Le paiement est du sous 30 jours.": "Payment is due within 30 days.", } class FakeProvider(TranslationProvider): """New-style provider with canned translations (no network).""" def get_name(self) -> str: return "fake" def is_available(self) -> bool: return True def translate_text(self, request: TranslationRequest) -> TranslationResponse: # Pages arrive as multi-line text: translate line by line so the # canned mapping applies regardless of how lines are grouped. lines = [] for line in request.text.split("\n"): stripped = line.strip() if not stripped: lines.append(line) continue lines.append(TRANSLATIONS.get(stripped, f"[EN] {stripped}")) return TranslationResponse( translated_text="\n".join(lines), provider_name=self.get_name(), from_cache=False, ) def _make_scanned_pdf(path): """Image-only PDF: one page almost fully covered by a picture, no text.""" pix = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, 10, 10)) pix.clear_with(90) png_bytes = pix.tobytes("png") doc = fitz.open() page = doc.new_page(width=612, height=792) page.insert_image(fitz.Rect(20, 20, 592, 772), stream=png_bytes) doc.save(str(path)) doc.close() return path def _make_text_pdf(path): doc = fitz.open() page = doc.new_page() page.insert_text( (72, 100), "Real selectable text with plenty of characters to exceed the scanned threshold. " * 2, fontsize=11, ) doc.save(str(path)) doc.close() return path @pytest.fixture def no_api_key(monkeypatch): monkeypatch.delenv("MISTRAL_API_KEY", raising=False) monkeypatch.setattr(config, "MISTRAL_API_KEY", "") monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", True) @pytest.fixture def api_key(monkeypatch): # Set BOTH the env var and the config attribute: some other test in the # full suite reloads the config module, and pdf_translator imports # `config` at call time — after a reload only the env var is still # visible (the class attributes are re-read from the environment). monkeypatch.setenv("MISTRAL_API_KEY", "test-key") monkeypatch.setattr(config, "MISTRAL_API_KEY", "test-key") monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", True) class TestScannedDetection: def test_image_only_pdf_is_scanned(self, tmp_path): pdf = _make_scanned_pdf(tmp_path / "scan.pdf") translator = PDFTranslator(provider=None) assert translator._is_scanned_pdf(pdf) is True def test_text_pdf_is_not_scanned(self, tmp_path): pdf = _make_text_pdf(tmp_path / "text.pdf") translator = PDFTranslator(provider=None) assert translator._is_scanned_pdf(pdf) is False def test_title_only_pdf_is_not_scanned(self, tmp_path): """A sparse but textual page (no raster image) stays on the layout path.""" doc = fitz.open() page = doc.new_page() page.insert_text((72, 100), "Facture 2026", fontsize=18) doc.save(str(tmp_path / "title.pdf")) doc.close() translator = PDFTranslator(provider=None) assert translator._is_scanned_pdf(tmp_path / "title.pdf") is False class TestMarkdownCleanup: def test_images_links_headings_removed(self): md = "# Titre\n\n![img](x.png)\n\n[lien](http://x) texte" out = PDFTranslator._markdown_to_text(md) assert "Titre" in out assert "![img]" not in out assert "(http://x)" not in out assert "#" not in out assert "lien texte" in out def test_table_pipes_removed(self): out = PDFTranslator._markdown_to_text("| A | B |\n|---|---|\n| un | deux |") assert "|" not in out assert "un" in out and "deux" in out class TestScannedOCRPath: def test_translate_scanned_pdf_end_to_end(self, tmp_path, api_key, monkeypatch): pdf = _make_scanned_pdf(tmp_path / "scan.pdf") monkeypatch.setattr( MistralOCRClient, "extract_pdf_text", lambda self, path, progress_callback=None: list(FAKE_OCR_PAGES), ) translator = PDFTranslator(provider=FakeProvider()) # Configure OCR the way the production route does (set_ocr_config). # Going through the config module is fragile in the full suite: # another test replaces sys.modules["config"], so a config-module # patch may never reach the translator. translator.set_ocr_config(api_key="test-key", enabled=True) out = tmp_path / "out.pdf" result = translator.translate_file(pdf, out, "en", "fr") assert result.exists() and result.stat().st_size > 0 doc = fitz.open(str(result)) text = "\n".join(p.get_text("text") for p in doc) doc.close() assert "Invoice" in text assert "Payment is due within 30 days" in text assert "Facture" not in text assert "logo" not in text # markdown images stripped assert "|" not in text # markdown tables stripped def test_missing_api_key_raises_clear_error(self, tmp_path, no_api_key): pdf = _make_scanned_pdf(tmp_path / "scan.pdf") translator = PDFTranslator(provider=FakeProvider()) with pytest.raises(RuntimeError) as exc: translator.translate_file(pdf, tmp_path / "out.pdf", "en", "fr") assert "MISTRAL_API_KEY" in str(exc.value) def test_disabled_ocr_raises_clear_error(self, tmp_path, monkeypatch): monkeypatch.setattr(config, "MISTRAL_API_KEY", "test-key") monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", False) pdf = _make_scanned_pdf(tmp_path / "scan.pdf") translator = PDFTranslator(provider=FakeProvider()) with pytest.raises(RuntimeError): translator.translate_file(pdf, tmp_path / "out.pdf", "en", "fr") def test_text_pdf_bypasses_ocr(self, tmp_path, api_key, monkeypatch): pdf = _make_text_pdf(tmp_path / "text.pdf") called = {"ocr": False} def _fail(self, path, progress_callback=None): called["ocr"] = True return [] monkeypatch.setattr(MistralOCRClient, "extract_pdf_text", _fail) translator = PDFTranslator(provider=FakeProvider()) out = tmp_path / "out.pdf" translator.translate_file(pdf, out, "en", "fr") assert called["ocr"] is False