Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

paged-deck

A print-quality document system in one HTML file. Real A4 geometry, backgrounds that survive the print dialog, vector text, controlled page breaks, and portrait and landscape pages in the same PDF.

No pagination polyfill, no headless-browser screenshot library, no build chain beyond Python and the Chrome you already have.

Cover page

Financials page, chart plus table


The problem

Print a designed HTML page from a browser and you get something that is not the design. The usual failures, in the order people hit them:

  • The colour disappears. The print dialog's "Background graphics" checkbox is off by default in some flows, and every background you drew goes white. White text on a dark panel becomes white text on white paper.
  • The paper is the wrong size. A scripted build without the right flag silently produces US Letter, so nothing lines up and the margins are wrong.
  • The pages break in the wrong places. Headings strand at the foot of a page, tables lose their header row halfway through, a panel splits across a break with an open edge.
  • The text is a picture. Anything built on html2canvas rasterises the page. It cannot be searched, selected, copied or reflowed, and it prints soft.
  • Chrome stamps its own header on your cover. The document title and the full local file:///Users/… path, printed across the artwork. No CSS suppresses it.
  • The file is four times bigger than it should be, and preflight rejects the fonts.

Every one of those has a fix that ships in the browser. This repository is the fixes, assembled, with a template that exercises them and the measurements that show they work.

What you get

  • template.src.html — a thirteen-page A4 landscape deck with a portrait appendix, carrying every archetype: cover, notice, contents, section divider, summary with stat tiles, at-a-glance, a chart page, a financials page with chart plus table, a process page, a team grid, an appendix, a back cover and a style sheet page. Design tokens, a twelve-column grid, a component library and hand-authored inline SVG charts.
  • build.py — inlines the fonts as base64, expands the logo slots to inline SVG, and renders the canonical PDF with headless Chrome.
  • docs/print-to-pdf.md — the technical reference: the CSS boilerplate, the break-control rules, the fourteen footguns with a fix for each, and the evidence.
  • docs/charts.md — the chart conventions, and a validated categorical palette.
  • docs/validate_palette.py — the validator, so the palette claim is reproducible and so is any replacement you make.

Quick start

git clone https://github.com/Proxuma/paged-deck.git
cd paged-deck
python3 build.py template.src.html

That writes template.html, a single self-contained file with no external requests at all, and template.pdf, the canonical render. Nothing to install: Python 3.9+ and a Chrome, Chromium or Edge on the machine.

HTML  template.html  (152 kB)
PDF   template.pdf  (166 kB)

Chrome is found automatically. Point at a different one if you need to:

export PAGED_DECK_CHROME='/Applications/Chromium.app/Contents/MacOS/Chromium'
python3 build.py template.src.html

Then make it yours:

  1. Replace assets/sample-logo.svg with your own mark. It is referenced from the HTML as <!--LOGO:sample-logo.svg--> and inlined at build time, so a single CSS rule can recolour it for dark and light grounds.
  2. Change the tokens at the top of template.src.html: a dark ink, a deeper ink for headings, a secondary dark field, one accent, one light surface. The rest of the sheet follows.
  3. Leave the chart series alone, or replace them and re-run python3 docs/validate_palette.py.
  4. Delete the archetypes you do not need and duplicate the ones you do.

Verify what came out:

pdfinfo template.pdf | grep "Page size"   # A4, and per-page for mixed orientation
pdffonts template.pdf                     # CID TrueType only, nothing you did not ship
pdfimages -list template.pdf              # zero rows: the charts are still vector

The three findings that matter

Each was established by rendering and measuring, not by reading documentation. All three are reproducible from this repository. The full evidence, with commands, is in docs/print-to-pdf.md.

1. print-color-adjust: exact overrides the print dialog

*, *::before, *::after {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

Rendered twice with backgrounds explicitly disabled — which is what Chrome does when the user leaves "Background graphics" unticked — with and without those two lines as the only difference. Without them the cover collapsed to a blank white page: the dark art field, the accent edge and the coloured panel all gone, taking the white logo and the white headline with them. Table headers lost their rule, zebra rows went white, panels lost their fill. With them, the render is identical to a full-colour one.

This is the single most important declaration in the file. It is also the one most often written without the -webkit- form, which older Chromium and Safari still need.

2. Chrome's own header and footer cannot be suppressed by any CSS

Rendered with headers enabled against a document whose CSS says @page { margin: 0 }, and with the renderer's own margins also set to zero:

injected header present on: 13 of 13 pages

The document title and the full local file:// path print straight across the cover artwork, and a page counter across the bottom. The CSS page box is not shifted; the furniture is simply overprinted. @page { margin: 0 } changes nothing, and no other declaration helps.

That is why the canonical PDF is built from the command line, where the flag exists:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --no-pdf-header-footer \
  --print-to-pdf=out.pdf "file:///absolute/path/template.html"

The in-page "Export to PDF" button is kept as a convenience for whoever opens the HTML, and it says so on screen. It is not the master copy.

3. Variable fonts embed as Type 3. Static instances embed as CID TrueType

Same template, same markup, only the @font-face sources changed:

fonts in the PDF file size
variable WOFF2 37 Type 3 objects 651,549 bytes
static instances 5 CID TrueType subsets 169,668 bytes

74% smaller. Type 3 is not a correctness failure — the glyph procedures are pure vector path fills, no raster anywhere, and text extraction still works — but it is rejected by several PDF/X and print-house preflight profiles and renders inconsistently in older Acrobat.

How to get static instances from Google Fonts. The css2 API serves a variable file when you ask for a weight range, and a static instance when you ask for a single weight. Take the @font-face block whose unicode-range begins U+0000-00FF — that is the Latin subset:

UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36'

curl -sS -A "$UA" 'https://fonts.googleapis.com/css2?family=Archivo:wght@600'
# ... /* latin */ @font-face { font-weight: 600; src: url(...woff2) ... }

curl -sS -A "$UA" 'https://fonts.googleapis.com/css2?family=Archivo:[email protected]'
# ... @font-face { font-weight: 400 600; ... }   <- variable, Type 3 in the PDF

Download that .woff2, one per weight you actually use, drop it in assets/fonts/, and build.py inlines it. The user agent matters: without a modern one, css2 serves TTF instead of WOFF2.

One corollary worth stating: every glyph in the document must come from a font you embedded. A single element styled in a system monospace pulls that system font into the PDF as a subset, which makes the output machine-dependent and may not be redistributable. pdffonts should list exactly what you shipped and nothing more.

How it works

The sheet model: one element, one printed page.

@page { size: A4 landscape; margin: 0; }

.page {
  position: relative; overflow: hidden;
  width: 297mm; height: 210mm;
  break-after: page; break-inside: avoid;
  counter-increment: pageno;
}
.page:last-child { break-after: auto; }
.page--portrait  { page: portrait; width: 210mm; height: 297mm; }

margin: 0 is what lets colour reach the paper edge. It also means the native @page margin boxes have nowhere to live, so running headers and folios are drawn into the sheet instead. In exchange, page breaks become structural: a fixed height cannot drift, so the document you see on screen is the document that prints.

For anything long and unstyled — schedules, definitions, an index — the template also documents the other model, a named page with real margins where native @page margin boxes and counter(page) " / " counter(pages) work with no JavaScript at all. Both models can live in one file.

Charts are hand-authored inline SVG, because a <canvas> chart is a bitmap by the time it reaches the PDF. The conventions are in docs/charts.md: no gridlines, one baseline, selective direct labels with the full numbers in an adjacent table, legend above, source line below, one axis and never a dual axis, a fixed series order, and a fifth series that becomes "Other" rather than inventing a hue.

Verified against

Google Chrome 151.0.7922.75 (PDF producer Skia/PDF m150), macOS 26.5 arm64. Nothing here is Chrome-version-sensitive in the parts that matter, but the measured numbers above come from that build.

Fonts and licensing

The repository ships two typefaces, both under the SIL Open Font License 1.1, which permits redistribution provided the licence text travels with the font. It does:

Font Files Licence
Archivo 2.001, static instances, weights 400/500/600 assets/fonts/archivo/ assets/fonts/archivo/OFL.txt
Source Serif 4 4.004, static italics, weights 400/500 assets/fonts/source-serif-4/ assets/fonts/source-serif-4/OFL.txt

Both are Latin subsets from the Google Fonts css2 API, and both report fsType 0 (installable embedding), which is what makes base64-inlining them into a distributed document legitimate. The template aliases them as IM Sans and IM Serif so you can swap the files without touching the CSS.

Everything else in this repository is MIT. See LICENSE.

Repository layout

build.py                 fonts + logos inlined, then the canonical PDF
template.src.html        the template. edit this one
assets/
  sample-logo.svg        placeholder wordmark. replace it
  fonts/
    archivo/             3 static weights + OFL.txt
    source-serif-4/      2 static italics + OFL.txt
docs/
  print-to-pdf.md        the technical reference and the evidence
  charts.md              chart conventions and the palette
  validate_palette.py    the palette validator
  img/                   the screenshots above

template.html and template.pdf are build output and are not committed.

About

Print-quality HTML documents that actually print as designed: real A4 geometry, backgrounds that survive the print dialog, vector text, controlled page breaks, mixed portrait and landscape.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages