Генерация PDF из HTML-шаблона: от самописных решений к API
Зачем вообще генерировать PDF программно
PDF остаётся де-факто стандартом для любых «официальных» документов: счета, акты, договоры, билеты, сертификаты, аналитические отчёты. Пользователь ожидает получить файл, который одинаково выглядит на любом устройстве, не редактируется без специального ПО и корректно печатается.
Для разработчика это значит, что генерация PDF должна быть программной и повторяемой — данные из базы подставляются в шаблон, документ рендерится и отдаётся пользователю или сохраняется в хранилище.
Популярные инструменты для генерации PDF из HTML
Puppeteer / Playwright
Самый распространённый подход в Node.js-мире — запустить chromium-headless и вызвать page.pdf(). Puppeteer и Playwright дают доступ к полному браузерному рендерингу: поддержка CSS Grid, Flexbox, веб-шрифтов, SVG, даже Canvas.
// Генерация PDF через Puppeteer
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html);
const pdf = await page.pdf({ format: "A4" });
await browser.close();Звучит просто, но в production сразу возникают проблемы. Chromium весит ~300 МБ и потребляет 200–400 МБ оперативной памяти на каждый параллельный рендер. Нужен пул процессов, тайм-ауты, логи падений. Если шаблоны поступают от пользователей — открываете XSS-вектор прямо в headless-браузере, то есть нужна строгая изоляция (Docker, seccomp, отдельная VM). При сколь-либо серьезной нагрузке это уже отдельный сервис с автоскейлингом, а если нагрузка невелика, то поддежка такой инфраструктуры просто не стоит того.
WeasyPrint
Python-библиотека для рендеринга HTML/CSS в PDF через собственный движок (без браузера). Легче Puppeteer, но поддержка CSS значительно хуже: нет Flexbox Grid в полном объёме, проблемы с кастомными шрифтами, специфические баги с page-break. Шаблоны приходится писать «под WeasyPrint», что сильно ограничивает дизайн.
# Генерация PDF через WeasyPrint
from weasyprint import HTML
HTML(string=html_content).write_pdf('output.pdf')Хорошо работает для простых документов, плохо масштабируется и требует установки системных зависимостей (libpango, libcairo и др.), что усложняет деплой в контейнерах.
PDFKit (Node.js / Ruby)
PDFKit — это генерация PDF снизу вверх: вы программно рисуете прямоугольники, добавляете текст, задаёте координаты. Никакого HTML — только low-level PDF API. Результат предсказуем и компактен, но подготовка шаблона требует в разы больше кода, а любое изменение макета — болезненный рефакторинг.
// Генерация PDF через PDFKit — низкоуровневый подход
const doc = new PDFDocument();
doc.fontSize(18).text("Счёт №123", 50, 50);
doc.fontSize(12).text(`Итого: ${amount} ₽`, 50, 100);wkhtmltopdf
Классика, основанная на WebKit. Больше не поддерживается активно (последний релиз — 2020 год), движок устарел, современный CSS поддерживается частично. На новых дистрибутивах Linux возникают конфликты зависимостей. Упоминаем для полноты картины — в новых проектах лучше избегать.
Краткое сравнение
| Инструмент | HTML/CSS поддержка | Нагрузка на сервер | Сложность деплоя |
|---|---|---|---|
| Puppeteer/Playwright | Полная (Chromium) | Высокая | Высокая |
| WeasyPrint | Частичная | Низкая | Средняя |
| PDFKit | Нет (low-level) | Низкая | Низкая |
| wkhtmltopdf | Устаревшая | Средняя | Высокая |
Проблема в том, что генерация PDF из HTML — это инфраструктурная задача.
Вот неполный список того, с чем придётся разобраться самостоятельно: изоляция шаблонов от XSS и SSRF-атак, управление пулом headless-браузеров и их падениями, мониторинг памяти и CPU при пиковой нагрузке, обновление Chromium после security-патчей, корректная работа кириллических и нестандартных шрифтов, очередь задач при асинхронной генерации.
Bulldoc - специализированный сервис для генерации документов
Bulldoc — это специализированный сервис для генерации и обработки печатных документов, доступный через REST API. Вместо того чтобы поддерживать инфраструктуру рендеринга самостоятельно, вы отправляете HTTP-запрос с HTML-шаблоном и данными — и получаете готовый PDF.
Гибкие динамические шаблоны
Шаблоны поддерживают синтаксис Handlebars: {{ переменная }}, {{#each список}}, условия {{#if}}. Это привычно, предсказуемо и работает как с простыми счетами, так и со сложными многостраничными отчётами. В шаблоне можно использовать JavaScript (включая сторонние библиотеки), произвольный CSS и любой CSS-фреймворк — рендеринг происходит в полноценном браузерном движке.
Отдельно можно задавать колонтитулы (header/footer), формат страницы (A4, A3 и др.), ориентацию и поля — прямо в параметрах шаблона или в теле запроса.
Простое и удобное API
API построен по стандарту OpenAPI, что позволяет сгенерировать клиентскую библиотеку для любого языка программирования.
Поддерживаются два режима работы: асинхронный (по умолчанию) и синхронный (параметр sync: true). В асинхронном режиме API сразу возвращает объект задачи с id, а статус можно опрашивать через GET /v1/tasks/{id} или получить уведомление через вебхук. В синхронном режиме соединение держится открытым до завершения генерации — удобно для простых документов, когда нужно дождаться результата сразу.
Гибкое ценообразование
Модель оплаты — предоплаченные пакеты документов. Вы платите за то, что реально генерируете, без подписок и абонентской платы. Это особенно выгодно для приложений с нерегулярной или непредсказуемой нагрузкой.
Пошаговое руководство: генерация PDF из HTML-шаблона через Bulldoc
Шаг 1. Регистрация в сервисе
Зарегистрируйтесь в сервисе и создайте аккаунт. Для начала работы достаточно email и пароля.
Шаг 2. Создание API-ключа
После входа в личный кабинет перейдите в раздел настроек и создайте новый API-ключ. Ключ используется как Bearer-токен во всех запросах:
Authorization: Bearer ВАШ_API_КЛЮЧСохраните ключ в надёжном месте — он отображается только один раз при создании.
Шаг 3. Добавление шаблона
Шаблон — это HTML-документ с Handlebars-разметкой для подстановки данных. Добавить шаблон можно двумя способами.
Через веб-интерфейс — в разделе «Шаблоны» вы можете создать новый HTML-шаблон, задать данные по умолчанию, настроить формат страницы и сразу увидеть превью. Это удобно для разработки и отладки шаблона.
Через API — отправьте POST /v1/templates с кодом и параметрами шаблона:
{
"name": "invoice",
"content": "<html><body><h1>Счёт № {{ invoiceNumber }}</h1><p>Клиент: {{ clientName }}</p><p><strong>Итого: {{ total }} ₽</strong></p></body></html>",
"data": {
"invoiceNumber": "001",
"clientName": "ООО Ромашка",
"total": "15 000"
},
"format": "A4",
"orientation": "portrait",
"margin": { "top": 20, "bottom": 20, "left": 20, "right": 20 }
}Поле data задаёт данные по умолчанию — они используются для предпросмотра и будут объединены с данными, которые вы передаёте при генерации конкретного документа.
Синтаксис шаблонов (Handlebars)
Простая подстановка переменной — {{ имя_переменной }}.
Перебор массива — с помощью {{#each}}:
<ul>
{{#each items}}
<li>{{this.name}} — {{this.price}} ₽</li>
{{/each}}
</ul>Условный блок — с помощью {{#if}}:
{{#if isPaid}}
<span class="badge">Оплачен</span>
{{else}}
<span class="badge">Ожидает оплаты</span>
{{/if}}В шаблоне работает полноценный JavaScript: можно подключать библиотеки для рендеринга схем и графиков, использовать любые CSS-фреймворки, добавлять кастомные шрифты.
Шаг 4. Синхронные и асинхронные запросы
По умолчанию все запросы на генерацию — асинхронные. Это правильная модель для production: вы ставите задачу в очередь, получаете taskId, и когда документ готов — забираете его по ссылке или получаете уведомление на вебхук. Такой подход не блокирует HTTP-соединение и хорошо работает при пиковых нагрузках.
// Ответ на асинхронный запрос
{
"id": "6ITGo",
"status": "pending",
"type": "pdf/from-template",
"documents": []
}Для простых сценариев (например, синхронная выгрузка документа пользователю прямо в браузер) удобен синхронный режим — добавьте "sync": true в тело запроса. API будет держать соединение открытым и вернёт финальный объект задачи с уже готовой ссылкой на документ:
// Ответ на синхронный запрос
{
"id": "6ITGo",
"status": "finished",
"documents": [
{
"id": "abc123",
"url": "https://...",
"bytesize": 34521,
"expiresAt": "2026-04-18T12:00:00Z"
}
]
}Шаг 5. Примеры запросов для генерации PDF
cURL
curl -X POST https://api.bulldoc.dev/v1/pdf/from/template \
-H "Authorization: Bearer ВАШ_API_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"templateId": "ВАШ_TEMPLATE_ID",
"sync": true,
"data": {
"invoiceNumber": "042",
"clientName": "ИП Иванов",
"total": "37 500"
}
}'JavaScript / Node.js
Ниже пример генерации PDF из шаблона в Node.js с использованием встроенного fetch (Node 18+). Обратите внимание, что мы используем синхронный режим и сразу получаем ссылку на скачивание.
// Node.js — генерация PDF из HTML-шаблона через Bulldoc API
async function generateInvoice(data) {
const response = await fetch("https://api.bulldoc.dev/v1/pdf/from/template", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BULLDOC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: "ВАШ_TEMPLATE_ID",
sync: true, // ждём готового результата
data: data,
}),
});
if (!response.ok) {
throw new Error(`Bulldoc API error: ${response.status}`);
}
const task = await response.json();
// task.documents[0].url — прямая ссылка на скачивание PDF
return task.documents[0].url;
}
// Использование
const url = await generateInvoice({
invoiceNumber: "042",
clientName: "ИП Иванов",
total: "37 500",
});
console.log("PDF готов:", url);Python
В Python удобнее всего использовать библиотеку httpx или requests. Пример ниже демонстрирует генерацию PDF по шаблону в синхронном режиме, а также вариант с inline-шаблоном — когда шаблон передаётся прямо в запросе, без предварительного сохранения.
import os
import httpx # pip install httpx
BULLDOC_API_KEY = os.environ["BULLDOC_API_KEY"]
BASE_URL = "https://api.bulldoc.dev"
def generate_pdf_from_template(template_id: str, data: dict) -> str:
"""
Python генерация PDF по шаблону через Bulldoc API.
Возвращает URL готового PDF-файла.
"""
response = httpx.post(
f"{BASE_URL}/v1/pdf/from/template",
headers={
"Authorization": f"Bearer {BULLDOC_API_KEY}",
"Content-Type": "application/json",
},
json={
"templateId": template_id,
"sync": True, # синхронный режим
"data": data,
},
timeout=60.0, # запас для сложных документов
)
response.raise_for_status()
task = response.json()
return task["documents"][0]["url"]
def generate_pdf_inline(html: str, data: dict) -> str:
"""
Конвертировать HTML в PDF без сохранённого шаблона.
Удобно для одноразовых или динамически формируемых документов.
"""
response = httpx.post(
f"{BASE_URL}/v1/pdf/from/template",
headers={
"Authorization": f"Bearer {BULLDOC_API_KEY}",
"Content-Type": "application/json",
},
json={
"template": html, # шаблон прямо в теле запроса
"sync": True,
"data": data,
},
timeout=60.0,
)
response.raise_for_status()
task = response.json()
return task["documents"][0]["url"]
# Пример использования
if __name__ == "__main__":
# Вариант 1: использовать сохранённый шаблон
pdf_url = generate_pdf_from_template(
template_id="ВАШ_TEMPLATE_ID",
data={"invoiceNumber": "042", "clientName": "ИП Иванов", "total": "37 500"},
)
print("PDF готов:", pdf_url)
# Вариант 2: inline-шаблон (конвертировать HTML в PDF на лету)
html_template = "<html><body><h1>Привет, {{ name }}!</h1></body></html>"
pdf_url = generate_pdf_inline(html_template, {"name": "Мир"})
print("Inline PDF:", pdf_url)Итог
Генерация PDF из HTML — задача, которая обманчиво выглядит простой. Puppeteer, WeasyPrint, wkhtmltopdf и PDFKit отлично справляются с базовыми сценариями, но в production каждый из них превращается в отдельный инфраструктурный проект: безопасность шаблонов, управление памятью, масштабирование, обновления.
Bulldoc позволяет вынести эту инфраструктуру за пределы вашего приложения и свести интеграцию к одному HTTP-запросу. Шаблоны на Handlebars, полная поддержка HTML/CSS/JS, синхронный и асинхронный режимы, прозрачное ценообразование — всё это делает его удобным вариантом как для стартапов, так и для зрелых продуктов, которым надоело поддерживать «PDF-сервис» внутри монолита.
Начните совершенно бесплатно: bulldoc.dev.