himelali / pdf-generator
One fluent, driver-independent PDF API for Laravel on top of DomPDF, Snappy (wkhtmltopdf), mPDF and FPDF.
Requires
- php: ^8.2
- ext-dom: *
- ext-gd: *
- ext-mbstring: *
- ext-zlib: *
- dompdf/dompdf: ^3.1.1
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- knplabs/knp-snappy: ^1.5
- mpdf/mpdf: ^8.2
- psr/log: ^2.0|^3.0
- setasign/fpdf: ^1.8.6
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^1.10|^2.0
- phpunit/phpunit: ^10.5|^11.0|^12.0|^13.0
Suggests
- h4cc/wkhtmltoimage-amd64: Ships a Linux amd64 wkhtmltoimage binary.
- h4cc/wkhtmltopdf-amd64: Ships a Linux amd64 wkhtmltopdf binary for the snappy driver.
Provides
None
Conflicts
- dompdf/php-font-lib: <1.0.2
- dompdf/php-svg-lib: <1.0.2
Replaces
None
This package is auto-updated.
Last update: 2026-10-26 20:40:36 UTC
README
๐ PdfGenerator for Laravel
One fluent PDF API for Laravel, four engines underneath. Write your PDF code once and switch between DomPDF, Snappy / wkhtmltopdf, mPDF and FPDF with a single config value.
use Himelali\PdfGenerator\Facades\Pdf; return Pdf::loadView('invoices.show', ['invoice' => $invoice]) ->setPaper('A4', 'landscape') ->setMargins(20, 15) ->setHeader('<strong>ACME Corp</strong>') ->setPageNumbers() ->setTitle("Invoice {$invoice->number}") ->download("invoice-{$invoice->number}.pdf");
The same chain produces the same page size, margins, header, footer, page numbers and metadata on every driver.
Table of contents
- Features
- Requirements
- Installation
- Configuration
- Package version
- Usage
- Same fonts on every driver
- Render log: time and memory
- API reference
- Choosing a driver
- Driver notes
- Dependency injection
- Custom drivers
- Testing your application
- Exceptions
- Architecture
- Upgrading from 1.x
- Contributing
- License
๐ Features
- One generic API for DomPDF, Snappy (wkhtmltopdf), mPDF and FPDF
- Paper sizes by name (
A4,Letter,B5, ...) or in millimetres, rendered identically by every driver - Orientation, CSS-style margins, header, footer, footer text and
Page {page} of {pages}numbering - The same default font and font size on every driver for text without its own CSS
- Metadata: title, author, subject, keywords, creator
- Load HTML, Blade views or HTML files, in any encoding
- Download, display inline, get the raw bytes, save to a path or to any filesystem disk
- Library-specific options when you need them (
setOption()), without leaving the generic API - Shared document defaults in config, with per-driver overrides
- Fast: library engines are configured once and reused; every document still starts fresh (Octane and queue safe)
- Render log with time and memory per driver (
Pdf::enableRenderLog()), off by default Pdf::fake()with assertions for testing your own code- Package version via
Pdf::VERSION/Pdf::version()andphp artisan about - Fully typed, documented and tested (PHPStan level 8)
โ Requirements
| Package | Version |
|---|---|
| PHP | 8.2 or higher |
| Laravel | 10.x, 11.x, 12.x, 13.x |
Each Laravel release has its own minimum PHP version (for example, Laravel 13 needs PHP 8.3+). Composer picks the right combination for you.
Required PHP extensions: dom, gd, mbstring, zlib.
The Snappy driver also needs a wkhtmltopdf binary (see Driver notes).
๐ฆ Installation
composer require himelali/pdf-generator
The service provider and the Pdf facade are registered automatically through Laravel package discovery.
Package discovery disabled?
Laravel 11 and newer, in bootstrap/providers.php:
return [ // ... Himelali\PdfGenerator\PdfServiceProvider::class, ];
Laravel 10, in config/app.php:
'providers' => ServiceProvider::defaultProviders()->merge([ Himelali\PdfGenerator\PdfServiceProvider::class, ])->toArray(), 'aliases' => Facade::defaultAliases()->merge([ 'Pdf' => Himelali\PdfGenerator\Facades\Pdf::class, ])->toArray(),
โ๏ธ Configuration
Publish the config file to config/pdf-generator.php:
php artisan vendor:publish --tag=pdf-generator-config
return [ // Driver used by the facade and by injected builders. 'default' => env('PDF_DRIVER', 'dompdf'), // Starting settings of every new document, shared by all drivers. 'defaults' => [ 'paper' => 'A4', // name, or [width, height] in mm 'orientation' => 'portrait', // or "landscape" 'margins' => 10, // mm: 10, [10, 15], [10, 15, 20, 15] or ['top' => 20] 'page_numbers' => false, // true, false or a format like "{page} / {pages}" 'default_font' => env('PDF_DEFAULT_FONT', 'sans-serif'), // "sans-serif", "serif", "monospace" or a font name 'default_font_size' => env('PDF_DEFAULT_FONT_SIZE', 12), // points 'font_dir' => storage_path('app/pdf/fonts'), // ignored until the directory exists 'metadata' => ['creator' => env('APP_NAME')], ], // Records the time and memory of every render. Off by default. 'log' => [ 'enabled' => env('PDF_LOG_ENABLED', false), 'channel' => env('PDF_LOG_CHANNEL'), // null = default log channel, false = memory only 'level' => env('PDF_LOG_LEVEL', 'debug'), ], 'drivers' => [ 'dompdf' => [ 'options' => [ /* DomPDF options, incl. 'chroot' => [public_path(), storage_path('app/public')] */ ], ], 'snappy' => [ 'binary' => env('SNAPPY_PDF_BINARY', '/usr/local/bin/wkhtmltopdf'), 'timeout' => 60, 'options' => ['no-outline' => true, 'images' => true, 'disable-smart-shrinking' => true], ], 'mpdf' => [ 'temp_dir' => storage_path('app/pdf/temp'), 'options' => [ /* mPDF constructor configuration */ ], ], 'fpdf' => [ // "font" is the fallback for fonts FPDF does not have; null line_height follows the font size. 'options' => ['font' => 'Helvetica', 'line_height' => null, 'compress' => true], ], ], ];
Font family and size are set once in defaults and apply to every driver (see Same fonts on every driver).
A driver can still override any document default in its own defaults array:
'fpdf' => [ 'defaults' => ['paper' => 'Letter', 'default_font' => 'monospace', 'default_font_size' => 10], 'options' => [/* ... */], ],
Precedence, from lowest to highest: defaults โ drivers.{name}.defaults โ the builder methods you call.
For library options: drivers.{name}.options โ setOption() / setOptions().
๐ Package version
The installed package version is available as a constant and as a method:
use Himelali\PdfGenerator\Facades\Pdf; use Himelali\PdfGenerator\PdfManager; Pdf::VERSION; // "2.0.0" (constant on the facade) Pdf::version(); // "2.0.0" (method) PdfManager::VERSION; // "2.0.0" (constant on the manager class)
From an injected factory:
use Himelali\PdfGenerator\Contracts\PdfFactoryInterface; public function diagnostics(PdfFactoryInterface $pdf): array { return ['pdf_generator' => $pdf->version()]; }
It is also listed by php artisan about, in the "PDF Generator" section:
php artisan about --only=pdf_generator
PDF Generator ..............................................................
Default Driver ...................................................... dompdf
Drivers ......................................... dompdf, snappy, mpdf, fpdf
Render Log ............................................................. OFF
Version .............................................................. 2.0.0
Useful for diagnostics, for logging, or for stamping generated files:
Pdf::loadView('report')->setCreator('ACME Portal (PDF Generator ' . Pdf::version() . ')');
๐ Usage
Loading content
Pdf::loadHtml('<h1>Hello</h1>'); // UTF-8 HTML Pdf::loadHtml($latin1Html, 'ISO-8859-1'); // converted to UTF-8 Pdf::loadView('reports.sales', ['rows' => $rows]); // Blade view Pdf::loadFile(resource_path('pdf/terms.html')); // HTML file
Both fragments (<p>...</p>) and complete documents (<!DOCTYPE html><html>...) are supported.
Layout
Pdf::loadHtml($html) ->setPaper('Letter') // A0โA10, B0โB10, C4โC6, DL, Letter, Legal, Tabloid, Ledger, Executive, Folio ->setPaper([100, 150]) // custom size in mm; the orientation decides which side is the width ->setPaper('A4', 'landscape') // size and orientation at once ->setOrientation('portrait') // "portrait"/"P" or "landscape"/"L" ->landscape() // shortcut ->setMargins(15) // all sides, in mm ->setMargins(20, 15) // vertical, horizontal ->setMargins(20, 15, 25, 15); // top, right, bottom, left
Header, footer and page numbers
Pdf::loadView('report') ->setHeader('<div style="text-align:right">ACME Corp</div>') ->setFooter('<small>123 Main Street, Springfield</small>') ->setFooterText('Confidential') // plain text, centred, HTML-escaped ->setPageNumbers() // "Page 1 of 4" ->setPageNumbers('{page} / {pages}') // custom format ->setPageNumbers(false); // turn off
The header and footer are drawn inside the top and bottom margins, so make those margins tall enough for their content. mPDF grows the margins for you.
Fonts
Pdf::setDefaultFont('serif') // sans-serif, serif, monospace, or a font name ->setDefaultFontSize(11) // points; for text without its own CSS ->setFontDirectory(storage_path('fonts')); // custom font files, see Driver notes
See Same fonts on every driver for how this works per library.
Metadata
Pdf::loadHtml($html) ->setTitle('Annual Report 2026') ->setAuthor('Finance Team') ->setSubject('Results') ->setKeywords('finance, annual, report') ->setCreator('ACME Portal'); // or all at once Pdf::loadHtml($html)->setMetadata(['title' => 'Annual Report', 'author' => 'Finance Team']);
Library-specific options
Anything the generic API does not cover can be passed straight to the active library:
Pdf::driver('dompdf')->setOption('dpi', 150)->loadHtml($html)->output(); Pdf::driver('snappy')->setOptions(['grayscale' => true, 'lowquality' => true])->loadHtml($html)->output(); Pdf::driver('mpdf')->setOptions(['autoScriptToLang' => true, 'autoLangToFont' => true])->loadHtml($html)->output();
Output
// HTTP responses (return them from a controller or route) return Pdf::loadView('invoice', $data)->download('invoice.pdf'); // save dialog return Pdf::loadView('invoice', $data)->stream('invoice.pdf'); // shown in the browser // Raw bytes, e.g. for a mail attachment or an API payload $bytes = Pdf::loadView('invoice', $data)->output(); // Files Pdf::loadView('invoice', $data)->save(storage_path('app/invoices/7.pdf')); // local path, folders created Pdf::loadView('invoice', $data)->save('invoices/7.pdf', 's3'); // any filesystem disk
Mail attachment example:
use Illuminate\Mail\Attachment; public function attachments(): array { return [ Attachment::fromData(fn () => Pdf::loadView('invoice', $this->data)->output(), 'invoice.pdf') ->withMime('application/pdf'), ]; }
Picking a driver
Pdf::driver('mpdf')->loadHtml($html)->stream(); // one document Pdf::setDefaultDriver('snappy'); // the rest of this request/process
Every static call (
Pdf::loadHtml(),Pdf::setPaper(),Pdf::driver(), ...) starts a new document. Keep chaining on the returned builder, or store it in a variable:$pdf = Pdf::driver(); $pdf->loadView('report', $data); if ($draft) { $pdf->setHeader('<p>DRAFT</p>'); } return $pdf->download('report.pdf');
๐ค Same fonts on every driver
Each library has its own built-in stylesheet, so the same loadHtml('<p>Text</p>') used to come out differently.
Measured in the generated PDFs with each library's own defaults:
| Driver | Library default | With this package's defaults |
|---|---|---|
| DomPDF | DejaVu Sans, 12 pt | DejaVu Sans, 12 pt |
| mPDF | DejaVu Serif Condensed, 11 pt | DejaVu Sans, 12 pt |
| FPDF | Helvetica, 12 pt | Helvetica, 12 pt |
| Snappy | WebKit default (16 px, rescaled to fit the page) | CSS 12 pt, printed at its real size |
The package now applies one default_font and default_font_size (config defaults, or setDefaultFont() / setDefaultFontSize()) to every driver:
// config/pdf-generator.php 'defaults' => [ 'default_font' => 'sans-serif', 'default_font_size' => 12, ],
Pdf::driver('mpdf')->loadHtml('<p>Text</p>')->output(); // 12 pt sans-serif Pdf::driver('fpdf')->loadHtml('<p>Text</p>')->output(); // 12 pt sans-serif Pdf::driver('dompdf')->setDefaultFontSize(10)->loadHtml('<p>Text</p>')->output(); // 10 pt
Only unstyled text is affected. The defaults are added before your own CSS, so any font-family or font-size in your
HTML (stylesheet, style attribute or class) still wins:
Pdf::loadHtml('<p>12 pt</p><p style="font-size: 8pt">8 pt</p>');
Use a generic family for portability. A specific font such as "DejaVu Sans" or "Arial" does not exist in every
library. A generic family is mapped by each driver to the best font it has:
default_font |
DomPDF | mPDF | Snappy (system fonts, in order) | FPDF |
|---|---|---|---|---|
sans-serif |
DejaVu Sans | DejaVu Sans | DejaVu Sans, Arial, Helvetica | Helvetica |
serif |
DejaVu Serif | DejaVu Serif | DejaVu Serif, Times New Roman, Times | Times |
monospace |
DejaVu Sans Mono | DejaVu Sans Mono | DejaVu Sans Mono, Courier New, Courier | Courier |
The DejaVu fonts cover a wide Unicode range. FPDF's core fonts only cover Windows-1252 (see FPDF).
How each driver applies the defaults:
- DomPDF, mPDF, Snappy: a base
bodyrule (plus atablerule for engines where tables do not inherit fonts) is put first in<head>. DomPDF and mPDF also get the font as their library default. - FPDF: the font and size are used directly, and the line height follows the size (1.2 ร size, like browsers) unless
line_heightis set. - Snappy:
disable-smart-shrinkingis on by default so that CSS sizes print at their real size instead of being rescaled by WebKit.
Set default_font and default_font_size to null to go back to each library's own defaults.
โฑ๏ธ Render log: time and memory
The render log records how long every PDF took to render, how much memory it used and how large the result is, per driver. It is off by default and adds no overhead while off.
use Himelali\PdfGenerator\Facades\Pdf; Pdf::enableRenderLog(); // start recording (and write to the configured log channel) Pdf::driver('dompdf')->loadView('report', $data)->output(); Pdf::driver('mpdf')->loadView('report', $data)->output(); Pdf::getRenderLog(); // every render Pdf::getRenderLog('mpdf'); // only mPDF renders Pdf::getRenderSummary(); // totals per driver, to compare drivers Pdf::disableRenderLog(); // stop recording (entries are kept) Pdf::flushRenderLog(); // clear the entries Pdf::isRenderLogEnabled(); // bool
Pdf::getRenderLog() returns one array per render:
[
'driver' => 'mpdf',
'time' => 45.12, // milliseconds
'memory' => 6815744, // peak memory used while rendering, in bytes
'memory_retained' => 12288, // memory still in use afterwards, in bytes
'size' => 18734, // PDF size in bytes
'html_size' => 5120, // input HTML size in bytes
'paper' => 'A4',
'orientation' => 'portrait',
'rendered_at' => '2026-10-27T10:15:03.123+00:00',
]
Pdf::getRenderSummary() groups them per driver, with readable units:
[
'fpdf' => [
'count' => 1,
'total_time' => '1.23 ms',
'average_time' => '1.23 ms',
'max_time' => '1.23 ms',
'max_memory' => '413.38 KB',
'total_size' => '1.37 KB',
],
'dompdf' => [ /* ... */ ],
]
| Key | Unit |
|---|---|
count |
number of renders |
total_time, average_time, max_time |
ms, or s from 1 second |
max_memory, total_size |
B, KB or MB (1 KB = 1024 bytes) |
The raw numbers of every render (milliseconds and bytes) are available from Pdf::getRenderLog().
Writing to a log file. Each render is also written to a Laravel log channel, as a readable line plus the data above:
[2026-10-27 10:15:03] local.DEBUG: PDF rendered with [mpdf] in 45.12 ms using 6.50 MB memory (18.30 KB PDF, 5.00 KB HTML, A4 portrait) {"driver":"mpdf",...}
Pdf::enableRenderLog(); // configured channel (log.channel; null = the app's default channel) Pdf::enableRenderLog('daily'); // a specific channel
Configure it in config/pdf-generator.php or .env:
PDF_LOG_ENABLED=true # record from the start of every request / command PDF_LOG_CHANNEL=daily # channel name; leave empty for the default channel PDF_LOG_LEVEL=info # default: debug
Set 'channel' => false to keep entries in memory only (read them with Pdf::getRenderLog()).
What is measured:
-
Only the PDF library. Time and memory cover just the call that turns the document into PDF bytes with DomPDF, Snappy, mPDF or FPDF. Loading the HTML or Blade view, building the download/stream response and saving the file are not included.
-
memoryis the memory the library consumed while rendering: the peak above the memory already in use when rendering started. PHP's peak memory counter is reset before each render (memory_reset_peak_usage()) while the log is enabled. -
The first render with each driver in a PHP process also loads that library's code, so it uses more time and memory than later ones. Measured with the same HTML:
Driver 1st render 2nd render FPDF 1.97 MB 417 KB mPDF 13.36 MB 3.98 MB DomPDF 13.32 MB 4.80 MB Compare drivers using renders after the first one (or several renders, with
average_time).
Notes:
- Only successful renders are recorded.
output(),download(),stream()andsave()all render once. - Entries stay in memory until flushed. In long-running processes (queue workers, Octane), call
Pdf::flushRenderLog()or leave the log disabled. php artisan about --only=pdf_generatorshows whether the render log is on.
๐ API reference
Builder (Himelali\PdfGenerator\Contracts\PdfBuilderInterface)
Every setter returns the builder, so calls can be chained.
| Method | Description |
|---|---|
loadHtml(string $html, string $encoding = 'UTF-8') |
Load HTML, converting it to UTF-8 |
loadView(string $view, array $data = [], array $mergeData = []) |
Render a Blade view and load it |
loadFile(string $path, string $encoding = 'UTF-8') |
Load HTML from a file |
setPaper(string|array|PageSize $paper, string|Orientation|null $orientation = null) |
Paper by name or [width, height] in mm |
setOrientation(string|Orientation $orientation) |
portrait/P or landscape/L |
landscape() / portrait() |
Orientation shortcuts |
setMargins($top, $right = null, $bottom = null, $left = null) |
Margins in mm, CSS shorthand (or a Margins object) |
setHeader(?string $html) |
HTML repeated at the top of each page (null removes it) |
setFooter(?string $html) |
HTML repeated at the bottom of each page |
setFooterText(?string $text) |
Plain, centred footer text |
setPageNumbers(bool|string $format = true) |
Page numbers, with {page} and {pages} placeholders |
setFontDirectory(string $path) |
Directory with custom fonts |
setDefaultFont(string $family) |
Font for unstyled text: sans-serif, serif, monospace (same on every driver) or a font name |
setDefaultFontSize(float|int $points) |
Font size in points for unstyled text, on every driver |
setMetadata(array $metadata) |
title, author, subject, keywords, creator |
setTitle() / setAuthor() / setSubject() / setKeywords() / setCreator() |
Single metadata values |
setOption(string $key, mixed $value) / setOptions(array $options) |
Library-specific options |
getDocument(): Document |
The immutable document built so far |
output(): string |
Render and return the PDF bytes |
save(string $path, ?string $disk = null) |
Render and write to a path or a disk |
download(string $filename = 'document.pdf'): Response |
Render as an attachment response |
stream(string $filename = 'document.pdf'): Response |
Render as an inline response |
The builder is macroable, so you can add your own shortcuts:
use Himelali\PdfGenerator\PdfBuilder; // In a service provider's boot() method: PdfBuilder::macro('letterhead', function () { return $this->setPaper('A4')->setMargins(30, 20)->setHeader(view('pdf.letterhead')->render()); }); Pdf::letterhead()->loadView('letters.welcome', $data)->download();
Manager (Pdf facade, Himelali\PdfGenerator\PdfManager)
| Method | Description |
|---|---|
VERSION (constant) / version(): string |
The package version, e.g. "2.0.0" |
driver(?string $name = null): PdfBuilderInterface |
New document on the given (or default) driver |
renderer(?string $name = null): PdfRendererInterface |
The shared renderer of a driver |
extend(string $name, string|Closure $renderer) |
Register or replace a driver |
getDefaultDriver() / setDefaultDriver(string $name) |
Read or change the default driver |
getDrivers(): array / hasDriver(string $name): bool |
Available drivers |
forgetRenderers(string ...$names) |
Rebuild renderers, e.g. after changing config at runtime |
enableRenderLog(?string $channel = null) |
Start recording render time and memory (and writing them to a log channel) |
disableRenderLog() / isRenderLogEnabled(): bool |
Stop recording / check the state |
getRenderLog(?string $driver = null): array |
Recorded renders, optionally for one driver |
getRenderSummary(): array |
Count, time, memory and size totals per driver, with readable units (1.23 ms, 413.38 KB) |
flushRenderLog() |
Clear the recorded renders |
fake(): PdfFake (facade only) |
Swap in the testing fake |
๐งฉ Choosing a driver
| DomPDF | Snappy | mPDF | FPDF | |
|---|---|---|---|---|
| Package | dompdf/dompdf |
knplabs/knp-snappy |
mpdf/mpdf |
setasign/fpdf |
| External binary | โ | wkhtmltopdf | โ | โ |
| HTML / CSS | CSS 2.1, some CSS 3 | Full WebKit (CSS 3, JS) | Good CSS 2.1, tables | Text only |
| Header / footer HTML | โ | โ | โ | As plain text |
| Page numbers | โ | โ | โ | โ |
| Metadata | All fields | Title only | All fields | All fields |
| Unicode | โ | โ | โ (incl. RTL) | Windows-1252 only |
| Speed | Medium | Fast for complex pages | Medium | Very fast |
| Best for | General use, no binaries | Pixel-perfect modern layouts | Multilingual / RTL documents | Simple, high-volume text |
๐ Driver notes
DomPDF
is_php_enabled(runs PHP embedded in the HTML) andis_remote_enabled(lets the HTML fetch any URL) are off by default. Only enable them for HTML you fully trust.- Local files (images, CSS) are only read from the
chrootdirectories. The default ispublic_path()andstorage_path('app/public'), the target of thepublic/storagelink, so<img src="{{ public_path('logo.png') }}">works. Add more directories todrivers.dompdf.options.chrootwhen needed. Files outside the chroot are silently skipped. - Custom fonts: call
setFontDirectory()with a writable directory and declare the font with@font-facein your CSS. The directory is also added to DomPDF's chroot, so the font files can be read. - Options accept camelCase (
isRemoteEnabled) or snake_case (is_remote_enabled). See DomPDF options.
Snappy (wkhtmltopdf)
Install wkhtmltopdf from wkhtmltopdf.org (use a build with patched Qt for header/footer HTML), or pull in a static Linux build through Composer:
composer require h4cc/wkhtmltopdf-amd64
SNAPPY_PDF_BINARY=/var/www/html/vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64
- The binary may also be a command name such as
wkhtmltopdf, which is looked up onPATH. - A missing binary throws
BinaryNotFoundExceptionthe first time the driver is used, not while the app boots. - wkhtmltopdf 0.12.6+ blocks local files (images, CSS) by default. Allow them with
setOption('enable-local-file-access', true)or indrivers.snappy.options, but only for HTML you trust. - Custom fonts: use
@font-facein your CSS or install the font on the system. The default font and size are applied with a base CSS rule. disable-smart-shrinkingis enabled in the config so CSS sizes print at their real size. Remove it if you rely on WebKit shrinking wide pages to fit.- Any
wkhtmltopdfoption can go inoptionsorsetOption()(seewkhtmltopdf --extended-help).
mPDF
-
The
temp_diris created automatically and must be writable. -
Custom fonts: put the
.ttffiles in the font directory and register them underfontdata. They are added on top of mPDF's bundled fonts:'mpdf' => [ 'defaults' => ['font_dir' => resource_path('fonts'), 'default_font' => 'inter'], 'options' => [ 'fontdata' => ['inter' => ['R' => 'Inter-Regular.ttf', 'B' => 'Inter-Bold.ttf']], ], ],
-
Any mPDF configuration variable can go in
optionsorsetOption().
FPDF
- FPDF does not lay out HTML. Markup is reduced to text, block elements become line breaks, and
<script>/<style>are dropped. - The core fonts (Courier, Helvetica/Arial, Times, Symbol, ZapfDingbats) only cover Windows-1252, so other characters print as
?. - Custom fonts: generate definition files with FPDF's
makefontutility (.jsonfor FPDF 1.9+,.phpfor older versions), put them in the font directory, and use the font's name withsetDefaultFont(). The core fonts stay available. - If the default font is not available to FPDF (for example
DejaVu Sansshared with the HTML drivers), FPDF falls back to its configuredfontinstead of failing. - Options:
fontandfont_size(fallbacks when the document sets no default font/size),line_heightin mm (nullfollows the font size) andcompress.
๐ Dependency injection
Type-hint the contracts instead of using the facade:
| Contract | Resolves to |
|---|---|
Himelali\PdfGenerator\Contracts\PdfFactoryInterface |
The manager (singleton): driver(), renderer(), version() |
Himelali\PdfGenerator\Contracts\PdfBuilderInterface |
A new document on the default driver |
Himelali\PdfGenerator\Contracts\PdfRendererInterface |
The default driver's shared renderer |
use Himelali\PdfGenerator\Contracts\PdfFactoryInterface; final class InvoicePdf { public function __construct(private readonly PdfFactoryInterface $pdf) { } public function render(Invoice $invoice): string { return $this->pdf->driver('mpdf') ->loadView('invoices.show', ['invoice' => $invoice]) ->setTitle("Invoice {$invoice->number}") ->output(); } }
use Himelali\PdfGenerator\Contracts\PdfBuilderInterface; Route::get('/report', fn (PdfBuilderInterface $pdf) => $pdf->loadView('report')->stream('report.pdf'));
๐ Custom drivers
A driver is a renderer: one class that turns a Document into PDF bytes. It gets the whole builder API (loading, layout, output, testing) for free.
use Himelali\PdfGenerator\Contracts\PdfRendererInterface; use Himelali\PdfGenerator\Domain\Document; final class GotenbergRenderer implements PdfRendererInterface { public function __construct(private readonly array $config = []) { } public function render(Document $document): string { [$width, $height] = $document->paper->dimensions($document->orientation); // mm // $document->html, ->margins, ->header, ->footer, ->footerText, ->pageNumbering, // ->metadata->toPdfInfo(), ->defaultFont, ->fontDirectory, ->options ... return $pdfBytes; } }
Register it in a service provider's boot() method:
use Himelali\PdfGenerator\Facades\Pdf; // Class name: resolved from the container with config('pdf-generator.drivers.gotenberg') as $config Pdf::extend('gotenberg', GotenbergRenderer::class); // Or a closure receiving the driver config and the container Pdf::extend('gotenberg', fn (array $config, $app) => new GotenbergRenderer($config)); Pdf::driver('gotenberg')->loadHtml('<h1>Custom</h1>')->download();
extend() can also replace a built-in driver, for example to build DomPDF your own way.
๐งช Testing your application
Pdf::fake() swaps every driver for an in-memory renderer. No PDF library runs, the outputs are a tiny valid PDF, and every rendered document is recorded:
use Himelali\PdfGenerator\Domain\Document; use Himelali\PdfGenerator\Facades\Pdf; public function test_invoices_can_be_downloaded(): void { $pdf = Pdf::fake(); $this->get('/invoices/7/pdf') ->assertOk() ->assertHeader('Content-Type', 'application/pdf'); $pdf->assertRenderedCount(1); $pdf->assertRendered(fn (Document $document) => $document->metadata->title === 'Invoice 7' && str_contains($document->html, 'ACME Corp') && $document->orientation->isLandscape() ); }
| Method | Description |
|---|---|
assertRendered(?Closure $callback = null) |
At least one document was rendered (and matched the callback) |
assertRenderedCount(int $count) |
Exactly $count documents were rendered |
assertNothingRendered() |
No document was rendered |
rendered(?Closure $callback = null): Document[] |
The recorded documents |
The fake also covers code that type-hints the contracts, and it keeps drivers registered with Pdf::extend() as well as a default driver changed at runtime.
โ Exceptions
Every exception implements Himelali\PdfGenerator\Exceptions\PdfException, so you can catch them all at once:
use Himelali\PdfGenerator\Exceptions\PdfException; try { $bytes = Pdf::loadView('report')->output(); } catch (PdfException $e) { report($e); }
| Exception | Extends | Thrown when |
|---|---|---|
InvalidDocumentException |
InvalidArgumentException |
An invalid paper size, orientation, margin, page number format or metadata key is given |
InvalidDriverException |
InvalidArgumentException |
An unknown driver is requested, or a custom driver does not implement PdfRendererInterface |
BinaryNotFoundException |
PdfGenerationException |
The wkhtmltopdf binary cannot be found |
PdfGenerationException |
RuntimeException |
A file, view or font directory cannot be read, or a PDF cannot be saved |
RenderException |
RuntimeException |
The underlying library fails; the original error is available from getPrevious() |
๐ Architecture
src/
โโโ Domain/ Pure, immutable model of a PDF, independent of any library
โ โโโ Document.php aggregate root: content, layout, decorations, metadata, options
โ โโโ PageSize.php named or custom size in mm
โ โโโ Orientation.php enum
โ โโโ Margins.php CSS-style shorthand
โ โโโ PageNumbering.php "{page} of {pages}" format
โ โโโ GenericFont.php enum: sans-serif, serif, monospace
โ โโโ Metadata.php title, author, subject, keywords, creator
โโโ Contracts/ PdfFactoryInterface, PdfBuilderInterface, PdfRendererInterface
โโโ Renderers/ One adapter per library (Strategy pattern)
โ โโโ DomPdfRenderer.php
โ โโโ SnappyRenderer.php
โ โโโ MpdfRenderer.php
โ โโโ FpdfRenderer.php
โโโ Output/ PdfResponseFactory (HTTP), PdfWriter (local path / disk)
โโโ Support/Html.php HTML composition shared by the renderers (base font CSS, footer, ...)
โโโ Profiling/ RenderProfiler (render log), RenderMetrics (one entry), HumanReadable (units)
โโโ Testing/ PdfFake + FakeRenderer
โโโ PdfBuilder.php Fluent API; collects settings into a Document and delegates
โโโ PdfManager.php Driver registry and factory; caches renderers
โโโ PdfServiceProvider.php
โโโ Facades/Pdf.php
How a call flows: Pdf::loadView() โ PdfManager::driver() creates a PdfBuilder with the driver's cached renderer and the configured default Document โ each setter replaces the immutable Document โ download() asks the renderer for bytes and PdfResponseFactory for the response.
Design principles:
- Single responsibility: the builder only collects settings; renderers only render;
PdfWriteronly stores;PdfResponseFactoryonly builds responses;RenderProfileronly measures and logs. - Open/closed: new libraries are added with
extend()and one renderer class, without touching the package. - Liskov and interface segregation: every renderer honours the same small
render(Document): stringcontract, and the same tests run against each one. - Dependency inversion and injection: the builder depends on contracts (
PdfRendererInterface, Laravel's view and filesystem contracts), injected by the container. No global helpers are used insrc/. - DRY: page sizes, margins, fonts, page number formats and footer composition are defined once and translated by each renderer.
- Domain-driven design: the
Domainlayer is the shared language (Document, PageSize, Margins, ...). It is immutable, validated and free of framework or library code. - Performance: renderers are stateless, so they are built once per process. That includes DomPDF options, the Snappy generator and mPDF's font tables. Default documents are immutable and shared too. Each document still gets a fresh builder.
โฌ๏ธ Upgrading from 1.x
2.0 is a rewrite around a driver-independent document model.
| 1.x | 2.x |
|---|---|
setPageSize('A4') |
setPaper('A4') |
setFonts($dir) |
setFontDirectory($dir) |
PdfDriverInterface |
PdfBuilderInterface (fluent API) and PdfRendererInterface (custom drivers) |
Drivers\*Driver classes |
Renderers\*Renderer classes |
Pdf::extend('x', CustomDriver::class) |
Pdf::extend('x', CustomRenderer::class); closures receive (array $config, Container $app) |
Per-driver format, orientation, margin_*, font_dir keys |
Shared defaults (plus optional drivers.{name}.defaults) |
mpdf.mode, mpdf.default_font_size, ... |
Move them under drivers.mpdf.options |
fpdf.unit |
Removed: all sizes are in millimetres |
| Snappy binary checked when the app boots | Checked when the driver is first used |
--tag=config |
--tag=pdf-generator-config |
New: loadFile(), setPaper(..., $orientation), landscape()/portrait(), CSS-style setMargins(), custom page number formats, metadata setters, setDefaultFont() / setDefaultFontSize() shared by all drivers, setOption(s)(), the render log (Pdf::enableRenderLog()), Pdf::fake(), Pdf::VERSION / Pdf::version() and the php artisan about section, the PdfException interface and InvalidDocumentException.
๐ค Contributing
composer test # PHPUnit composer analyse # PHPStan (level 8) composer fix # PHP CS Fixer
Set SNAPPY_PDF_BINARY to a wkhtmltopdf binary to also run the real Snappy rendering test.
Releasing a version
- Update
PdfManager::VERSIONinsrc/PdfManager.php(the facade'sPdf::VERSIONfollows it). - Add a matching
## x.y.zheading toCHANGELOG.md. A test fails if the two differ. - Tag the release with the same number:
git tag v2.0.0.
License
This package is open-sourced software licensed under the MIT license.
It wraps the following libraries, each under its own license:
- dompdf/dompdf โ LGPL-2.1-only
- mpdf/mpdf โ GNU GPL v2
- knplabs/knp-snappy โ MIT License
- setasign/fpdf โ Free to use, non-copyleft
Make sure to review and comply with each library's license when you use this package in your projects.