docs(humans): ship the MD explainer as a PDF with the images baked in
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>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user