106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
"""PDF→Text-Konvertierung: pymupdf4llm primär, pdftotext-Fallback, mtime-Cache."""
|
||
|
||
import os
|
||
import time
|
||
|
||
import fitz # PyMuPDF
|
||
import pytest
|
||
|
||
import blocks as blx
|
||
|
||
|
||
def _mini_pdf(path, text="Approximationsalgorithmen sind wichtig."):
|
||
doc = fitz.open()
|
||
page = doc.new_page()
|
||
page.insert_text((72, 72), text, fontsize=12)
|
||
doc.save(str(path))
|
||
doc.close()
|
||
|
||
|
||
def test_convert_writes_markdown_txt(tmp_path):
|
||
_mini_pdf(tmp_path / "skript.pdf")
|
||
blx._convert_pdfs(tmp_path)
|
||
out = (tmp_path / "skript.txt").read_text(encoding="utf-8")
|
||
assert "Approximationsalgorithmen" in out
|
||
|
||
|
||
def test_cache_skips_fresh_txt(tmp_path):
|
||
_mini_pdf(tmp_path / "a.pdf")
|
||
(tmp_path / ".pdf-txt-norm").write_text(str(blx._PDF_NORM_VERSION), encoding="utf-8")
|
||
marker = tmp_path / "a.txt"
|
||
marker.write_text("MARKER", encoding="utf-8")
|
||
now = time.time() + 60
|
||
os.utime(marker, (now, now))
|
||
blx._convert_pdfs(tmp_path)
|
||
assert marker.read_text(encoding="utf-8") == "MARKER" # nicht neu konvertiert
|
||
|
||
|
||
def test_norm_version_erzwingt_rekonvertierung(tmp_path, monkeypatch):
|
||
"""Fehlender/alter .pdf-txt-norm-Marker ignoriert den mtime-Cache einmalig."""
|
||
_mini_pdf(tmp_path / "a.pdf")
|
||
stale = tmp_path / "a.txt"
|
||
stale.write_text("ALT", encoding="utf-8")
|
||
now = time.time() + 60
|
||
os.utime(stale, (now, now))
|
||
monkeypatch.setattr(blx, "_pdf_markdown", lambda p: None)
|
||
monkeypatch.setattr(blx, "_pdf_plaintext", lambda p: "neu konvertiert")
|
||
blx._convert_pdfs(tmp_path)
|
||
assert stale.read_text(encoding="utf-8") == "neu konvertiert"
|
||
assert (tmp_path / ".pdf-txt-norm").read_text(encoding="utf-8") == str(blx._PDF_NORM_VERSION)
|
||
stale.write_text("BLEIBT", encoding="utf-8")
|
||
os.utime(stale, (now, now))
|
||
blx._convert_pdfs(tmp_path) # Marker aktuell → Cache greift wieder
|
||
assert stale.read_text(encoding="utf-8") == "BLEIBT"
|
||
|
||
|
||
def test_entzerre_pdf_woerter():
|
||
"""Small-Caps-Splits mergen; Variablen-Schutzfall und unbelegte Paare bleiben."""
|
||
# Regel A inkl. Kette
|
||
assert blx._entzerre_pdf_woerter("Das H ITTING S ET Problem") == "Das HITTING SET Problem"
|
||
assert blx._entzerre_pdf_woerter("V ERTEX C OVER ist schwer") == "VERTEX COVER ist schwer"
|
||
# Schutzfall: Großlauf klein fortgesetzt → L ist Variable
|
||
assert blx._entzerre_pdf_woerter("die Sprache L NP-vollständig ist") == "die Sprache L NP-vollständig ist"
|
||
# Regel B: Paar nur mit Frequenz-Beleg (≥3× ungespalten im Dokument)
|
||
belegt = "N P ist zentral. " + "NP NP NP."
|
||
assert blx._entzerre_pdf_woerter(belegt).startswith("NP ist zentral.")
|
||
assert blx._entzerre_pdf_woerter("C Y bleibt getrennt") == "C Y bleibt getrennt"
|
||
# kein Match mitten im Wort
|
||
assert blx._entzerre_pdf_woerter("HALTTM IST hier") == "HALTTM IST hier"
|
||
|
||
|
||
def test_fallback_to_pdftotext(tmp_path, monkeypatch):
|
||
_mini_pdf(tmp_path / "b.pdf")
|
||
monkeypatch.setattr(blx, "_pdf_markdown", lambda p: None)
|
||
monkeypatch.setattr(blx, "_pdf_plaintext", lambda p: "fallback")
|
||
blx._convert_pdfs(tmp_path)
|
||
assert (tmp_path / "b.txt").read_text(encoding="utf-8") == "fallback"
|
||
|
||
|
||
def test_ocr_languages_from_tessdata(tmp_path, monkeypatch):
|
||
import pymupdf
|
||
|
||
monkeypatch.setattr(pymupdf, "get_tessdata", lambda: str(tmp_path))
|
||
assert blx._ocr_languages() is None # keine Sprachdaten → OCR aus
|
||
(tmp_path / "eng.traineddata").touch()
|
||
assert blx._ocr_languages() == "eng"
|
||
(tmp_path / "deu.traineddata").touch()
|
||
assert blx._ocr_languages() == "deu+eng"
|
||
monkeypatch.setattr(pymupdf, "get_tessdata", lambda: (_ for _ in ()).throw(RuntimeError()))
|
||
assert blx._ocr_languages() is None
|
||
|
||
|
||
def test_fidelity_guard_prefers_faithful_plaintext():
|
||
plain = "Definition. P = {L ⊆ Σ | A ∈ L} und ≤ sowie häufig über. " * 20
|
||
# Markdown verlor die Formeln (Symbole weg) → plain gewinnt
|
||
md_lossy = "Definition. und sowie h¨aufig ¨uber. " * 20
|
||
text, tool = blx._pick_conversion(md_lossy, plain)
|
||
assert tool == "pdftotext"
|
||
# Markdown treu (Symbole + Länge da) → md gewinnt
|
||
md_ok = "# Def\n" + plain
|
||
text, tool = blx._pick_conversion(md_ok, plain)
|
||
assert tool == "pymupdf4llm"
|
||
# nur eine Quelle verfügbar
|
||
assert blx._pick_conversion(None, plain)[1] == "pdftotext"
|
||
assert blx._pick_conversion(md_ok, None)[1] == "pymupdf4llm"
|
||
assert blx._pick_conversion(None, None) is None
|