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())
|