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 Weasyprint Laravel Package

fruitcake/laravel-weasyprint

Laravel wrapper for WeasyPrint that converts HTML and CSS, including Blade views, into high-quality PDFs. Offers a simple API for generating, saving, and streaming PDFs directly from your Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the package

composer require fruitcake/laravel-weasyprint

Auto-discovery registers Fruitcake\WeasyPrint\WeasyPrintServiceProvider and the WeasyPrint facade alias. As of v0.3.3 the alias entry in composer.json is correctly formed — if you are upgrading from ≤0.3.2, run:

composer dump-autoload
php artisan config:clear   # if you cache config

and remove any manual 'WeasyPrint' => Fruitcake\WeasyPrint\Facades\WeasyPrint::class line you previously had to add to config/app.php to work around the "Class 'WeasyPrint' not found" error.

2. Install the WeasyPrint binary (this is the step people skip)

The package is a thin wrapper — it shells out to the Python weasyprint CLI. Nothing works until that binary exists for the user PHP runs as:

pip install weasyprint        # or: pipx install weasyprint / apt install weasyprint
weasyprint --version          # run as www-data / the queue worker user, not just your shell

On Debian/Ubuntu you also need libpango-1.0-0, libpangoft2-1.0-0, libcairo2, libgdk-pixbuf-2.0-0. Inside Docker, install these in the runtime image, not just the build stage.

3. Publish and read the config first

php artisan vendor:publish --tag=weasyprint-config

config/weasyprint.php is the whole surface area: binary path, timeout, and default CLI options. Read it once end-to-end — it tells you what the wrapper actually passes to the binary, which saves an hour of guessing later.

4. First real use case: an invoice from a Blade view

use Fruitcake\WeasyPrint\Facades\WeasyPrint;

$pdf = WeasyPrint::view('pdf.invoice', ['invoice' => $invoice]);

return $pdf->download("invoice-{$invoice->number}.pdf");
// or ->save(storage_path("invoices/{$invoice->id}.pdf"))
// or ->stream() for an inline browser preview

Point the binary at an absolute path (WEASYPRINT_BINARY in .env) rather than relying on PATH — queue workers and FPM pools routinely have a different PATH than your SSH session, and this is the single most common "works locally, fails in prod" report.

Implementation Patterns

Wrap generation in a single action class, not in controllers. Invoices, receipts, statements and reports all converge on the same three steps: render view → run binary → persist. Give it one home:

final class GenerateInvoicePdf
{
    public function __invoke(Invoice $invoice): string
    {
        $path = "invoices/{$invoice->id}.pdf";

        WeasyPrint::view('pdf.invoice', ['invoice' => $invoice])
            ->save(Storage::disk('local')->path($path));

        return $path;
    }
}

Controllers, queued jobs, artisan commands and mail attachments then call the same thing, and you have one place to add caching or logging.

Push generation onto the queue. WeasyPrint typically takes 200 ms–3 s per document depending on images and fonts. Anything user-facing that isn't a single small document belongs in a job:

final class GenerateInvoicePdfJob implements ShouldQueue
{
    public int $timeout = 120;   // must exceed the weasyprint config timeout
    public int $tries   = 3;
}

Set the job $timeout above the package's process timeout so your job gets a meaningful error instead of being killed by the worker with no output.

Bulk exports: chunk and fan out. A "download all invoices" button should dispatch one job per document (or Bus::batch), write to a temp directory, then zip in a final job. Rendering 500 documents in one request will exhaust memory and hold a PHP-FPM worker hostage.

Keep PDF views separate and self-contained. Use resources/views/pdf/ with its own layout. Print CSS is a different dialect from browser CSS:

@page { size: A4; margin: 20mm 15mm; @bottom-right { content: counter(page) " / " counter(pages); } }

No JavaScript, ever — WeasyPrint does not execute it. Inline your CSS or pass it as a stylesheet; use absolute filesystem paths for images and fonts.

Cache generated PDFs on the model. PDF generation is deterministic given the same view + data. Hash the view contents and the model's updated_at, store that alongside the file, regenerate only on mismatch. For invoices this turns a 1.5 s request into a disk read.

Testing strategy. Two layers:

  • Feature tests — bind a fake implementation into the container (or WeasyPrint::shouldReceive(...)) and assert you dispatched the job / returned the right filename. Fast, no binary required in CI.
  • One integration test — actually generate a small PDF and assert the output starts with %PDF and is over a few hundred bytes. This is what catches a broken container image, and it's cheap to keep.

Don't try to assert PDF content in PHP; that's a job for a snapshot compared out-of-band, not for your test suite.

Multi-tenant / per-call overrides. When one tenant needs a different letterhead or page size, pass options at the call site rather than mutating global config — config is cached and shared across the whole process, so mutating it in one request leaks into the next under Octane or a long-running queue worker.

Gotchas and Tips

Missing fonts are the #1 rendering bug. WeasyPrint resolves fonts via fontconfig on the machine running the binary. Fonts you installed on macOS don't exist in the Alpine/Debian container, so text silently falls back or renders as boxes. Install the TTFs in the image and run fc-cache -fv; verify with fc-list | grep "Your Font".

A remote asset can hang the whole request. An <img src="https://..."> or @import pointing at a slow host blocks the child process. The config timeout is your only defence — set it deliberately (e.g. 30 s), and prefer local assets or pre-downloaded images over live URLs in templates.

Debug by running the exact command yourself. When generation fails in production, capture the command line and stderr from the exception, then run that command verbatim as the web user. Nine times out of ten you'll see the real error (ModuleNotFoundError: No module named 'weasyprint', Permission denied, cannot load library 'libgobject-2.0-0') instead of a generic process-failure message.

config:cache freezes the binary path. Changing WEASYPRINT_BINARY in .env does nothing until you re-run php artisan config:cache. This bites people mid-deploy.

The WeasyPrint root alias is global. v0.3.3 fixes the malformed alias, which is good news, but it means the short class name WeasyPrint is now registered everywhere in your app. If you (or another package) define your own WeasyPrint class, you'll get a conflict or a confusing "wrong class" error. Either import the namespaced facade explicitly (use Fruitcake\WeasyPrint\Facades\WeasyPrint;) or drop the alias from extra.laravel.aliases in your own composer.json — a cleaner habit regardless.

Permissions on storage/. FPM writes as www-data; artisan commands you run as your own user write as you. A directory generated during a manual test can be unwritable by the web process. Keep PDF output under a disk with consistent ownership, ideally a dedicated pdf disk.

Base64 images bloat memory and time. Embedding a 2 MB logo as a data URI means the HTML string itself is ~2.7 MB and WeasyPrint parses it all. File paths are cheaper for anything reused across documents.

0.3.x is pre-1.0. Pin the constraint ("^0.3") and read the changelog before minor bumps — 0.3.3 itself is a safe, narrowly-scoped patch (composer metadata only), but the surrounding 0.3 line has no BC guarantee. Nothing here touches your application code, so upgrading is low-risk; just remember composer dump-autoload so the corrected alias takes effect.

Know when not to use it. WeasyPrint is excellent at CSS Paged Media: running headers, page counters, footnotes, print-grade typography, and it's much lighter than a Chromium-based renderer. It cannot do JavaScript, and its flexbox/grid support lags browsers. If your template needs charts rendered by Chart.js or a modern dashboard layout, use a headless-Chrome solution (Browsershot) instead — fighting WeasyPrint's CSS support is a losing trade.

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.
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
elmogy/larafast