Стів Бауман (DirectoryTree) випустив Screenshot - пакет, який робить з HTML, Blade-шаблонів і сторінок за URL зображення PNG, JPEG або PDF. Головна його особливість - Chrome керується прямо з PHP через Chrome PHP, тож Node.js і Puppeteer у проєкті не потрібні. Пакет помітив Laravel News, а перша стабільна версія 1.0.0 вийшла 14 жовтня, за нею 1.1.0 і 1.2.0.
Генерація сертифікатів, OG-картинок, прев'ю сторінок і PDF-звітів - задача, яка трапляється майже в кожному проєкті. Найвідоміше рішення в екосистемі, Browsershot від Spatie, теж працює через Chrome, але керує ним через Puppeteer, тобто тягне за собою Node.js і npm-пакет на сервері. Screenshot прибирає цю ланку: у Docker-образі достатньо PHP і самого браузера.
Встановлення і вимоги
Пакет потребує PHP 8.2+, інтеграція з Laravel підтримує Laravel 12 і 13 (для Laravel 13 - PHP 8.3+). Сервіс-провайдер реєструється автоматично:
composer require directorytree/screenshot
php artisan screenshot:install
screenshot:install завантажує останній стабільний Chrome for Testing для вашої платформи в storage/app/private/screenshot/browsers. Для завантаження потрібне розширення zip. Прапорець --update перевіряє наявність нової версії, --browser-version=... фіксує конкретну. Якщо на сервері вже є Chrome або Chromium, достатньо вказати шлях:
SCREENSHOT_CHROME_PATH=/usr/bin/chromium
Важливе застереження з README: на сервері чи в контейнері потрібні системні бібліотеки Chrome і шрифти, якими користуються ваші сторінки. На Alpine Linux може знадобитися системний Chromium замість завантаженого.
Як ним користуватися
Схема завжди та сама: спершу джерело (html(), view() або url()), потім формат (png(), jpeg() або pdf()) з його налаштуваннями, наприкінці capture(), save() або store():
use DirectoryTree\Screenshot\Facades\Screenshot;
$image = Screenshot::html('<h1>Hello, world!</h1>')->capture();
Screenshot::view('reports.summary', data: ['report' => $report])
->save(storage_path('app/summary.png'));
Screenshot::url('https://example.com')
->javascript()
->save(storage_path('app/example.png'));
Кілька деталей, які легко пропустити:
- Формат задає метод, а не розширення файлу.
save('preview.jpg') без jpeg() збереже PNG під назвою .jpg. За замовчуванням - PNG, jpeg() приймає якість від 0 до 100 (типово 80).
- JavaScript вимкнений за замовчуванням. Для сторінок, які малюються скриптами, викликайте
javascript().
- Знімок робиться після події
load. Пакет не чекає, поки ваш JavaScript дотягне дані чи домалює графік, тож асинхронний контент може не потрапити на картинку.
- Розміри.
size() задає viewport для верстки (за замовчуванням 1024 × 768). Типово знімається вся сторінка з обрізанням до 2048 × 10000 пікселів, viewport() обмежує знімок видимою областю, maxSize() обрізає без масштабування.
- Ресурси в HTML і шаблонах мають бути з абсолютними URL або з
<base href="...">, інакше Chrome не знайде стилі, картинки і шрифти.
Час очікування налаштовує timeouts(load: ..., browser: ...) у мілісекундах. Якщо сторінка не встигла завантажитися, пакет за замовчуванням знімає те, що вже відмалювалося; failOnTimeout() змушує кинути виняток.
Три типові сценарії
Сертифікат або картка для соцмереж. Шаблон рендериться в JPEG і одразу йде на s3:
Screenshot::view('certificates.show', data: ['certificate' => $certificate])
->size(width: 1440, height: 900)
->jpeg(quality: 85)
->store(path: "certificates/{$certificate->id}/preview.jpg", disk: 's3');
store() без disk пише на диск за замовчуванням, а в options можна передати, наприклад, ['visibility' => 'private'].
Прев'ю сторінки. Якщо на сторінці є навігація і кнопки, які в знімку зайві, element() знімає лише перший елемент за CSS-селектором. Викликати його треба до вибору формату:
$source = Screenshot::url($page->url)
->javascript()
->size(1280, 720)
->element('#preview');
$source->png()->save($imagePath);
PDF-звіт або рахунок. pdf() друкує сторінку через print-верстку Chrome: стилі для друку і розриви сторінок працюють. За замовчуванням - A4, книжкова орієнтація, поля 10 мм і фони:
use DirectoryTree\Screenshot\Enums\PaperSize;
Screenshot::view('invoices.preview', ['invoice' => $invoice])
->pdf()
->paper(PaperSize::Letter)
->landscape()
->store(path: 'invoices/invoice.pdf', disk: 's3');
Є власні розміри паперу (paperSize()), поля (margins()) і preferCssPageSize(), щоб розмір з CSS @page мав пріоритет. PDF окремого елемента теж можливий, але пакет переносить елемент прямо в body, тож стилі, що залежали від батьківських обгорток, доведеться підправити.
Кожен capture(), save() чи store() на джерелі запускає новий знімок. Якщо одну картинку треба і зберегти локально, і завантажити на s3, збережіть результат capture() і викликайте save() і store() на ньому. Результат також віддає contents(), base64() і dataUri().
Черги, Gotenberg і тести
За замовчуванням кожен знімок запускає власний браузер. Для воркерів черги є socket(key: 'invoices'): процеси на одному сервері з тим самим ключем ділять один запущений Chrome, кожен знімок отримує окрему вкладку. Ці браузери ділять cookies і сховище, тож для ізольованих сценаріїв потрібні різні ключі.
Версія 1.1.0 додала власні драйвери через Screenshot::extend() і контракт Contracts\Factory для впровадження залежностей - це ламаюча зміна для тих, хто встиг інжектити конкретний клас Screenshot. Версія 1.2.0 принесла драйвер для Gotenberg 8.38+: рендеринг іде на окремий сервіс, і Chrome на сервері застосунку не потрібен узагалі. Тут є нюанс: Gotenberg не перемикає JavaScript для окремого запиту, тож налаштування знімка мають збігатися з налаштуваннями сервісу.
Для тестів є Screenshot::fake() з assertCaptured(), assertCapturedCount() і assertNothingCaptured() - Chrome у CI не запускається.
Чи варто переходити
Якщо у вас уже працює Browsershot і Node.js на сервері все одно є для збирання фронтенду, причини терміново переписувати код немає. Screenshot цікавіший там, де Node тримали лише заради Puppeteer: в окремих Docker-образах для воркерів черги, на серверах без npm, у нових проєктах. Він також добре вписується в Laravel - диски, фасад, фейк і конфіг config/screenshot.php (публікується через vendor:publish --tag=screenshot-config).
Перед продакшеном варто перевірити три речі: шрифти і системні бібліотеки Chrome в образі, чи потрібен вашим сторінкам javascript(), і хто контролює контент, який ви знімаєте. README прямо попереджає: вимкнений JavaScript не захищає від того, що ресурси сторінки чи редиректи звернуться до внутрішніх сервісів, а перевірка схеми HTTP/HTTPS - не захист від SSRF. Знімки сторінок за адресами від користувачів варто робити в ізольованому Chrome з обмеженою мережею.