"""PDF rendering: Chromium via Playwright, US Letter, running header + footer.
Requires: pip install playwright && playwright install chromium
(Claude's cloud workspace already has both.)
"""
import base64
import re
from pathlib import Path
from . import brand, i18n
MARGIN = {"top": "1.0in", "bottom": "0.7in", "left": "0.62in", "right": "0.62in"}
def _header(doc, fonts, logo):
return (f'
'
f'
'
f'

'
f'
{doc.header_label.upper()}
'
f'
{doc.number} · REV {doc.rev}
')
def _footer(doc, fonts):
"""MESSAGE POINT MEDIA · tagline (left) | CONTEXT [· MARKING] · NUMBER · Page x of y (right)."""
ctx = doc.footer.upper().replace("MESSAGE POINT MEDIA · ", "", 1)
return (f''
f'MESSAGE POINT MEDIA · {brand.TAGLINE}'
f'{ctx} · {doc.number} · {i18n.page_of(getattr(doc, "lang", "en"))}
')
def _header_logo(width=1400, path=None):
"""Brand logo scaled for the running header (the MPM master PNG is ~5800 px wide)."""
raw = (path or brand.LOGO_HEADER).read_bytes()
try:
import io
from PIL import Image
im = Image.open(io.BytesIO(raw))
if im.width > width:
im = im.resize((width, round(im.height * width / im.width)), Image.LANCZOS)
buf = io.BytesIO()
im.save(buf, "PNG", optimize=True)
return buf.getvalue()
except ImportError:
pass
return raw
PAGE_H_PT = 792.0 # US Letter
CONTENT_BOTTOM_PT = PAGE_H_PT - 0.7 * 72 # bottom margin 0.7 in.
HEADER_BOTTOM_PT = 1.0 * 72
def _rev_spacer_px(pdf_bytes, content_bottom_pt=None, header_bottom_pt=None):
"""Find the revision block via its sentinels. If it shares the last page with other content,
return the spacer (px) that moves it to the bottom of that page; else 0. Needs poppler's pdftotext."""
import re
import shutil
import subprocess
import tempfile
if not shutil.which("pdftotext"):
import sys
print("warning: poppler's pdftotext not found; the revision block will not be anchored to the page "
"bottom (install poppler: brew install poppler / apt install poppler-utils)", file=sys.stderr)
return 0, None
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
f.write(pdf_bytes)
f.flush()
xml = subprocess.run(["pdftotext", "-bbox", f.name, "-"], capture_output=True, text=True).stdout
pages = re.findall(r"]*>(.*?)", xml, re.S)
word = re.compile(r'([^<]*)')
for i, pg in enumerate(pages):
words = [(float(a), float(b), t) for a, b, t in word.findall(pg)]
top = [w for w in words if "QQREVTOPQQ" in w[2]]
end = [w for w in words if "QQREVENDQQ" in w[2]]
if not top or not end:
continue
y_top, y_end = top[0][0], end[0][1]
above = [w for w in words if (header_bottom_pt or HEADER_BOTTOM_PT) < w[1] < y_top - 1 and "QQREV" not in w[2]]
if not above: # block sits alone on its page: leave it at the top
return 0, len(pages)
free_pt = (content_bottom_pt or CONTENT_BOTTOM_PT) - y_end
return max(0.0, free_pt * 96 / 72 - 4), len(pages)
return 0, len(pages)
def render_pdf(html_path, out_path, doc):
from playwright.sync_api import sync_playwright
fonts = brand.FONTS_CSS.read_text()
logo = "data:image/png;base64," + base64.b64encode(_header_logo()).decode()
if getattr(doc, "lang", "en") != "en":
fonts += doc._cjk_css # same subset the page loaded (see Doc.html)
hdr, ftr, margin = _header(doc, fonts, logo), _footer(doc, fonts), MARGIN
if getattr(doc, "lang", "en") != "en":
hdr, ftr = i18n.add_fallback(hdr), i18n.add_fallback(ftr)
bounds = (CONTENT_BOTTOM_PT, HEADER_BOTTOM_PT)
opts = dict(format="Letter", print_background=True, display_header_footer=True,
header_template=hdr, footer_template=ftr, margin=margin)
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page()
pg.goto(Path(html_path).resolve().as_uri(), wait_until="load")
# wait for every image to decode and every font to load, so pass 1 measures the final layout
pg.wait_for_function("Array.from(document.images).every(i => i.complete && i.naturalWidth > 0)")
pg.evaluate("() => Promise.all([document.fonts.ready, ...Array.from(document.images).map(i => i.decode().catch(() => null))])")
pg.wait_for_timeout(150)
first = pg.pdf(**opts) # pass 1: find the revision block
spacer, n_pages = _rev_spacer_px(first, *bounds)
pg.add_style_tag(content=f".sent{{display:none}}#rev-spacer{{height:{spacer:.1f}px}}")
final = pg.pdf(**opts) # pass 2: block anchored, sentinels hidden
if spacer and n_pages and len(re.findall(rb"/Type\s*/Page[^s]", final)) != n_pages:
pg.add_style_tag(content="#rev-spacer{height:0}")
final = pg.pdf(**opts)
b.close()
Path(out_path).write_bytes(final)