laravel-pdf-legacy maintained by mavorion
This package provides a simple way to create PDFs in Laravel apps. It supports multiple drivers: Browsershot (Chromium), Gotenberg (Docker-based), Cloudflare Browser Run, WeasyPrint (Python-based), DOMPDF (pure PHP), and chrome-php/chrome (Chromium). You can use modern CSS features like grid and flexbox with the Chromium-based drivers, use WeasyPrint for excellent CSS Paged Media support, or choose DOMPDF for a zero-dependency setup.
legacy-php71branch note: this branch is a manual backport targeting PHP 7.1 / Laravel 5.2-5.5, for projects that can't run the PHP 8.2+ main branch. It ships only the Browsershot driver (spatie/browsershot pinned to^3.26, the newest release whosesymfony/processrequirement still overlaps with Laravel 5.5's). Dropped compared tomain: every other driver, PDF encryption/password-protection (->encrypt()),->scale(),->tagged(), AWS Lambda (->onLambda()), and non-mmunits for->margins()/->paperSize()- all of those throwCouldNotGeneratePdfif called, since either the dependency has no PHP 7.1-compatible release or this old Browsershot version doesn't support the option. Seeconfig/laravel-pdf.phpandsrc/Exceptions/CouldNotGeneratePdf.phpfor specifics. This branch is not maintained in lockstep withmain.
PdfBuilderdoesn't implementIlluminate\Contracts\Support\Responsable(that interface doesn't exist before Laravel 5.5, and this branch supports 5.2+) - soreturn Pdf::view(...)->download();from a controller won't auto-convert to a response. Call->toResponse(request())explicitly instead (see the example below).->saveQueued()needsIlluminate\Foundation\Bus\PendingDispatch, which doesn't exist before Laravel 5.3 either - queued generation isn't available on 5.2.
Here's a quick example:
use Mavorion\LaravelPdf\Facades\Pdf;
Pdf::view('pdfs.invoice', ['invoice' => $invoice])
->format('a4')
->save('invoice.pdf')
This will render the Blade view pdfs.invoice with the given data and save it as a PDF file.
You can also return the PDF as a response from your controller:
use Mavorion\LaravelPdf\Facades\Pdf;
class DownloadInvoiceController
{
public function __invoke(Invoice $invoice)
{
return Pdf::view('pdfs.invoice', ['invoice' => $invoice])
->format('a4')
->name('your-invoice.pdf')
->toResponse(request());
}
}
You can use also test your PDFs:
use Mavorion\LaravelPdf\Facades\Pdf;
it('can render an invoice', function () {
Pdf::fake();
$invoice = Invoice::factory()->create();
$this->get(route('download-invoice', $invoice))
->assertOk();
Pdf::assertRespondedWithPdf(function (PdfBuilder $pdf) {
return $pdf->contains('test');
});
});
Laravel Boost
This package ships with a Laravel Boost skill. After installing the package, run php artisan boost:install to register the skill. This will help AI agents in your project generate correct PDF code.
Support us
We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.
Documentation
All documentation is available on our documentation site.
Testing
For running the testsuite, you'll need to have Puppeteer installed. Pleaser refer to the Browsershot requirements here. Usually npm -g i puppeteer will do the trick.
Additionally, you'll need the pdftotext CLI which is part of the poppler-utils package. More info can be found in in the spatie/pdf-to-text readme. Usually brew install poppler-utils will suffice.
Finally run the tests with:
composer test
Production deployment (legacy-php71 branch)
Browsershot doesn't render PDFs itself - it shells out to Node, which drives a real Chrome/Chromium. Both have to be present on whatever host actually runs PHP-FPM, not just wherever composer install was run.
Requirements
| Requirement | Version | Notes |
|---|---|---|
| PHP | 7.1 | With ext-json. EOL upstream - see Security below. |
| Node.js | >= 10.18.1 | Anything older can't run the puppeteer version this branch needs. |
| puppeteer | exactly 13.7.0, installed globally via npm |
See the warning below - do not take latest. |
| Chrome or Chromium | any current release | Referenced by path in config, not downloaded by puppeteer. |
| System libraries | ~20 packages | NSS, GTK, X11, ALSA - see the install commands below. |
| spatie/browsershot | ^3.26 |
Already pinned in this branch's composer.json. |
Pin puppeteer to
13.7.0- do not install latest. Puppeteer 22+ changedpage.pdf()andpage.screenshot()to return aUint8Arrayinstead of a NodeBuffer. Browsershot's bundledbrowser.jsdriver script calls.toString('base64')on the result - on aBufferthat produces valid base64; on aUint8Arrayit silently returns a comma-joined list of decimal byte values instead, with no error thrown. Anything that reads PDF content back into PHP (->base64(),->meta()) ends up corrupted. File-based->save($path)is unaffected, since Chrome writes that file directly regardless of the bug.
Install (Docker)
Recommended: bake everything into the image so it's self-contained.
FROM php:7.1-fpm
# --- Node.js LTS ---
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
RUN apt-get install -y nodejs
# --- Google Chrome + everything headless Chrome links against ---
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN echo "deb https://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list
RUN apt-get update && apt-get install -y \
google-chrome-stable fonts-liberation libasound2 \
libatk-bridge2.0-0 libatk1.0-0 libcairo2 libcups2 libdbus-1-3 \
libdrm2 libexpat1 libgbm1 libglib2.0-0 libgtk-3-0 libnspr4 \
libnss3 libpango-1.0-0 libx11-6 libxcomposite1 libxdamage1 \
libxext6 libxfixes3 libxkbcommon0 libxrandr2 xdg-utils \
&& rm -rf /var/lib/apt/lists/*
# --- puppeteer, pinned, using system Chrome instead of its own download ---
ENV PUPPETEER_SKIP_DOWNLOAD=true
RUN npm install -g puppeteer@13.7.0
# run as a real non-root user so Chrome's own sandbox can initialize
RUN useradd -m -u 1000 app
USER app
Install (bare metal / VM)
Same dependencies, installed on the host directly. PHP 7.1 no longer ships in current Ubuntu/Debian repos, so it comes from a third-party PPA.
# PHP 7.1 (EOL upstream, needs a maintained third-party repo)
sudo add-apt-repository ppa:ondrej/php # Ubuntu; use packages.sury.org on Debian
sudo apt-get update
sudo apt-get install -y php7.1-fpm php7.1-cli php7.1-mbstring php7.1-xml php7.1-curl php7.1-zip php7.1-sqlite3
# Node.js LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs
# Google Chrome + its shared-library dependencies
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb https://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list
sudo apt-get update && sudo apt-get install -y google-chrome-stable \
fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libcairo2 \
libcups2 libdbus-1-3 libdrm2 libexpat1 libgbm1 libglib2.0-0 libgtk-3-0 \
libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcomposite1 libxdamage1 \
libxext6 libxfixes3 libxkbcommon0 libxrandr2 xdg-utils
# puppeteer, pinned, no bundled Chromium download
sudo PUPPETEER_SKIP_DOWNLOAD=true npm install -g puppeteer@13.7.0
If Chrome fails with error while loading shared libraries: lib<name>.so: cannot open shared object file, that library is still missing - run ldd $(which google-chrome-stable) to see exactly what's unresolved. Package names differ on RHEL/Alma/Fedora (nss, atk, at-spi2-atk, cups-libs, libXcomposite, etc.).
Using nvm instead of a system-wide Node install
If Node is managed per-user via nvm (common when a server already runs an older Node for other apps), install a second version rather than upgrading the existing one:
nvm install 20 # any LTS >= 10.18.1
nvm use 20
PUPPETEER_SKIP_DOWNLOAD=true npm install -g puppeteer@13.7.0
Then point the config at the resolved absolute paths (nvm which 20 / npm root -g), not at nvm/node on PATH:
LARAVEL_PDF_NODE_BINARY=/home/<user>/.nvm/versions/node/v20.20.2/bin/node
LARAVEL_PDF_NODE_MODULES_PATH=/home/<user>/.nvm/versions/node/v20.20.2/lib/node_modules
This matters because PHP-FPM's non-interactive process never sources nvm.sh - node/npm won't be on its PATH and the nvm shell function won't exist, so a bare LARAVEL_PDF_NODE_BINARY=node silently fails. Setting the explicit NODE_MODULES_PATH also skips the npm root -g shell-out Browsershot would otherwise run on every single PDF render.
Configuration
All of the following read from config/laravel-pdf.php, already wired to these env vars - nothing in the package needs editing.
| Variable | Purpose | Typical production value |
|---|---|---|
LARAVEL_PDF_DRIVER |
Which driver builds the PDF | browsershot (only driver on this branch) |
LARAVEL_PDF_CHROME_PATH |
Absolute path to the Chrome/Chromium binary | /usr/bin/google-chrome-stable |
LARAVEL_PDF_NODE_BINARY |
Override if node isn't on PHP-FPM's PATH |
absolute path, see nvm note above |
LARAVEL_PDF_NODE_MODULES_PATH |
Skips the npm root -g shell-out at request time |
absolute path, see nvm note above |
LARAVEL_PDF_NO_SANDBOX |
Adds Chrome's --no-sandbox flag |
false - only true if running as root in a container (see Security) |
Security
- PHP 7.1 receives no security patches (EOL since December 2019). Keep this service behind a reverse proxy, scoped to only the routes it needs, and track a real upgrade path off PHP 7.1 - this guide doesn't change that fact.
- Rendering untrusted HTML is a real attack surface. Headless Chrome fetches anything referenced by the HTML it's given - remote images, stylesheets, fonts - and can be pointed at internal network addresses (SSRF) if the input isn't trusted. Sanitize any user-supplied HTML or view data before rendering it to PDF.
--no-sandboxis a trade-off, not a formality. It disables a real isolation boundary between a compromised renderer process and the host. Prefer running the service as a genuine non-root user (see the Dockerfile above) so Chrome's sandbox can initialize normally, instead of reaching for this flag.- Give the render process a memory ceiling and a timeout (
->timeout($seconds)on the builder, plus your process supervisor's memory limit). A stuck or oversized headless Chrome instance is the most common way this kind of service falls over.
Verifying the setup
php -v # 7.1.x
node -v # >= 10.18.1
npm ls -g puppeteer # 13.7.0
google-chrome-stable --headless --disable-gpu --version # no missing-library errors
Then hit a real PDF-generating route and confirm the response starts with %PDF- and, separately, that a ->base64() call also decodes to something starting with %PDF- (confirms the puppeteer pin, not just file-based saves which work even with the buggy version).
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.