Введение

Расскажу вам о кейсе про генерацию PDF из HTML страницы основанной на Ant Design для на который ушло гораздо больше времени, чем планировалось, при этом решение оказалось довольно простым. Расскажу какие решения мы пытались применить и с какими проблемами пришлось столкнуться.

Описание задачи

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

Первичные попытки решения   

Когда я только принял задачу в работу, в проекте уже существовало два способа генерации: средствами chrome и незавершённое решение с использованием jsPDF.

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

jsPDF сгенерировал PDF с некорректными шрифтами
jsPDF сгенерировал PDF с некорректными шрифтами

 jsPDF предоставляет конвертер шрифтов, чтобы их использовать в самом jsPDF. Шрифты не удавалось корректно конвертировать, и мы долго не могли понять почему, пока не выяснили, что шрифт в принципе не поддерживает кириллицу. Как так вышло – непонятно, но в итоге запросив новые шрифты и уже их конвертировав через конвертер jsPDF удалось корректно отобразить.

 Вторая проблема у jsPDF: обрезается текст в таблицах.

jsPDF разделил строку надвое
jsPDF разделил строку надвое

Хранение сгенерированного PDF не требовалось и некоторое время это устраивало заказчика, до тех пор, пока не появилось требование генерировать PDF на стороне сервера. 

Дальнейшие примеры будут представлены на основе следующей таблицы:

HTML с таблицей для конвертации в PDF
HTML с таблицей для конвертации в PDF

Решения на сервере

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

Использовали всё ту же утилиту wkhtmltopdf. Здесь мы столкнулись с несколькими проблемами:

  • Формат печати фиксированный: альбомный А4 с определенными полями и колонтитулом. Но отображение в браузере в полную ширину на разных экранах может отличаться, а значит и текст тоже перенесется на другую строку для PDF (построчно тоже должно было совпадать). Если уменьшить размер контента пользователя до размеров A4, то потеряем юзабилити, поскольку текст редактируется.

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

  • PDF должен генерироваться на сервере, а значит серверу нужно отдать HTML.

    В результате пришлось самостоятельно извлекать HTML и CSS, сжать для уменьшения отправляемого контента с помощью pako (напомню, что контент может достигать 100 страниц) и отправлять на сервер. Бэкенд уже сам это всё обрабатывал и генерировал PDF. Здесь было бы лучшим решением рендерить контент из существующих данных на сервере и там же генерировать PDF, но такой возможности не предоставилось.

  • Больше не поддерживается как сказано здесь и рекомендуется применить другие утилиты. Соответственно, при наличии проблем придётся решать их самим, а это дополнительные затраты времени.

  • В версии 0.12.3 wkhtmltopdf обрезал часть текста в местах разрыва страниц внутри таблиц и некорректно отображал стили таблиц из-за чего пришлось отказаться от него.

wkhtmltopdf некорректно сформировал PDF
wkhtmltopdf некорректно сформировал PDF

Альтернативные решения

На странице статуса wkhtmltopdf можно найти несколько предложений утилит на замену.

Утилиту Prince сразу отклонили, поскольку требует оплаты чего позволить себе не могли: необходимо составить обоснование приобретения, затем само согласование, которое может затянуться, и множество других проблем.

Weasyprint сначала показал себя отлично, пользователи использовали этот вариант, но затем выяснились две проблемы:

  • Часть отображаемого текста в таблицах может просто не отображаться при неправильной настройке css

WeasyPrint некорректно сгенерировал PDF
WeasyPrint некорректно сгенерировал PDF
  • не поддерживает большинство новых стилей CSS и это проблема, потому что: во-первых, Ant Design использует новые стили, во-вторых, это ограничило бы нас во внедрении новых стилей к нам в проект. Чтобы хоть как-то исправить ситуацию в Ant Design отключали where селектор с помощью <StyleProvider hashPriority="high">

weasyprint html_for_print.html weasyprint_output.pdf выдал большое количество предупреждений об игнорировании стилей:

WeasyPrint выдаёт предупреждения об игнорировании стилей
WeasyPrint выдаёт предупреждения об игнорировании стилей

Утилита Puppeteer показала самый идеальный вариант для нас: полностью корректное отображение таблиц, никаких потерь текста, полностью поддерживаются самые новые CSS стили.

Puppeteer отлично отрендерил pdf
Puppeteer отлично отрендерил pdf

На первый взгляд, мы нашли решение всех проблем, однако, это был не конец. Puppeteer — это библиотека для NodeJS, которая управляет headless Chrome. NodeJS у нас отсутствовал на тот момент, и чтобы его запустить требовалось согласование. Согласование провалилось, нам не одобрили развертывания контейнера только для генерации PDF, поэтому, мы пошли дальше искать решение.

Финальное решение

В поисках утилит генерации PDF опробовал ещё несколько из них и с каждым была какая-либо проблема: некорректно работает с таблицами, либо невозможно запустить. В поисках решения я задал себе вопрос: почему большая часть утилит некорректно обрабатывает таблицы и почему только с Puppeteer получилось корректно сгенерировать PDF, в чём различие?

Не скажу, что это хороший для вас ответ, но хорошо видна закономерность, что wkhtmltopdf, weasyprint используют какой-нибудь Qt Webkit, которые плохо справляются с рендерингом, и только Puppeteer с Headless Chrome под капотом достойно с этим справился.

Исходя из этого вывода нам нужна утилита или библиотека с использованием Headless Chrome, но здесь же проблема – все они запускаются с помощью NodeJS, playwright, selenium, которые мы не можем использовать. Решение нашлось, вернувшись к тому, с чего начинали – браузер chrome. Посмотрев документацию, удалось найти параметры для генерации PDF. Опробовав их, стало понятно – работает отлично. Отдав нужные параметры бэкенду, мы решили проблему с рендерингом PDF.

Chrome отрендерил pdf но с ненужными колонтитулами
Chrome отрендерил pdf но с ненужными колонтитулами

В windows в git bash для windows попробовать можно следующей строкой:

"C:/Program Files/Google/Chrome/Application/chrome.exe" –headless –disable-gpu --print-to-pdf="путь_до_каталога_куда_сгенерировать_pdf/chrome_output.pdf" "путь_до_каталога_где_лежит_html/html_for_print.html"

Оставалась только одна проблема: я не нашёл способа изменить вариант нумерации страниц в колонтитуле средствами chrome и его пришлось отключить. Попросив решение для добавления нумерации страниц к сгенерированному PDF у ChatGPT он выдал подходящее решение в виде утилиты latex и pdftk. Процесс пагинации опишу подробней.

Отключить колонтитулы при использовании chrome можно опцией --no-pdf-header-footer:

"C:/Program Files/Google/Chrome/Application/chrome.exe" –headless –disable-gpu --no-pdf-header-footer --print-to-pdf="путь_до_каталога_куда_сгенерировать_pdf/chrome_output.pdf" "путь_до_каталога_где_лежит_html/html_for_print.html"

В следующем примере с помощью latex создаём шаблон pdf с нумерацией в виде номер_страницы/всего_страниц и объединяем его со сгенерированным pdf с помощью pdftk. Все действия производились в WSL.

1) Установка:

sudo apt update

sudo apt install pdftk texlive-latex-base texlive-latex-extra

2) Создаём файл шаблона page_numbers_and_count_pages.tex, создаём shell скрипт page_numbers_and_count_pages.sh и вписываем в него текст, который добавит нужные данные для пагинации:

num_pages=$(pdftk chrome_output.pdf dump_data | grep NumberOfPages | awk '{print $2}')

echo "\documentclass{article}

\usepackage[a4paper,landscape,margin=1in]{geometry}

\usepackage{fancyhdr}

\usepackage{lastpage}

\pagestyle{fancy}

\fancyhf{}

\fancyfoot[C]{\Large \thepage\ / \pageref{LastPage}}

\renewcommand{\headrulewidth}{0pt}

\begin{document}

" > page_numbers_and_count_pages.tex

for ((i=1; i<=num_pages; i++)); do

echo "\null\newpage" >> page_numbers_and_count_pages.tex

done

echo "\end{document}" >> page_numbers_and_count_pages.tex

3) Компилируем два раза (по какой-то причине с первого раза не устанавливается общее количество страниц):

pdflatex page_numbers_and_count_pages.tex

pdflatex page_numbers_and_count_pages.tex

Получаем пустой пронумерованный PDF

latex сгенерировал пронумерованный pdf
latex сгенерировал пронумерованный pdf

4) Собираем всё вместе:

pdftk chrome_output.pdf multistamp page_numbers_and_count_pages.pdf output chrome_output_paginated_and_count_pages.pdf

В результате, получили отлично отрисованный PDF. Обратите внимание, что на второй странице нумерация отображается поверх контента. Решается это путём добавления @page c margin.

pdftk объединил два pdf в один
pdftk объединил два pdf в один

Заключение

В заключение скажу, что прибегать к рендерингу с помощью chrome следует в самую последнюю очередь. С большинством задач jsPDF, WeasyPrint, Puppeteer справляются отлично. Нам пришлось это сделать, потому что стояла слишком специфичная задача с множеством ограничений.

Полагаю, ещё более лучшим решением было бы перейти на редакторы текста, которые предоставляют встроенную генерацию PDF (например, TinyMCE), но у нас проект без их использования и применить редакторы невозможно без выделения ресурсов на перенос страницы в редактор.

Надеюсь, что кому-то это будет полезным. Удачи!

Комментарии (0)