zentiq-labs / fast-pdf
Framework-agnostic, high-performance PHP-to-PDF rendering via direct headless Chromium IPC pipes. Works in Core PHP, Laravel, CodeIgniter 4, Symfony, and WordPress.
Requires
- php: ^8.3
- symfony/process: ^7.0
Requires (Dev)
- mockery/mockery: ^1.6
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.10
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
High-performance, sub-second, pixel-perfect HTML-to-PDF rendering for PHP applications powered by direct headless Chromium IPC pipes. Zero Node.js, NPM, or Puppeteer required. Zero framework lock-in.
Official Package: Maintained by Zentiq Labs.
For security disclosures and support, visit zentiqlabs.com.
Table of Contents
- Why Fast PDF?
- Requirements
- Installation
- Quick Start
- Configuration
- Fluent Builder API
- PHP Template Files
- Tailwind CSS Support
- Output Methods
- Paper Sizes & Orientation
- Framework Integration
- Docker & Alpine Linux
- Manual Test Script
- Testing
- Contributing
- License
Why Fast PDF?
Most PHP PDF solutions fall into one of two traps: they ship a bundled renderer with incomplete CSS support, or they delegate to a Node.js + Puppeteer stack — adding hundreds of megabytes and an IPC overhead tax to every request.
Fast PDF takes a third path. It speaks directly to the system's headless Chromium binary through native OS process pipes. Your HTML is rendered by the full Blink engine and the binary PDF stream is returned — no Node.js runtime, no browser automation framework, no intermediate file conversion.
| Approach | CSS Fidelity | Node.js | Framework Required |
|---|---|---|---|
| DOMPDF / mPDF | Partial (CSS 2.1) | No | No |
| Puppeteer / Playwright | Full | Yes | No |
| wkhtmltopdf | Partial (WebKit 2014) | No | No |
| Fast PDF | Full (Blink engine) | No | No |
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.3 |
| Chromium / Google Chrome | Any headless-capable build |
Chromium must be installed on the host. The package probes common system paths automatically and raises
BinaryNotFoundExceptionwith actionable instructions if it cannot locate a binary.
- Alpine Linux:
apk add chromium- Debian / Ubuntu:
apt-get install chromium-browser- macOS (Homebrew):
brew install --cask chromium
Installation
composer require zentiq-labs/fast-pdf
Quick Start
use ZentiqLabs\FastPdf\FastPdf; $pdf = new FastPdf(); // From a raw HTML string $pdf->fromHtml('<html><body><h1>Hello World</h1></body></html>') ->save('/var/www/output.pdf'); // From a PHP template file $pdf->fromFile(__DIR__ . '/templates/invoice.php', ['invoice' => $invoice]) ->landscape() ->download('invoice.pdf'); // streams + exits
Configuration
Pass any subset of the configuration array to the constructor. Unset keys fall back to environment variables, then to built-in defaults.
$pdf = new FastPdf([ 'binary' => '/usr/bin/chromium-browser', 'timeout' => 15, 'paper_size' => 'a4', 'orientation' => 'portrait', 'emulate_media' => 'print', 'margins' => ['top' => 15, 'right' => 15, 'bottom' => 15, 'left' => 15, 'unit' => 'mm'], 'chromium_flags' => [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', ], 'tailwind_cdn_url' => 'https://cdn.tailwindcss.com', 'temp_dir' => '/tmp', ]);
Alternatively, publish and require the bundled config file:
$pdf = new FastPdf(require __DIR__ . '/config/fast-pdf.php');
Environment variables
| Variable | Default | Description |
|---|---|---|
FAST_PDF_BINARY |
(auto-discovered) | Absolute path to the Chromium binary |
FAST_PDF_TIMEOUT |
30 |
Max seconds before PdfGenerationFailedException |
FAST_PDF_PAPER_SIZE |
a4 |
Default paper size |
FAST_PDF_ORIENTATION |
portrait |
Default orientation |
FAST_PDF_MEDIA |
print |
CSS media type to emulate |
FAST_PDF_TAILWIND_CDN |
https://cdn.tailwindcss.com |
Tailwind standalone CDN URL |
FAST_PDF_TEMP_DIR |
sys_get_temp_dir() |
Scratch directory |
Fluent Builder API
All methods return $this and can be chained in any order.
| Method | Description |
|---|---|
fromHtml(string $html) |
Set a raw HTML string as the source |
fromFile(string $path, array $data = []) |
Render a PHP template file as the source |
paper(string $format = 'a4', string $orientation = 'portrait') |
Set paper format and orientation in one call |
paperSize(PaperSize|string $size) |
Set paper size (see enum values) |
landscape() |
Landscape orientation |
portrait() |
Portrait orientation |
margins(float $t, float $r, float $b, float $l, string $unit = 'mm') |
Override margins. Unit: mm, cm, in, px |
withTailwind() |
Inject standalone Tailwind CDN before rendering |
emulateMedia(string $media = 'print') |
CSS media type: 'print' or 'screen' |
output(): string |
Render and return raw binary PDF bytes |
save(string $path) |
Render and write to a file path |
toInlineResponse(?string $filename = null): PdfResponse |
Return a PdfResponse for inline browser display (no exit) |
toDownloadResponse(?string $filename = null): PdfResponse |
Return a PdfResponse for file download (no exit) |
download(string $filename = 'document.pdf') |
Emit download headers, stream, and exit |
stream(string $filename = 'document.pdf') |
Emit inline headers, stream, and exit |
PHP Template Files
fromFile() works exactly like a simple view engine: variables in $data are extracted into the template scope, and the output buffer contents become the HTML source.
// templates/invoice.php ?> <!DOCTYPE html> <html> <body> <h1><?= htmlspecialchars($invoice['number']) ?></h1> <p>Total: $<?= number_format($invoice['total'], 2) ?></p> </body> </html>
$pdf->fromFile(__DIR__ . '/templates/invoice.php', [ 'invoice' => ['number' => 'INV-001', 'total' => 499.00], ])->save('/var/invoices/INV-001.pdf');
Tailwind CSS Support
Chain ->withTailwind() to inject the standalone Tailwind CDN script into the <head> before Chromium renders the page. No build pipeline required.
$pdf->fromHtml($html) ->withTailwind() ->paperSize('a4') ->save('/tmp/report.pdf');
For offline environments or strict CSP policies, override
tailwind_cdn_urlto a self-hosted build of the Tailwind standalone CLI output.
Output Methods
Raw binary string
$bytes = $pdf->fromHtml($html)->output(); file_put_contents('/var/cache/report.pdf', $bytes);
Save to disk
$pdf->fromFile('/templates/receipt.php', $data)->save('/receipts/001.pdf');
Browser download
// Sends Content-Disposition: attachment and calls exit() $pdf->fromFile('/templates/receipt.php', $data)->download('receipt-001.pdf');
Inline browser render
// Sends Content-Disposition: inline and calls exit() $pdf->fromFile('/templates/receipt.php', $data)->stream('receipt-001.pdf');
Paper Sizes & Orientation
Available PaperSize enum cases (accepts the lowercase string or the enum value):
A0 A1 A2 A3 A4 A5 A6 Letter Legal Tabloid Ledger
use ZentiqLabs\FastPdf\Enums\PaperSize; $pdf->fromHtml($html)->paperSize(PaperSize::Letter)->output(); $pdf->fromHtml($html)->paperSize('legal')->landscape()->output();
Framework Integration
Core PHP
require 'vendor/autoload.php'; use ZentiqLabs\FastPdf\FastPdf; $pdf = new FastPdf(['binary' => '/usr/bin/chromium']); $pdf->fromHtml('<h1>Core PHP</h1>')->save('/tmp/output.pdf');
CodeIgniter 4
Register a singleton in app/Config/Services.php:
use ZentiqLabs\FastPdf\FastPdf; public static function fastPdf(bool $getShared = true): FastPdf { if ($getShared) { return static::getSharedInstance('fastPdf'); } return new FastPdf(['binary' => env('FAST_PDF_BINARY')]); }
Use it in a controller:
service('fastPdf') ->fromFile(APPPATH . 'Views/pdf/invoice.php', compact('invoice')) ->download('invoice.pdf');
Symfony
Register as a service in config/services.yaml:
ZentiqLabs\FastPdf\FastPdf: arguments: $config: binary: '%env(FAST_PDF_BINARY)%' timeout: 30
Inject into a controller or service via constructor injection:
public function __construct(private readonly FastPdf $pdf) {} public function export(): Response { $bytes = $this->pdf->fromHtml($html)->output(); return new Response($bytes, 200, ['Content-Type' => 'application/pdf']); }
Laravel
The package is framework-agnostic but integrates cleanly with Laravel via manual binding in a service provider:
// app/Providers/AppServiceProvider.php use ZentiqLabs\FastPdf\FastPdf; $this->app->singleton(FastPdf::class, fn () => new FastPdf([ 'binary' => config('services.chromium.binary'), ]));
Then inject FastPdf anywhere the service container resolves it.
WordPress
// In a plugin bootstrap file require_once WP_CONTENT_DIR . '/vendor/autoload.php'; use ZentiqLabs\FastPdf\FastPdf; function zentiq_pdf(): FastPdf { static $instance; return $instance ??= new FastPdf(['binary' => defined('CHROMIUM_BIN') ? CHROMIUM_BIN : null]); }
Paper Setup
The paper() method is a convenience shorthand for setting both the paper format and orientation in one call.
$pdf->fromHtml($html)->paper('a4', 'portrait')->output(); $pdf->fromHtml($html)->paper('letter', 'landscape')->save('/tmp/report.pdf');
Supported formats: a0 a1 a2 a3 a4 a5 a6 letter legal tabloid ledger.
The individual paperSize(), landscape(), and portrait() methods remain available for finer-grained control.
Response Helpers
toInlineResponse() and toDownloadResponse() render the PDF and return a PdfResponse value object instead of calling exit. This fits naturally into any framework that manages its own response lifecycle.
// Framework-agnostic — inspect headers and body yourself $response = $pdf->fromHtml($html)->toInlineResponse('report.pdf'); foreach ($response->headers as $name => $value) { header("{$name}: {$value}"); } echo $response->body; // Or let PdfResponse emit directly $pdf->fromHtml($html)->toDownloadResponse('invoice.pdf')->send();
PdfResponse exposes:
$response->body— raw binary PDF bytes$response->headers— associative array of HTTP header name → value$response->send()— emit headers + body via the PHP SAPI
For Symfony or any PSR-7 framework:
$r = $pdf->fromHtml($html)->toDownloadResponse('invoice.pdf'); return new Response($r->body, 200, $r->headers); // Symfony return new \Laminas\Diactoros\Response($r->body, 200, $r->headers); // PSR-7
Testing with FastPdf::fake()
Call FastPdf::fake() to get an instance that records every render call without spawning Chromium.
$pdf = FastPdf::fake(); $pdf->fromHtml('<h1>Invoice #42</h1>')->output(); $pdf->fromView('invoices.pdf', ['id' => 42])->save('/tmp/out.pdf'); // hypothetical // Assert at least one render occurred $pdf->assertRendered(); // Assert an exact number of renders $pdf->assertRenderedCount(1); // Assert a specific HTML fragment was rendered $pdf->assertRenderedHtmlContains('Invoice #42'); // Inspect all recorded calls $calls = $pdf->renderedCalls(); // array of ['html' => ..., 'options' => ...]
The fake returns %PDF-1.4 fake bytes by default. This is intentional — unit tests should not depend on real PDF content. To override the fake output, reach into the engine:
// Not needed in most tests, but available if you must assert on the bytes use ZentiqLabs\FastPdf\Testing\FakePdfEngine; $engine = new FakePdfEngine(); $engine->returns('%PDF-1.5 custom'); $pdf = new FastPdf([], $engine);
Docker & Alpine Linux
A ready-to-use test image is included in docker/Dockerfile.test. It runs the full Pest suite against a real Chromium binary inside an Alpine container.
docker build -f docker/Dockerfile.test -t fast-pdf-test .
docker run --rm fast-pdf-test
Minimal additions for your own application Dockerfile:
FROM php:8.3-fpm-alpine RUN apk add --no-cache chromium ENV FAST_PDF_BINARY=/usr/bin/chromium-browser
Always include --no-sandbox and --disable-setuid-sandbox in chromium_flags (the package defaults already include them) when running inside unprivileged Docker containers.
Manual Test Script
A standalone smoke-test script is included at bin/test-render.php. It generates a Tailwind-styled invoice PDF to output.pdf in the current working directory.
# Auto-discover Chromium php bin/test-render.php # Explicit binary path php bin/test-render.php /usr/bin/chromium-browser
Testing
composer test
With coverage:
composer test:coverage
Static analysis (PHPStan level 9):
composer check
Code style (PSR-12):
composer cs
Contributing
Contributions, issues, and feature requests are welcome. Please ensure any pull request:
- Targets the
developbranch. - Ships with corresponding test coverage for all new behaviour.
- Passes the full CI pipeline (
test,check,cs) locally before opening a PR. - Follows Conventional Commits.
License
The MIT License (MIT). See LICENSE for details.
Developed and maintained by Usman Khan at Zentiq Labs.