惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
D
Docker
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Simon Willison's Weblog
Simon Willison's Weblog
S
Security Affairs
NISL@THU
NISL@THU
T
Tor Project blog
A
About on SuperTechFans
宝玉的分享
宝玉的分享
腾讯CDC
S
Schneier on Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Privacy & Cybersecurity Law Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
雷峰网
雷峰网
C
Cyber Attacks, Cyber Crime and Cyber Security
Vercel News
Vercel News
Cisco Talos Blog
Cisco Talos Blog
D
DataBreaches.Net
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google Online Security Blog
Google Online Security Blog
Recorded Future
Recorded Future
L
LINUX DO - 热门话题
Microsoft Security Blog
Microsoft Security Blog
Latest news
Latest news
C
Check Point Blog
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
月光博客
月光博客
V
Vulnerabilities – Threatpost
T
Threat Research - Cisco Blogs
P
Palo Alto Networks Blog
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Отказываемся от wkhtmltopdf: как мы искали решение для генерации нетривиальных PDF
true_enginee · 2026-05-07 · via Все публикации подряд на Хабре

Время на прочтение8 мин

Охват и читатели1

wkhtmltopdf долгое время был одним из основных инструментов для генерации PDF из HTML. Мы столкнулись с ним на собственном проекте, но, когда потребовалось реализовать сложные макеты, колонтитулы и повторяющиеся заголовки в многостраничных документах — возникли проблемы.

В этой статье — краткий обзор альтернатив (Headless Chrome, Puppeteer, Playwright, WeasyPrint, Gotenberg), их плюсы и минусы, а также наш итоговый выбор и подводные камни, которые всплыли в процессе внедрения.

Введение

Одним из наших проектов является разработка корпоративной информационной системы для автоматизации сложных операционных бизнес-процессов. Продукт предназначен для стандартизации, повышения прозрачности и значительного ускорения процессов планирования, выполнения и расчета стоимости услуг.

Одной из подсистем является модуль для создания и согласования документов на основе заявок в понятных конечному заказчику терминах. Модуль представляет собой комплексную форму с большим количеством секций. Секции же состоят уже из конкретных полей с разными типами данных и вариантами их отображения. В конечном итоге, после заполнения всей формы, сформируется PDF файл для последующей печати, который отображает всю введенную информацию.

Пример страницы сформированного PDF-файла

Пример страницы сформированного PDF-файла

Основные требования:

  • соответствие дизайну и дополнительная индикация для разделов документа;

  • верхние и нижние колонтитулы на каждом листе;

  • повторение заголовков секций полей;

  • жесткие правила для переносов контента на разных страницах.

Исследование инструментов для печати PDF

Конечно, задача печати документов не единичная, и уже решалась на проектах не раз. Однако, технологии меняются, подходы к формированию печатных форм тоже не стоят на месте, и было решено провести разведочный анализ на проектах и в существующих технологиях на рынке.

Исследование существующих решений на проектах показало, что используется wkhtmltopdf разных версий. Инструмент представляет собой CLI для генерации PDF с помощью WebKit, актуального примерно на 2012-2015 года в лучшем случае. На момент исследования репозиторий с проектом уже был заархивирован.

Использование wkhtmltopdf вносит определенные ограничения в то, какие фичи из CSS мы можем использовать при генерации документа. К примеру, нет удобной работы с позиционированием, направлением текста, границами элементов и прочими удобствами. При исследовании обнаружили, что повторяемость документов, получаемых с помощью wkhtmltopdf, не постоянна – одни и те же данные могли занимать разные высоты, что в длинных печатных формах было заметно.

Пример того, как расположить текст вертикально в колонке таблицы для wkhtmltopdf

Пример того, как расположить текст вертикально в колонке таблицы для wkhtmltopdf

Таким образом, пришли на проекте к тому, что хотелось бы отказаться от wkhtmltopdf и решили искать альтернативные решения. На выбор было несколько решений:

  • Headless Chrome

  • Puppeteer

  • Playwright

  • WeasyPrint

  • Gotenberg

Интересно, что команда вспомнила даже про XSL-FO, который позволяет декларативно описывать разметку документа и генерировать из неё PDF, но технологию сразу отмели ввиду сложности поддержки.

Требования к инструментам были следующие:

  • Безопасность – не должно быть утечек

  • Стабильность – возможность фиксации версии

  • Актуальность – инструмент не является чем-то древним 🦕

Отдельно отметим, что хотелось использовать команду Фронтенда для верстки страниц самостоятельно.

Итак, рассмотрим эти варианты подробнее.

Headless Chrome

Пожалуй, самое простое и свежее решение – бинарник браузера, который поддерживает генерацию PDF при помощи CLI:

chrome --headless --disable-gpu –-no-sandbox --print-to-pdf=path/to/output.pdf

https://example.com

Такой подход позволяет легко запускать генерацию PDF, результатом которой будет документ, максимально похожий на вариант из Фигмы. Из-за запуска браузера на каждую генерацию этот подход будет неоправданно ресурсоемким. Безопасность у такого подхода тоже страдает – Chrome собирает телеметрию, статистику посещенных страниц и краш-репорты.

Решением проблем с безопасностью может быть упаковка в отдельный контейнер с жесткой фильтрацией трафика.

Примеры реализации можно посмотреть во множестве статей на Хабре, например, этой.

Puppeteer

Puppeteer – библиотека для работы с Chrome или Firefox в headless режиме. Имеет огромное количество возможностей, которые в первичном приближении точно не нужны проекту. Позволяет формировать PDF файлы с помощью await page.pdf(). Для подключения такого решения в проект потребуется написать отдельный микросервис на NodeJS, который плохо знаком команде бэкенда. Такое решение дает много контроля над headless-браузером, но требует за это цену – слишком многое нужно настраивать вручную, вроде горизонтального масштабирования и отслеживания утечек памяти.

Playwright

Playwright – фреймворк, очень похожий на Puppeteer, также позволяет работать с Chrome и Firefox, и даже с WebKit. Имеет намного больше возможностей, чем вышеупомянутый Puppeteer, и предназначен в основном для тестирования: шардинг, сбор трейсов, формирование отчетов, удаленное исполнение, тестирование компонентов, e2e, интеграционное и скриншотное тестирования. Решение поддерживает Java, что позволяет легко интегрировать его в существующую кодовую базу. Однако минусы остаются – слишком большие возможности – это все равно, что «стрелять из пушки по воробьям».

WeasyPrint

WeasyPrint – библиотека с упором на генерацию PDF из HTML разметки. Имеет хорошую производительность, поддержку большого количества спецификаций (одна из них решила бы большинство проблем во всех инструментах, но об этом позже) и больше время присутствия на рынке. Увы, без минусов не обошлось – библиотека написана на Python, что потребует отдельного микросервиса на незнакомом языке, и решение не поддерживает JavaScript, а значит необходимо подготовить всю страницу заранее.

Gotenberg

Gotenberg – набор инструментов, написанный на Go, предназначенный для конвертации документов в PDF. Поддерживает Headless Chromium (не путать с Chrome) и LibreOffice. Взаимодействие происходит через HTTP запросы, поэтому решение никак не привязано к конкретному языку программирования. Поставляется Gotenberg в виде Docker-образа, что сильно упрощает с ним взаимодействие в проекте, который уже использует Docker для деплоя. По сравнению с Puppeteer – из коробки отслеживает запущенный браузер, периодически его рестартует для устранения утечек памяти и имеет достаточно конфигурационных параметров в запросах, чтобы не сильно отставать от headless фреймворков.

Проанализировав варианты, остановились на Gotenberg в виду его легкой настройки, интеграции в проект и доступным фичам.

Путь к вершине – как мы добивались успеха

После того, как мы выбрали инструмент, стояла следующая задача: научится верстать большую страницу, состоящую из двух десятков уникальных разделов. Решено было использовать React, который уже использовался для приложения – код уже оброс некоторой логикой, требуемой для корректного отображения данных в документе.

Gotenberg позволяет загрузить страницу по URL, поэтому самым простым и быстрым решением было создать отдельный роут, который бы отвечал за рендеринг всего документа для печати, и передать бэкенду ссылку для открытия этого роута с необходимыми параметрами. Благодаря такому подходу мы очень быстро получили первый результат – генерацию PDF с необходимыми разделами, которые соответствовали дизайну. Однако, оставались и другие требования.

Отображение колонтитулов с изменением номера страницы

Первое, что требовалось решить, – каким образом можно отрендерить колонтитулы на каждой странице. Очевидным решением было изучить, что умеет CSS для работы с печатью, и нашлось целых три спеки – CSS Paged Media Module Level 3, CSS Generated Content for Paged Media Module Level 3 и CSS Generated Content Module Level 3. Спеки описывает, как работать со страницами, разделяют их на секции, в которые можно вставить какой-то контент, вводят счетчик для работы с нумерацией, позволяют отображать динамический контент и сноски, оформлять содержания (использовать leader для заполнения) и еще много интересных фич. Очень крутые идеи, но в реальности до браузеров добралось очень немногое: размеры, отступы, работа с секциями страницы, но не рендер динамического контента (целых блоков в верстке).

Интересно, что эти фичи поддерживает WeasyPrint, о чем мы узнали уже слишком поздно, и, если вам необходимо работать с подобными фичами, возможно, WeasyPrint хороший вариант.

Осознав, что спеки существуют только «на бумаге», мы пошли искать решение, пока отвергая реализацию Gotenberg, – он требует заранее подготовленную разметку и стили для верхних и нижних колонтитулов отдельно. Решение нашли довольно быстро – это Paged.js, библиотека для преобразования HTML в подготовленный вариант к печати в PDF. Увы, библиотека не имеет типизации 😬.

Paged.js сам реализует некоторую часть фичей из спек, позволяя использовать недоступный в браузерах функционал. Это была победа… но недолго. Оказалось, что использование библиотеки решало задачу отображения колонтитулов, но не решало задачи повторения заголовков секций полей на каждой новой странице – потребовался отдельный плагин для этого, уже существовавший в недрах ишью на GitHub. Теперь точно победа!

Наши ощущения, пока мы боролись с Paged.js

Наши ощущения, пока мы боролись с Paged.js

К сожалению, битву Paged.js мы проиграли – при тестировании обнаружилось, что с реальными данными часть текста на страницах просто исчезала, а часть накладывалась друг на друга. Решено было отказаться от Paged.js и использовать то, что предлагал Gotenberg.

Gotenberg ожидает готовую разметку для колонтитулов и не может исполнять JavaScript в них. Для решения этой задачи решили использовать метод renderToStaticMarkup из react-dom/server – подготавливаем HTML и CSS в React и отправляем вместе с ссылкой на страницу бэкенду.

Пример кода, отвечающего за рендер компонентов в HTML, формирование и скачивание PDF файла

Пример кода, отвечающего за рендер компонентов в HTML, формирование и скачивание PDF файла

Повторение заголовков секций полей

Вот эту часть необходимо повторять при разрывах страниц

Вот эту часть необходимо повторять при разрывах страниц

Параллельно с отображением колонтитулов мы решали вопрос повторения названий полей. Например, в какой-то секции есть текстовое поле с очень длинным значением, если такое поле находится внизу листа A4, то при переносе его значения на новый лист – надо повторить название этого поля. Первая мысль, которая возникает – а как это вообще сделать, наверняка что-то существует для этого, и лучше не пытаться изобрести свой велосипед.

Оказалось, что для такого повторения подходит старый добрый <table /> и его <thead /> и <tbody /> – название поля мы обернули в thead, а значения в tbody. Секции документа собрали из таких «табличек» и получили требуемый функционал. Кстати, пока пробовали использовать Paged.js, именно он и сломался при настройке колонтитулов с помощью библиотеки.

Фронтенд разработчики, когда пришли к использованию <table />

Фронтенд разработчики, когда пришли к использованию <table />

Для корректного отображения переносов внутри таблиц и секций настраиваем break-before, break-after и break-inside.

Из минусов использования Gotenberg для отображения колонтитулов отметим следующее – он не поддерживает (а если точнее, то этого не поддерживает Chromium) отображение различных колонтитулов на разных страницах (например, в нашем случае нужны были отличающиеся колонтитулы на первой и последней страницах), а при генерации PDF отдельными частями начинаются проблемы с нумерацией страниц (которую некоторые решают через тот самый Paged.js).

Генерация PDF и сохранение в S3

На стороне бэкенда добавили небольшой микросервис, который оборачивает multipart/form-data в более подходящий формат. Микросервис добавляет дефолтные параметры для Gotenberg и загружает полученный результат в S3 – целых 300 строк кода на Java и Spring Boot 😁. Существуют даже готовый Java клиент для взаимодействия, но на момент внедрения пару лет назад у этого клиента не хватало части опций.

Кстати, чтобы при генерации PDF цвета соответствовали дизайну, необходимо настроить свойство print-color-adjust в exact.

Заключение

В рамках исследования и разработки функционала мы добились генерации PDF, которые соответствуют макетам, имеют повторяемость между генерациям на одном и том же наборе данных, поддерживают отображение колонтитулов и повторение заголовков полей, да и дополнительные графические элементы позиционируются корректно.

Получили интересный опыт по формированию HTML верстки из React прямо в браузере. Благодаря такому опыту можно лучше изучить то, как React в принципе работает с серверными компонентами, контекстами и подобным.

Разработанное решение хоть и далеко от идеала, однако стало востребованным на соседних проектах – желание избавиться от wkhtmltopdf заставляет команды искать альтернативы.

Когда сделал выводы в конце пути

Когда сделал выводы в конце пути

Что мы можем сделать дальше? Хорошим вариантом будет разделения кода на микрофронтенды: отделить формирование документа для печати от остального приложения, лучше изучить серверный подход в React и перевести микрофронтенд на него.

Теперь мы знаем, что при необходимости генерировать сложные и комплексные документы, в которых нет различных колонтитулах на разных листах, то можно сверстать документ с помощью любой удобной библиотеки и отдать генерацию Gotenberg. Ну а если потребуется генерировать более сложные варианты, то следует обратить внимание на серверный рендеринг и WeasyPrint, который поддерживает большое количество спек для работы с Paged Media.

А вы сталкивались с такими задачами? Какие варианты решения использовали? Делитесь опытом.