Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Pdf Chrome Driver Laravel Package

breuer/laravel-pdf-chrome-driver

A Laravel PDF driver that turns Blade/HTML into PDFs using headless Chrome, so modern CSS, fonts, and layouts render faithfully. Register it as your PDF driver and generate invoices, reports, and other documents with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites. You need spatie/laravel-pdf installed first — this package is a driver for it, not a standalone PDF library. You also need a Chrome/Chromium binary reachable from your app (local, in the container, or a remote debug endpoint). No Node.js/Puppeteer install is the whole point of the driver, so if you were previously fighting npx puppeteer in CI, that's your motivation.

Install and wire it up.

composer require breuer/laravel-pdf-chrome-driver

Pin at least ^2.0.2 — earlier 2.x releases silently dropped inline base64 @font-face declarations, which is exactly the kind of bug you'd waste a day on (see Gotchas).

Then point spatie/laravel-pdf at this driver. The driver is selected via Spatie's own config, so publish that first if you haven't:

php artisan vendor:publish --tag="laravel-pdf-config"

and set the driver key in config/laravel-pdf.php to the identifier this package registers (check the README — it's a short string, typically something like chrome or chrome-devtools). Configure the binary path and remote-debugging port in the same config block or via env vars.

Where to look first. In this order: (1) this package's README for the exact driver key + config keys, (2) Spatie's docs for the fluent API, since 95% of your day-to-day code is Spatie's API, not this package's, (3) the driver's config for timeout/port/binary settings.

First use case — an invoice download. The API is Spatie's, unchanged:

use Spatie\LaravelPdf\Facades\Pdf;

Route::get('/orders/{order}/invoice', function (Order $order) {
    return Pdf::view('pdf.invoice', ['order' => $order])
        ->format('A4')
        ->save(storage_path("invoices/{$order->id}.pdf"))
        ->download("invoice-{$order->id}.pdf");
});

If that renders, the driver is working. If it hangs, jump to the Gotchas section — it's almost always Chrome not being reachable.

Implementation Patterns

Treat the driver swap as transparent — and keep it that way. The biggest mistake is letting driver-specific calls leak into your app code. Wrap generation in a single action/service class so switching back to the Browsershot driver (or to a different host running Chrome) is a one-line config change, not a refactor.

final class RenderInvoicePdf
{
    public function __invoke(Order $order): string
    {
        $path = storage_path("invoices/{$order->id}.pdf");

        Pdf::view('pdf.invoice', ['order' => $order])
            ->format('A4')
            ->margins(15, 15, 20, 15, 'mm')
            ->save($path);

        return $path;
    }
}

Generate off the request cycle. Launching/connecting to Chrome and waiting for network idle is slow — hundreds of ms to seconds. Always push to a queued job for anything user-facing, and notify/stream the result when done. If you must render inline, cache the resulting file keyed by a content hash.

One layout, print-specific — and self-contained. Use a dedicated Blade layout (resources/views/pdf/layout.blade.php) with inlined CSS. External stylesheet loading through CDP is the most common source of "works locally, blank in prod" reports because relative asset() URLs resolve against nothing meaningful in a headless context. Since v2.0.2 you can put fonts directly in that inlined <style> as data: URIs and have them actually apply — that's the most portable way to get brand typography into a minimal container without installing system fonts.

Bulk generation: reuse, don't relaunch. If the driver connects to an already-running Chrome endpoint, generate N PDFs over one connection rather than spawning N Chromes. Chunk your batches (say 25–50 per job) and dispatch multiple jobs — this parallelizes well and keeps any single job from bloating memory.

Per-environment drivers. A common, sane split:

'driver' => env('PDF_DRIVER', 'browsershot'), // prod later flips to chrome

Keep the default working for developers who haven't installed Chromium locally, and switch in staging/prod after verifying parity on your real templates.

Test with the fake. Spatie ships Pdf::fake(), and it works regardless of the underlying driver because the fake short-circuits before the driver is invoked. Assert on the file being saved rather than on driver internals:

Pdf::fake();
$this->post("/orders/{$order->id}/invoice")->assertOk();
Pdf::assertSaved(fn ($pdf) => $pdf->viewName === 'pdf.invoice');

Note this also means Pdf::fake() will not catch font/resource regressions — keep at least one real-render smoke test in CI (see Gotchas).

Headers, footers, and page numbers go through Spatie's header/footer view API, which the driver maps to CDP's printToPDF header/footer template support. Keep those templates self-contained (inline styles, system fonts) — they're rendered in a separate context.

Gotchas and Tips

Maturity is the headline risk. Seven stars, zero dependents, one author. Treat this as "pin the exact version in composer.json and keep a fallback." Since the driver is swappable, the fallback is free: leave the Browsershot driver configured and flip with an env var if an upgrade breaks. Do not build features that depend on driver-specific APIs.

Base64 fonts: fixed in 2.0.2, broken before it. Inline @font-face blocks using data: URIs in a <style> tag were silently ignored in 2.0.0/2.0.1 — Chrome printed the fallback font and you'd see the wrong typeface, not an error. The fix makes the driver wait for the page load event after Page.setDocumentContent. Symptoms of the old bug: your font renders in a real browser and in the Browsershot driver, but not through this driver. If you're on an affected version, upgrade rather than working around it; if you can't upgrade, install the font system-wide in the image instead. Also worth knowing: the fix adds a wait, so expect slightly more latency per render on font-heavy documents — that's correctness, not a regression.

Chrome unreachable → everything hangs. If the driver dials a --remote-debugging-port endpoint, verify Chrome is actually listening before blaming the package: curl http://localhost:9222/json/version should return JSON. In Docker this means the Chrome process must be started alongside PHP (supervisor, s6, or a sidecar). A connection timeout often surfaces as a generic PDF exception, not a "connection refused."

The sandbox flag. In Docker/CI as root, Chrome dies instantly without --no-sandbox (and usually --disable-dev-shm-usage to avoid /dev/shm exhaustion). These flags are passed through the driver's Chrome-launch config — check the README for the exact key, and set them only in containerized environments.

Zombie Chrome processes. If the driver launches Chrome per render and an exception is thrown mid-render, the child can survive. Watch ps aux | grep chrome in staging after a week. If processes accumulate, either switch to a long-lived shared Chrome endpoint or wrap renders in try/finally that kills the PID.

Wait strategy is your #1 render-quality lever. Blank or half-rendered PDFs are almost always a race: fonts or images haven't loaded when the snapshot is taken. Prefer waiting on a real signal (Spatie's network-idle/wait helpers, or a window.__ready flag set by your template's load handler) over increasing a sleep. Increasing timeouts makes it slower and still flaky. The 2.0.2 load-event wait only covers the HTML you hand the driver directly — if your template pulls images or webfonts over the network on top of that, you still need your own signal.

Page-break CSS behaves like print, not like a browser preview. break-inside: avoid on table rows and cards; avoid flex/grid layouts that can't fragment across pages; test long, real-data documents (200-line invoices), not the 3-line fixture. Chrome's print path is generally excellent but unforgiving of position: fixed and 100vh sections.

Relative assets fail silently. asset('img/logo.png') in a headless render resolves against about:blank unless the driver sets a base URL. Use absolute URLs (APP_URL), inline SVGs, or base64 data URIs for logos and fonts — the latter is now reliable for fonts as of 2.0.2, and is the safest option when you can't guarantee system fonts.

Fonts in minimal containers. google-chrome in a slim image has no DejaVu/Liberation fonts — text renders as tofu or falls back oddly. Either install fonts-liberation (or your brand fonts) in the image, or embed them as base64 @font-face in your inlined style block; verify with a real smoke-test PDF in CI, because Pdf::fake() won't render anything.

Debugging loop that actually works. Keep a Chrome running with --remote-debugging-port=9222, generate the PDF, and inspect the live target list at http://localhost:9222/json. If the page appears there mid-render you can attach DevTools and see exactly what the snapshot captured — for the 2.0.2 font class of bug, this is how you'd confirm the @font-face was parsed but the print fired too early. Also enable the driver's verbose/log channel if it offers one, and log the resolved Chrome binary path at boot — most "it works on my machine" cases are a wrong path or a stale cached config.

Watch for config caching. php artisan config:cache in deploy scripts is the classic cause of "I changed the driver and nothing happened." Re-cache after any config/laravel-pdf.php change, and remember env() calls outside config files return null once cached.

Extension points are limited by design — and that's fine. Because the public surface is Spatie's PdfDriver contract, the clean extension path is: implement/wrap your own driver (decorate this one to add logging, retries, or metric collection), not to monkey-patch internals. If you need CDP-specific printToPDF options this driver doesn't expose (e.g. preferCSSPageSize, transferMode), open an issue or fork — but keep that behind your own service class so you're not coupled to a 7-star package's internals.

Upgrade discipline for 2.x. 2.0.2 is a patch release with no API changes, so the upgrade is a composer update and a re-run of your real-render smoke test. Given the package's size, keep the changelog in your release notes: the failure mode of these fixes is silent visual regression (wrong font, missing glyphs), not an exception, so nothing in your test suite will shout at you unless you're diffing actual output or eyeballing a fixture PDF.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus