- Substitui xobj._data por page.images[].replace() em compress.py e pdfa.py - Adiciona compress_with_stats() com feedback honesto na UI - Trata PDFTOOLKIT_API_KEY vazia como modo dev (auth desabilitado) - Atualiza engine.py e app.py para exibir redução real ou 'já otimizado'
115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
"""
|
|
Orquestrador principal de transformações de PDF.
|
|
|
|
Coordena validação, compressão, operações e conversão PDF/A
|
|
encadeando as funções dos submódulos.
|
|
"""
|
|
|
|
import io
|
|
from typing import List, Tuple
|
|
|
|
from .validators import validate_pdf, assert_safe_filename
|
|
from .compress import compress_pdf, compress_pdf_with_stats
|
|
from .pdfa import detect_pdfa, strip_pdfa, compress_pdfa
|
|
from .operations import merge_pdfs, split_pdf, extract_pages, rotate_pages
|
|
from .operations import extract_text, extract_images, preview_pages
|
|
|
|
|
|
class PDFEngine:
|
|
"""
|
|
Interface única para todas as operações do toolkit.
|
|
Cada método recebe bytes e retorna bytes (ou metadados).
|
|
"""
|
|
|
|
@staticmethod
|
|
def inspect(data: bytes, filename: str = "") -> dict:
|
|
"""Valida e retorna metadados do PDF."""
|
|
info = validate_pdf(data, filename)
|
|
if info["valid"]:
|
|
pdfa_info = detect_pdfa(data)
|
|
info.update(pdfa_info)
|
|
return info
|
|
|
|
@staticmethod
|
|
def compress(data: bytes, preset: str = "ebook") -> bytes:
|
|
"""Comprime PDF sem retornar um resultado maior que o original.
|
|
|
|
A regravação com pypdf pode aumentar PDFs que já estão comprimidos
|
|
ou que não possuem imagens adequadas para reamostragem. Nesse caso,
|
|
preservamos o original em vez de entregar um arquivo maior.
|
|
"""
|
|
result, stats = compress_pdf_with_stats(data, preset=preset)
|
|
if stats["reduced"]:
|
|
return result
|
|
return data
|
|
|
|
@staticmethod
|
|
def compress_with_stats(data: bytes, preset: str = "ebook") -> Tuple[bytes, dict]:
|
|
"""Comprime e retorna (bytes, estatísticas) para feedback na UI."""
|
|
result, stats = compress_pdf_with_stats(data, preset=preset)
|
|
if stats["reduced"]:
|
|
return result, stats
|
|
return data, stats
|
|
|
|
@staticmethod
|
|
def pdfa_to_regular(data: bytes) -> bytes:
|
|
"""Remove restrições PDF/A gerando PDF comum."""
|
|
return strip_pdfa(data)
|
|
|
|
@staticmethod
|
|
def compress_pdfa(data: bytes, quality: int = 60) -> bytes:
|
|
"""Comprime mantendo a estrutura PDF/A."""
|
|
return compress_pdfa(data, quality=quality)
|
|
|
|
@staticmethod
|
|
def detect_pdfa(data: bytes) -> dict:
|
|
"""Detecta se é PDF/A e qual nível."""
|
|
return detect_pdfa(data)
|
|
|
|
@staticmethod
|
|
def merge(files: List[Tuple[bytes, str]]) -> bytes:
|
|
"""Junta múltiplos PDFs."""
|
|
return merge_pdfs(files)
|
|
|
|
@staticmethod
|
|
def split(data: bytes, ranges: List[Tuple[int, int]], base: str = "pagina"):
|
|
"""Divide PDF em intervalos de páginas."""
|
|
return split_pdf(data, ranges, base)
|
|
|
|
@staticmethod
|
|
def extract(data: bytes, pages: List[int]) -> bytes:
|
|
"""Extrai páginas específicas."""
|
|
return extract_pages(data, pages)
|
|
|
|
@staticmethod
|
|
def rotate(data: bytes, rotations: List[Tuple[int, int]]) -> bytes:
|
|
"""Rotaciona páginas."""
|
|
return rotate_pages(data, rotations)
|
|
|
|
@staticmethod
|
|
def text(data: bytes) -> str:
|
|
"""Extrai texto de todas as páginas."""
|
|
return extract_text(data)
|
|
|
|
@staticmethod
|
|
def images(data: bytes):
|
|
"""Extrai imagens do PDF."""
|
|
return extract_images(data)
|
|
|
|
@staticmethod
|
|
def get_info_string(info: dict) -> str:
|
|
"""Formata metadados para exibição."""
|
|
lines = [
|
|
f"📄 **{info.get('filename', 'documento')}**",
|
|
f" - Tamanho: {info['file_size_mb']:.2f} MB",
|
|
f" - Páginas: {info['pages']}",
|
|
]
|
|
if info.get("valid"):
|
|
if info["is_pdfa"]:
|
|
level = info.get("level", "PDF/A")
|
|
lines.append(f" - 📋 Tipo: {level}")
|
|
else:
|
|
lines.append(" - 📋 Tipo: PDF comum")
|
|
else:
|
|
lines.append(f" - ⚠️ Erro: {info['error']}")
|
|
return "\n".join(lines) |