h4cc/wkhtmltopdf-i386
Composer package that bundles the 32-bit Linux wkhtmltopdf binary, so PHP and Laravel apps can convert HTML to PDF without a system-wide install. Works well with Snappy-style wrappers for invoices, reports, and other printable documents. 32-bit only; check architecture compatibility before use.
This package contains no PHP code — it is a Composer-shipped binary (wkhtmltopdf 0.12.4, patched QtWebKit, Linux i386/glibc). Your day-to-day work is really "wire this binary into a wrapper and keep it runnable in your environment."
Minimal steps:
composer require h4cc/wkhtmltopdf-i386
composer require barryvdh/laravel-snappy # knplabs/knp-snappy + Laravel glue
vendor/h4cc/wkhtmltopdf-i386/bin/wkhtmltopdf-i386 --version
# -> wkhtmltopdf 0.12.4 (with patched qt)
echo '<h1>hello</h1>' > /tmp/t.html
vendor/h4cc/wkhtmltopdf-i386/bin/wkhtmltopdf-i386 /tmp/t.html /tmp/t.pdf
If step 2 fails, nothing in Laravel will fix it (see Gotchas — the i386/glibc constraint is the usual culprit).// config/snappy.php
'pdf' => [
'enabled' => true,
'binary' => env('WKHTMLTOPDF_BINARY', base_path('vendor/h4cc/wkhtmltopdf-i386/bin/wkhtmltopdf-i386')),
'timeout' => 120,
'options' => [
'enable-local-file-access' => true, // required on 0.12.4 for file:// and relative assets
'encoding' => 'UTF-8',
],
'env' => ['LANG' => 'C.UTF-8'],
],
public function download(Invoice $invoice, PdfWrapper $pdf)
{
$pdf->setOption('page-size', 'A4');
$pdf->setOption('margin-top', 15);
$pdf->setOption('margin-bottom', 15);
return response()->streamDownload(
fn () => print $pdf->loadView('pdf.invoice', ['invoice' => $invoice])->output(),
"invoice-{$invoice->number}.pdf",
['Content-Type' => 'application/pdf']
);
}
Where to look first: the bin/ directory of the package (that single path is the whole integration surface), then config/snappy.php for options. There is no source to read — the vendor package is a binary plus a composer.json.
Resolve the path, don't hardcode it. Put the binary path in one config key or a small service provider so it can be overridden per environment (CI has no binary, prod does):
$this->app->singleton(PdfRenderer::class, fn () => tap(
new SnappyPdf(app('files'), config('snappy.pdf.binary')),
fn ($pdf) => $pdf->setTimeout(config('snappy.pdf.timeout'))
));
Then inject PdfRenderer everywhere instead of SnappyPDF — makes it swappable and mockable.
Guard for architecture at boot. Since this is an i386 build, add a cheap self-check so failures are loud and early rather than a cryptic sh: not found at request time:
// AppServiceProvider::boot()
if (app()->runningInConsole() === false && ! is_executable(config('snappy.pdf.binary'))) {
Log::critical('wkhtmltopdf binary is missing or not executable', ['path' => config('snappy.pdf.binary')]);
}
Asset handling — pick one strategy deliberately.
$pdf->loadView(...) with asset()/signed routes for CSS, JS, images. Works with local file access disabled, but requires the app to be reachable from the server itself and slows things down.--enable-local-file-access, and use absolute file:// paths for assets. Fastest and most deterministic; make sure you clean up temp files in a finally.Headers and footers need real files (not strings) on 0.12.4:
$header = tempnam(sys_get_temp_dir(), 'hdr') . '.html';
file_put_contents($header, view('pdf.header', compact('invoice'))->render());
$pdf->setOption('header-html', $header);
$pdf->setOption('header-spacing', 5);
$pdf->setOption('margin-top', 25);
Queue anything bigger than an invoice. Rendering a 40-page report inline will stall a PHP-FPM worker for seconds to tens of seconds:
final class GenerateReportPdf implements ShouldQueue
{
public int $timeout = 180;
public function handle(PdfRenderer $pdf): void
{
Storage::disk('s3')->put($this->path, $pdf->loadView('pdf.report', $this->data)->output());
$this->user->notify(new ReportReady($this->path));
}
}
Bump the job $timeout above the Snappy timeout; the default 60s job timeout kills long renders mid-flight.
Testing. Never shell out in feature tests. Bind a fake PdfRenderer that returns a canned minimal PDF (%PDF-1.4 ...), assert on the view data and response headers. Keep exactly one @group binary smoke test that runs in CI against the real binary to catch environment drift.
Docker. Install the package during the composer stage of a glibc base image (php:8.x-fpm, not Alpine) and copy vendor/ forward; then chmod +x in the same layer (see below). If you must use Alpine, run wkhtmltopdf from a sidecar container or prebuilt static binary instead of this package — the i386 ELF won't load.
libc6:i386, lib32z1, usually ia32-libs on older images). On ARM (Apple Silicon, Graviton, M1 CI runners) it flatly will not run. If your fleet is amd64 or ARM, you want h4cc/wkhtmltopdf-amd64 or a maintained image — not this package.not found even though the file exists, because the dynamic loader path /lib/ld-linux.so.2 is missing. Don't chase "file doesn't exist" messages on Alpine; it's a libc mismatch.COPY from a --no-install stage) strips the +x flag, producing Permission denied. Fix deterministically:
"scripts": { "post-install-cmd": "chmod +x vendor/h4cc/wkhtmltopdf-i386/bin/wkhtmltopdf-i386" }
--enable-local-file-access is mandatory on 0.12.4 for file:// URLs and relative assets. Without it you get pages of unstyled HTML with no error and exit code 0. This is the single most common "my CSS is missing" cause.fontconfig, fonts-liberation, fonts-dejavu, and any brand font, then fc-cache -f. Set LANG=C.UTF-8 via the env array or non-ASCII text comes out as ????.'dpi' => 96 and 'disable-smart-shrinking' => true when your layout is designed for screen rendering. Expect to iterate once on margins after enabling this.--quiet, and read stderr — "Exit with code 1 due to network error: ContentNotFoundError" tells you exactly which asset 404'd.ProcessTimedOutException; the default Snappy timeout is 60s. Raise both the config timeout and the queue job timeout together, or you'll get half-written PDFs.--javascript-delay (ms) rather than --window-status. Complex charts often need --no-stop-slow-scripts and can still segfault on large DOMs — render chart images server-side instead.PdfRenderer interface.vendor/ into the repo; keep vendor/ out of the webroot and out of any path the app can serve.PdfRenderer contract (swap Snappy → sidecar → cloud without touching call sites), an artisan pdf:check command that runs --version and renders one fixture, and a WKHTMLTOPDF_BINARY env override so dev machines without the i386 binary can point at a local build.How can I help you explore Laravel packages today?