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)
199 lines
6.3 KiB
Python
199 lines
6.3 KiB
Python
"""
|
|
Operações gerais sobre PDFs: merge, split, rotate, extração de
|
|
páginas/texto/imagens.
|
|
"""
|
|
|
|
import io
|
|
from typing import List, Optional, Tuple
|
|
|
|
from PIL import Image
|
|
from pypdf import PdfReader, PdfWriter, PageObject
|
|
|
|
|
|
# ── Merge ───────────────────────────────────────────────────────────
|
|
|
|
def merge_pdfs(files_data: List[Tuple[bytes, str]]) -> bytes:
|
|
"""
|
|
Junta múltiplos PDFs em um único arquivo, na ordem fornecida.
|
|
|
|
Args:
|
|
files_data: Lista de (conteúdo_bytes, nome_arquivo).
|
|
|
|
Retorna bytes do PDF mesclado.
|
|
"""
|
|
writer = PdfWriter()
|
|
|
|
for data, fname in files_data:
|
|
reader = PdfReader(io.BytesIO(data))
|
|
for page in reader.pages:
|
|
writer.add_page(page)
|
|
|
|
out_buf = io.BytesIO()
|
|
writer.write_stream(out_buf)
|
|
return out_buf.getvalue()
|
|
|
|
|
|
# ── Split ───────────────────────────────────────────────────────────
|
|
|
|
def split_pdf(
|
|
data: bytes,
|
|
page_ranges: List[Tuple[int, int]],
|
|
filename_base: str = "pagina",
|
|
) -> List[Tuple[str, bytes]]:
|
|
"""
|
|
Extrai intervalos de páginas de um PDF.
|
|
|
|
Args:
|
|
data: Conteúdo do PDF.
|
|
page_ranges: Lista de (inicio, fim), 0-indexado. Ex: [(0,2), (5,7)].
|
|
filename_base: Base para nomes.
|
|
|
|
Retorna lista de (nome, bytes).
|
|
"""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
total = len(reader.pages)
|
|
results = []
|
|
|
|
for i, (start, end) in enumerate(page_ranges):
|
|
writer = PdfWriter()
|
|
writer.metadata = reader.metadata
|
|
for idx in range(start, min(end + 1, total)):
|
|
writer.add_page(reader.pages[idx])
|
|
|
|
out_buf = io.BytesIO()
|
|
writer.write_stream(out_buf)
|
|
label = f"{filename_base}_p{start+1}-{min(end,total-1)+1}.pdf"
|
|
results.append((label, out_buf.getvalue()))
|
|
|
|
return results
|
|
|
|
|
|
def extract_pages(
|
|
data: bytes,
|
|
pages: List[int],
|
|
) -> bytes:
|
|
"""
|
|
Extrai páginas específicas de um PDF e retorna um novo PDF
|
|
contendo apenas essas páginas (na ordem fornecida).
|
|
|
|
Args:
|
|
data: Conteúdo do PDF.
|
|
pages: Lista de índices de página (0-indexado).
|
|
|
|
Retorna bytes do novo PDF.
|
|
"""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
writer = PdfWriter()
|
|
writer.metadata = reader.metadata
|
|
for idx in pages:
|
|
if idx < len(reader.pages):
|
|
writer.add_page(reader.pages[idx])
|
|
out_buf = io.BytesIO()
|
|
writer.write_stream(out_buf)
|
|
return out_buf.getvalue()
|
|
|
|
|
|
# ── Rotação ─────────────────────────────────────────────────────────
|
|
|
|
def rotate_pages(
|
|
data: bytes,
|
|
rotations: List[Tuple[int, int]],
|
|
) -> bytes:
|
|
"""
|
|
Rotaciona páginas específicas de um PDF.
|
|
|
|
Args:
|
|
data: Conteúdo do PDF.
|
|
rotations: Lista de (pagina_idx, angulo_em_graus).
|
|
Angulo = 90, 180, 270 (sentido anti-horário).
|
|
|
|
Retorna bytes do PDF rotacionado.
|
|
"""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
writer = PdfWriter()
|
|
writer.metadata = reader.metadata
|
|
|
|
rot_map = {p: a for p, a in rotations}
|
|
|
|
for i, page in enumerate(reader.pages):
|
|
if i in rot_map:
|
|
page.rotate(rot_map[i])
|
|
writer.add_page(page)
|
|
|
|
out_buf = io.BytesIO()
|
|
writer.write_stream(out_buf)
|
|
return out_buf.getvalue()
|
|
|
|
|
|
# ── Extração de texto ──────────────────────────────────────────────
|
|
|
|
def extract_text(data: bytes) -> str:
|
|
"""Extrai texto de todas as páginas do PDF."""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
pages_text = []
|
|
for page in reader.pages:
|
|
text = page.extract_text()
|
|
if text:
|
|
pages_text.append(text)
|
|
return "\n".join(pages_text)
|
|
|
|
|
|
# ── Extração de imagens ────────────────────────────────────────────
|
|
|
|
def extract_images(data: bytes) -> List[Tuple[str, bytes]]:
|
|
"""
|
|
Extrai todas as imagens de um PDF.
|
|
|
|
Retorna lista de (nome_extraido, bytes_imagem).
|
|
"""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
images = []
|
|
counter = 0
|
|
|
|
for page_num, page in enumerate(reader.pages):
|
|
if "/XObject" not in page.get("/Resources", {}):
|
|
continue
|
|
xobjects = page["/Resources"]["/XObject"]
|
|
for obj_name, xobj in xobjects.items():
|
|
if xobj.get("/Subtype") == "/Image":
|
|
try:
|
|
raw = xobj.get_data()
|
|
fmt = xobj.get("/Filter", "")
|
|
ext = ".jpg" if "/DCTDecode" in str(fmt) else ".png"
|
|
name = f"img_p{page_num+1}_{obj_name.strip('/')}{ext}"
|
|
images.append((name, raw))
|
|
counter += 1
|
|
except Exception:
|
|
pass
|
|
|
|
return images
|
|
|
|
|
|
# ── Preview de páginas (como imagens) ──────────────────────────────
|
|
|
|
def preview_pages(
|
|
data: bytes,
|
|
max_pages: int = 10,
|
|
dpi: int = 100,
|
|
) -> List[Image.Image]:
|
|
"""
|
|
Gera miniaturas/thumbnails das primeiras páginas de um PDF.
|
|
|
|
NOTA: pypdf não renderiza páginas; este método retorna uma
|
|
imagem em branco com o número da página e o tamanho.
|
|
Para preview real, seria necessário pdf2image (poppler).
|
|
|
|
Esta implementação serve como placeholder — o Streamlit
|
|
usará pypdf para mostrar metadados e contagem de páginas,
|
|
enquanto a renderização real depende de poppler-utils.
|
|
"""
|
|
reader = PdfReader(io.BytesIO(data))
|
|
previews = []
|
|
for i, page in enumerate(reader.pages[:max_pages]):
|
|
# Cria uma imagem placeholder com dimensões proporcionais
|
|
w = int(page.mediabox.width) if page.mediabox else 595
|
|
h = int(page.mediabox.height) if page.mediabox else 842
|
|
scale = min(400 / w, 500 / h)
|
|
img = Image.new("RGB", (int(w * scale), int(h * scale)), (245, 245, 245))
|
|
previews.append(img)
|
|
return previews |