dobrovolskiy.com
EN
← Заметки2026-08-31 · Дмитрий Добровольский

PDF в CMYK для офсета из HTML: WeasyPrint, Ghostscript и pikepdf под требования типографии

Как база продукции превращается в готовый к офсету каталог: WeasyPrint для вёрстки, Ghostscript для перевода RGB в DeviceCMYK со сверкой по принятому типографией мастер-файлу, pikepdf для чистых красок, TrimBox и BleedBox, и ошибка с прозрачностью, из-за которой каждая полоса молча растрировалась.

WeasyPrint прекрасно верстает каталог из HTML и выдаёт RGB PDF 1.7 с одним лишь MediaBox — такой файл не примет ни одна типография. Ghostscript переводит его в DeviceCMYK через ICC-конвейер (сверено с принятым типографией мастером: #0a0a0a даёт C71 M67 Y67 K85 против их C72 M67 Y67 K85), pikepdf возвращает краскообразующие цвета к чистым сепарациям, чтобы мелкий текст остался 100 K, а предварительный проход снимает безусловные группы прозрачности WeasyPrint, из-за которых конвертация в PDF 1.3 растрирует всю страницу.

Задача

FOURLEDS печатает каталог продукции. Типография принимает файлы InDesign и выдаёт в ответ двухстраничные технические требования к макетам: CMYK, PDF 1.3, изображения не более 350 ppi, суммарная краска до 300 %, никакого составного чёрного на мелком тексте, TrimBox и BleedBox на каждой полосе, без встроенного выходного ICC-профиля. Сам каталог живёт в базе — 31 модель светильников со спецификациями, фотометрией и фотографиями, — так что пересобирать его руками в InDesign при каждом изменении цены невозможно.

Поэтому сервис PDF (Flask + WeasyPrint + pikepdf + Ghostscript) рендерит каталог из тех же данных о продукции, что и сайт, а потом доводит результат до печатной готовности. Сложность не в вёрстке. Сложность в последних 5 %.

Что даёт WeasyPrint и чего не даёт

WeasyPrint верстает печатный шаблон на листе 234,8 x 321,8 мм: дообрезной формат A4, 5 мм вылетов и 7,4 мм служебного поля с метками реза, приводочными крестами и шкалами контроля — повторяя уже принятый заказчиком оригинал в InDesign. Эта часть — удовольствие: обычные HTML и CSS, пагинатор на TypeScript измеряет высоту блоков в миллиметрах и раскладывает серии по разворотам, предпросмотр одной полосы мгновенный.

На выходе — RGB PDF 1.7, в котором есть только MediaBox. Ни одно требование из списка выше ещё не выполнено.

Проход 1 — Ghostscript: RGB в DeviceCMYK

gs -dBATCH -dNOPAUSE -dSAFER -sDEVICE=pdfwrite \
   -dCompatibilityLevel=1.3 \
   -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK \
   -dEmbedAllFonts=true -dSubsetFonts=true \
   -dDownsampleColorImages=true -dColorImageResolution=350 \
   -dColorImageDownsampleThreshold=1.0 -dColorImageDownsampleType=/Bicubic

Результат я сверял с мастер-файлом, который типография уже приняла. Тёмная плашка #0a0a0a конвертируется в C71 M67 Y67 K85 против C72 M67 Y67 K85 в оригинале; серая шкала #B7B7B7 даёт C28 M23 Y24 K0 — совпадение точное. Фотографии сохраняют насыщенность. Разница в цветоделении на печати неразличима.

Подход, который я выбросил: -dUseFastColor с единичными кривыми BG/UCR, то есть полный GCR. Он соблазнителен тем, что делает чёрный чистым 100 K. Но заодно он заменяет чёрной краской хроматическую тёмную составляющую каждого полутона: измеренная насыщенность упала до 90 против 160 в оригинале, фотографии посерели, а все глубокие чёрные схлопнулись в 100 K. Не лечите проблему чёрного текста через GCR.

Проход 2 — pikepdf: чистые краски, TrimBox, BleedBox

ICC-конвертация верна для фотографий и неверна для тех немногих цветов, которые шаблон использует как краску. Мелкий чёрный текст обязан быть чистым 100 K; поля шкал контроля и серый клин в служебном поле должны остаться одноканальными, иначе они не выполнят свою функцию на печатном листе. При этом большие тёмные плашки в шаблоне намеренно заданы как #0a0a0a и #060606, а не #000 — они должны воспроизвестись тем самым составным чёрным около 290 %, что и в оригинале.

Поэтому pikepdf проходит по содержимому страниц и возвращает короткий список краскообразующих исходных цветов к чистым сепарациям, оставляя всё остальное — и в первую очередь пиксели изображений — с полной ICC-конвертацией. Затем он проставляет на каждой полосе TrimBox (210 x 297) и BleedBox (дообрезной + 5 мм) и не встраивает выходной профиль, как и требуется. Суммарная краска в пике — около 294 % при их лимите 300 %.

Приём, который делает это поддерживаемым: таблица замен выучивается у самого Ghostscript. Пробный PDF с этими исходными цветами прогоняется через ровно тот же вызов gs, что и документ, и те литералы CMYK, которые gs для них пишет, становятся ключами поиска. Обновится Ghostscript или его профили — таблица переедет вместе с ними, ничего версионно-зависимого в коде не зашито. Если проба выглядит подозрительно, таблица остаётся пустой и замена просто не выполняется.

Ошибка, стоившая дороже всех

В PDF 1.3 нет прозрачности, поэтому Ghostscript её сплющивает — растрированием. Первые печатные сборки вышли страницами картинок вместо векторного текста, притом что прозрачности в документе нет вообще.

Причина: WeasyPrint безусловно ставит /Group << /S /Transparency >> на каждую страницу и каждый form XObject и вешает записи ExtGState на заведомо непрозрачные обводки SVG. Одного их присутствия хватает, чтобы gs перешёл в режим сплющивания. Лечится проходом pikepdf перед Ghostscript: снять обёртку группы там, где внутри нет ни маски прозрачности, ни режима наложения, ни постоянной альфы — на композит это повлиять не может, — и нормализовать пустые ExtGState. Настоящую прозрачность трогать не нужно, её gs сплющит локально.

Эксплуатация

Самый прожорливый по памяти узел здесь — WeasyPrint. Два одновременных рендера полного каталога в оперативную память не помещаются, поэтому рендеры сериализованы семафором независимо от того, сколько потоков gunicorn свободно, с большим таймаутом очереди; health-check и предпросмотр одной полосы при этом отвечают сразу.

Что бы я сделал иначе

Я бы сначала измерил принятый мастер-файл и только потом писал конвертацию. На подход с GCR ушёл день, потому что в теории он звучал правильно; десять минут с пипеткой по файлу самой типографии закрыли бы вопрос сразу. Всё удачное в этом конвейере появилось из того, что принятый заказчиком PDF считался спецификацией, а письменные требования — его кратким пересказом.

Вопросы

Почему не InDesign?

Потому что источник истины — база из 31 модели светильников со спецификациями, фотометрией, фотографиями и ценами. Любое изменение цены означало бы ручную переверстку. HTML-шаблон и пагинатор, измеряющий блоки в миллиметрах, пересобирают весь каталог из актуальных данных за один прогон.

Умеет ли WeasyPrint в CMYK?

Нет. WeasyPrint выдаёт RGB PDF 1.7 только с MediaBox. CMYK, PDF 1.3, понижение разрешения изображений, TrimBox/BleedBox и контроль чистых красок обеспечивают последующие проходы Ghostscript и pikepdf.

Почему после конвертации в CMYK через Ghostscript векторный текст превращается в картинку?

В PDF 1.3 нет прозрачности, поэтому Ghostscript сплющивает её растрированием, а WeasyPrint ставит /Group << /S /Transparency >> на каждую страницу и form XObject, даже когда альфы в документе нет вовсе. Снимите эти обёртки через pikepdf перед вызовом gs — только там, где внутри нет маски, режима наложения и постоянной альфы, — и текст останется векторным.

Нужно что-то похожее?

Делаю это вживую на звонке в Zoom / Яндекс Телемост, вы смотрите на экран, таймер останавливается по вашему слову. Первые 15 минут бесплатно.

Ещё проекты

Каталог светильников FOURLEDSЕдиный источник данных о продукции FOURLEDS: публичный каталог, генератор печатных PDF, API для остального ПО семейства, ИИ-помощь с контентом