4bbd1a1732
The HTML references screenshots relatively, so it only renders inside a checkout -- mailing the file on its own gives broken images. The PDF is one self-contained file, which is what actually gets sent to Ozlem. - tools/humans_to_pdf.py: renders .humans/*.html via headless Chrome (or Edge), so the output is what the browser shows. HTML stays the source. - @media print block on the page: the wide tables and the pipeline diagram scroll on screen but would be cut off at the paper edge; this reflows them and keeps figures, callouts and table rows off page breaks. Verified at A4 content width -- 6 pages, both screenshots embedded. - *.pdf -> LFS. Each regeneration is a whole new ~2 MB blob, and unlike png/jpg there are no PDFs tracked raw yet, so there is no history to rewrite by adding it now. Also corrects working-files.md: it claimed .gitattributes does not LFS-track .blend, which was true when written and is no longer -- .blend/.zprj/.obj/ .npy/.npz have since been added. The note now covers what is actually still excluded (png/jpg, deliberately) and why that makes QA renders scratch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Render a .humans/ HTML page to a self-contained PDF via headless Chrome.
|
|
|
|
The pages in `.humans/` reference screenshots relatively
|
|
(`../tools/tailor/screenshots/...`), so the HTML only works from inside a repo
|
|
checkout. The PDF embeds those images, which makes it the thing you actually
|
|
send to someone.
|
|
|
|
The HTML stays the source of truth: edit the page, re-run this, commit both.
|
|
|
|
python tools/humans_to_pdf.py .humans/marvelous-designer.html
|
|
python tools/humans_to_pdf.py --all
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
CHROME_CANDIDATES = [
|
|
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
|
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
"/usr/bin/google-chrome",
|
|
"/usr/bin/chromium",
|
|
]
|
|
|
|
|
|
def find_chrome() -> Path:
|
|
for c in CHROME_CANDIDATES:
|
|
p = Path(c)
|
|
if p.exists():
|
|
return p
|
|
raise SystemExit("error: no Chrome or Edge found; tried:\n "
|
|
+ "\n ".join(CHROME_CANDIDATES))
|
|
|
|
|
|
def to_pdf(html: Path, chrome: Path) -> Path:
|
|
out = html.with_suffix(".pdf")
|
|
# Chrome refuses to reuse a running profile, so give it a throwaway one.
|
|
with tempfile.TemporaryDirectory() as profile:
|
|
cmd = [
|
|
str(chrome),
|
|
"--headless=new",
|
|
"--disable-gpu",
|
|
f"--user-data-dir={profile}",
|
|
"--no-pdf-header-footer", # drop the URL/date furniture
|
|
"--virtual-time-budget=15000", # let local images decode first
|
|
f"--print-to-pdf={out}",
|
|
html.resolve().as_uri(),
|
|
]
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
|
if not out.exists():
|
|
sys.stderr.write(r.stderr or "")
|
|
raise SystemExit(f"error: Chrome produced no PDF for {html}")
|
|
return out
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("page", type=Path, nargs="?", help="HTML page under .humans/")
|
|
ap.add_argument("--all", action="store_true", help="render every .humans/*.html")
|
|
args = ap.parse_args()
|
|
|
|
repo = Path(__file__).resolve().parent.parent
|
|
if args.all:
|
|
pages = sorted((repo / ".humans").glob("*.html"))
|
|
elif args.page:
|
|
pages = [args.page]
|
|
else:
|
|
ap.error("give a page, or --all")
|
|
|
|
if not pages:
|
|
print("nothing to render")
|
|
return 0
|
|
|
|
chrome = find_chrome()
|
|
print(f"using {chrome.name}")
|
|
for html in pages:
|
|
if not html.is_file():
|
|
print(f" skip (missing): {html}")
|
|
continue
|
|
out = to_pdf(html, chrome)
|
|
print(f" {out.stat().st_size / 1048576:.2f} MB {out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|