РЕШЕНИЯ

Генерация PDF из HTML-шаблона: от самописных решений к API

Рано или поздно любое приложение обрастает задачей «выгрузить это в PDF». Счёт, договор, отчёт — неважно. Задача кажется простой ровно до первого боевого деплоя. В этой статье разбираем, почему самостоятельная реализация генерации PDF из HTML превращается в отдельный инфраструктурный проект, и как Bulldoc решает её одним 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.