""" pdf-toolkit — Interface Web via Streamlit. Permite upload de múltiplos PDFs, seleção de operações, visualização de metadados e download dos resultados. """ import io import os import zipfile from pathlib import Path import streamlit as st from config import QUALITY_PRESETS, DEFAULT_QUALITY, MAX_FILES_PER_BATCH from core.engine import PDFEngine # ── Configuração da página ────────────────────────────────────────── st.set_page_config( page_title="pdf-toolkit", page_icon="📄", layout="wide", initial_sidebar_state="expanded", ) st.title("📄 pdf-toolkit") st.markdown("Transforme, otimize e gerencie seus arquivos PDF.") engine = PDFEngine() # ── SIDEBAR: Upload ───────────────────────────────────────────────── st.sidebar.header("1. Upload de PDFs") uploaded_files = st.sidebar.file_uploader( "Selecione um ou mais PDFs", type=["pdf"], accept_multiple_files=True, help=f"Máximo de {MAX_FILES_PER_BATCH} arquivos por vez.", ) if not uploaded_files: st.info("Faça upload de arquivos PDF na barra lateral para começar.") st.stop() if len(uploaded_files) > MAX_FILES_PER_BATCH: st.error(f"Máximo de {MAX_FILES_PER_BATCH} arquivos por vez.") st.stop() # ── Lê arquivos em memória ────────────────────────────────────────── files_data = {} for f in uploaded_files: data = f.read() info = engine.inspect(data, f.name) files_data[f.name] = {"data": data, "info": info, "file": f} st.sidebar.success(f"{len(files_data)} arquivo(s) carregado(s).") # ── SIDEBAR: Seleção de operação ──────────────────────────────────── st.sidebar.header("2. Operação") operation = st.sidebar.selectbox( "O que deseja fazer?", [ "📦 Comprimir PDF", "📄 PDF/A → PDF comum", "📋 Comprimir (mantendo PDF/A)", "🔗 Merge (juntar PDFs)", "✂️ Split (extrair páginas)", "🔄 Rotacionar páginas", "📝 Extrair texto", "🖼️ Extrair imagens", "🔍 Inspecionar metadados", ], index=0, ) # ── Opções específicas por operação ────────────────────────────────── extra_opts = {} if operation in ["📦 Comprimir PDF", "📋 Comprimir (mantendo PDF/A)"]: preset = st.sidebar.selectbox( "Nível de compressão", options=list(QUALITY_PRESETS.keys()), format_func=lambda k: f"{k} — {QUALITY_PRESETS[k]['desc']}", index=list(QUALITY_PRESETS.keys()).index(DEFAULT_QUALITY), ) extra_opts["preset"] = preset elif operation == "✂️ Split (extrair páginas)": if len(files_data) == 1: fname = list(files_data.keys())[0] total_pages = files_data[fname]["info"]["pages"] st.sidebar.markdown(f"O PDF tem **{total_pages} página(s)**.") range_input = st.sidebar.text_input( "Intervalos (ex: 1-3, 5, 7-9)", placeholder="1-3, 5, 7-9", ) extra_opts["range_input"] = range_input extra_opts["total_pages"] = total_pages else: st.sidebar.warning("Split funciona com 1 PDF por vez. Remova arquivos extras.") elif operation == "🔄 Rotacionar páginas": if len(files_data) == 1: fname = list(files_data.keys())[0] total_pages = files_data[fname]["info"]["pages"] st.sidebar.markdown(f"PDF tem **{total_pages} página(s)**.") pages = st.sidebar.text_input( "Páginas (ex: 1, 3, 5 ou 'todas')", placeholder="todas", ) angle = st.sidebar.selectbox("Ângulo", [90, 180, 270], index=0) extra_opts["pages"] = pages extra_opts["angle"] = angle extra_opts["total_pages"] = total_pages else: st.sidebar.warning("Rotação funciona com 1 PDF por vez.") elif operation == "📝 Extrair texto": pass # Sem opções extras elif operation == "🖼️ Extrair imagens": pass elif operation == "🔍 Inspecionar metadados": pass elif operation == "🔗 Merge (juntar PDFs)": if len(files_data) < 2: st.sidebar.warning("Merge precisa de pelo menos 2 PDFs.") # ── BOTÃO DE EXECUÇÃO ────────────────────────────────────────────── run = st.sidebar.button("▶️ Executar", type="primary", use_container_width=True) if not run: # Apenas mostra preview dos arquivos carregados st.subheader("📁 Arquivos carregados") for fname, fobj in files_data.items(): st.markdown(engine.get_info_string(fobj["info"])) st.stop() # ── EXECUÇÃO ──────────────────────────────────────────────────────── st.subheader("📊 Resultados") progress = st.progress(0, text="Processando...") try: results = [] if operation == "📦 Comprimir PDF": progress.progress(0.1, "Comprimindo...") for i, (fname, fobj) in enumerate(files_data.items()): result_data, stats = engine.compress_with_stats(fobj["data"], preset=extra_opts["preset"]) orig_mb = fobj["info"]["file_size_mb"] new_mb = round(len(result_data) / (1024 * 1024), 2) reduction = round((1 - (new_mb / orig_mb)) * 100, 1) if orig_mb > 0 else 0 results.append((fname.replace(".pdf", f"_comprimido.pdf"), result_data)) if stats["reduced"]: msg = f"{fname}: {orig_mb}MB → {new_mb}MB ({reduction}% redução)" else: msg = f"{fname}: já otimizado — {len(result_data)} bytes (sem redução adicional)" if stats["images_replaced"] == 0 else f"{fname}: {orig_mb}MB → {new_mb}MB (compressão não reduziu o tamanho, original preservado)" progress.progress((i + 1) / len(files_data), msg) elif operation == "📄 PDF/A → PDF comum": progress.progress(0.1, "Convertendo PDF/A...") for i, (fname, fobj) in enumerate(files_data.items()): if fobj["info"].get("is_pdfa"): result_data = engine.pdfa_to_regular(fobj["data"]) results.append((fname.replace(".pdf", "_comum.pdf"), result_data)) else: st.warning(f"{fname} não é PDF/A. Mantido original.") results.append((fname, fobj["data"])) progress.progress((i + 1) / len(files_data)) elif operation == "📋 Comprimir (mantendo PDF/A)": progress.progress(0.1, "Comprimindo PDF/A...") q = QUALITY_PRESETS[extra_opts["preset"]]["quality"] for i, (fname, fobj) in enumerate(files_data.items()): if fobj["info"].get("is_pdfa"): result_data = engine.compress_pdfa(fobj["data"], quality=q) else: # Se não for PDF/A, aplica compressão normal com stats result_data, _ = engine.compress_with_stats(fobj["data"], preset=extra_opts["preset"]) orig_mb = fobj["info"]["file_size_mb"] new_mb = round(len(result_data) / (1024 * 1024), 2) results.append((fname.replace(".pdf", f"_otimizado.pdf"), result_data)) progress.progress((i + 1) / len(files_data), f"{fname}: {orig_mb}MB → {new_mb}MB") elif operation == "🔗 Merge (juntar PDFs)": progress.progress(0.3, "Mesclando PDFs...") merge_input = [(fobj["data"], fname) for fname, fobj in files_data.items()] result_data = engine.merge(merge_input) results.append(("merged.pdf", result_data)) progress.progress(1.0, "Merge concluído!") elif operation == "✂️ Split (extrair páginas)": fname = list(files_data.keys())[0] range_input = extra_opts.get("range_input", "") total = extra_opts["total_pages"] if not range_input.strip(): st.error("Informe os intervalos de páginas.") st.stop() # Parse: "1-3, 5, 7-9" ranges = [] parts = range_input.replace(" ", "").split(",") for part in parts: if "-" in part: a, b = part.split("-") start = max(0, int(a) - 1) end = min(int(b) - 1, total - 1) ranges.append((start, end)) else: idx = int(part) - 1 ranges.append((idx, idx)) progress.progress(0.3, "Extraindo páginas...") splitted = engine.split(fobj["data"], ranges, fname.replace(".pdf", "")) for nome, data in splitted: results.append((nome, data)) progress.progress(1.0, "Split concluído!") elif operation == "🔄 Rotacionar páginas": fname = list(files_data.keys())[0] fobj = files_data[fname] pages_str = extra_opts.get("pages", "todas") angle = extra_opts["angle"] total = extra_opts["total_pages"] if pages_str.lower() in ("", "todas", "all"): pages_list = list(range(total)) else: pages_list = [int(p.strip()) - 1 for p in pages_str.replace(" ", "").split(",")] pages_list = [p for p in pages_list if 0 <= p < total] rotations = [(p, angle) for p in pages_list] progress.progress(0.5, "Rotacionando...") result_data = engine.rotate(fobj["data"], rotations) results.append((fname.replace(".pdf", f"_rotacionado.pdf"), result_data)) progress.progress(1.0) elif operation == "📝 Extrair texto": for fname, fobj in files_data.items(): text = engine.text(fobj["data"]) txt_name = fname.replace(".pdf", ".txt") results.append((txt_name, text.encode("utf-8"))) with st.expander(f"📝 Texto extraído de **{fname}**"): st.text(text[:5000] + ("..." if len(text) > 5000 else "")) elif operation == "🖼️ Extrair imagens": for fname, fobj in files_data.items(): imgs = engine.images(fobj["data"]) for img_name, img_data in imgs: results.append((f"{fname.replace('.pdf', '')}_{img_name}", img_data)) st.write(f"🔍 {len(imgs)} imagem(ns) extraída(s) de **{fname}**") elif operation == "🔍 Inspecionar metadados": for fname, fobj in files_data.items(): st.markdown(engine.get_info_string(fobj["info"])) progress.empty() # ── DISPONIBILIZA DOWNLOAD ────────────────────────────────────── if results: if len(results) == 1: nome, dados = results[0] st.download_button( label=f"⬇️ Download {nome}", data=dados, file_name=nome, mime="application/pdf" if nome.endswith(".pdf") else "text/plain", use_container_width=True, ) else: # Cria ZIP com todos os resultados zip_buf = io.BytesIO() with zipfile.ZipFile(zip_buf, "w", zipfile.ZIP_DEFLATED) as zf: for nome, dados in results: zf.writestr(nome, dados) zip_buf.seek(0) st.download_button( label=f"⬇️ Download ZIP ({len(results)} arquivo(s))", data=zip_buf, file_name="pdf-toolkit-resultados.zip", mime="application/zip", use_container_width=True, ) except Exception as e: progress.empty() st.error(f"❌ Erro durante o processamento: {str(e)}") st.exception(e)