MPM Documentation Kit v1.3 (CW-034)
MPM Document Standard 1.3, mpmdoc build engine, templates for TN, INS, SOP, BOM, SVY, TAP, DWG, TSB, PRO, SOW and WO (Chinese editions for TN and DWG), glossary, MPM fonts and logos, and the mpm-technical-documents plugin 1.3.0. Worked examples ship in the kit zip on Drive only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWatTMmqmGatgHjDZvWs5b
This commit is contained in:
143 files changed
+4097
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"""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'<style>{fonts}</style><div style="width:100%;margin:0 0.62in;font-family:Montserrat;-webkit-print-color-adjust:exact;">'
|
||||
f'<div style="display:flex;justify-content:space-between;align-items:center;padding-bottom:7px;border-bottom:2px solid {brand.GOLD}">'
|
||||
f'<img src="{logo}" style="height:36px">'
|
||||
f'<div style="text-align:right"><div style="font-weight:800;font-size:10px;letter-spacing:2.2px;color:{brand.INK}">{doc.header_label.upper()}</div>'
|
||||
f'<div style="font-weight:700;font-size:8px;letter-spacing:1.2px;color:{brand.KICK};margin-top:3px">{doc.number} · REV {doc.rev}</div></div></div></div>')
|
||||
|
||||
|
||||
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'<style>{fonts}</style><div style="width:100%;margin:0 0.62in;display:flex;justify-content:space-between;'
|
||||
f'font-family:Montserrat;font-weight:600;font-size:7px;letter-spacing:1.1px;white-space:nowrap;color:{brand.FOOT}">'
|
||||
f'<span>MESSAGE POINT MEDIA · <span style="letter-spacing:.3px;font-size:7.5px;color:{brand.KICK}">{brand.TAGLINE}</span></span>'
|
||||
f'<span>{ctx} · {doc.number} · {i18n.page_of(getattr(doc, "lang", "en"))}</span></div>')
|
||||
|
||||
|
||||
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"<page[^>]*>(.*?)</page>", xml, re.S)
|
||||
word = re.compile(r'<word xMin="[\d.]+" yMin="([\d.]+)" xMax="[\d.]+" yMax="([\d.]+)">([^<]*)</word>')
|
||||
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)
|
||||
Reference in new issue
Block a user