Реализация генерации 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 выгоднее по памяти.
Процесс работы
- Аналитика. Определяем типы документов (счета, договоры, отчёты). Выявляем сложность вёрстки.
- Проектирование. Создаём шаблон на Blade или Handlebars. Настраиваем шрифты.
- Реализация. Пишем сервис генерации, подключаем очередь для асинхронной обработки.
- Тестирование. Проверяем на 50+ документах. Сравниваем размер и качество.
- Деплой. Настраиваем 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 и убедитесь, что шрифт загружен до генерации. Для офлайн-среды встройте шрифт локально.







