"""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()