Menu
Sign up

Convert HTML to image in PHP: Browsershot, Snappy or an API

HTML to PNG in PHP: Browsershot with Puppeteer and Chrome, why wkhtmltoimage (Snappy) and Imagick fall short, and the same image via Guzzle or Laravel.

Published November 4, 2026 · 5 min read

To convert HTML to an image in PHP, use Browsershot (spatie/browsershot): Browsershot::html($html)->windowSize(1200, 630)->save('card.png') renders the markup in headless Chrome and writes a PNG. PHP itself has no HTML renderer — Imagick and GD draw pixels, and the old wkhtmltoimage route (via knp-snappy) runs an archived WebKit. Browsershot's catch is that it needs Node.js, Puppeteer and Chrome on the server; if that is a problem, an HTML to image API does the same with one HTTP request from Guzzle or Laravel's Http.

We ran each snippet below: Browsershot 5.4.2 on PHP 8.4 with Node 22, and the API calls with Guzzle 7 and Laravel 13.

Browsershot: HTML to PNG with Chrome

composer require spatie/browsershot
npm install puppeteer        # in the project directory (or globally)

Browsershot's requirements are Node 22 or higher and Puppeteer 23 or higher; Puppeteer downloads its own Chrome on install.

<!-- card.html -->
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { margin: 0; }
    .card {
      width: 1200px; height: 630px; box-sizing: border-box;
      display: grid; align-content: center; gap: 16px; padding: 80px;
      background: linear-gradient(135deg, #0f172a, #1e3a8a);
      color: #f8fafc; font: 700 64px/1.15 system-ui, sans-serif;
    }
    .card small { font-size: 28px; font-weight: 400; color: #93c5fd; }
  </style>
</head>
<body>
  <div class="card">
    <small>Release notes · v2.4</small>
    HTML to image in PHP, rendered by Chrome
  </div>
</body>
</html>
require __DIR__.'/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

$html = file_get_contents(__DIR__.'/card.html');   // or a Blade view: view('og.card', $data)->render()

Browsershot::html($html)
    ->windowSize(1200, 630)
    ->deviceScaleFactor(2)          // a 2400×1260 file
    ->select('.card')               // screenshot of this element only
    ->timeout(30)
    ->save(__DIR__.'/card.png');

// Or keep the bytes in memory, e.g. to upload them to S3:
$png = Browsershot::html($html)->windowSize(1200, 630)->screenshot();

If you inline the markup in a heredoc instead, do not label it HTML: since PHP 7.3 an indented line that starts with the label closes the heredoc, and a line such as "HTML to image in PHP" above ends it with a parse error — it happened to our first draft. Other useful methods: fullPage(), clip($x, $y, $w, $h), setScreenshotType('jpeg', 85), hideBackground(), and setChromePath() / setNodeBinary() when the binaries are not on PATH.

What Browsershot needs on the server

Three things went wrong while we set it up, and each is typical:

  1. "Cannot find module 'puppeteer'" — Browsershot runs a Node script that must be able to require('puppeteer'). Install it in the project, globally, or point setNodeModulePath() at it.
  2. "error while loading shared libraries: libglib-2.0.so.0" — Chrome launched inside a slim PHP container that lacked its system libraries. Chrome needs a few dozen shared libraries and fonts; on Debian or Ubuntu install them with the browser, or use an image that ships them.
  3. "net::ERR_FILE_NOT_FOUND" with a remote Chrome — Browsershot::html() writes your HTML to a temporary file and opens it with a file:// URL. A Chrome in another container (setWSEndpoint(), setRemoteInstance()) cannot see that file.

Beyond setup: render in a queued job, not in the web request (the PHP worker waits for Node and Chrome the whole time); cap how many Chrome processes run at once; and if users write the HTML, remember it runs in a browser on your network. Browsershot refuses HTML containing file:// URLs or links to localhost and 127.x, which helps, but other internal addresses still need a network-level block. For Laravel specifics — jobs, storage, the og:image tag — see dynamic OG images in Laravel.

knp-snappy and wkhtmltoimage: legacy only

knplabs/knp-snappy is still maintained (1.7.3, July 2026), and its image class is easy to use — (new Knp\Snappy\Image('/usr/local/bin/wkhtmltoimage'))->getOutputFromHtml($html). The problem is the binary underneath. The wkhtmltopdf repository was archived on 2 January 2023, the last release (0.12.6) is from June 2020, and the project status page explains that its Qt WebKit is years behind WebKit and warns against untrusted HTML. Expect no CSS grid and patchy flexbox. Keep it where it already works; do not start new code on it.

Imagick and GD cannot render HTML

GD and Imagick are drawing libraries: you place text and shapes at coordinates. ImageMagick's formats table does list HTML, but reading it "requires html2ps", an external HTML-to-PostScript converter — not a browser, with no flexbox, grid or web fonts. For a fixed card with one line of text, drawing it with Imagick is fine. For "this HTML and CSS as a PNG", you need a browser engine.

The same image from an API with Guzzle

An HTML to image API keeps Chrome off your servers. With Shareglint you save the HTML and CSS once as a template with named fields, then send only the values per image:

require __DIR__.'/vendor/autoload.php';

use GuzzleHttp\Client;

$api = new Client([
    'base_uri' => 'https://shareglint.com/v1/',
    'headers' => ['Authorization' => 'Bearer '.getenv('IMAGE_API_KEY')],
    'timeout' => 60,
]);

// 1. Save the template once (a migration or a deploy step).
$api->post('templates', ['json' => [
    'name' => 'Article card',
    'slug' => 'article-card',
    'width' => 1200,
    'height' => 630,
    'fields' => ['kicker', 'title', 'author'],
    'html' => '<div class="card"><p class="kicker">{{ kicker }}</p>'
        .'<div class="fitbox"><h1 class="fit" data-autofit>{{ title }}</h1></div>'
        .'<p class="by">{{ author }}</p></div>',
    'css' => '.card{height:100%;padding:72px;display:grid;grid-template-rows:auto 1fr auto;'
        .'gap:24px;background:#0f172a;color:#f8fafc;font-family:Roboto,sans-serif}'
        .'.kicker{color:#fbbf24;font-size:26px;text-transform:uppercase}'
        .'h1{font-size:76px;line-height:1.08}.by{font-size:28px;color:#94a3b8}',
]]);

// 2. One request per image. Guzzle throws on 4xx/5xx by default.
$response = $api->post('images?wait=true', ['json' => [
    'template' => 'article-card',
    'format' => 'png',
    'modifications' => [
        'kicker' => 'Engineering',
        'title' => 'How we cut our build time in half',
        'author' => 'Ada Lovelace',
    ],
]]);

echo json_decode((string) $response->getBody(), true)['url'];

In Laravel, step 2 is three lines with the Http client:

use Illuminate\Support\Facades\Http;

$url = Http::withToken(config('services.images.key'))
    ->baseUrl('https://shareglint.com/v1')->timeout(60)->throw()
    ->post('/images?wait=true', [
        'template' => 'article-card',
        'format' => 'png',
        'modifications' => ['kicker' => 'Engineering', 'title' => $post->title, 'author' => $post->author->name],
    ])->json('url');

Step 1 runs once; repeating it answers 422 because the slug is taken (change a template with PATCH /v1/templates/{id}), and built-in templates such as blog-og need no step 1. Identical fields return the existing file without using quota, so a retried job costs nothing. The limits are the price of the sandbox: no scripts in templates, no external URLs in the HTML or CSS (images arrive through image fields), and fonts from the installed list only. Browsershot has none of those limits. Free accounts get 100 images a month with a small watermark; Starter is $9 a month for 2,000 (pricing).

Which one to choose

  • Browsershot if you can run Node and Chrome next to PHP and want full control (scripts, your own fonts).
  • An API on shared hosting, serverless PHP, or anywhere installing Chrome is a fight.
  • Imagick/GD for a simple card drawn by hand.

The same task in Python, JavaScript and C#, plus a comparison of npm packages. To capture a live page instead of your own HTML, use the screenshot API.

FAQ

How do I convert HTML to an image in PHP?

The reliable way is spatie/browsershot: Browsershot::html($html)->windowSize(1200, 630)->save('card.png'). It drives Chrome through Puppeteer, so the server needs Node.js 22+ and the puppeteer npm package next to PHP.

Can Imagick or GD render HTML and CSS?

No. GD and Imagick draw pixels. ImageMagick lists HTML as a format it can read only through the external html2ps converter, which is not a browser and does not lay out modern CSS such as flexbox or grid.

Is knp-snappy (wkhtmltoimage) still a good option?

The PHP wrapper is maintained, but the wkhtmltoimage binary it runs is not: the wkhtmltopdf repository was archived on 2 January 2023 and the last release is from June 2020, with an old Qt WebKit engine. Use it only for existing code.

Can I convert HTML to an image in PHP without Node.js?

Not with a local browser: Browsershot needs Node.js and Chrome, and other PHP Chrome libraries such as chrome-php/chrome still need Chrome installed. Without installing either, call an HTML to image API over HTTP with Guzzle or Laravel's Http client.