fix(backend): detect corrupted PDF text layer by control character density

This commit is contained in:
omp committed 2026-11-01 19:30:07 -03:00
1 parent b25bf8ff7d
commit 8f79993646
2 files changed
+109 -2

No files matched your search

+35 -1
View File
@@ -54,6 +54,27 @@ def legible_ratio(text: str) -> float:
return legible / len(meaningful)
def control_char_ratio(text: str) -> float:
"""Proporção de caracteres de controle C0 (fora de tab/newline/CR).
É o sintoma direto de uma camada textual sem mapeamento Unicode: o extrator
devolve bytes de controle no lugar das letras. A razão de caracteres
legíveis não basta — um cabeçalho como "ÍNDICE" entre letras soltas eleva
o placar acima do limiar, e a página seguiria corrompida.
"""
if not text:
return 0.0
meaningful = [ch for ch in text if not ch.isspace()]
if not meaningful:
return 0.0
controls = sum(
1
for ch in meaningful
if unicodedata.category(ch) == "Cc" and ch not in "\t\n\r"
)
return controls / len(meaningful)
def _is_legible_char(ch: str) -> bool:
"""Caractere que um humano consegue ler."""
if unicodedata.category(ch).startswith("C"):
@@ -102,6 +123,7 @@ class PdfExtractor:
ocr_lang: str = "por+eng",
ocr_timeout: int = 120,
min_legible_ratio: float = 0.35,
max_control_ratio: float = 0.01,
pymupdf_module=None,
):
self.cache_dir = Path(cache_dir)
@@ -109,6 +131,7 @@ class PdfExtractor:
self.ocr_lang = ocr_lang
self.ocr_timeout = ocr_timeout
self.min_legible_ratio = min_legible_ratio
self.max_control_ratio = max_control_ratio
self._pymupdf = pymupdf_module if pymupdf_module is not None else pymupdf
def extract_document(
@@ -141,12 +164,23 @@ class PdfExtractor:
text = ""
failed = False
if legible_ratio(text) < self.min_legible_ratio:
if self._is_corrupted(text):
text, failed = self._ocr_page(page)
self._write_cache(cache_file, text)
return PageText(page=page_number, text=text, failed=failed)
def _is_corrupted(self, text: str) -> bool:
"""Diz se a camada textual é inutilizável e exige OCR.
Dois sinais independentes: texto pouco legível e densidade de
caracteres de controle. O segundo é o que pega a página do índice do
PDF real, em que letras avulsas elevam a legibilidade acima do limiar.
"""
if control_char_ratio(text) > self.max_control_ratio:
return True
return legible_ratio(text) < self.min_legible_ratio
def _ocr_page(self, page) -> tuple[str, bool]:
"""Rasteriza a página e aplica OCR. Devolve (texto, falhou).