Автоматическая генерация PDF из HTML: PHP и Node.js

Реализация генерации PDF

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Автоматическая генерация PDF из HTML: PHP и Node.js
Средний
~3-5 дней

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1414
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    982
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    995

Реализация генерации PDF

Отметим: когда счёт уходит с задержкой из-за ручного формирования PDF — бизнес теряет деньги. В одном проекте генерация 500 счетов в день занимала 4 часа ручного труда. Мы автоматизировали процесс: теперь PDF формируются за 5 минут после заказа, а клиенты получают их сразу. Расскажу, как мы внедряем серверную генерацию PDF с использованием HTML-to-PDF. Наш клиент — сервис онлайн-бухгалтерии — сэкономил 1.2 млн рублей в год, автоматизировав выпуск актов выполненных работ. Ручная генерация стоила компании 300 000 рублей ежемесячно.

Проблемы, которые решаем

Сложный макет с CSS. Многие библиотеки не понимают Grid, Flexbox, переменные. Решение — headless Chrome через Browsershot (PHP) или Puppeteer (Node.js). Они рендерят HTML как браузер — идеально для счетов, договоров, отчётов.

Шрифты и кириллица. Без правильной настройки символы превращаются в кракозябры. Подключаем Google Fonts через @import или встраиваем локальные шрифты. В TCPDF используем DejaVu Sans — он поддерживает кириллицу без проблем.

Производительность. Генерация одного PDF через браузер занимает 2-5 секунд. При 5000 документах в час — это критично. Решение: асинхронная очередь (Laravel Queue / Bull) и сохранение в S3. Пользователь получает мгновенный ответ, а PDF генерируется в фоне.

Как мы это делаем: стек и кейс

Для клиента — сервиса онлайн-бухгалтерии — внедрили генерацию актов выполненных работ на Laravel 11. Использовали spatie/browsershot (на базе Puppeteer). Шаблон написан на Blade с CSS Grid для таблицы услуг.

Laravel: Browsershot (Puppeteer)

Browsershot использует headless Chrome для рендеринга HTML в PDF — поддерживает CSS Grid, Flexbox, переменные, шрифты.

use Spatie\Browsershot\Browsershot; class InvoicePdfService { public function generate(Invoice $invoice): string { $html = view('pdf.invoice', ['invoice' => $invoice])->render(); $path = storage_path("app/invoices/invoice-{$invoice->id}.pdf"); Browsershot::html($html) ->format('A4') ->margins(15, 15, 15, 15) // мм ->showBackground() ->emulateMedia('print') ->waitUntilNetworkIdle() // дождаться загрузки шрифтов ->save($path); return $path; } } // Controller public function download(Invoice $invoice): Response { $path = $this->invoicePdfService->generate($invoice); return response()->download( $path, "invoice-{$invoice->number}.pdf", ['Content-Type' => 'application/pdf'] ); } 
<!-- resources/views/pdf/invoice.blade.php --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', sans-serif; font-size: 12px; color: #1a1a1a; } .header { display: flex; justify-content: space-between; margin-bottom: 40px; } .invoice-number { font-size: 24px; font-weight: 700; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th { background: #f3f4f6; padding: 8px; text-align: left; font-weight: 600; } td { padding: 8px; border-bottom: 1px solid #e5e7eb; } .total { font-size: 16px; font-weight: 700; text-align: right; margin-top: 20px; } @media print { .page-break { page-break-after: always; } } </style> </head> <body> <div class="header"> <div> <img src="{{ public_path('logo.png') }}" height="40"> <div>{{ $invoice->company->name }}</div> </div> <div> <div class="invoice-number">Счёт #{{ $invoice->number }}</div> <div>Дата: {{ $invoice->date->format('d.m.Y') }}</div> </div> </div> <table> <thead> <tr><th>Описание</th><th>Кол-во</th><th>Цена</th><th>Сумма</th></tr> </thead> <tbody> @foreach($invoice->items as $item) <tr> <td>{{ $item->description }}</td> <td>{{ $item->quantity }}</td> <td>{{ number_format($item->price, 2) }} ₽</td> <td>{{ number_format($item->total, 2) }} ₽</td> </tr> @endforeach </tbody> </table> <div class="total">Итого: {{ number_format($invoice->total, 2) }} ₽</div> </body> </html> 

Node.js: Puppeteer

import puppeteer from 'puppeteer'; import Handlebars from 'handlebars'; async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> { const templateSource = await fs.readFile('./templates/invoice.html', 'utf-8'); const template = Handlebars.compile(templateSource); const html = template(invoice); const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); try { const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); return await page.pdf({ format: 'A4', margin: { top: '15mm', right: '15mm', bottom: '15mm', left: '15mm' }, printBackground: true, }); } finally { await browser.close(); } } 

TCPDF: PHP-нативная генерация (без браузера)

Подходит для простых документов без сложного CSS:

use TCPDF; class ContractPdfService { public function generate(Contract $contract): string { $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8'); $pdf->SetCreator('MyApp'); $pdf->SetAuthor($contract->company->name); $pdf->SetTitle('Договор №' . $contract->number); $pdf->SetFont('dejavusans', '', 10); $pdf->AddPage(); $html = view('pdf.contract-simple', compact('contract'))->render(); $pdf->writeHTML($html, true, false, true, false, ''); $path = storage_path("app/contracts/contract-{$contract->id}.pdf"); $pdf->Output($path, 'F'); return $path; } } 

Асинхронная генерация в очереди

class GenerateInvoicePdfJob implements ShouldQueue { public int $timeout = 120; public function __construct(private Invoice $invoice) {} public function handle(InvoicePdfService $service): void { $path = $service->generate($this->invoice); // Загрузить в S3 $s3Key = "invoices/{$this->invoice->user_id}/{$this->invoice->id}.pdf"; Storage::disk('s3')->put($s3Key, file_get_contents($path)); $this->invoice->update(['pdf_key' => $s3Key, 'pdf_generated_at' => now()]); unlink($path); // Уведомить пользователя $this->invoice->user->notify(new InvoiceReadyNotification($this->invoice)); } } 

Как выбрать между Browsershot и TCPDF?

Критерий Browsershot (Laravel) Puppeteer (Node.js) TCPDF (PHP)
CSS Grid/Flexbox Поддерживает Поддерживает Нет
Скорость рендеринга 2-5 сек 2-5 сек <1 сек
Кириллица Через Google Fonts Через Google Fonts Встроенный DejaVu
Сложность настройки Средняя Средняя Низкая
Использование памяти ~200 МБ ~200 МБ ~50 МБ

Browsershot в 2 раза быстрее TCPDF для сложных макетов, но для простых таблиц TCPDF выгоднее по памяти.

Процесс работы

  1. Аналитика. Определяем типы документов (счета, договоры, отчёты). Выявляем сложность вёрстки.
  2. Проектирование. Создаём шаблон на Blade или Handlebars. Настраиваем шрифты.
  3. Реализация. Пишем сервис генерации, подключаем очередь для асинхронной обработки.
  4. Тестирование. Проверяем на 50+ документах. Сравниваем размер и качество.
  5. Деплой. Настраиваем S3, CDN, мониторинг (лог ошибок генерации).

Сроки реализации

  • Базовая генерация (Browsershot/Puppeteer для одного шаблона): от 2 до 3 дней.
  • С асинхронной очередью и S3: от 3 до 4 дней.
  • С электронной подписью: добавляется 2 дня.

Что входит в работу

  • Разработка шаблона PDF с учётом корпоративного стиля.
  • Настройка очереди и хранения в облаке (S3, MinIO).
  • Документация по API генерации.
  • Передача доступов к серверу и репозиторию.
  • Обучение сотрудника запуску и мониторингу.
  • Пост-релизная поддержка 2 недели.

Почему стоит доверить эту задачу нам?

У нас 10+ лет опыта в веб-разработке и более 50 проектов с генерацией PDF. Используем только проверенные связки: Laravel + Browsershot, Node.js + Puppeteer. Гарантируем стабильность: код покрыт тестами, очередь перезапускается при сбоях. Получите консультацию по вашему проекту — оценим его за один день.

Чек-лист: типичные ошибки при генерации PDF

Ошибка Причина Решение
Элементы вылезают за границы Отсутствие @media print Добавить медиа-запрос с page-break
Текст прилипает к краям Не заданы margin Установить отступы (15 мм)
Кракозябры вместо кириллицы Неправильные шрифты Использовать DejaVu или Google Fonts
Размер PDF > 10 МБ Большие изображения base64 Оптимизировать, использовать внешние ссылки
Ошибка рендеринга Не закрытые теги HTML Валидировать HTML перед генерацией

Установите лимит на количество страниц и размер файла — это спасёт от зависания генерации. Для более детальной информации обратитесь к официальной документации Puppeteer.

Настройка шрифтов для стабильной кириллицы

В TCPDF используйте шрифт dejavusans — он уже встроен и поддерживает кириллицу. В Browsershot подключите Google Fonts через @import и убедитесь, что шрифт загружен до генерации. Для офлайн-среды встройте шрифт локально.