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)
This commit is contained in:
CM Consultor
2026-08-09 02:15:22 +00:00
commit 84d4a1eab5
17 changed files with 1998 additions and 0 deletions

143
tests/test_core.py Normal file
View File

@@ -0,0 +1,143 @@
"""
Testes unitários do core do pdf-toolkit.
"""
import io
from pathlib import Path
import pytest
from PIL import Image
from core.validators import validate_pdf, is_pdf_stream, is_pdf_file
from core.engine import PDFEngine
# ── Helpers ─────────────────────────────────────────────────────────
def make_dummy_pdf() -> bytes:
"""Gera um PDF mínimo válido para testes."""
return (
b"%PDF-1.4\n"
b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]\n"
b" /Resources << /Font << /F1 4 0 R >> >>\n"
b" /Contents 5 0 R\n>>\nendobj\n"
b"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"
b"5 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hello) Tj ET\nendstream\nendobj\n"
b"xref\n0 6\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000266 00000 n \n0000000340 00000 n \n"
b"trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n462\n%%EOF\n"
)
engine = PDFEngine()
# ── Tests ───────────────────────────────────────────────────────────
class TestCompress:
def test_compress_ebook(self):
pdf = make_dummy_pdf()
result = engine.compress(pdf, preset="ebook")
assert isinstance(result, bytes)
assert result[:4] == b"%PDF"
# PDF mínimo não tem imagens, então tamanho fica similar
# mas a estrutura deve ser válida
info = engine.inspect(result)
assert info["valid"] is True
def test_compress_screen(self):
pdf = make_dummy_pdf()
result = engine.compress(pdf, preset="screen")
assert isinstance(result, bytes)
assert result[:4] == b"%PDF"
def test_compress_invalid_preset(self):
pdf = make_dummy_pdf()
# Preset inválido usa o padrão (ebook) sem levantar erro
# O comportamento é fallback seguro
result = engine.compress(pdf, preset="invalid")
assert isinstance(result, bytes)
assert result[:4] == b"%PDF"
class TestInspect:
def test_inspect_valid_pdf(self):
pdf = make_dummy_pdf()
info = engine.inspect(pdf, "test.pdf")
assert info["valid"] is True
assert info["pages"] == 1
assert info["file_size_mb"] >= 0
assert info["filename"] == "test.pdf"
def test_inspect_invalid_data(self):
info = engine.inspect(b"not a pdf", "fake.pdf")
assert info["valid"] is False
assert info["error"] != ""
class TestPdfa:
def test_detect_pdfa_returns_dict(self):
pdf = make_dummy_pdf()
result = engine.detect_pdfa(pdf)
assert isinstance(result, dict)
assert "is_pdfa" in result
assert "level" in result
assert "method" in result
def test_pdfa_to_regular_noop_on_common(self):
pdf = make_dummy_pdf()
result = engine.pdfa_to_regular(pdf)
assert isinstance(result, bytes)
assert result[:4] == b"%PDF"
def test_compress_pdfa_preserves_structure(self):
pdf = make_dummy_pdf()
result = engine.compress_pdfa(pdf, quality=50)
assert isinstance(result, bytes)
assert result[:4] == b"%PDF"
class TestOperations:
def test_merge_single(self):
pdf = make_dummy_pdf()
result = engine.merge([(pdf, "a.pdf")])
assert result[:4] == b"%PDF"
info = engine.inspect(result)
assert info["pages"] == 1
def test_merge_multiple(self):
pdf = make_dummy_pdf()
result = engine.merge([(pdf, "a.pdf"), (pdf, "b.pdf")])
info = engine.inspect(result)
assert info["pages"] == 2
def test_split(self):
pdf = make_dummy_pdf()
results = engine.split(pdf, [(0, 0)], "teste")
assert len(results) == 1
nome, dados = results[0]
assert "teste" in nome
assert dados[:4] == b"%PDF"
def test_extract_pages(self):
pdf = make_dummy_pdf()
result = engine.extract(pdf, [0])
assert result[:4] == b"%PDF"
info = engine.inspect(result)
assert info["pages"] == 1
def test_rotate(self):
pdf = make_dummy_pdf()
result = engine.rotate(pdf, [(0, 90)])
assert result[:4] == b"%PDF"
def test_extract_text(self):
pdf = make_dummy_pdf()
text = engine.text(pdf)
assert "Hello" in text
def test_extract_images_empty(self):
pdf = make_dummy_pdf()
imgs = engine.images(pdf)
assert imgs == []