# app.py — versão final p/ Hugging Face (tema + UI padronizados) import gradio as gr import re from pathlib import Path from typing import List from processador import processar_misto # devolve [(caminho_img, legenda)], caminho_zip # -------------------- Tema + CSS do portfólio -------------------- CUSTOM_CSS = """ :root{ --bg:#000; /* fundo geral */ --panel:#0b0b0b; /* blocos/painéis */ --panel-2:#0e0e0e; /* inputs/dropdowns */ --border:#2a2a2a; /* borda padrão */ --text:#e5e5e5; /* texto branco suave */ --muted:#a3a3a3; /* texto secundário */ --accent:#6ee7b7; /* cor do foco/seleção (verde menta) */ } /* fonte geral (system UI) */ html, body, .gradio-container { background: var(--bg)!important; color: var(--text)!important; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif !important; } /* blocos/painéis */ .gradio-container .block, .gradio-container .gr-box, .gradio-container .gr-panel { background: var(--panel) !important; border: 1px solid var(--border) !important; border-radius: 12px !important; } /* remover o bloco atrás do TÍTULO */ .gradio-container .block:has(h1){ background: transparent !important; border: 0 !important; box-shadow: none !important; } /* botões estilo "pílula" */ button, .gr-button{ border-radius: 9999px !important; border: 1px solid var(--border) !important; background: var(--panel-2) !important; } button:hover{ border-color:#4a4a4a !important; } /* inputs/textarea/file/dropdown */ input, textarea, select, .gradio-container .gr-textbox, .gradio-container .gr-input, .gradio-container .gradio-dropdown, .gradio-container .gr-file, .gradio-container .gr-file-download, .gradio-container .gr-select-container, .gradio-container .wrap .items-center select { background: var(--panel-2) !important; border: 1px solid var(--border) !important; color: var(--text) !important; border-radius: 12px !important; } select > option { background: var(--panel-2); color: var(--text); } /* foco/seleção visível */ input:focus, textarea:focus, select:focus, .gradio-container .gr-textbox:focus-within, .gradio-container .gr-input:focus-within, .gradio-container .gradio-dropdown:focus-within, .gradio-container .gr-file:focus-within, .gradio-container .gr-select-container:focus-within { outline: none !important; border-color: var(--accent) !important; box-shadow: 0 0 0 2px rgba(110,231,183,0.18) !important; } /* uploader QUADRADO */ .gradio-container [data-testid="file"] .rounded-full, .gradio-container [data-testid="files"] .rounded-full { border-radius:12px !important; } .gradio-container [data-testid="file"] [class*="aspect-"], .gradio-container [data-testid="files"] [class*="aspect-"] { aspect-ratio:auto !important; } .gradio-container [data-testid="file"] .h-full.w-full, .gradio-container [data-testid="files"] .h-full.w-full { border-radius:12px !important; } /* gallery e saídas */ .gradio-container .gr-gallery, .gradio-container .gr-file-download{ background: var(--panel-2) !important; border: 1px solid var(--border) !important; border-radius: 12px !important; } /* badges/labels */ .badge, .token { background:#0f0f0f !important; border-radius:9999px !important; } /* esconder rodapé do Gradio */ .gradio-container .fixed.bottom-0, .gradio-container div[class*="fixed"][class*="bottom-0"], .gradio-container footer, body > div.fixed.bottom-0, div.fixed.bottom-0 { display: none !important; visibility: hidden !important; height: 0 !important; overflow: hidden !important; pointer-events: none !important; } """ THEME = gr.themes.Soft(primary_hue="zinc", neutral_hue="zinc") # -------------------- Util -------------------- def _normalize_paths(arquivos) -> List[Path]: """gr.Files pode entregar str, Path, objeto com .name ou até dict-like.""" items = arquivos if isinstance(arquivos, list) else [arquivos] paths: List[Path] = [] for a in items: if isinstance(a, (str, Path)): paths.append(Path(a)) else: name = getattr(a, "name", None) if name is None: try: name = a["name"] # alguns retornam dict-like except Exception: name = str(a) paths.append(Path(name)) return paths # -------------------- Função do app -------------------- def processar(arquivos, modo, paginas_input, formato_opcao): if not arquivos: return "Por favor, envie ao menos um PDF (ou .zip com PDFs).", None paths = _normalize_paths(arquivos) # pega "jpeg" de "jpeg [Recomendado - ...]" formato = formato_opcao.split(" [")[0].strip().lower() paginas = None if "específica" in modo.lower(): # aceita "Específicas"/"Específica" nums = re.findall(r"\d+", paginas_input or "") if not nums: return "Nenhuma página válida foi informada.", None paginas = [int(n) for n in nums] try: galeria, zip_path = processar_misto(paths, modo, paginas, formato) return galeria, zip_path # Gallery aceita lista de (caminho, legenda) except Exception as e: return f"Erro ao processar: {e}", None # opções do dropdown (sem espaços no início) FORMATOS = [ "jpeg [Recomendado - compacto e boa qualidade]", "png [Alta qualidade, suporta transparência]", "bmp [Sem compressão - ideal para edição bruta]", "ico [Favicon para sites, atalhos e apps]", ] demo = gr.Interface( fn=processar, inputs=[ gr.Files( label="Envie PDF(s) ou .zip com PDFs", file_count="multiple", file_types=[".pdf", ".zip"], ), gr.Radio( ["Extrair todas as páginas", "Extrair páginas específicas"], label="Modo", value="Extrair todas as páginas", ), gr.Textbox( label="Páginas (se usar 'Específicas')", placeholder="Ex.: 3 - 5 - 10", ), gr.Dropdown(choices=FORMATOS, value=FORMATOS[0], label="Formato das imagens"), ], outputs=[ gr.Gallery(label="Imagens"), gr.File(label="ZIP com as imagens"), ], title="Extrator de Imagens de PDF", allow_flagging="never", theme=THEME, css=CUSTOM_CSS, ) if __name__ == "__main__": demo.launch()