---
title: "Screenshot: PNG, JPEG і PDF з Blade без Node.js"
url: https://laravelukraine.com/blog/screenshot-png-jpeg-i-pdf-z-blade-bez-nodejs
date: 2026-10-29
---

# Screenshot: PNG, JPEG і PDF з Blade без Node.js

[Стів Бауман](https://github.com/stevebauman) (DirectoryTree) випустив [Screenshot](https://github.com/DirectoryTree/Screenshot) - пакет, який робить з HTML, Blade-шаблонів і сторінок за URL зображення PNG, JPEG або PDF. Головна його особливість - Chrome керується прямо з PHP через [Chrome PHP](https://github.com/chrome-php/chrome), тож Node.js і Puppeteer у проєкті не потрібні. Пакет [помітив Laravel News](https://laravel-news.com/screenshot), а перша стабільна версія 1.0.0 вийшла 14 жовтня, за нею 1.1.0 і 1.2.0.

Генерація сертифікатів, OG-картинок, прев'ю сторінок і PDF-звітів - задача, яка трапляється майже в кожному проєкті. Найвідоміше рішення в екосистемі, [Browsershot](https://github.com/spatie/browsershot) від Spatie, теж працює через Chrome, але керує ним через Puppeteer, тобто тягне за собою Node.js і npm-пакет на сервері. Screenshot прибирає цю ланку: у Docker-образі достатньо PHP і самого браузера.

### Встановлення і вимоги

Пакет потребує PHP 8.2+, інтеграція з Laravel підтримує Laravel 12 і 13 (для Laravel 13 - PHP 8.3+). Сервіс-провайдер реєструється автоматично:

```bash
  composer require directorytree/screenshot
  php artisan screenshot:install
```

`screenshot:install` завантажує останній стабільний Chrome for Testing для вашої платформи в `storage/app/private/screenshot/browsers`. Для завантаження потрібне розширення `zip`. Прапорець `--update` перевіряє наявність нової версії, `--browser-version=...` фіксує конкретну. Якщо на сервері вже є Chrome або Chromium, достатньо вказати шлях:

```dotenv
  SCREENSHOT_CHROME_PATH=/usr/bin/chromium
```

Важливе застереження з README: на сервері чи в контейнері потрібні системні бібліотеки Chrome і шрифти, якими користуються ваші сторінки. На Alpine Linux може знадобитися системний Chromium замість завантаженого.

### Як ним користуватися

Схема завжди та сама: спершу джерело (`html()`, `view()` або `url()`), потім формат (`png()`, `jpeg()` або `pdf()`) з його налаштуваннями, наприкінці `capture()`, `save()` або `store()`:

```php
  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:

```php
  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-селектором. Викликати його треба до вибору формату:

```php
  $source = Screenshot::url($page->url)
      ->javascript()
      ->size(1280, 720)
      ->element('#preview');

  $source->png()->save($imagePath);
```

**PDF-звіт або рахунок.** `pdf()` друкує сторінку через print-верстку Chrome: стилі для друку і розриви сторінок працюють. За замовчуванням - A4, книжкова орієнтація, поля 10 мм і фони:

```php
  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](https://gotenberg.dev) 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 з обмеженою мережею.
