|
Professor Seleznov
|
wkhtmltopdf долгое время был одним из основных инструментов для генерации PDF из HTML. Мы столкнулись с ним на собственном проекте, но, когда потребовалось реализовать сложные макеты, колонтитулы и повторяющиеся заголовки в многостраничных документах — возникли проблемы. В этой статье — краткий обзор альтернатив (Headless Chrome, Puppeteer, Playwright, WeasyPrint, Gotenberg), их плюсы и минусы, а также наш итоговый выбор и подводные камни, которые всплыли в процессе внедрения. Введение Одним из наших проектов является разработка корпоративной информационной системы для автоматизации сложных операционных бизнес-процессов. Продукт предназначен для стандартизации, повышения прозрачности и значительного ускорения процессов планирования, выполнения и расчета стоимости услуг. Одной из подсистем является модуль для создания и согласования документов на основе заявок в понятных конечному заказчику терминах. Модуль представляет собой комплексную форму с большим количеством секций. Секции же состоят уже из конкретных полей с разными типами данных и вариантами их отображения. В конечном итоге, после заполнения всей формы, сформируется PDF файл для последующей печати, который отображает всю введенную информацию.

Пример страницы сформированного PDF-файла Основные требования:
- соответствие дизайну и дополнительная индикация для разделов документа;
- верхние и нижние колонтитулы на каждом листе;
- повторение заголовков секций полей;
- жесткие правила для переносов контента на разных страницах.
Исследование инструментов для печати PDF Конечно, задача печати документов не единичная, и уже решалась на проектах не раз. Однако, технологии меняются, подходы к формированию печатных форм тоже не стоят на месте, и было решено провести разведочный анализ на проектах и в существующих технологиях на рынке. Исследование существующих решений на проектах показало, что используется wkhtmltopdf разных версий. Инструмент представляет собой CLI для генерации PDF с помощью WebKit, актуального примерно на 2012-2015 года в лучшем случае. На момент исследования репозиторий с проектом уже был заархивирован. Использование wkhtmltopdf вносит определенные ограничения в то, какие фичи из CSS мы можем использовать при генерации документа. К примеру, нет удобной работы с позиционированием, направлением текста, границами элементов и прочими удобствами. При исследовании обнаружили, что повторяемость документов, получаемых с помощью 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 и использовать то, что предлагал Gotenberg. Gotenberg ожидает готовую разметку для колонтитулов и не может исполнять JavaScript в них. Для решения этой задачи решили использовать метод renderToStaticMarkup из react-dom/server – подготавливаем HTML и CSS в React и отправляем вместе с ссылкой на страницу бэкенду.

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

Вот эту часть необходимо повторять при разрывах страниц Параллельно с отображением колонтитулов мы решали вопрос повторения названий полей. Например, в какой-то секции есть текстовое поле с очень длинным значением, если такое поле находится внизу листа A4, то при переносе его значения на новый лист – надо повторить название этого поля. Первая мысль, которая возникает – а как это вообще сделать, наверняка что-то существует для этого, и лучше не пытаться изобрести свой велосипед. Оказалось, что для такого повторения подходит старый добрый и его и – название поля мы обернули в thead, а значения в tbody. Секции документа собрали из таких «табличек» и получили требуемый функционал. Кстати, пока пробовали использовать Paged.js, именно он и сломался при настройке колонтитулов с помощью библиотеки.

Фронтенд разработчики, когда пришли к использованию Для корректного отображения переносов внутри таблиц и секций настраиваем 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. А вы сталкивались с такими задачами? Какие варианты решения использовали? Делитесь опытом.-Источник
|