dodil/erp-brick-contracts

src/brick_contracts/infrastructure/renderer.py

"""The Renderer adapter (the domain's port): contract words → PDF bytes, and the executed copy = the signed version's
EXACT bytes + a certificate page, appended as a PDF incremental update.

WHY A SMALL WRITER OF OUR OWN. An earlier version rendered with WeasyPrint (+99 MB of system libraries). No pure-Python PDF
library ships with the platform image, and a contract needs only text: so this is ~150 lines of PDF 1.4 — Helvetica
(a standard font every reader has), A4, wrapped text, no images, no timestamps, so the same words give the same
bytes. Text is encoded WinAnsi (cp1252): a character outside it prints as '?' in the PDF (the signing page and the
frozen text_body keep the exact words; the sha256 covers the PDF bytes either way).

THE EXECUTED COPY KEEPS THE SIGNED BYTES. The certificate page is appended as an incremental update (new objects, a
new page tree, an xref section pointing back with /Prev), so the executed file STARTS WITH the frozen version byte
for byte: anyone can check that sha256(executed[:len(version)]) is the version's sha256 printed on the certificate.
"""
import re
import textwrap

PAGE_W, PAGE_H = 595, 842                    # A4 in points
MARGIN, SIZE, LEADING = 56, 10, 13
CHARS = 92                                   # a conservative line for 10 pt Helvetica in 483 pt
LINES = (PAGE_H - 2 * MARGIN) // LEADING


def _esc(s: str) -> bytes:
    b = s.encode("cp1252", "replace")
    return b.replace(b"\\", b"\\\\").replace(b"(", b"\\(").replace(b")", b"\\)")


def _wrap(text: str) -> list:
    out = []
    for para in (text or "").replace("\r\n", "\n").split("\n"):
        if not para.strip():
            out.append("")
            continue
        indent = len(para) - len(para.lstrip(" "))
        out += textwrap.wrap(para, CHARS, subsequent_indent=" " * indent, break_long_words=True,
                             replace_whitespace=False) or [""]
    return out


def _pages(text: str) -> list:
    lines = _wrap(text)
    return [lines[i:i + LINES] for i in range(0, max(len(lines), 1), LINES)] or [[]]


def _content(lines: list, footer: str) -> bytes:
    parts = [b"BT", f"/F1 {SIZE} Tf".encode(), f"{LEADING} TL".encode(),
             f"{MARGIN} {PAGE_H - MARGIN} Td".encode()]
    for ln in lines:
        parts.append(b"(" + _esc(ln) + b") Tj T*")
    parts.append(b"ET")
    if footer:
        parts += [b"BT", b"/F1 8 Tf", f"{MARGIN} {MARGIN // 2} Td".encode(), b"(" + _esc(footer) + b") Tj", b"ET"]
    return b"\n".join(parts)


def _obj(n: int, body: bytes) -> bytes:
    return f"{n} 0 obj\n".encode() + body + b"\nendobj\n"


def _stream(data: bytes) -> bytes:
    return f"<< /Length {len(data)} >>\nstream\n".encode() + data + b"\nendstream"


def render(text: str, *, footer: str = "") -> bytes:
    """A PDF of the words: objects 1 catalog, 2 page tree, 3 font, then (page, content) pairs."""
    pages = _pages(text)
    objs = [b"<< /Type /Catalog /Pages 2 0 R >>", None,
            b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>"]
    kids = []
    for i, lines in enumerate(pages):
        page_no, content_no = 4 + 2 * i, 5 + 2 * i
        kids.append(f"{page_no} 0 R")
        objs.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {PAGE_W} {PAGE_H}] /Resources << /Font << /F1 3 0 R "
                    f">> >> /Contents {content_no} 0 R >>".encode())
        objs.append(_stream(_content(lines, f"{footer}   page {i + 1} of {len(pages)}".strip())))
    objs[1] = f"<< /Type /Pages /Kids [{' '.join(kids)}] /Count {len(kids)} >>".encode()
    out = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = []
    for n, body in enumerate(objs, start=1):
        offsets.append(len(out))
        out += _obj(n, body)
    xref = len(out)
    out += f"xref\n0 {len(objs) + 1}\n0000000000 65535 f \n".encode()
    for off in offsets:
        out += f"{off:010d} 00000 n \n".encode()
    out += f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
    return bytes(out)


_STARTXREF = re.compile(rb"startxref\s+(\d+)\s+%%EOF\s*$")
_SIZE = re.compile(rb"/Size\s+(\d+)")
_KIDS = re.compile(rb"2 0 obj\s*<<\s*/Type /Pages /Kids \[([^\]]*)\]\s*/Count (\d+)\s*>>")


def append(pdf: bytes, text: str, *, footer: str = "") -> bytes:
    """`pdf` (one this module wrote) + pages of `text`, as an incremental update: the original bytes are kept as they
    are, at the start of the result."""
    m = _STARTXREF.search(pdf)
    sizes = _SIZE.findall(pdf)
    kids_all = _KIDS.findall(pdf)
    if not m or not sizes or not kids_all:
        raise ValueError("not a PDF this renderer wrote")
    prev, size = int(m.group(1)), int(sizes[-1])
    kids = kids_all[-1][0].decode().split()
    kids = [" ".join(kids[i:i + 3]) for i in range(0, len(kids), 3)]
    pages = _pages(text)
    total = len(kids) + len(pages)
    out = bytearray(pdf if pdf.endswith(b"\n") else pdf + b"\n")
    offsets = {}
    n = size
    new_kids = []
    for i, lines in enumerate(pages):
        page_no, content_no = n, n + 1
        n += 2
        new_kids.append(f"{page_no} 0 R")
        offsets[page_no] = len(out)
        out += _obj(page_no, f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {PAGE_W} {PAGE_H}] /Resources << /Font << "
                             f"/F1 3 0 R >> >> /Contents {content_no} 0 R >>".encode())
        offsets[content_no] = len(out)
        out += _obj(content_no, _stream(_content(lines, f"{footer}   page {len(kids) + i + 1} of {total}".strip())))
    offsets[2] = len(out)
    out += _obj(2, f"<< /Type /Pages /Kids [{' '.join(kids + new_kids)}] /Count {total} >>".encode())
    xref = len(out)
    out += b"xref\n" + f"2 1\n{offsets[2]:010d} 00000 n \n".encode()
    out += f"{size} {n - size}\n".encode()
    for k in range(size, n):
        out += f"{offsets[k]:010d} 00000 n \n".encode()
    out += f"trailer\n<< /Size {n} /Root 1 0 R /Prev {prev} >>\nstartxref\n{xref}\n%%EOF\n".encode()
    return bytes(out)


def page_count(pdf: bytes) -> int:
    kids = _KIDS.findall(pdf)
    return int(kids[-1][1]) if kids else 0


__all__ = ["render", "append", "page_count"]