Update processador.py
Browse files- processador.py +101 -195
processador.py
CHANGED
|
@@ -1,195 +1,101 @@
|
|
| 1 |
-
# processador.py
|
| 2 |
-
from
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
-
import
|
| 6 |
-
|
| 7 |
-
import
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
# -
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
"""
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
img_io.seek(0)
|
| 103 |
-
|
| 104 |
-
base_name = Path.cwd() / nome_pdf # zip ficará como <nome_pdf>.zip
|
| 105 |
-
zip_path = shutil.make_archive(str(base_name), "zip", temp_dir)
|
| 106 |
-
return zip_path
|
| 107 |
-
|
| 108 |
-
# ------------------------------------------------------------
|
| 109 |
-
# Coleta PDFs a partir de paths “mistos”: PDFs diretos e PDFs dentro de .zip
|
| 110 |
-
# ------------------------------------------------------------
|
| 111 |
-
def _coletar_pdfs(paths: List[Path]) -> List[Tuple[str, bytes]]:
|
| 112 |
-
"""
|
| 113 |
-
Recebe caminhos de arquivos enviados (podem ser PDFs ou .zip).
|
| 114 |
-
Retorna uma lista de tuplas (nome_base_pdf, pdf_bytes) para cada PDF encontrado.
|
| 115 |
-
- Se for .zip, itera pelos itens *.pdf (case-insensitive) e lê os bytes sem extrair em disco.
|
| 116 |
-
- Se for .pdf, lê direto do disco.
|
| 117 |
-
"""
|
| 118 |
-
coletados: List[Tuple[str, bytes]] = []
|
| 119 |
-
for p in paths:
|
| 120 |
-
suf = p.suffix.lower()
|
| 121 |
-
if suf == ".pdf":
|
| 122 |
-
coletados.append((p.stem, p.read_bytes()))
|
| 123 |
-
elif suf == ".zip":
|
| 124 |
-
with zipfile.ZipFile(p, "r") as zf:
|
| 125 |
-
for zi in zf.infolist():
|
| 126 |
-
if zi.is_dir():
|
| 127 |
-
continue
|
| 128 |
-
if zi.filename.lower().endswith(".pdf"):
|
| 129 |
-
base = Path(zi.filename).stem
|
| 130 |
-
with zf.open(zi, "r") as f:
|
| 131 |
-
coletados.append((base, f.read()))
|
| 132 |
-
else:
|
| 133 |
-
# Ignora outros tipos (p. ex., .txt) para este app
|
| 134 |
-
continue
|
| 135 |
-
return coletados
|
| 136 |
-
|
| 137 |
-
# ------------------------------------------------------------
|
| 138 |
-
# Pipeline “mistão”: processa 1..N PDFs (e PDFs dentro de .zip) de uma vez
|
| 139 |
-
# ------------------------------------------------------------
|
| 140 |
-
def processar_misto(
|
| 141 |
-
paths: List[Path],
|
| 142 |
-
modo: str,
|
| 143 |
-
paginas: Optional[List[int]],
|
| 144 |
-
formato: str
|
| 145 |
-
) -> Tuple[List[Tuple[str, io.BytesIO]], str]:
|
| 146 |
-
"""
|
| 147 |
-
Processa tudo em uma passada:
|
| 148 |
-
- Lê todos os PDFs enviados (diretos e dentro de .zip)
|
| 149 |
-
- Converte páginas segundo 'modo' e 'paginas'
|
| 150 |
-
- Escreve TODAS as imagens em uma mesma pasta temporária com nomes:
|
| 151 |
-
NomeDoPDF_pagina_X.<ext>
|
| 152 |
-
- Gera UM único ZIP com todas as imagens
|
| 153 |
-
- Retorna:
|
| 154 |
-
imagens_galeria -> [(legenda, BytesIO), ...]
|
| 155 |
-
zip_path -> caminho do ZIP único
|
| 156 |
-
"""
|
| 157 |
-
pdfs = _coletar_pdfs(paths)
|
| 158 |
-
if not pdfs:
|
| 159 |
-
raise RuntimeError("Nenhum PDF encontrado nos arquivos enviados.")
|
| 160 |
-
|
| 161 |
-
# Pasta temporária na qual vamos gravar TUDO
|
| 162 |
-
temp_dir = tempfile.mkdtemp()
|
| 163 |
-
temp_path = Path(temp_dir)
|
| 164 |
-
ext = "ico" if formato == "ico" else formato.lower()
|
| 165 |
-
|
| 166 |
-
imagens_galeria: List[Tuple[str, io.BytesIO]] = []
|
| 167 |
-
|
| 168 |
-
for base, pdf_bytes in pdfs:
|
| 169 |
-
if modo == "Extrair todas as páginas":
|
| 170 |
-
lista_paginas, _ = extrair_todas_as_paginas(pdf_bytes, base, formato)
|
| 171 |
-
elif modo == "Extrair páginas específicas":
|
| 172 |
-
if not paginas:
|
| 173 |
-
raise RuntimeError("Nenhuma página válida foi informada.")
|
| 174 |
-
lista_paginas, _ = extrair_paginas_especificas(pdf_bytes, base, paginas, formato)
|
| 175 |
-
else:
|
| 176 |
-
raise RuntimeError("Modo inválido.")
|
| 177 |
-
|
| 178 |
-
# Grava no diretório TEMP (um único ZIP final) e prepara galeria
|
| 179 |
-
for pagina, img_io in lista_paginas:
|
| 180 |
-
# nome do arquivo no ZIP final
|
| 181 |
-
filename = f"{base}_pagina_{pagina}.{ext}"
|
| 182 |
-
(temp_path / filename).write_bytes(img_io.getvalue())
|
| 183 |
-
img_io.seek(0)
|
| 184 |
-
|
| 185 |
-
# legenda para galeria
|
| 186 |
-
imagens_galeria.append((f"{base} — pág {pagina}", img_io))
|
| 187 |
-
|
| 188 |
-
# Nome do ZIP: se for 1 PDF, usa o nome dele; senão, nome genérico com timestamp
|
| 189 |
-
if len(pdfs) == 1:
|
| 190 |
-
base_zip = pdfs[0][0]
|
| 191 |
-
else:
|
| 192 |
-
base_zip = f"imagens_extraidas_{time.strftime('%Y%m%d-%H%M%S')}"
|
| 193 |
-
|
| 194 |
-
zip_path = shutil.make_archive(str(Path.cwd() / base_zip), "zip", temp_dir)
|
| 195 |
-
return imagens_galeria, zip_path
|
|
|
|
| 1 |
+
# processador.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import List, Optional, Tuple
|
| 5 |
+
import platform, shutil, tempfile, zipfile
|
| 6 |
+
|
| 7 |
+
from pdf2image import convert_from_path, convert_from_bytes
|
| 8 |
+
|
| 9 |
+
IS_WINDOWS = platform.system() == "Windows"
|
| 10 |
+
# No Windows você aponta abaixo; no Linux (Hugging Face) deixe como None
|
| 11 |
+
POPPLER_PATH = r"C:\poppler-25.07.0\Library\bin" if IS_WINDOWS else None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _ensure_poppler() -> None:
|
| 15 |
+
"""Garante que o Poppler está disponível no ambiente."""
|
| 16 |
+
if IS_WINDOWS:
|
| 17 |
+
p = Path(POPPLER_PATH or "")
|
| 18 |
+
if not p.exists():
|
| 19 |
+
raise RuntimeError(
|
| 20 |
+
"Poppler não encontrado. Ajuste POPPLER_PATH para ...\\poppler-XX\\Library\\bin"
|
| 21 |
+
)
|
| 22 |
+
else:
|
| 23 |
+
# No Linux (HF) esperamos 'pdftoppm' no PATH via packages.txt (poppler-utils)
|
| 24 |
+
if shutil.which("pdftoppm") is None:
|
| 25 |
+
raise RuntimeError(
|
| 26 |
+
"pdftoppm não encontrado. No Hugging Face inclua 'packages.txt' com 'poppler-utils'."
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _ext(formato: str) -> str:
|
| 31 |
+
"""Normaliza a extensão."""
|
| 32 |
+
return "jpg" if formato.lower() in ("jpeg", "jpg") else formato.lower()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _convert_bytes(data: bytes, formato: str, paginas: Optional[List[int]]):
|
| 36 |
+
fmt = _ext(formato)
|
| 37 |
+
if not paginas:
|
| 38 |
+
return convert_from_bytes(data, dpi=200, fmt=fmt, poppler_path=POPPLER_PATH)
|
| 39 |
+
# páginas específicas: chamamos 1 a 1
|
| 40 |
+
out = []
|
| 41 |
+
for p in paginas:
|
| 42 |
+
img = convert_from_bytes(
|
| 43 |
+
data, dpi=200, fmt=fmt, first_page=p, last_page=p, poppler_path=POPPLER_PATH
|
| 44 |
+
)[0]
|
| 45 |
+
out.append(img)
|
| 46 |
+
return out
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _convert_path(pdf_path: Path, formato: str, paginas: Optional[List[int]]):
|
| 50 |
+
fmt = _ext(formato)
|
| 51 |
+
if not paginas:
|
| 52 |
+
return convert_from_path(str(pdf_path), dpi=200, fmt=fmt, poppler_path=POPPLER_PATH)
|
| 53 |
+
out = []
|
| 54 |
+
for p in paginas:
|
| 55 |
+
img = convert_from_path(
|
| 56 |
+
str(pdf_path), dpi=200, fmt=fmt, first_page=p, last_page=p, poppler_path=POPPLER_PATH
|
| 57 |
+
)[0]
|
| 58 |
+
out.append(img)
|
| 59 |
+
return out
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def processar_misto(
|
| 63 |
+
paths: List[Path], modo: str, paginas: Optional[List[int]], formato: str
|
| 64 |
+
) -> Tuple[List[Tuple[str, str]], str]:
|
| 65 |
+
"""
|
| 66 |
+
paths: lista de caminhos (PDFs ou ZIPs contendo PDFs)
|
| 67 |
+
modo: "Extrair todas as páginas" | "Extrair páginas específicas"
|
| 68 |
+
paginas: lista de ints (ou None) quando modo = específicas
|
| 69 |
+
formato: "jpeg" | "png" | "bmp" | "ico"
|
| 70 |
+
Retorna: [(caminho_imagem_temp, legenda)], caminho_zip_temp
|
| 71 |
+
"""
|
| 72 |
+
_ensure_poppler()
|
| 73 |
+
|
| 74 |
+
usar_paginas = paginas if "Específicas" in modo else None
|
| 75 |
+
ext = _ext(formato)
|
| 76 |
+
|
| 77 |
+
galeria: List[Tuple[str, str]] = []
|
| 78 |
+
tmp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
| 79 |
+
with zipfile.ZipFile(tmp_zip.name, "w", compression=zipfile.ZIP_DEFLATED) as zout:
|
| 80 |
+
for path in paths:
|
| 81 |
+
if path.suffix.lower() == ".zip":
|
| 82 |
+
with zipfile.ZipFile(path, "r") as zin:
|
| 83 |
+
for info in zin.infolist():
|
| 84 |
+
if info.filename.lower().endswith(".pdf"):
|
| 85 |
+
data = zin.read(info.filename)
|
| 86 |
+
images = _convert_bytes(data, formato, usar_paginas)
|
| 87 |
+
stem_zip = Path(info.filename).stem.replace("/", "_")
|
| 88 |
+
for i, img in enumerate(images, 1):
|
| 89 |
+
img_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f".{ext}")
|
| 90 |
+
img.save(img_tmp.name)
|
| 91 |
+
galeria.append((img_tmp.name, f"{stem_zip} — p{i}"))
|
| 92 |
+
zout.write(img_tmp.name, arcname=f"{stem_zip}_p{i}.{ext}")
|
| 93 |
+
else:
|
| 94 |
+
images = _convert_path(path, formato, usar_paginas)
|
| 95 |
+
for i, img in enumerate(images, 1):
|
| 96 |
+
img_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f".{ext}")
|
| 97 |
+
img.save(img_tmp.name)
|
| 98 |
+
galeria.append((img_tmp.name, f"{path.stem} — p{i}"))
|
| 99 |
+
zout.write(img_tmp.name, arcname=f"{path.stem}_p{i}.{ext}")
|
| 100 |
+
|
| 101 |
+
return galeria, tmp_zip.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|