Printing to PDF

How a doc or a deck becomes a PDF that looks exactly like the page in the browser: the devpage pdf command, the Chrome it drives, and what print mode leaves out. It is a task and mechanism article under the viewer.

devpage pdf

$ devpage pdf design/sync
Sync design.pdf
$ devpage pdf talks/launch -o launch.pdf
launch.pdf

The first prints the doc in design/sync/devpage.yml, the second a deck to a chosen file; devpage pdf design.md prints one Markdown file as a doc.

It takes the same argument as devpage and writes <title>.pdf in the current folder unless -o names the file; characters a file name can't hold — /, :, ? and the like — become -. It prints the file's name when it is done. It prints a doc — on A4 or Letter, with its header and footer (Writing a doc) — or a deck, one slide per 16:9 page (Making slides). A site is many pages, not one, and devpage pdf refuses it; print one of its pages from the browser instead. It refuses a design too; a page that embeds the design prints it, and its PNG is its picture.

The browser's own print — ⌘ P, Save as PDF — produces the same pages from an open doc or deck, since both print the same print-mode page. devpage pdf adds the doc's header and footer and needs no clicking, which is what a script or an agent wants.

The Chrome it drives

sequenceDiagram
  participant pdf as devpage pdf
  participant server as Its own server
  participant chrome as chrome-headless-shell
  pdf->>server: start, in the background
  pdf->>chrome: open the document in print mode
  chrome->>server: GET the page
  chrome-->>pdf: every diagram, widget and embedded design drawn
  pdf->>chrome: Page.printToPDF
  chrome-->>pdf: the PDF

devpage does not render PDF itself. It starts its own server in the background, which logs nothing, opens the document in print mode in a headless Chrome — laid out as paper from the start, with the print media type, so every slide of a deck is shown and a design embedded on any of them is fitted where it will print — waits until every diagram is drawn and every embedded design is painted — for at most 20 seconds, after which it prints whatever the page shows, so a design that never paints (one with problems, say) costs that wait but doesn't stop the PDF; devpage check is what reports it — and has Chrome print it — the same Page.printToPDF call Playwright makes, spoken directly over the DevTools protocol by the chromiumoxide crate, with no Node and no extra process.

Each run gives Chrome a profile folder of its own in the system's temporary folder, so several prints can run at once, and deletes it when the run ends.

The Chrome is always chrome-headless-shell, never a Chrome installed on the machine. devpage downloads it the first time it needs one — into ~/Library/Caches/devpage/chrome-headless-shell on macOS, ~/.cache/devpage/chrome-headless-shell elsewhere — and uses that copy from then on. It is a bare program with no app bundle, so nothing devpage launches appears in the macOS Dock, and every machine prints with the same browser. A slide's PNG and the Chrome tests drive the same copy. Only devpage video drives another, Chrome for Testing, because the shell can't capture a tab.

Why the PDF matches the screen

Print mode is the same page; it only leaves things out, and sets the page size (A PDF is the page the browser shows):

Kept on paper Left out
the Markdown, the stylesheet live updates
highlighted code comments
diagrams and widgets (their first render) navigation and the deck's controls
the doc's header and footer soft shadows (PDF viewers draw them as grey boxes)

A deck's slides are laid out at a fixed 1920 × 1080 and only scaled to the window, so the printed page is the slide as it shows.