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

Weasyprint Bundle Laravel Package

mfabdelaziz/weasyprint-bundle

Symfony bundle that integrates WeasyPrint so your PHP app can convert HTML and CSS into high-quality PDFs—reports, invoices, tickets, and more. Register it as a service, render Twig templates to PDF, stream or save the output, and skip fragile headless-browser setups.

View on GitHub
Deep Wiki
Context7

Getting Started

mfabdelaziz/WeasyPrintBundle is a thin PHP wrapper around the WeasyPrint CLI (a Python HTML→PDF engine). It does not render PDFs in PHP — it shells out to the weasyprint binary — so setup is as much about the host environment as it is about Composer.

  1. Install the dependency
    composer require mfabdelaziz/weasyprintbundle
    
  2. Install WeasyPrint on the host (this is the step people skip):
    python3 -m pip install weasyprint
    weasyprint --version   # must succeed as the same user PHP-FPM runs as
    
    WeasyPrint needs system libraries (Pango, cairo/pangoft2, gdk-pixbuf, libffi). On Debian/Ubuntu: apt install libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0.
  3. Publish the config (if the package ships one) and set the binary path explicitly rather than relying on PATH:
    // config/weasyprint.php
    'binary' => env('WEASYPRINT_BINARY', '/usr/local/bin/weasyprint'),
    
  4. First use case — the one that pays for itself immediately: rendering an invoice or statement from an existing Blade view to a PDF and streaming it back.
    $html = view('invoices.pdf', ['invoice' => $invoice])->render();
    $pdf  = WeasyPrint::fromHtml($html);
    return response($pdf, 200, [
        'Content-Type'        => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice-'.$invoice->number.'.pdf"',
    ]);
    

Where to look first: the package's service class (usually src/ root) and its config file. Confirm three things before writing feature code: (a) whether it accepts raw HTML and a URL, (b) whether it returns a string, a stream, or writes to a path, and (c) whether it wraps Symfony Process (good — safe escaping) or builds a shell string (bad — you must escape).

Implementation Patterns

Render from a Blade view, not string concatenation. Build a dedicated resources/views/pdf/ tree with its own layout that inlines CSS. WeasyPrint's CSS support is strong for paged media but partial for modern web CSS — no flexbox quirks you'd expect from Chrome, no JS execution at all.

Use @page rules for pagination. This is WeasyPrint's superpower and the reason to choose it over headless Chrome:

@page { size: A4; margin: 20mm 15mm;
        @bottom-center { content: "Page " counter(page) " of " counter(pages); } }
thead { display: table-header-group; }  /* repeat table headers across pages */
tr    { page-break-inside: avoid; }

Resolve asset URLs absolutely. WeasyPrint fetches CSS/images relative to the document's base URL. When you pass an HTML string there is no base, so relative href="css/app.css" silently fails. Fix with either an explicit base tag or a file path:

$html = view('pdf.invoice', $data)->render();
$html = str_replace('<head>', '<head><base href="'.public_path().'/">', $html);

For CSS use asset()/public_path() so URLs are absolute in every environment. file:// URLs work but must be absolute and readable by the FPM user.

Push generation to a queue for anything nontrivial. WeasyPrint is CPU- and memory-bound; a 60-page report can take seconds and hundreds of MB. Generate in a job, store to disk, and let the controller redirect to a signed URL:

class GenerateInvoicePdf implements ShouldQueue {
    public $timeout = 120;
    public function handle(WeasyPrint $wp) {
        $path = "invoices/{$this->invoice->id}.pdf";
        Storage::put($path, $wp->fromHtml(view('pdf.invoice', [...])->render()));
    }
}

Set the job timeout below your worker's retry_after, and raise memory_limit for the worker specifically — not globally.

Wrap it in your own service. Bind a small PdfRenderer interface in a service provider and have it delegate to the package. When WeasyPrint's API shifts (or you swap to Chromium for a page that needs JS), you change one class instead of forty call sites.

Test against the real binary in CI. Assert %PDF- is the response prefix and page count matches expectations; a unit test with a mocked renderer tells you nothing about the environment.

Gotchas and Tips

  • weasyprint: command not found in production but fine in your terminal. This is the #1 issue. PHP-FPM runs with a minimal PATH and a different user. Always configure an absolute binary path, and verify with sudo -u www-data /usr/local/bin/weasyprint --version.
  • Version 1.2.1 is housekeeping only. The changelog shows a 1.2.0 tag merge into develop and a composer.json edit — no new public API, no deprecations, no breaking changes. The composer.json change is the only thing worth a second look: run composer update mfabdelaziz/weasyprintbundle and diff the constraint, since metadata edits sometimes tighten the PHP version or add/move dependencies. If your lockfile resolves cleanly and php artisan about is happy, no code changes are needed.
  • Missing fonts render as tofu boxes (□□□). WeasyPrint uses fontconfig on the host, not webfonts. Install the fonts you reference (fonts-dejavu, fonts-liberation, or copy TTFs into ~/.fonts and run fc-cache -f). Non-Latin scripts (Arabic, CJK) almost always need an explicit font-family plus an installed font.
  • No JavaScript, ever. Any chart or map your HTML depends on client-side rendering will be blank. Pre-render charts server-side to SVG or a base64 <img>.
  • Shell escaping. If the wrapper accepts a command string, never interpolate user-supplied HTML into it. Prefer passing HTML via stdin (Process::setInput()) over a temp file; if you must use a temp file, create it with tempnam() and delete it in a finally block.
  • Timeouts surface as empty output, not exceptions. Capture stderr and log it — WeasyPrint writes diagnostics there and exits nonzero. A blank PDF with no log line means you're swallowing the exit code.
  • Long HTML hits an ARG_MAX limit if it's passed as a CLI argument. Another reason to use stdin.
  • base_url versus file:// matters for security. If user-controlled HTML is ever rendered, disable remote resource loading — otherwise you've built an SSRF vector (WeasyPrint will happily fetch http://169.254.169.254/).
  • Extension points: the binary path and any CLI flags (--optimize-images, --dpi, --presentational-hints) are the main knobs. If the package's config exposes a "custom arguments" array, prefer it over sub-classing; if it doesn't, that's the one place a fork or a thin adapter is justified.
  • Pin the Python package version alongside the Composer version. weasyprint>=60 changed several rendering defaults; a server rebuild that silently upgrades Python deps can shift pagination in production PDFs. Put the version in your provisioning Dockerfile/Ansible role, not just in a README.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager