Files
pdf-toolkit/app.py
CM Consultor 84d4a1eab5 feat: initial release — pdf-toolkit v1.0.0
Core operations:
- Compress PDF (3 presets: screen, ebook, printer, prepress)
- PDF/A detection and conversion to regular PDF
- Compress while preserving PDF/A structure
- Merge multiple PDFs
- Split/extract pages by range
- Rotate pages
- Extract text and images

Interface:
- Streamlit web GUI (app.py)
- FastAPI REST API with auto OpenAPI docs (api.py)

Docs:
- README.md, ARCHITECTURE.md, API.md
- Unit tests (15/15 passing)
2026-08-09 02:15:22 +00:00

288 lines
11 KiB
Python

"""
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(10, "Comprimindo...")
for i, (fname, fobj) in enumerate(files_data.items()):
result_data = engine.compress(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))
progress.progress((i + 1) / len(files_data) * 100,
f"{fname}: {orig_mb}MB → {new_mb}MB ({reduction}% redução)")
elif operation == "📄 PDF/A → PDF comum":
progress.progress(10, "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) * 100)
elif operation == "📋 Comprimir (mantendo PDF/A)":
progress.progress(10, "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
result_data = engine.compress(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) * 100)
elif operation == "🔗 Merge (juntar PDFs)":
progress.progress(30, "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(100, "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(30, "Extraindo páginas...")
splitted = engine.split(fobj["data"], ranges, fname.replace(".pdf", ""))
for nome, data in splitted:
results.append((nome, data))
progress.progress(100, "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(50, "Rotacionando...")
result_data = engine.rotate(fobj["data"], rotations)
results.append((fname.replace(".pdf", f"_rotacionado.pdf"), result_data))
progress.progress(100)
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)