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:
@@ -66,7 +66,9 @@ pinned.
|
|||||||
|
|
||||||
## Committing
|
## Committing
|
||||||
|
|
||||||
Scratch patterns are in `.gitignore` and must never be committed. Note that
|
Scratch patterns are in `.gitignore` and must never be committed. `.blend`,
|
||||||
`.gitattributes` LFS-tracks `.glb/.fbx/.mp4/.task` but **not `.blend`** — an
|
`.zprj`, `.obj`, `.npy`/`.npz` and the rest of the heavy formats are LFS-tracked
|
||||||
80 MB master committed today goes into git proper, not LFS. Add `.blend` to
|
in `.gitattributes`, so a pinned master can be committed safely — but note that
|
||||||
`.gitattributes` first if masters are ever to be tracked.
|
`.png`/`.jpg` deliberately are **not** (≈250 are already tracked raw; adding them
|
||||||
|
would rewrite every one without shrinking history). A lane's QA renders are
|
||||||
|
scratch for that reason too, not just a disk one.
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
*.npy filter=lfs diff=lfs merge=lfs -text
|
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||||
*.npz filter=lfs diff=lfs merge=lfs -text
|
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Generated .humans/ PDFs (tools/humans_to_pdf.py). Each regeneration is a whole
|
||||||
|
# new ~2 MB blob, so LFS from the start — unlike png/jpg above, none are tracked
|
||||||
|
# raw yet, so there is no history to rewrite.
|
||||||
|
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
# Reallusion CC / iClone asset containers
|
# Reallusion CC / iClone asset containers
|
||||||
*.iAvatar filter=lfs diff=lfs merge=lfs -text
|
*.iAvatar filter=lfs diff=lfs merge=lfs -text
|
||||||
*.iavatar filter=lfs diff=lfs merge=lfs -text
|
*.iavatar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
+19
-5
@@ -1,7 +1,7 @@
|
|||||||
# .humans/ — pages written for people, not agents
|
# .humans/ — pages written for people, not agents
|
||||||
|
|
||||||
Self-contained HTML pages explaining how parts of this repo work, for reading in
|
Explainers about how parts of this repo work. Same convention as
|
||||||
a browser. Same convention as `ariki-game/.humans/`.
|
`ariki-game/.humans/`: an HTML page per topic, read in a browser.
|
||||||
|
|
||||||
The operating detail lives in `.claude/skills/` and `.agents/` and is written for
|
The operating detail lives in `.claude/skills/` and `.agents/` and is written for
|
||||||
whoever is driving the tools. These pages are the **explanation** — what we built,
|
whoever is driving the tools. These pages are the **explanation** — what we built,
|
||||||
@@ -10,7 +10,21 @@ skill/wiki is the source of truth.
|
|||||||
|
|
||||||
| page | about |
|
| page | about |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `marvelous-designer.html` | How we author Ariki's garments as real sewn cloth in Marvelous Designer — the process, what's been made, the traps, what's still unsolved. Written for Özlem and Jeremy. |
|
| `marvelous-designer` | How we author Ariki's garments as real sewn cloth in Marvelous Designer — the process, what's been made, the traps, what's still unsolved. Written for Özlem and Jeremy. |
|
||||||
|
|
||||||
Images are referenced relatively (`../tools/tailor/screenshots/…`), so open these
|
## HTML is the source; the PDF is what you send
|
||||||
from inside the repo rather than copying the file out on its own.
|
|
||||||
|
The HTML references screenshots relatively (`../tools/tailor/screenshots/…`), so it
|
||||||
|
only renders from inside a checkout — mail someone the `.html` on its own and they
|
||||||
|
get broken images. The **PDF has the images baked in** and travels as one file.
|
||||||
|
|
||||||
|
Edit the HTML, then regenerate:
|
||||||
|
|
||||||
|
```
|
||||||
|
python tools/humans_to_pdf.py --all # or a single page
|
||||||
|
```
|
||||||
|
|
||||||
|
That drives headless Chrome, so what you get is exactly what the browser shows.
|
||||||
|
Each page carries an `@media print` block controlling the paper layout — without it
|
||||||
|
the wide tables and the pipeline diagram, which merely scroll on screen, get cut
|
||||||
|
off at the page edge. Commit the HTML and the PDF together so they don't drift.
|
||||||
|
|||||||
@@ -114,6 +114,36 @@
|
|||||||
.callout p:last-child { margin-bottom: 0; }
|
.callout p:last-child { margin-bottom: 0; }
|
||||||
.meta { color: var(--muted); font-size: .85rem; border-top: 1px solid var(--line); margin-top: 3.5rem; padding-top: 1.2rem; }
|
.meta { color: var(--muted); font-size: .85rem; border-top: 1px solid var(--line); margin-top: 3.5rem; padding-top: 1.2rem; }
|
||||||
hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0 0; }
|
hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0 0; }
|
||||||
|
|
||||||
|
/* PDF export — `python tools/humans_to_pdf.py` drives headless Chrome.
|
||||||
|
Screen rules scroll wide content; on paper it would simply be cut off. */
|
||||||
|
@media print {
|
||||||
|
@page { size: A4; margin: 12mm; }
|
||||||
|
html, body {
|
||||||
|
background: #0b1113 !important;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
body { padding: 0; font-size: 10pt; line-height: 1.5; }
|
||||||
|
h1 { font-size: 26pt; margin-bottom: .6rem; }
|
||||||
|
h2 { font-size: 13pt; margin: 20pt 0 6pt; }
|
||||||
|
h3 { font-size: 10.5pt; margin: 12pt 0 4pt; }
|
||||||
|
p, li { max-width: none; }
|
||||||
|
a { text-decoration: none; }
|
||||||
|
/* Wide content is scrollable on screen; on paper it must reflow. */
|
||||||
|
.wrap { overflow: visible; }
|
||||||
|
table { min-width: 0; font-size: 8.5pt; }
|
||||||
|
th, td { padding: .5rem .7rem; }
|
||||||
|
.flow { padding: .8rem 1rem; overflow: visible; }
|
||||||
|
.flow pre { font-size: 7pt; line-height: 1.5; }
|
||||||
|
/* Never split a figure, table row, callout or diagram across a page. */
|
||||||
|
figure, .callout, .flow, .wrap, tr { break-inside: avoid; page-break-inside: avoid; }
|
||||||
|
h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
|
||||||
|
figure img { max-height: 16cm; width: auto; margin: 0 auto; }
|
||||||
|
figcaption { font-size: 8.5pt; }
|
||||||
|
.shots { display: block; }
|
||||||
|
.meta { margin-top: 20pt; font-size: 8pt; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
|
Before
After
|
Binary file not shown.
@@ -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