fix(backend): detect corrupted PDF text layer by control character density
This commit is contained in:
1 parent
b25bf8ff7d
commit
8f79993646
2 files changed
+109
-2
No files matched your search
@@ -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).
|
||||
|
||||
|
||||
Reference in new issue
Block a user