Генерация PDF-документов это практически неотъемлемая часть при создании отчетов. Ведь гораздо проще иметь один HTML-шаблон который просто редактируется, и в нужные места подставить необходимые данные.
В статье отобрал самые популярные (по звездам на GitHub на момент написания статьи) инструменты для генерирования PDF из HTML, DOCX и некоторых других форматов. Рассмотрим только те, которые реализованы непосредственно на Go ИЛИ имеют обертки над API на Go.
Список следующий:
https://github.com/go-pdf/fpdf (574 ⭐)
https://github.com/SebastiaanKlippert/go-wkhtmltopdf (1.1K ⭐)
FPDF
Начнем с наименее популярной из моего списка - https://github.com/go-pdf/fpdf
При своей простоте эта библиотека имеет самый обширный функционал для создания PDF-документов, который включает в себя добавление и удаление страниц, вставку изображений/текста/графиков, регулировку цветов текста и фона, добавление сторонних шрифтов, управление заголовками и полями. Самыми необычными функциями для меня оказались рисование кривых Безье, в том числе и составных.
Отмечу самые важные на мой взгляд плюсы и минусы этого инструмента.
Плюсы:
Легкая библиотека с минимальным количеством зависимостей.
Очень гибкая в плане настройки выходного PDF-документа.
Простой и понятный API.
Минусы:
Не получится редактировать существующие PDF-файлы. Библиотека поддерживает только создание новых.
Нет возможности работать с метаданными. Данный пункт изначально не собирался включать в список минусов, попало оно сюда благодаря тому, что следующие инструменты включают в себя поддержку такого функционала.
Последнее обновление репозитория было более года назад. Возможно, автор его забросил, так как висят много открытых issue без ответов.
Самый простой код для создания PDF выглядит так (для еще большего упрощения игнорирую ошибку при сохранении):
package main
import "github.com/go-pdf/fpdf"
func main() {
pdf := fpdf.New(fpdf.OrientationPortrait, fpdf.UnitPoint, fpdf.PageSizeA4, "")
// Устаналиваем шрифт. Courier - один из встроенных в библиотеку,
// остальные шрифты придется добавлять с помощью метода AddFont().
pdf.SetFont("Courier", "", 12)
pdf.SetFontSize(22)
pdf.AddPage()
pdf.SetTextColor(0, 191, 155)
pdf.Cell(40, 10, "Hello, World!")
_ = pdf.OutputFileAndClose("hello.pdf")
}
go-wkhtmltopdf
Эта библиотека представляет собой обертку над инструментом wkhtmltopdf, который предназначен для конвертации HTML в PDF и работает на движке WebKit. А это в свою очередь означает, что для использования библиотеки-клиента необходимо установить сам wkhtmltopdf на машину.
При перечислении функциональных возможностей буду опираться именно на возможности wkhtmltopdf, так как обертка по описанию предоставляет его полный функционал.
wkhtmltopdf имеет хорошую поддержку современных стандартов HTML, CSS и JavaScript - а значит можно создавать динамические PDF с интерактивными элементами. Для примера напишем простую генерацию документа по URL (да, он умеет и такое) и снова проигнорируем все ошибки ради упрощения кода:
package main
import "github.com/SebastiaanKlippert/go-wkhtmltopdf"
func main() {
// Создаем генератор и устанавливаем ориентацию страницы.
generator, _ := wkhtmltopdf.NewPDFGenerator()
generator.Orientation.Set(wkhtmltopdf.OrientationLandscape)
// "Скармливаем" страницу из Википедии.
generator.AddPage(wkhtmltopdf.NewPage("https://ru.wikipedia.org/wiki/Lorem_ipsum"))
_ = generator.Create() // Создание PDF-документа и сохранение в буфер.
_ = generator.WriteFile("wkhtmltopdf.pdf") // Запись по указанному пути на диске.
}
Все ссылки и стили сохранились корректно, тут ставим галочку и идем смотреть на плюсы и минусы.
Плюсы:
Умеет генерировать PDF по ссылке - "скормите" ей ссылку на страницу Википедии и получите PDF с сохранением оригинальных стилей.
Большой набор возможностей для регулировки качества выходного документа
Можно управлять всеми параметрами из кода, используя обертку для Go, без необходимости глубокого понимания работы самого wkhtmltopdf.
Минусы:
Использовать обертку, что логично, нельзя без установки бинарника wkhtmltopdf.
Скорость обработки больших и/или сложных файлов становится узким местом, так как эта операция довольно ресурсоемкая.
Не всегда корректно обрабатывает сложные и асинхронные элементы JavaScript.
Могут возникнуть сложности с установкой в контейнерной среде, так как может потребоваться установка дополнительных библиотек для wkhtmltopdf.
Gotenberg
Самый популярный инструмент из перечисленных, отчасти от того, что имеет наиболее обширный функционал работы с PDF-документами. Умеет генерировать PDF не только из HTML (CSS + JavaScript), но и поддерживает генерацию из DOCX/PPTX/XLS, Markdown.
Gotenberg содержит внутри себя сразу несколько движков, основными из которых являются Chromium и LibreOffice, но так же есть ExifTool, PDFtk, pdfcpu, QPDF, UNO.
Это своеобразный «мультитул» для генерации PDF‑документов. Еще одним важным аспектом является отлично оформленная документация, а еще поставляется в виде Docker-образа, что упрощает его настройку и запуск в контейнерной среде до максимума.
Gotenberg имеет множество библиотек-клиентов для ранзых языков, самые популярные из которых выделены в специальном репозитории. Хотя сама по себе API у него довольно простая, стоит присмотреться к использованию клиентов ради того, чтобы не реализовывать обработку и формирование multipart-содержимого, не следить постоянно за обновлениями и добавлением новых функций, которые при необходимости придется реализовать и на своей стороне.
Перечислю основные возможности Gotenberg:
Конвертация Markdown и HTML в PDF с поддержкой CSS и JavaScript.
Конвертация DOCX, PPTX и XLS в PDF (использует модуль LibreOffice).
Чтение и запись метаданных в PDF (модуль exiftool).
Широкий набор настроек для управления содержимым генерируемого PDF-документа.
Возможность создавать скриншоты из сгенерированного файла в форматах PNG, JPEG и WebP (появилась в версии 8).
В последней на данный момент 8.15-ой версии появились новые функции разделения PDF-документов постранично с указанием интервала пропуска или конкретного диапазона страниц.
Возможность объединять несколько документов в один.
Из минусов можно отметить следующие:
Зависимость от Docker усложняет задачу при развертывании вне контейнерной среды.
Довольно тяжеловесный и ресурсоемкий инструмент. Мы проводили нагрузочные тесты на подах с лимитами CPU=1000MiB и RAM=1Gb, но смогли добиться обработки только 4 одновременных запросов, а остальные Gotenberg уже отказывался выполнять.
Нельзя распараллелить операции в самом Gotenberg, о чем автор сам не раз упоминал в обсуждениях в репозитории. Этого можно добиться только добавлением новых инстансов с Gotenberg, так как один экземпляр работает только с одним запущенным движков Chromium и/или LibreOffice.
Небольшой пример генерации PDF из HTML покажу с использованием своей библиотеки https://github.com/starwalkn/gotenberg-go-client. Это форк оригинального клиента от автора, который он к сожалению перестал поддерживать после 7-ой версии.
package main
import (
"net/http"
"os"
"github.com/starwalkn/gotenberg-go-client/v8"
"github.com/starwalkn/gotenberg-go-client/v8/document"
)
func main() {
// Создаем клиента для подключения к запущенному инстансу Gotenberg.
client, err := gotenberg.NewClient("localhost:3000", http.DefaultClient)
// Создаем "документ" - абстракцию над файлами или ридерами.
index, err := document.FromPath("index.html", "/path/to/file")
// Создаем запрос на конвертацию HTML в PDF.
req := gotenberg.NewHTMLRequest(index)
// Фича downloadFrom - появилясь в 8-ой версии.
// Позволяет загружать файлы из сторонних источников, например, из вашего S3-хранилища.
downloads := make(map[string]map[string]string)
downloads["http://my.style.css"] = nil
downloads["http://my.img.gif"] = map[string]string{"X-Header": "Foo"}
req.DownloadFrom(downloads)
// Gotenberg так же позволяет установить базовую аутентификацию.
req.UseBasicAuth("username", "password")
// Далее у нас есть два варианта.
// 1. Сохраняем сгенерированный файл сразу на диск по указанному пути.
err = client.Store(context.Background(), req, "path/to/store.pdf")
// 2. Получаем сгенерированный документ в байтовом представлении в теле ответа.
resp, err := client.Send(context.Background(), req)
}
Стоит учитывать, что функций загрузки нескольких файлов из сторонних источников и базовой аутентификации нет в версиях ниже 7, поэтому не стесняемся обновлять образы.
Больше примеров я постарался описать в README своей библиотеки. Но если вы сторонитесь использования оберток - в официальной документации есть абсолютно все, что вам нужно.
Подводя итоги
Выбор инструмента всегда или почти всегда зависит от ваших требований. FPDF можно использовать для создания документов "с нуля" и если вам не нужен остальной функционал, который предлагают другие инструменты. Выбор между wkhtmltopdf и Gotenberg по большей части зависит от среды, в котором вы планируете их запускать. Gotenberg отлично подходит для запуска в контейнерной среде, но может стать узким местом при большой потоке запросов. wkhtmltopdf в свою очередь больше подойдет для работы вне контейнерной среды в связи с более сложной установкой и настройкой вашего Dockerfile.
Надеюсь вы нашли что-то новое для себя в этой статье, или по крайней мере освежили знания о существующих инструментах работы с PDF‑документами. Если я забыл еще какой‑нибудь популярный инструмент — буду рад упоминания в комментариях.
tuxi
XSL-FO завезли бы в golang, все было бы сильно проще с PDF