"""Real-PDF honeydoc generator (uses :mod:`pikepdf`). Builds a one-page PDF with the same Q3-review body as the HTML/DOCX flavors and installs an ``/OpenAction`` ``/URI`` action on the catalog so most viewers fire the callback the moment the document opens. Pikepdf is now a hard dependency for this generator (the operator installed it explicitly so we can use it). We still surface a clear :class:`InstrumenterRejectedError` when imports fail, so a deployment without pikepdf can fall back to the DOCX or HTML generators rather than crashing the API. """ from __future__ import annotations import io from decnet.canary.base import ( CanaryArtifact, CanaryContext, CanaryGenerator, InstrumenterRejectedError, ) _BODY_LINES = ( ("Q3 Operations Review (DRAFT — DO NOT DISTRIBUTE)", 14), ("", 12), ("Forecast and remediation timeline below.", 11), ("Numbers are preliminary, subject to revision.", 11), ("", 12), ("Region Incidents MTTR (h)", 11), ("us-east 14 3.2", 11), ("us-west 9 4.7", 11), ("eu-central 22 2.1", 11), ("", 12), ("Internal contact: secops@internal", 11), ) class HoneydocPdfGenerator(CanaryGenerator): name = "honeydoc_pdf" def generate(self, ctx: CanaryContext) -> CanaryArtifact: try: from pikepdf import Pdf, Name, Dictionary, String except ImportError as e: raise InstrumenterRejectedError( "honeydoc_pdf requires pikepdf; install it (`pip install " "pikepdf`) or pick honeydoc / honeydoc_docx instead." ) from e url = f"{ctx.http_base.rstrip('/')}/c/{ctx.callback_token}" pdf = Pdf.new() # Helvetica is one of the 14 PDF base fonts — every viewer ships # it, so no font embedding is required. font = pdf.make_indirect(Dictionary( Type=Name("/Font"), Subtype=Name("/Type1"), BaseFont=Name("/Helvetica"), )) # Build a single content stream that writes each body line at a # decreasing y-coordinate. PDF coordinates start at the bottom- # left (US Letter = 612 x 792 points); we lay out lines roughly # 18 points apart starting near the top. ops: list[str] = ["BT /F1 12 Tf 72 750 Td"] first = True for line, size in _BODY_LINES: if not first: ops.append("0 -18 Td") first = False ops.append(f"/F1 {size} Tf") ops.append(f"({_pdf_escape(line)}) Tj") ops.append("ET") content_bytes = "\n".join(ops).encode("latin-1") content_stream = pdf.make_stream(content_bytes) page = pdf.add_blank_page(page_size=(612, 792)) page[Name("/Resources")] = Dictionary( Font=Dictionary(F1=font), ) page[Name("/Contents")] = content_stream # OpenAction fires the URI when the file is opened in Acrobat, # Preview, the browser PDF viewer, etc. Most viewers prompt # before fetching; that prompt itself is a tell, and an # auto-allow viewer fetches silently. pdf.Root[Name("/OpenAction")] = Dictionary( Type=Name("/Action"), S=Name("/URI"), URI=String(url), ) out = io.BytesIO() pdf.save(out) return CanaryArtifact( path="", content=out.getvalue(), mode=0o644, mtime_offset=-86400 * 21, generator=self.name, notes=[ "synthesised one-page PDF with realistic Q3 review body", f"/OpenAction /URI -> {url}", ], ) def _pdf_escape(s: str) -> str: """Escape parens and backslashes for PDF literal-string syntax. PDF string literals are wrapped in ``( … )``; inner ``(``, ``)``, and ``\\`` need backslash escapes. Everything else (including UTF-8 multibyte sequences) round-trips fine because Helvetica's encoding is WinAnsi-ish — we'll lose exotic glyphs but the realistic body sticks to ASCII anyway. Em-dashes are downgraded to ``--`` to avoid the WinAnsi gap. """ return ( s.replace("\\", r"\\") .replace("(", r"\(") .replace(")", r"\)") .replace("—", "--") )