The tool — WeasyPrint. It is one of three candidates that can do what printing is actually for: headers and footers with page numbers, predictable breaks, and control via @page.
0. First, what costs an hour
On macOS WeasyPrint crashes on import, although it is installed:
OSError: cannot load library 'libgobject-2.0-0'
The libraries are installed in homebrew but not in the loader path. Cured with one variable:
export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib
uv run --with weasyprint python build.py
⚠️ The variable is needed both in the script and in CI and in the hook — the hook's environment differs from the shell environment. If there is nowhere to set it, at the beginning of the script:
import os; os.environ.setdefault("DYLD_FALLBACK_LIBRARY_PATH", "/opt/homebrew/lib")
1. What NOT to print with
| Tool | What's wrong |
|---|---|
headless Chrome --print-to-pdf |
It prints, but does not exit: the process hangs until timeout. Verified 2026-09-13 — the PDF was written, but subprocess crashed due to TimeoutExpired after 180 s. And it cannot @page margin-boxes, that is, there will be no page numbers at all |
| wkhtmltopdf | Qt WebKit engine: neither flexbox, nor columns, nor modern break-* |
| pandoc → LaTeX | Good for a book, overkill for a one-pager; Cyrillic requires font configuration |
Chrome remains appropriate in exactly one case: when the page is drawn by JavaScript. WeasyPrint does not execute scripts — the data must be provided already in HTML.
2. A skeleton that works
# /// script
# dependencies = ["weasyprint"]
# ///
from weasyprint import HTML
HTML(filename="out.html").write_pdf("out.pdf")
@page {
size: A4 portrait; /* landscape — для лент и таблиц */
margin: 15mm 14mm 16mm 14mm;
@bottom-right { content: counter(page) " / " counter(pages);
font: 8pt Georgia, serif; color: #888; }
}
@page :first { @bottom-right { content: ""; } } /* на титуле номер не нужен */
section { break-before: page; } /* не page-break-before: он устарел */
.card { break-inside: avoid; } /* карточку не рвать между страницами */
h3 { break-after: avoid; } /* заголовок не бросать внизу листа */
Units — mm and pt, not px: on paper a pixel means nothing. Set the card width in mm so that after cutting they are a predictable size.
3. Cyrillic and Bashkir letters
System Georgia / Times New Roman cover Cyrillic completely, including ә ҙ ҡ ң ө ҫ ү һ. Web fonts are not needed for printing and only slow down the build. If a font is nevertheless included — check exactly these letters: substitution of a glyph from another font is visible as a jump in style in the middle of a word.
4. Check with your eyes, not pdftotext
pdftotext pulls text in flow order and will show neither overlaps, nor
clipped blocks, nor empty pages. You need to look at the picture:
pdftoppm -f 1 -l 2 -r 76 -png out.pdf /tmp/page # первые две страницы в PNG
and open the PNG. What is caught only this way:
- text that has run outside the frame of a fixed-height card;
- half a page of emptiness because of
break-beforeon an empty section; - duplication: a brief summary and the full text, where the summary is a quote from it;
- emoji, which on screen are markers, but on paper are motley pictures. In print they are usually cut out; structure is carried by frames and captions.
5. Markdown → PDF
A ready example with Russian typography — 6-crm/bin/md_to_pdf.py: markdown +
weasyprint, tables, @bottom-right, a hard break between files.
DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib \
uv run python bin/md_to_pdf.py --out OUT.pdf FILE1.md FILE2.md
6. When printing for people, not for an archive
- Font size — from 10pt. 8pt is readable on screen and not readable on paper.
- One thought — one block. A callout with a question is more noticeable than a paragraph with a question.
- Page number is mandatory, if there are more than three sheets: otherwise a scattered stack cannot be reassembled.
- Cards for cutting — frame
.8ptsolid, margins inside at least 4pt, and a 2-3 mm allowance for cutter inaccuracy. - Printing can be black-and-white: hierarchy should hold up on boldness and frames, color — only a hint.
Connections
Text style — /solo:humanize. A ready generator for a multi-page document
with a tree and cards — ~/personal/family/bin/shezhere-print.py.