""" pdf-toolkit API — FastAPI com documentação OpenAPI automática. Endpoints REST para todas as operações do toolkit. Documentação interativa disponível em /docs (Swagger) e /redoc. Uso: uvicorn api:app --host 0.0.0.0 --port 8555 --reload """ import io import zipfile from typing import List, Optional from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Query from fastapi.responses import Response, StreamingResponse from fastapi.middleware.cors import CORSMiddleware from config import API_TITLE, API_VERSION, API_DESCRIPTION, MAX_UPLOAD_SIZE_BYTES, QUALITY_PRESETS from core.engine import PDFEngine engine = PDFEngine() # ── App ───────────────────────────────────────────────────────────── app = FastAPI( title=API_TITLE, version=API_VERSION, description=API_DESCRIPTION, docs_url="/docs", redoc_url="/redoc", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Helpers ───────────────────────────────────────────────────────── def _read_file(file: UploadFile) -> bytes: data = file.file.read() if len(data) > MAX_UPLOAD_SIZE_BYTES: raise HTTPException(413, f"Arquivo excede o limite de {MAX_UPLOAD_SIZE_BYTES / 1024 / 1024:.0f} MB.") return data def _pdf_response(data: bytes, filename: str) -> Response: return Response( content=data, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) def _zip_response(files: List[tuple], zip_name: str = "resultados.zip") -> StreamingResponse: buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: for nome, dados in files: zf.writestr(nome, dados) buf.seek(0) return StreamingResponse( buf, media_type="application/zip", headers={"Content-Disposition": f'attachment; filename="{zip_name}"'}, ) # ── Endpoint Raiz ─────────────────────────────────────────────────── @app.get("/") def root(): return { "app": API_TITLE, "version": API_VERSION, "docs": "/docs", "redoc": "/redoc", } # ── Inspect ───────────────────────────────────────────────────────── @app.post("/inspect", summary="Inspeciona metadados de um PDF") def inspect(file: UploadFile = File(...)): """ Retorna metadados detalhados do PDF: número de páginas, tamanho, detecção de PDF/A, validade. """ data = _read_file(file) info = engine.inspect(data, file.filename or "documento.pdf") if not info["valid"]: raise HTTPException(400, info["error"]) return info # ── Compress ──────────────────────────────────────────────────────── @app.post("/compress", summary="Comprime um PDF") def compress( file: UploadFile = File(...), preset: str = Form("ebook"), ): """ Comprime um PDF re-amostrando imagens. - **preset**: screen | ebook | printer | prepress """ if preset not in QUALITY_PRESETS: raise HTTPException(400, f"Preset inválido. Opções: {', '.join(QUALITY_PRESETS.keys())}") data = _read_file(file) result = engine.compress(data, preset=preset) out_name = (file.filename or "documento.pdf").replace(".pdf", f"_comprimido.pdf") return _pdf_response(result, out_name) # ── PDF/A → PDF comum ────────────────────────────────────────────── @app.post("/pdfa-to-regular", summary="Converte PDF/A para PDF comum") def pdfa_to_regular(file: UploadFile = File(...)): """Remove restrições PDF/A gerando um PDF comum padrão.""" data = _read_file(file) info = engine.inspect(data) if info.get("is_pdfa"): result = engine.pdfa_to_regular(data) else: result = data # já é PDF comum out_name = (file.filename or "documento.pdf").replace(".pdf", "_comum.pdf") return _pdf_response(result, out_name) # ── Compress PDF/A ───────────────────────────────────────────────── @app.post("/compress-pdfa", summary="Comprime mantendo PDF/A") def compress_pdfa( file: UploadFile = File(...), quality: int = Form(60, ge=10, le=100), ): """Comprime as imagens internas preservando a estrutura PDF/A.""" data = _read_file(file) result = engine.compress_pdfa(data, quality=quality) out_name = (file.filename or "documento.pdf").replace(".pdf", "_otimizado.pdf") return _pdf_response(result, out_name) # ── Merge ─────────────────────────────────────────────────────────── @app.post("/merge", summary="Junta múltiplos PDFs em um") async def merge(files: List[UploadFile] = File(...)): """Mescla 2 ou mais PDFs em ordem, retornando um único PDF.""" if len(files) < 2: raise HTTPException(400, "Informe ao menos 2 PDFs.") inputs = [] for f in files: data = _read_file(f) inputs.append((data, f.filename or "documento.pdf")) result = engine.merge(inputs) return _pdf_response(result, "merged.pdf") # ── Split ─────────────────────────────────────────────────────────── @app.post("/split", summary="Extrai intervalos de páginas de um PDF") def split( file: UploadFile = File(...), ranges: str = Form(..., description='Ex: "1-3,5,7-9"'), ): """ Divide um PDF em múltiplos PDFs baseado em intervalos de páginas. - **ranges**: string com intervalos. Ex: "1-3,5,7-9" (1-indexado) """ data = _read_file(file) reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data)) total = len(reader.pages) parts = ranges.replace(" ", "").split(",") page_ranges = [] for part in parts: if "-" in part: a, b = part.split("-") start = max(0, int(a) - 1) end = min(int(b) - 1, total - 1) page_ranges.append((start, end)) else: idx = int(part) - 1 if 0 <= idx < total: page_ranges.append((idx, idx)) if not page_ranges: raise HTTPException(400, "Nenhum intervalo válido informado.") base = (file.filename or "pagina").replace(".pdf", "") results = engine.split(data, page_ranges, base) return _zip_response(results, "split.zip") # ── Extract pages ────────────────────────────────────────────────── @app.post("/extract-pages", summary="Extrai páginas específicas") def extract_pages( file: UploadFile = File(...), pages: str = Form(..., description='Ex: "1,3,5" (1-indexado)'), ): """ Extrai páginas específicas de um PDF em ordem. """ data = _read_file(file) reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data)) total = len(reader.pages) indices = [] for p in pages.replace(" ", "").split(","): p = p.strip() if not p: continue idx = int(p) - 1 if 0 <= idx < total: indices.append(idx) if not indices: raise HTTPException(400, "Nenhuma página válida informada.") result = engine.extract(data, indices) out_name = (file.filename or "documento.pdf").replace(".pdf", "_extraido.pdf") return _pdf_response(result, out_name) # ── Rotate ────────────────────────────────────────────────────────── @app.post("/rotate", summary="Rotaciona páginas de um PDF") def rotate( file: UploadFile = File(...), pages: str = Form(..., description='Páginas: "1,3,5" ou "todas"'), angle: int = Form(90, ge=90, le=270), ): """Rotaciona páginas em 90, 180 ou 270 graus.""" data = _read_file(file) reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data)) total = len(reader.pages) if pages.lower() in ("", "todas", "all"): page_indices = list(range(total)) else: page_indices = [int(p.strip()) - 1 for p in pages.replace(" ", "").split(",")] page_indices = [p for p in page_indices if 0 <= p < total] rotations = [(p, angle) for p in page_indices] result = engine.rotate(data, rotations) out_name = (file.filename or "documento.pdf").replace(".pdf", "_rotacionado.pdf") return _pdf_response(result, out_name) # ── Extract text ──────────────────────────────────────────────────── @app.post("/extract-text", summary="Extrai texto de um PDF") def extract_text_endpoint(file: UploadFile = File(...)): """Extrai todo o texto legível de um PDF e retorna como .txt.""" data = _read_file(file) text = engine.text(data) return Response( content=text, media_type="text/plain; charset=utf-8", headers={ "Content-Disposition": f'attachment; filename="{file.filename or "documento"}.txt"' }, ) # ── Extract images ────────────────────────────────────────────────── @app.post("/extract-images", summary="Extrai imagens de um PDF como ZIP") def extract_images_endpoint(file: UploadFile = File(...)): """Extrai todas as imagens embutidas no PDF e retorna um ZIP.""" data = _read_file(file) images = engine.images(data) if not images: raise HTTPException(404, "Nenhuma imagem encontrada neste PDF.") return _zip_response(images, "imagens.zip") # ── Health check ──────────────────────────────────────────────────── @app.get("/health") def health(): """Health check simples.""" return {"status": "ok", "version": API_VERSION} # ── Entrypoint ────────────────────────────────────────────────────── if __name__ == "__main__": import uvicorn uvicorn.run("api:app", host="0.0.0.0", port=8555, reload=True)