WallaceBrasil commited on
Commit
d494f2d
·
verified ·
1 Parent(s): 4bce065

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +42 -14
  2. app.py +194 -0
  3. packages.txt +1 -0
  4. processador.py +195 -0
  5. requirements.txt +3 -0
README.md CHANGED
@@ -1,14 +1,42 @@
1
- ---
2
- title: Extrator Pdf Web
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.44.1
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: Converte PDFs em imagens por página
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Extrator de Imagens de PDF (Gradio)
2
+
3
+ Converta um ou vários **PDFs** (inclusive dentro de **.zip**) em **imagens por página**. Interface simples, tema escuro e saída em **galeria** + **ZIP**.
4
+
5
+ > Tecnologias: **Python**, **Gradio 4**, **pdf2image** (Poppler), **Pillow**.
6
+
7
+ ## Demonstração
8
+ - **App online (HF Space):** _adicione o link do seu Space aqui_
9
+ - **Repositório (código):** _este repositório_
10
+
11
+ ## Recursos
12
+ - ✅ Suporta PDF *ou* `.zip` com vários PDFs.
13
+ - ✅ Dois modos: **todas as páginas** ou **páginas específicas** (ex.: `3 - 5 - 10`).
14
+ - Gera **galeria** para pré-visualizar e **ZIP** com todas as imagens.
15
+ - ✅ Formatos: **JPEG**, **PNG**, **BMP**, **ICO**.
16
+
17
+ ## Como funciona (resumo)
18
+ - Usa **pdf2image** (Poppler) para rasterizar cada página do PDF.
19
+ - Salva as imagens no formato escolhido e empacota tudo em **ZIP**.
20
+ - Mostra prévias na **Gallery** do Gradio.
21
+
22
+ ## Instalação local
23
+ Requisitos: Python 3.10+.
24
+
25
+ 1. **(Windows) Poppler**
26
+ Baixe o Poppler (ex.: `poppler-XX`) e aponte para `...\Library\bin` dentro do `processador.py` (constante `POPPLER_PATH`).
27
+ No Linux/macOS, não precisa setar (usa do sistema).
28
+
29
+ 2. **Instalar dependências**
30
+ ```bash
31
+ python -m venv .venv
32
+ # Windows
33
+ .venv\Scripts\activate
34
+ # macOS/Linux
35
+ # source .venv/bin/activate
36
+
37
+ python -m pip install -U pip
38
+ python -m pip install -r requirements.txt
39
+
40
+
41
+ 2. **Executar**
42
+ python app.py
app.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — versão com tema/estilo padronizado
2
+ import gradio as gr
3
+ import tempfile
4
+ from pathlib import Path
5
+ from processador import processar_misto # pipeline que aceita PDF(s) e ZIP(s)
6
+
7
+ # -------------------- Tema + CSS do portfólio --------------------
8
+ CUSTOM_CSS = """
9
+ :root{
10
+ --bg:#000; /* fundo geral */
11
+ --panel:#0b0b0b; /* blocos/painéis */
12
+ --panel-2:#0e0e0e; /* inputs/dropdowns */
13
+ --border:#2a2a2a; /* borda padrão */
14
+ --text:#e5e5e5; /* texto branco suave */
15
+ --muted:#a3a3a3; /* texto secundário */
16
+ --accent:#6ee7b7; /* cor do foco/seleção (verde menta) */
17
+ }
18
+
19
+ /* fonte geral (system UI) */
20
+ html, body, .gradio-container {
21
+ background: var(--bg)!important;
22
+ color: var(--text)!important;
23
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif !important;
24
+ }
25
+
26
+ /* blocos/painéis */
27
+ .gradio-container .block,
28
+ .gradio-container .gr-box,
29
+ .gradio-container .gr-panel {
30
+ background: var(--panel) !important;
31
+ border: 1px solid var(--border) !important;
32
+ border-radius: 12px !important;
33
+ }
34
+
35
+ /* remover o bloco atrás do TÍTULO (qualquer .block que contenha h1) */
36
+ .gradio-container .block:has(h1){
37
+ background: transparent !important;
38
+ border: 0 !important;
39
+ box-shadow: none !important;
40
+ }
41
+
42
+ /* botões estilo "pílula" */
43
+ button, .gr-button{
44
+ border-radius: 9999px !important;
45
+ border: 1px solid var(--border) !important;
46
+ background: var(--panel-2) !important;
47
+ }
48
+ button:hover{ border-color:#4a4a4a !important; }
49
+
50
+ /* inputs/textarea/file/dropdown: mesmo tom escuro */
51
+ input, textarea, select,
52
+ .gradio-container .gr-textbox,
53
+ .gradio-container .gr-input,
54
+ .gradio-container .gradio-dropdown,
55
+ .gradio-container .gr-file,
56
+ .gradio-container .gr-file-download,
57
+ .gradio-container .gr-select-container,
58
+ .gradio-container .wrap .items-center select {
59
+ background: var(--panel-2) !important;
60
+ border: 1px solid var(--border) !important;
61
+ color: var(--text) !important;
62
+ border-radius: 12px !important;
63
+ }
64
+
65
+ /* opções do select */
66
+ select > option { background: var(--panel-2); color: var(--text); }
67
+
68
+ /* foco/seleção visível (inputs, selects, file, etc.) */
69
+ input:focus, textarea:focus, select:focus,
70
+ .gradio-container .gr-textbox:focus-within,
71
+ .gradio-container .gr-input:focus-within,
72
+ .gradio-container .gradio-dropdown:focus-within,
73
+ .gradio-container .gr-file:focus-within,
74
+ .gradio-container .gr-select-container:focus-within {
75
+ outline: none !important;
76
+ border-color: var(--accent) !important;
77
+ box-shadow: 0 0 0 2px rgba(110,231,183,0.18) !important; /* halo */
78
+ }
79
+
80
+ /* uploader QUADRADO (remove círculo/arredondamento exagerado) */
81
+ .gradio-container [data-testid="file"] .rounded-full,
82
+ .gradio-container [data-testid="files"] .rounded-full { border-radius:12px !important; }
83
+ .gradio-container [data-testid="file"] [class*="aspect-"],
84
+ .gradio-container [data-testid="files"] [class*="aspect-"] { aspect-ratio:auto !important; }
85
+ .gradio-container [data-testid="file"] .h-full.w-full,
86
+ .gradio-container [data-testid="files"] .h-full.w-full { border-radius:12px !important; }
87
+
88
+ /* gallery e saídas de arquivo */
89
+ .gradio-container .gr-gallery,
90
+ .gradio-container .gr-file-download{
91
+ background: var(--panel-2) !important;
92
+ border: 1px solid var(--border) !important;
93
+ border-radius: 12px !important;
94
+ }
95
+
96
+ /* badges/labels */
97
+ .badge, .token { background:#0f0f0f !important; border-radius:9999px !important; }
98
+
99
+ .gradio-container .fixed.bottom-0,
100
+ .gradio-container div[class*="fixed"][class*="bottom-0"],
101
+ .gradio-container footer,
102
+ body > div.fixed.bottom-0,
103
+ div.fixed.bottom-0 {
104
+ display: none !important;
105
+ visibility: hidden !important;
106
+ height: 0 !important;
107
+ overflow: hidden !important;
108
+ pointer-events: none !important;
109
+ }
110
+ """
111
+
112
+ THEME = gr.themes.Soft(primary_hue="zinc", neutral_hue="zinc")
113
+
114
+ # -----------------------------------------------------------
115
+
116
+ # Converte buffers em arquivos temporários para a Gallery
117
+ def _galeria_temp(imagens, formato):
118
+ gal = []
119
+ ext = "ico" if formato == "ico" else formato.lower()
120
+ for legenda, img_io in imagens:
121
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f".{ext}")
122
+ tmp.write(img_io.read())
123
+ tmp.close()
124
+ img_io.seek(0)
125
+ gal.append((tmp.name, legenda))
126
+ return gal
127
+
128
+ # Função chamada pelo submit
129
+ def processar(arquivos, modo, paginas_input, formato_opcao):
130
+ if not arquivos:
131
+ return "Por favor, envie ao menos um PDF (ou .zip contendo PDFs).", None
132
+
133
+ # gr.Files pode vir string ou lista:
134
+ paths = [Path(arquivos)] if isinstance(arquivos, str) else [Path(p) for p in arquivos]
135
+
136
+ # Do dropdown “jpeg [Recomendado …]” pegamos só o formato:
137
+ formato = formato_opcao.split(" [")[0].lower()
138
+
139
+ # Interpreta páginas quando necessário
140
+ paginas = None
141
+ if modo == "Extrair páginas específicas":
142
+ try:
143
+ paginas = [int(p.strip()) for p in paginas_input.split("-") if p.strip().isdigit()]
144
+ if not paginas:
145
+ return "Nenhuma página válida foi informada.", None
146
+ except Exception as e:
147
+ return f"Erro ao interpretar as páginas: {e}", None
148
+
149
+ try:
150
+ imagens, zip_path = processar_misto(paths, modo, paginas, formato)
151
+ except Exception as e:
152
+ return f"Erro ao processar: {e}", None
153
+
154
+ galeria = _galeria_temp(imagens, formato)
155
+ return galeria, zip_path
156
+
157
+ FORMATOS = [
158
+ " jpeg [Recomendado - compacto e boa qualidade]",
159
+ " png [Alta qualidade, suporta transparência]",
160
+ " bmp [Sem compressão - ideal para edição bruta]",
161
+ " ico [Favicon para sites, atalhos e apps]",
162
+ ]
163
+
164
+ demo = gr.Interface(
165
+ fn=processar,
166
+ inputs=[
167
+ gr.Files(
168
+ label="Envie PDF(s) ou .zip com PDFs",
169
+ file_count="multiple",
170
+ file_types=[".pdf", ".zip"],
171
+ ),
172
+ gr.Radio(
173
+ ["Extrair todas as páginas", "Extrair páginas específicas"],
174
+ label="Modo",
175
+ value="Extrair todas as páginas",
176
+ ),
177
+ gr.Textbox(
178
+ label="Páginas (se usar 'Específicas')",
179
+ placeholder="Ex.: 3 - 5 - 10",
180
+ ),
181
+ gr.Dropdown(choices=FORMATOS, value=FORMATOS[0], label="Formato das imagens"),
182
+ ],
183
+ outputs=[
184
+ gr.Gallery(label="Imagens"),
185
+ gr.File(label="ZIP com as imagens"),
186
+ ],
187
+ title="Extrator de Imagens de PDF",
188
+ allow_flagging="never",
189
+ theme=THEME,
190
+ css=CUSTOM_CSS,
191
+ )
192
+
193
+ if __name__ == "__main__":
194
+ demo.launch()
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ poppler-utils
processador.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # processador.py
2
+ from pdf2image import convert_from_bytes
3
+ from typing import List, Tuple, Optional
4
+ from pathlib import Path
5
+ import io
6
+ import shutil
7
+ import tempfile
8
+ import platform
9
+ import zipfile
10
+ import time
11
+
12
+ # --- Poppler ---
13
+ # Windows costuma precisar do caminho do Poppler; no Hugging Face (Linux) já está no PATH.
14
+ if platform.system() == "Windows":
15
+ POPPLER_PATH = r"C:\poppler-25.07.0\Library\bin" # ajuste se necessário no seu PC
16
+ else:
17
+ POPPLER_PATH = None # no HF não precisa
18
+
19
+ def _kwargs_poppler():
20
+ """Monta kwargs para o pdf2image dependendo da plataforma."""
21
+ return {"poppler_path": POPPLER_PATH} if POPPLER_PATH else {}
22
+
23
+ # ------------------------------------------------------------
24
+ # Helpers de conversão por PDF (mantidos para reuso)
25
+ # ------------------------------------------------------------
26
+ def extrair_todas_as_paginas(
27
+ pdf_bytes: bytes,
28
+ nome_pdf: str,
29
+ formato: str = "jpeg",
30
+ dpi: int = 300
31
+ ) -> Tuple[List[Tuple[int, io.BytesIO]], str]:
32
+ """
33
+ Converte TODAS as páginas do PDF para o formato escolhido.
34
+ Retorna lista [(numero_pagina, buffer), ...] e caminho de um ZIP contendo SÓ este PDF.
35
+ (No fluxo 'misto', o ZIP final será refeito juntando PDFs.)
36
+ """
37
+ pil_pages = convert_from_bytes(pdf_bytes, dpi=dpi, fmt="ppm", **_kwargs_poppler())
38
+
39
+ resultado: List[Tuple[int, io.BytesIO]] = []
40
+ for i, pil_img in enumerate(pil_pages, start=1):
41
+ buf = io.BytesIO()
42
+ img_rgb = pil_img.convert("RGB")
43
+ if formato.lower() == "ico":
44
+ img_rgb.save(buf, format="ICO")
45
+ else:
46
+ img_rgb.save(buf, format=formato.upper())
47
+ buf.seek(0)
48
+ resultado.append((i, buf))
49
+
50
+ # ZIP individual (não usado no 'misto', mas mantemos por compatibilidade)
51
+ zip_path = _salvar_e_zipar(resultado, nome_pdf, formato)
52
+ return resultado, zip_path
53
+
54
+ def extrair_paginas_especificas(
55
+ pdf_bytes: bytes,
56
+ nome_pdf: str,
57
+ paginas: List[int],
58
+ formato: str = "jpeg",
59
+ dpi: int = 300
60
+ ) -> Tuple[List[Tuple[int, io.BytesIO]], str]:
61
+ """
62
+ Converte apenas páginas específicas (ex.: [3, 5, 10]).
63
+ Retorna lista [(numero_pagina, buffer), ...] e caminho de um ZIP só deste PDF.
64
+ """
65
+ paginas_ord = sorted(set(paginas))
66
+ first, last = min(paginas_ord), max(paginas_ord)
67
+
68
+ pil_interval = convert_from_bytes(
69
+ pdf_bytes, dpi=dpi, first_page=first, last_page=last, fmt="ppm", **_kwargs_poppler()
70
+ )
71
+
72
+ resultado: List[Tuple[int, io.BytesIO]] = []
73
+ for pagina in paginas_ord:
74
+ idx = pagina - first # índice relativo dentro do intervalo
75
+ buf = io.BytesIO()
76
+ img_rgb = pil_interval[idx].convert("RGB")
77
+ if formato.lower() == "ico":
78
+ img_rgb.save(buf, format="ICO")
79
+ else:
80
+ img_rgb.save(buf, format=formato.upper())
81
+ buf.seek(0)
82
+ resultado.append((pagina, buf))
83
+
84
+ zip_path = _salvar_e_zipar(resultado, nome_pdf, formato)
85
+ return resultado, zip_path
86
+
87
+ # ------------------------------------------------------------
88
+ # ZIP utilitário (para um único PDF) — usado nos helpers acima
89
+ # ------------------------------------------------------------
90
+ def _salvar_e_zipar(
91
+ imagens: List[Tuple[int, io.BytesIO]],
92
+ nome_pdf: str,
93
+ formato: str
94
+ ) -> str:
95
+ temp_dir = tempfile.mkdtemp()
96
+ temp_path = Path(temp_dir)
97
+ ext = "ico" if formato == "ico" else formato.lower()
98
+
99
+ for pagina, img_io in imagens:
100
+ filename = f"{nome_pdf}_pagina_{pagina}.{ext}"
101
+ (temp_path / filename).write_bytes(img_io.getvalue())
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
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio
2
+ pdf2image
3
+ pillow