solo-pdf-print · v1.0.0 · 2026-09-13 · sha256 37d0c2239c0f5c8f
solo-pdf-print v1.0.0A
Immutable. This exact content is served forever at /api/v1/blob/37d0c2239c0f5c8f.
---
name: solo-pdf-print
description: Use when "сделай пдф", "make a PDF", "на печать", "распечатать", "prepare for A4", "печатные карточки", "PDF из markdown", or a PDF renders but the page layout has to be checked before handing it over. Covers WeasyPrint on macOS, why headless Chrome hangs, page breaks, page numbers and Cyrillic.
license: MIT
metadata:
author: fortunto2
version: "1.0.0"
openclaw:
emoji: "🖨️"
allowed-tools: Bash, Read, Write, Edit
argument-hint: "[что печатаем, напр. «отчёт из markdown» или «карточки A4»]"
---
# HTML → печатный PDF
Инструмент — **WeasyPrint**. Он один из трёх кандидатов умеет то, ради чего печать
вообще затевается: колонтитулы с номерами страниц, предсказуемые разрывы и
управление через `@page`.
## 0. Сначала то, на чём теряют час
**На macOS WeasyPrint падает на импорте**, хотя установлен:
```
OSError: cannot load library 'libgobject-2.0-0'
```
Библиотеки стоят в homebrew, но не в пути загрузчика. Лечится одной переменной:
```bash
export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib
uv run --with weasyprint python build.py
```
⚠️ Переменная нужна **и в скрипте, и в CI, и в хуке** — окружение хука отличается
от окружения оболочки. Если её негде поставить, в начале скрипта:
```python
import os; os.environ.setdefault("DYLD_FALLBACK_LIBRARY_PATH", "/opt/homebrew/lib")
```
## 1. Чем НЕ печатать
| Инструмент | Что не так |
|---|---|
| **headless Chrome** `--print-to-pdf` | Печатает, но **не завершается**: процесс висит до таймаута. Проверено 13.09.2026 — PDF записался, а `subprocess` упал по `TimeoutExpired` через 180 с. И он **не умеет `@page` margin-boxes**, то есть номеров страниц не будет вовсе |
| **wkhtmltopdf** | Движок Qt WebKit: ни flexbox, ни `columns`, ни современных `break-*` |
| **pandoc → LaTeX** | Хорош для книги, избыточен для одностраничника; кириллица требует настройки шрифтов |
Chrome остаётся уместен ровно в одном случае: когда страницу **рисует JavaScript**.
WeasyPrint скрипты не выполняет — данные надо отдать уже в HTML.
## 2. Скелет, который работает
```python
# /// script
# dependencies = ["weasyprint"]
# ///
from weasyprint import HTML
HTML(filename="out.html").write_pdf("out.pdf")
```
```css
@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; } /* заголовок не бросать внизу листа */
```
Единицы — **мм и pt**, не px: на бумаге пиксель ничего не значит. Ширину карточек
задавать в мм, чтобы после разрезания они были предсказуемого размера.
## 3. Кириллица и башкирские буквы
Системные Georgia / Times New Roman покрывают кириллицу целиком, **включая
ә ҙ ҡ ң ө ҫ ү һ**. Веб-шрифты для печати не нужны и только тормозят сборку.
Если шрифт всё же подключается — проверить именно эти буквы: подстановка
глифа из другого шрифта видна как скачок начертания в середине слова.
## 4. Проверять глазами, а не `pdftotext`
`pdftotext` вытаскивает текст в порядке потока и **не покажет ни наложений, ни
обрезанных блоков, ни пустых страниц**. Смотреть надо картинку:
```bash
pdftoppm -f 1 -l 2 -r 76 -png out.pdf /tmp/page # первые две страницы в PNG
```
и открыть PNG. Что ловится только так:
- текст, вылезший за рамку карточки фиксированной высоты;
- полстраницы пустоты из-за `break-before` на пустой секции;
- **дублирование**: краткая выжимка и полный текст, где выжимка — цитата из него;
- эмодзи, которые на экране маркеры, а на бумаге — пёстрые картинки. В печать
их обычно вырезают, структуру несут рамки и подписи.
## 5. Markdown → PDF
Готовый пример с русской типографикой — `6-crm/bin/md_to_pdf.py`: markdown +
weasyprint, таблицы, `@bottom-right`, жёсткий разрыв между файлами.
```bash
DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib \
uv run python bin/md_to_pdf.py --out OUT.pdf FILE1.md FILE2.md
```
## 6. Когда печатают для людей, а не для архива
- **Размер шрифта — от 10pt.** 8pt читается на экране и не читается на бумаге.
- **Одна мысль — один блок.** Врезка с вопросом заметнее абзаца с вопросом.
- **Номер страницы обязателен**, если листов больше трёх: иначе рассыпавшуюся
стопку не собрать.
- **Карточки на разрез** — рамка `.8pt` сплошная, поля внутри не меньше 4pt,
и запас 2-3 мм на неточность резака.
- Печать бывает **чёрно-белой**: иерархия должна держаться на жирности и
рамках, цвет — только подсказка.
## Связи
Стиль текста — `/solo:humanize`. Готовый генератор многостраничного документа
с деревом и карточками — `~/personal/family/bin/shezhere-print.py`.