← All guides

How to convert HTML to PDF in PHP and Laravel

PHP developers usually reach for Dompdf first, discover its CSS limits, then move to Browsershot and discover the cost of running Chromium next to PHP. This guide walks through both honestly and then shows the HTTP API approach with PDFOven.

Option 1: Dompdf

Dompdf is a pure-PHP renderer. No external binaries, installs with Composer, works on shared hosting.

composer require dompdf/dompdf
<?php
require 'vendor/autoload.php';

use Dompdf\Dompdf;
use Dompdf\Options;

$options = new Options();
$options->set('isRemoteEnabled', true);
$options->set('defaultFont', 'DejaVu Sans');

$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();

file_put_contents('invoice.pdf', $dompdf->output());

What to know:

  • Dompdf implements most of CSS 2.1 and some CSS 3, but not flexbox or grid. Layouts built with them collapse. Tables and floats are the reliable tools.
  • No JavaScript.
  • Fonts must be TTF and registered via @font-face; the bundled DejaVu fonts cover many scripts, but CJK needs extra fonts and large font files increase memory use.
  • Large tables can be slow and memory hungry, since everything happens inside the PHP process.
  • Leave isRemoteEnabled off unless you need it, and never with user-controlled HTML, or the renderer can be used to fetch arbitrary URLs.

For simple receipts Dompdf is great. For anything designed in a modern browser, you will fight it.

Option 2: Browsershot (Puppeteer from PHP)

Spatie's Browsershot shells out to Node and Puppeteer, so you get real Chromium rendering.

composer require spatie/browsershot
npm install puppeteer
<?php
use Spatie\Browsershot\Browsershot;

Browsershot::html($html)
    ->format('A4')
    ->margins(12, 12, 12, 12)
    ->showBackground()
    ->waitUntilNetworkIdle()
    ->save('invoice.pdf');

$bytes = Browsershot::html($html)->format('A4')->pdf();

What to know:

  • Your PHP servers now need Node, npm and a Chromium build plus its system libraries. On Forge, Vapor or shared hosts that is often the hard part. Laravel Vapor runs on Lambda, where Chromium does not fit without extra work.
  • Each render starts a Node process and a browser, which is noticeably slower than Dompdf and uses hundreds of megabytes of memory. Run it in a queue worker rather than inline in a web request.
  • Fonts have to be installed at the OS level on every server.

The PDFOven way: one HTTP call

With PDFOven, PHP only needs to make an HTTPS request. No binaries, no Node.

Guzzle

<?php
use GuzzleHttp\Client;

$client = new Client(['timeout' => 60]);

$response = $client->post('https://pdfoven.com/v1/pdf', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('PDFOVEN_KEY')],
    'json' => [
        'html' => $html,
        'options' => [
            'format' => 'A4',
            'margin' => '0.5in',
            'print_background' => true,
        ],
    ],
    'http_errors' => false,
]);

if ($response->getStatusCode() !== 200) {
    $error = json_decode((string) $response->getBody(), true)['error'];
    throw new RuntimeException($error['code'] . ': ' . $error['message']);
}

file_put_contents('invoice.pdf', (string) $response->getBody());

Plain cURL

<?php
$ch = curl_init('https://pdfoven.com/v1/pdf');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('PDFOVEN_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'html' => $html,
        'options' => ['format' => 'Letter'],
    ]),
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException('PDF render failed: ' . $pdf);
}

Laravel controller

Render a Blade view to a string and return the PDF:

<?php
use Illuminate\Support\Facades\Http;

public function show(Invoice $invoice)
{
    $html = view('invoices.pdf', ['invoice' => $invoice])->render();

    $response = Http::withToken(config('services.pdfoven.key'))
        ->timeout(60)
        ->post('https://pdfoven.com/v1/pdf', [
            'html' => $html,
            'options' => ['format' => 'A4', 'print_background' => true],
            'filename' => "invoice-{$invoice->number}.pdf",
        ]);

    $response->throw();

    return response($response->body(), 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice-' . $invoice->number . '.pdf"',
    ]);
}

Because PDFOven renders with Chromium, the flexbox and grid layouts that broke in Dompdf work as they do in Chrome. If your Blade view loads charts with JavaScript, add 'wait_for_selector' => '#charts-ready'.

Other useful options

  • css: send styles separately from your markup.
  • options.header_template / options.footer_template: repeating headers and footers, with <span class="pageNumber"></span> and <span class="totalPages"></span> for page numbers.
  • options.landscape, options.scale, and formats A3, A4, A5, Letter, Legal and Tabloid.
  • response: "base64" returns JSON with data, content_type and bytes, convenient for storing in a queue payload.
  • POST /v1/image renders PNG (or JPEG with ?type=jpeg).

Summary

Dompdf Browsershot PDFOven
Flexbox / grid No Yes Yes
JavaScript No Yes Yes
Server dependencies None Node, Chromium, fonts None
Runs on serverless PHP Yes Difficult Yes

PDFOven's free plan covers 100 renders per month; paid plans are $12, $39 and $99 for 2,000, 15,000 and 60,000 renders. Errors are JSON ({ "error": { "code", "message" } }), and 429 quota_exceeded signals the monthly limit. Want to test a template first? Paste it into the free HTML to PDF tool.

Skip the browser plumbing

PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.

Get a free API key