Собрать PDF из markdown - документ на чтение и рассылку: "сделай pdf", "отчет в pdf", "нужен pdf для печати". Документ пойдет на правки - скилл `md-docx`.
Installs into .claude/skills of the current project.
Are you the author of Md Pdf?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dewil-md-pdf)
---
name: md-pdf
description: Собрать PDF из markdown - документ на чтение и рассылку: "сделай pdf", "отчет в pdf", "нужен pdf для печати". Документ пойдет на правки - скилл `md-docx`.
---
# md-pdf
Конвертация markdown -> PDF без зависимостей: md -> HTML (мини-конвертер и дефолтные стили внутри скрипта) -> печать установленным Chrome в headless-режиме. Скрипт-эталон - `scripts/md-pdf.py` (top-level папка `scripts/` в корне проекта). Не нужны pandoc/LaTeX/weasyprint - только Chrome.
## Когда применять
- Нужно отдать md-контент наружу файлом: отчет заказчику, саммари встречи, КП, конспект на печать.
- Проектный документ регулярно пересобирается в PDF (см. "Специализация").
## Когда НЕ применять
- **Документ пойдет на правки** (вычитка, согласование, режим рецензирования) - нужен `.docx`, см. скилл `md-docx`. PDF - для чтения, рассылки и печати; docx - когда получатель правит прямо в файле. Разбор markdown у скриптов общий, контракт таблиц и мягкого переноса одинаковый.
- Получателю подойдет сам markdown или текст в чате - PDF ради PDF не делать.
- Документ со сложной версткой (многоколоночность, вложенные списки) - конвертер их не умеет; либо дорабатывать конвертер, либо собирать HTML вручную и печатать тем же Chrome. Простые GFM-таблицы (однострочные ячейки) он умеет.
## CLI
```bash
python3 scripts/md-pdf.py note.md # note.pdf рядом с исходником
python3 scripts/md-pdf.py note.md --out /path/x.pdf # свой путь
python3 scripts/md-pdf.py note.md --css custom.css --title "Отчет"
python3 scripts/md-pdf.py note.md --author "Имя" # /Author в метаданные PDF
python3 scripts/md-pdf.py cv.md --photo фото.jpg --author "Имя Фамилия" # резюме с фото
```
`--footer` добавляет нижний колонтитул, `--header` - верхний. В тексте работают плейсхолдеры `{page}`, `{pages}` и `{date}` (дата сборки), поэтому типовой случай пишется коротко:
```bash
python3 scripts/md-pdf.py "отчет.md" --out "Отчет.pdf" \
--footer "стр. {page}/{pages} · версия от {date}"
```
Номера подставляет сам Chrome при печати - посчитать их заранее нельзя, поэтому и печать идет через CDP (`Page.printToPDF`), а не CLI-флагом: у CLI кастомного колонтитула нет вовсе, а CSS-обходов не существует - Chrome не поддерживает margin-боксы `@page`. Одностраничному документу колонтитул не нужен, поэтому по умолчанию он выключен.
Отдельного флага под типовой случай ("только номера страниц") нет намеренно: `--footer "стр. {page}/{pages}"` короче, чем запоминать второй флаг, и не плодит два способа сделать одно и то же.
Текст колонтитула - буквальный текст, а не разметка: `<b>черновик</b>` напечатается как есть. Обратная сторона - фигурные скобки зарезервированы под плейсхолдеры, и напечатать буквальное `{page}` нельзя.
Пока идет печать, Chrome поднимает локальный отладочный порт (это единственный способ получить колонтитул). Порт живет секунды, профиль временный и одноразовый, но на общей машине или CI-раннере другой пользователь в этот момент технически может к нему подключиться и прочитать печатаемый документ. Для секретных документов на общем хосте это стоит учитывать.
Колонтитул рисуется **внутри поля страницы**, заданного `@page` в CSS. Стандартных 18 мм хватает; если в своем `--css` поле меньше ~12 мм, текст колонтитула наедет на содержимое - увеличьте поле.
`--separators` включает горизонтальные разделители: черта под заголовком секции (H2) и тонкая над должностью (H4) - в длинном структурированном документе они держат ритм страницы лучше, чем одни отступы. По умолчанию выключено (в ТЗ или протоколе встречи такие черты неуместны). Тот же флаг с тем же смыслом есть у `md-docx.py` - один markdown собирается в оба формата, и настройка оформления не должна жить только в одной ветке.
`--photo` кладет фотографию в правый верхний угол первой страницы с обтеканием текстом (стандарт резюме на российском рынке). Фото не кладется в markdown-исходник - тот остается чистым для Obsidian и сборки docx, раскладка живет в параметрах сборки. Размер - `--photo-width`/`--photo-height` (по умолчанию 30x38 мм, пропорция 3x4; произвольные пропорции входного фото кадрируются, не растягиваются). Стили фото дописываются после `--css`, так что флаг работает и с пользовательскими стилями.
Chrome ищется сам: типовые пути macOS и Linux (включая Chromium и snap), затем PATH. Нестандартная установка - `MD_PDF_CHROME`: он имеет приоритет над поиском и принимает как полный путь, так и имя команды из PATH.
## Грабли (проверено практикой)
- **Блоки кода** открываются тремя и более обратными кавычками или тильдами, с отступом до трех пробелов, в том числе под пунктом списка. Закрываются тем же символом в количестве не меньше открывающего, после него допустимы только пробелы и табы. Другой забор, короткий забор или забор с текстом остается кодом. Незакрытый блок идет до конца файла; содержимое выводится буквально. Язык после открывающего забора принимается, но не меняет HTML-разметку и не включает подсветку.
- **Нумерованные списки: номера не теряются.** Фрагмент, начатый с `1.`, новый список; начатый с другого номера - продолжение предыдущего, и его старт считается как конец предыдущего плюс один, а не берется из исходника: вставил пункт в первый фрагмент - второй сдвинулся сам. Внутри фрагмента номера исходника игнорируются. В docx то же через старт нумерации у списка; разбор общий с `md-docx`.
- **Текст в Chrome-PDF не грепается** (глифовые индексы, не юникод). Верифицировать содержимое по сгенерированному HTML (функция `md_to_html`), а не по PDF; глазами PDF смотрит пользователь. Для таблиц - считать `<table>`/`<tr>`; для мягкого переноса - убедиться, что нет `<p>`, обрывающихся на предлоге/запятой (признак не склеенного абзаца).
- **Мягкий перенос склеивает соседние текстовые строки** в один абзац (стандартная семантика markdown). Намеренно раздельные строки-шапки (`**Версия:** ...` и `**Дата:** ...` на соседних строках) сольются в один параграф - оформляй их списком (`- **Версия:** ...`) или разделяй пустой строкой.
- **Контракт таблицы строгий - соблюдай его, иначе блок молча станет абзацем.** Требуется: каждая строка (шапка, разделитель, все строки тела) начинается с `|`; вторая строка - разделитель `|---|`, совпадающий с шапкой по числу ячеек; таблица начинается с новой строки, а не внутри абзаца (как и в GFM, таблица абзац не прерывает - поэтому `|x| - модуль числа` посреди текста остается текстом). Ширина строк тела приводится к шапке: недостающие ячейки добиваются пустыми, а лишние **склеиваются в последнюю колонку** с предупреждением в stderr - молча терять текст в документе, ушедшем наружу, нельзя (до 26.07.2026 лишние отбрасывались беззвучно). Увидел это предупреждение - поправь число колонок в шапке, склейка выглядит некрасиво. Экранированная `\|` ячейку не делит. Выравнивание (`:--:`) как разделитель принимается, но на верстку не влияет; таблицы без ведущего `|` и жесткий перенос (два пробела в конце строки) не поддерживаются.
- **Перед перезаписью существующего PDF** - сначала сам сделай его копию в scratchpad (скрипт бэкап не делает): файл мог быть собран другим способом или содержать ручные правки.
- **Локальные картинки** конвертер встраивает base64 data-URI автоматически; `file://`-ссылки в печать не попадают.
- **Chrome не пишет /Author в метаданные PDF** (только Title/Creator/Producer/даты). Скрипт выбирает первое непустое значение `--author` -> `DOC_AUTHOR` -> `dwl` и дописывает поле сам, в том числе без флага. Поле добавляется инкрементальным обновлением Info-словаря после сборки. Пустые строки пропускаются, имя передается без типографической правки. Обновление рассчитано на классическую структуру trailer + xref-таблица, которую пишет Skia (при другой структуре PDF остается без /Author, сборка не падает).
- YAML-frontmatter исходника пропускается, титул PDF = первый H1 (или `--title`).
- Экранированные маркеры звездочки, подчеркивания и обратной кавычки вне кода выводятся буквально; внутри кода сохраняется и обратный слеш. Этот инлайн-разбор общий для PDF, DOCX и PPTX.
- **HTML-сущности поддерживаются, произвольные HTML-теги - нет.** Сущности вроде ` ` вне кода проходят в PDF как в любом markdown (штатный способ поставить неразрывный пробел или отступ); внутри `` `кода` `` печатаются буквально; одиночный `&` экранируется. До 06.09.2026 любая сущность печаталась литералом " " - заметили на описи вложения перед печатью. Теги (`<b>`, `<br>`) конвертер по-прежнему не интерпретирует.
- `nb_hyphen` ставит U+2011 вместо дефиса в составных словах тела и колонтитулов PDF (против переноса и потери дефиса при извлечении текста). Конвертер не правит типографику автора; исходник не переписывается.
- **HTML-комментарии (`<!-- ... -->`) в документ не попадают** - вырезаются, число сообщается в stderr. До 10.08.2026 они выводились видимым абзацем, и служебный блок с внутренними заметками уехал в собранное резюме: сборка не падает, PDF выглядит нормально, а увидеть это можно только прочитав готовый файл глазами (греп по Chrome-PDF не работает, см. выше). **В коде комментарии остаются** - там это пример, а не заметка: и в fenced-блоке (оба забора, ``` и `~~~`, в том числе с отступом под пунктом списка, и незакрытый до конца файла), и в inline-бэктиках посреди строки. Границы кода берутся из того же разбора, что и вся остальная сборка, - иначе защита оказалась бы уже обещанной. Держать служебное в комментариях можно, но **единственной защитой это считать нельзя**: другой конвертер поведет себя иначе, а исходник могут отдать получателю как есть.
## Специализация под проект
Если PDF-документ регулярный и со своим оформлением (пример: резюме с фото и контактами в шапке) - в проекте заводится локальный скилл-обертка и/или скрипт поверх этого (свой шаблон HTML/CSS, захардкоженные реквизиты, дефолтные пути). Канонический `md-pdf` остается generic; реквизиты и фирменный стиль в канон не тащить.
Скиллы-соседи по документам: `md-docx` (Word), `csv-xlsx` (таблицы Excel), `md-pptx` (презентации).