h4cc/wkhtmltoimage-i386
Static i386 binaries of wkhtmltoimage from the wkhtmltopdf project, installable via Composer. Creates the binary in vendor/h4cc/wkhtmltoimage-i386/bin and symlinks it into vendor/bin for easy command-line use.
This package ships no PHP code — it is a Composer-installed static wkhtmltoimage binary (Qt WebKit 0.12.4, i386). Treat it as a system dependency, not a library.
-i386 build only runs on 32-bit Linux, or 64-bit Linux with 32-bit compat libraries (glibc:i386, libstdc++:6:i386, etc.). Almost all modern servers are x86_64; if file vendor/bin/wkhtmltoimage-i386 reports ELF 32-bit, don't assume it runs. Prefer h4cc/wkhtmltoimage-amd64 on 64-bit hosts, or skip both and install wkhtmltopdf from your distro.composer require h4cc/wkhtmltoimage-i386:^0.12.4
The binary lands at vendor/h4cc/wkhtmltoimage-i386/bin/wkhtmltoimage-i386, with a symlink in vendor/bin/.vendor/bin/wkhtmltoimage-i386 --version
vendor/bin/wkhtmltoimage-i386 --width 800 https://example.com /tmp/example.png
If this fails, nothing in Laravel will work — fix the runtime first.barryvdh/laravel-snappy (which provides the Knp\Snappy\Image API) rather than shelling out by hand:
// config/snappy.php
'image' => [
'binary' => base_path('vendor/h4cc/wkhtmltoimage-i386/bin/wkhtmltoimage-i386'),
'options' => ['width' => 1200, 'format' => 'png'],
],
$image = App::make('snappy.image');
$image->generate(view('reports.invoice', $data)->render(), storage_path("app/previews/{$id}.png"));
Render-then-convert, always. wkhtmltoimage has no way to run Laravel templates; render the Blade view to a string (or a temp .html file) first:
$html = view('reports.dashboard', $payload)->render();
$tmp = tempnam(sys_get_temp_dir(), 'wk').'.html';
file_put_contents($tmp, $html);
$proc = Process::fromShellCommandline(
escapeshellarg(config('snappy.image.binary')).
' --width 1200 --format png --quality 90 --quiet --javascript-delay 500'.
' - '.escapeshellarg($out)
);
$proc->setTimeout(30)->run();
if (!$proc->isSuccessful()) {
throw new RuntimeException($proc->getErrorOutput());
}
Wrap it in a dedicated service. Inject options from config so tests can stub the wrapper, and so swapping to Chrome headless later is a one-class change:
final class HtmlToImage
{
public function __construct(private array $options) {}
public function fromHtml(string $html): string { /* process, return raw bytes */ }
public function toStorage(string $html, string $path): string { /* write to Storage::disk('public') */ }
}
Always queue it. Conversion takes 1–5 seconds and pins CPU. Use a ShouldBeQueued job with a unique key derived from sha1($html.$options); cache the resulting path so repeated requests are served from disk, not from the binary.
Options that matter day-to-day:
| Need | Flag |
|---|---|
| Screenshot width / height | --width 1200, --height 800, --crop-h 800, --crop-w 1200 |
| Fixed viewport zoom | --zoom 0.8 |
| Wait for JS chart libs | --javascript-delay 1000 |
| Ignore 404 assets | --load-error-handling ignore |
| JPEG quality | --format jpg --quality 90 |
| Suppress noise | --quiet |
| Allow local assets (default in 0.12.4) | --enable-local-file-access |
Integration with Laravel storage. Write to storage/app/public/previews/x.png, symlink via php artisan storage:link, and serve with Storage::disk('public')->url(...). For private previews, stream through a controller with a signed route.
Assets and fonts. Inline CSS and use data: URIs for images where possible. If you reference file:// paths or public_path(), pass the absolute path and ensure your PHP process can read it. Fonts must be installed system-wide (fonts-liberation, fonts-dejavu) — otherwise text renders as tofu boxes.
i386 trap. A missing 32-bit loader produces the misleading No such file or directory error even though the file exists. Debug with file <binary> and ldd <binary>; install libc6:i386 libstdc++6:i386 (Debian/Ubuntu) or use the -amd64 package. Don't try this on Alpine/musl — it's glibc-linked.spatie/browsershot) or mpdf for PDFs; keep this only for legacy compatibility.wkhtmltoimage will happily fetch file:///etc/passwd, hit http://169.254.169.254, and download arbitrary URLs. Disable with --disable-local-file-access --disable-javascript for user-generated content, sandbox the process (dedicated user, no network if possible), and never pass raw user HTML without sanitising.setInterval keep the renderer alive forever. Always set a Process timeout (15–30s) and pass --javascript-delay; consider --no-stop-slow-scripts only if you actually need it.vendor/bin symlink isn't guaranteed if the consumer overrides Composer's bin-dir. Reference the full path under vendor/h4cc/... in config whenever possible.xvfb-run -a around the binary is the classic workaround, and missing font packages are the next most common cause of empty/garbled output.config/snappy.php, and keep a thin interface so you can migrate to Chrome headless without touching call sites.h4cc/wkhtmltoimage-amd64 (same age), distro wkhtmltopdf package (patched builds), browsershot (Chrome, faster, better CSS), or a hosted screenshot API for serverless/Vapor deployments where 32-bit binaries cannot run at all.How can I help you explore Laravel packages today?