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

Knp Snappy Laravel Package

knplabs/knp-snappy

Snappy is a lightweight PHP wrapper around wkhtmltopdf and wkhtmltoimage for generating PDFs, thumbnails, and snapshots from URLs or HTML pages. Install via Composer, with ready-made Symfony and Laravel integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

KnpSnappy is the low-level driver; in Laravel you almost always use it through barryvdh/laravel-snappy, which binds a configured Pdf/Image instance into the container and exposes the PDF / SnappyImage facades.

1. Install the binary first (Snappy is only a process wrapper — no binary, no PDF):

# Debian/Ubuntu — use the patched-QT build, not the distro package
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
dpkg -i wkhtmltox_0.12.6.1-3.jammy_amd64.deb
wkhtmltopdf --version   # must report 0.12.6 (with patched qt)

2. Require the package and publish config:

composer require barryvdh/laravel-snappy
php artisan vendor:publish --provider="Barryvdh\Snappy\ServiceProvider"

3. Point config/snappy.php at the binary (absolute path, no arguments — see Gotchas):

'pdf' => [
    'enabled' => true,
    'binary'  => env('WKHTMLTOPDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
    'timeout' => 120,
    'options' => [
        'enable-local-file-access' => false,
        'encoding' => 'utf-8',
        'margin-top' => 15, 'margin-bottom' => 15,
        'margin-left' => 10, 'margin-right' => 10,
    ],
    'env' => [],
],

4. First use case — invoice PDF from a Blade view:

use Barryvdh\Snappy\Facades\SnappyPdf as PDF;

Route::get('/invoices/{invoice}/pdf', function (Invoice $invoice) {
    return PDF::loadView('pdf.invoice', ['invoice' => $invoice])
        ->setOption('footer-center', 'Page [page] of [topage]')
        ->download("invoice-{$invoice->number}.pdf");
});

Where to look first: config/snappy.php, then wkhtmltopdf -H for the full option list — every setOption() key maps 1:1 to a CLI flag.

Implementation Patterns

Wrap generation in a dedicated service, not controllers. One class per document type keeps options next to the template that depends on them:

final class InvoicePdfBuilder
{
    public function __construct(private Invoice $invoice) {}

    public function render(): string
    {
        return SnappyPdf::loadView('pdf.invoice', ['invoice' => $this->invoice])
            ->setOption('footer-html', view('pdf.partials.footer')->render())
            ->setPaper('a4')
            ->setOrientation('portrait')
            ->setOption('print-media-type', true)
            ->output();
    }
}

Generate into a queue job, store on a disk. PDF generation takes seconds — never block a request for bulk work. Note Storage::path() (absolute local path) is required; Snappy writes with the binary, so it cannot stream to S3 directly:

public function handle(): void
{
    $path = "invoices/{$this->invoice->id}.pdf";
    Storage::disk('local')->put($path, app(InvoicePdfBuilder::class, ['invoice' => $this->invoice])->render());
    $this->invoice->update(['pdf_path' => $path]);
}

Always build absolute asset URLs. The binary runs outside the request lifecycle, so asset() must resolve against APP_URL; relative /css/app.css silently 404s and you get an unstyled PDF with no error. Use url()/asset() with a configured host, or inline CSS in the view.

Headers/footers need real HTML files. --footer-html won't take a URL to your session-authenticated route; render the partial with view()->render(), Storage::put() it to a temp file, and pass the absolute path. Remember headers/footers don't inherit the body's CSS or fonts.

Merging and covers are first-class — pass an array of URLs or reuse cover/toc:

SnappyPdf::getOutput(['https://a.test', 'https://b.test'])
    ->setOption('cover', view('pdf.cover')->render())
    ->setOption('toc', true)
    ->setOption('xsl-style-sheet', public_path('toc.xsl'));

Per-document options vs. the shared singleton. The facade resolves one instance; PDF::setOption() mutates it for the rest of the request. Either call PDF::resetOptions() when finished, or resolve a fresh instance: app('snappy.pdf.wrapper')->... inside a scoped binding.

Testing. Mock the facade so CI needs no binary, and assert the view data, not the bytes:

PDF::shouldReceive('loadView')->once()->with('pdf.invoice', $data)->andReturnSelf();
PDF::shouldReceive('download')->andReturn(response('', 200));

For a smoke test that a real PDF is produced, keep one test behind a WKHTMLTOPDF_BINARY env check — otherwise pipelines on Alpine/minimal images fail confusingly.

Image generation uses the same API (SnappyImage) with setOption('width', 800) / height; good for og:image thumbnails of a Blade-rendered card.

Gotchas and Tips

  • Binary string must be a bare path (1.7.1+). is_executable() is now checked on the whole string, so new Pdf('/usr/bin/wkhtmltopdf --enable-local-file-access') throws. Pass flags via setOption()/the options array instead. If you upgraded and PDFs broke, this is almost always why.
  • --enable-local-file-access is a security boundary, not a convenience flag. With user-supplied HTML it can read local files and, combined with JS, lead to RCE. Sanitise input, disable JavaScript (--disable-javascript) for untrusted content, and consider --disable-local-file-access with everything inlined. Advisory GHSA-c5fp-p67m-gq56 exists; keep the package current.
  • Old WebKit, not Chrome. No CSS grid gaps, partial flexbox, no modern JS syntax. Test templates in the real binary before styling the Blade view in a browser — they will look different, and it isn't a Snappy bug.
  • Async JS needs --javascript-delay. Charts rendered client-side will be blank by default; add setOption('javascript-delay', 1000) (or window-status if the page exposes a flag) and --no-stop-slow-scripts when needed.
  • Exit code 1 with empty stderr is the classic Qt/X11 failure in Docker: set the env QT_QPA_PLATFORM=offscreen (or run xvfb-run), and mount /dev/shm larger — the default 64 MB crashes on big documents.
  • Temp files pile up. Set --temp-dir and --cache-dir to a writable, monitored directory in config/snappy.php; containers with a read-only /tmp produce "Exit with code 1 due to network error" style red herrings.
  • A single 404 asset aborts rendering only sometimes. Add setOption('load-error-handling', 'ignore') and setOption('load-media-error-handling', 'ignore') — the default abort gives you a stack trace instead of the one broken image that caused it.
  • Missing fonts render as boxes. Install fonts-dejavu / fonts-liberation (and any brand font) in the image; wkhtmltopdf does not fall back gracefully, and it won't error either.
  • setOption() value types matter. Booleans become --flag / omitted, arrays become repeated flags (allow, cookie, post), and image-dpi/image-quality are cast to int. Passing false explicitly is the only way to override a config default — null is ignored.
  • Debugging: call setLogger(app('log')) (Snappy implements LoggerAwareInterface) and log getCommand() output; the CLI string it builds is the fastest way to reproduce outside PHP — copy it into a shell and add --debug-javascript.
  • Timeouts: Symfony Process timeout in config silently kills long documents; set it to false for the job and rely on queue timeouts, which give better visibility.
  • Extension point: subclass Knp\Snappy\Pdf and swap the container binding if you need pre-processing (e.g. inlining images as base64 or rewriting <img> sources) before the command is built — cleaner than sprinkling str_replace over rendered HTML in every controller.
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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json