114 lines
4.5 KiB
Python
114 lines
4.5 KiB
Python
"""PDF format guardrails for customer-facing document delivery.
|
|
|
|
ClientFlow normally receives Jasmin PDFs as opaque bytes. For customer-facing
|
|
invoice sends we still need one cheap safety check: invoices should be printable
|
|
A4, not a narrow receipt-like page returned by a wrong Jasmin print layout.
|
|
|
|
The parser is deliberately dependency-free. It reads common uncompressed page
|
|
boxes such as /MediaBox and /CropBox, which are present in PDFs returned by the
|
|
Jasmin print endpoints observed in production.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
from typing import Any, Dict, List
|
|
|
|
A4_PORTRAIT_PT = (595.0, 842.0)
|
|
POINT_TO_MM = 25.4 / 72.0
|
|
|
|
|
|
def _looks_a4(width_pt: float, height_pt: float, tolerance_pt: float = 20.0) -> bool:
|
|
pairs = [(width_pt, height_pt), (height_pt, width_pt)]
|
|
return any(
|
|
abs(w - A4_PORTRAIT_PT[0]) <= tolerance_pt and abs(h - A4_PORTRAIT_PT[1]) <= tolerance_pt
|
|
for w, h in pairs
|
|
)
|
|
|
|
|
|
def extract_pdf_boxes(data: bytes) -> List[Dict[str, Any]]:
|
|
latin = data.decode("latin-1", errors="ignore")
|
|
pattern = re.compile(
|
|
r"/(MediaBox|CropBox)\s*\[\s*([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s*\]",
|
|
re.I,
|
|
)
|
|
boxes: List[Dict[str, Any]] = []
|
|
seen: set[tuple[str, float, float, float, float]] = set()
|
|
for m in pattern.finditer(latin):
|
|
kind = m.group(1)
|
|
x0, y0, x1, y1 = (float(m.group(i)) for i in range(2, 6))
|
|
key = (kind.lower(), x0, y0, x1, y1)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
w = abs(x1 - x0)
|
|
h = abs(y1 - y0)
|
|
boxes.append({
|
|
"kind": kind,
|
|
"width_pt": round(w, 2),
|
|
"height_pt": round(h, 2),
|
|
"width_mm": round(w * POINT_TO_MM, 1),
|
|
"height_mm": round(h * POINT_TO_MM, 1),
|
|
"orientation": "landscape" if w > h else "portrait",
|
|
"looks_a4": _looks_a4(w, h),
|
|
})
|
|
return boxes
|
|
|
|
|
|
def analyze_pdf_format(data: bytes, content_type: str = "application/pdf") -> Dict[str, Any]:
|
|
info: Dict[str, Any] = {
|
|
"content_type": content_type,
|
|
"bytes": len(data or b""),
|
|
"sha256_12": hashlib.sha256(data or b"").hexdigest()[:12] if data else "",
|
|
"is_pdf_header": bool(data and data[:5] == b"%PDF-"),
|
|
"page_count_hint": None,
|
|
"boxes": [],
|
|
"format_warning": "",
|
|
}
|
|
if not data:
|
|
info["format_warning"] = "empty_response"
|
|
return info
|
|
if not info["is_pdf_header"]:
|
|
info["format_warning"] = "not_pdf_header"
|
|
return info
|
|
latin = data.decode("latin-1", errors="ignore")
|
|
page_count = len(re.findall(r"/Type\s*/Page(?!s)\b", latin))
|
|
info["page_count_hint"] = page_count or None
|
|
boxes = extract_pdf_boxes(data)
|
|
info["boxes"] = boxes
|
|
media_boxes = [b for b in boxes if str(b.get("kind") or "").lower() == "mediabox"]
|
|
if media_boxes and not any(bool(b.get("looks_a4")) for b in media_boxes):
|
|
info["format_warning"] = "non_a4_or_unexpected_mediabox"
|
|
elif len(data) < 15_000:
|
|
info["format_warning"] = "very_small_pdf"
|
|
elif len(data) > 2_500_000:
|
|
info["format_warning"] = "very_large_pdf"
|
|
return info
|
|
|
|
|
|
def first_box_label(info: Dict[str, Any]) -> str:
|
|
boxes = info.get("boxes") if isinstance(info, dict) else []
|
|
if not boxes:
|
|
return "sem MediaBox/CropBox legível"
|
|
b = boxes[0]
|
|
return f"{b.get('kind') or 'Box'} {b.get('width_mm')}x{b.get('height_mm')}mm a4={b.get('looks_a4')}"
|
|
|
|
|
|
def is_customer_send_blocking_pdf_warning(info: Dict[str, Any]) -> bool:
|
|
return str((info or {}).get("format_warning") or "") in {"not_pdf_header", "empty_response", "non_a4_or_unexpected_mediabox"}
|
|
|
|
|
|
def invoice_pdf_block_message(document_number: str, info: Dict[str, Any]) -> str:
|
|
warning = str((info or {}).get("format_warning") or "")
|
|
box = first_box_label(info)
|
|
if warning == "non_a4_or_unexpected_mediabox":
|
|
return (
|
|
f"PDF da fatura {document_number or ''} bloqueado: o Jasmin devolveu formato não-A4 "
|
|
f"({box}). Verificar layout/template de impressão da série/tipo FA no Jasmin antes de enviar ao cliente."
|
|
).strip()
|
|
if warning == "not_pdf_header":
|
|
return f"PDF da fatura {document_number or ''} bloqueado: o endpoint Jasmin não devolveu um PDF válido.".strip()
|
|
if warning == "empty_response":
|
|
return f"PDF da fatura {document_number or ''} bloqueado: resposta vazia do Jasmin.".strip()
|
|
return f"PDF da fatura {document_number or ''} bloqueado: {warning or 'formato inesperado'}.".strip()
|