Files
pdf-toolkit/core/operations.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

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