Files
pdf-toolkit/core/pdfa.py
CM Consultor e97adca143 fix: compressão real p/ stream DCTDecode + auth c/ chave vazia
- 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'
2026-08-09 05:01:16 +00:00

174 lines
5.3 KiB
Python

"""
Detecção e manipulação de PDF/A.
PDF/A é uma versão padronizada do PDF para arquivamento de longo prazo.
Ele restringe certos recursos (JavaScript, fontes não-embutidas, etc.)
e adiciona metadados XMP específicos.
Este módulo permite:
1. Detectar se um PDF é PDF/A (e qual parte/versão).
2. Converter PDF/A → PDF comum (remove restrições).
3. Otimizar mantendo PDF/A (comprime imagens, limpa objetos).
"""
import io
import re
from typing import Optional
from pypdf import PdfReader, PdfWriter
# ── Detecção ────────────────────────────────────────────────────────
def _check_xmp_pdfa(xml_data: str) -> Optional[str]:
"""Retorna string tipo 'PDF/A-1b' se metadados XMP indicarem PDF/A."""
match = re.search(
r'(pdfaid:part[\'\"]?\s*[:=]\s*[\'\"]?(\d+)[\'\"]?.*?'
r'pdfaid:conformance[\'\"]?\s*[:=]\s*[\'\"]?([A-Za-z])[\'\"]?)',
xml_data,
re.DOTALL | re.IGNORECASE,
)
if match:
part = match.group(2)
conf = match.group(3).upper()
return f"PDF/A-{part}{conf}"
# Fallback mais simples
if "pdfa" in xml_data.lower():
return "PDF/A (indeterminado)"
return None
def _check_xref_pdfa(reader: PdfReader) -> bool:
"""
Verifica presença do dicionário /OutputIntents que é obrigatório
em PDF/A e raro em PDF comum.
"""
try:
catalog = reader.trailer.get("/Root", {})
if not catalog:
return False
output_intents = catalog.get("/OutputIntents", None)
return output_intents is not None and len(output_intents) > 0
except Exception:
return False
def detect_pdfa(data: bytes) -> dict:
"""
Detecta se o PDF é PDF/A e retorna detalhes.
Retorna:
- is_pdfa: bool
- level: str ou None (ex: "PDF/A-2b")
- method: str ("metadata", "output_intent" ou "none")
- confidence: float (0-1)
"""
result = {
"is_pdfa": False,
"level": None,
"method": "none",
"confidence": 0.0,
}
try:
reader = PdfReader(io.BytesIO(data))
# 1. Verifica metadados XMP
if reader.metadata and reader.metadata.raw_data:
raw = str(reader.metadata.raw_data)
level = _check_xmp_pdfa(raw)
if level:
result["is_pdfa"] = True
result["level"] = level
result["method"] = "metadata"
result["confidence"] = 0.9
return result
# 2. Verifica OutputIntents
if _check_xref_pdfa(reader):
result["is_pdfa"] = True
result["level"] = "PDF/A (OutputIntent)"
result["method"] = "output_intent"
result["confidence"] = 0.7
except Exception:
pass
return result
# ── Conversão PDF/A → PDF comum ─────────────────────────────────────
def strip_pdfa(data: bytes) -> bytes:
"""
Remove metadados PDF/A e restrições de arquivamento,
gerando um PDF comum padrão.
O que é removido:
- Metadados XMP com declaração PDF/A
- Dicionário /OutputIntents
- /PieceInfo (se presente)
- Restrições de /Legal e /Copyright
O conteúdo visual é preservado integralmente.
"""
reader = PdfReader(io.BytesIO(data))
writer = PdfWriter()
writer.metadata = reader.metadata
# Copia todas as páginas
for page in reader.pages:
writer.add_page(page)
# Remove XMP metadata que declara PDF/A
if writer.metadata:
# Substitui por metadados limpos (sem PDF/A)
clean_meta = {}
for k, v in reader.metadata.raw_items():
if not any(x in str(k).lower() for x in ["pdfaid", "xmp", "adobe:nsmeta"]):
clean_meta[k] = v
writer.metadata = type(reader.metadata)(clean_meta)
out_buf = io.BytesIO()
writer.write_stream(out_buf)
return out_buf.getvalue()
# ── Otimização mantendo PDF/A ───────────────────────────────────────
def compress_pdfa(data: bytes, quality: int = 60, max_dim: int = 2000) -> bytes:
"""
Comprime imagens internas mantendo a estrutura PDF/A.
Usa a mesma lógica de compress_pdf (ImageFile.replace)
mas preserva metadados XMP e OutputIntents.
"""
from PIL import Image as PILImage
from .compress import _prepare_image
reader = PdfReader(io.BytesIO(data))
writer = PdfWriter(clone_from=reader)
replaced = 0
for page in writer.pages:
try:
images = list(page.images)
except Exception:
continue
for img_file in images:
if getattr(img_file, "indirect_reference", None) is None:
continue
try:
pil = img_file.image
if pil is None:
continue
prepared, save_kwargs = _prepare_image(
pil, quality=quality, max_dim=max_dim
)
img_file.replace(prepared, **save_kwargs)
replaced += 1
except Exception:
pass
out_buf = io.BytesIO()
writer.write(out_buf)
return out_buf.getvalue()