ScreenshotNeo

BlogHTML to image & PDF

How to Export HTML to PDF in PHP

Convert HTML to PDF in PHP with Dompdf, mPDF, tc-lib-pdf, or Chromium, including code, security, troubleshooting, and downloads.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: In PHP, install a Composer PDF library, build a complete and sanitized HTML document, render it, then stream or save the PDF bytes. Use Dompdf for modest CSS 2.1-style layouts, mPDF for UTF-8 business documents and pagination features, tc-lib-pdf for PHP 8.2+ and explicit PDF controls, or a Chromium-backed renderer when modern CSS and JavaScript must match a browser.

Choose the right rendering path

Option Best fit Trade-offs
Dompdf Invoices, receipts, simple reports Low deployment overhead; CSS support is centered on CSS 2.1-style layouts
mPDF UTF-8 business documents, headers, footers, page numbers, tables of contents and barcodes HTML-driven workflow; sanitize markup and external resources carefully
tc-lib-pdf PHP 8.2+, HTML/CSS/SVG and stronger PDF-generation controls Requires PHP ^8.2; more explicit configuration
Chromium-backed renderer Modern CSS, web fonts, JavaScript and browser-level layout fidelity Requires Node/Chromium, a binary, or a rendering service; output can change when the runtime changes

Export HTML with Dompdf

1. Install the package

composer require dompdf/dompdf

Dompdf describes its engine as a CSS 2.1-compliant HTML layout and rendering engine written in PHP. Enable its HTML5 parser for tolerant markup, and deliberately restrict remote resources and filesystem access.

2. Create a complete document and stream it

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

use Dompdf\Dompdf;
use Dompdf\Options;

function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$data = [
    'number' => 'INV-1042',
    'customer' => 'Ada Lovelace',
    'total' => '$1,250.00',
];

$html = '<!doctype html>
<html><head><meta charset="utf-8">
<style>
@page { margin: 22mm 18mm; }
body { font-family: DejaVu Sans, sans-serif; color: #222; font-size: 12px; }
h1 { font-size: 22px; margin: 0 0 16px; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #ddd; padding: 8px; text-align: left; }
.total { text-align: right; font-size: 16px; font-weight: bold; }
</style></head><body>
<h1>Invoice ' . e($data['number']) . '</h1>
<p>Customer: ' . e($data['customer']) . '</p>
<table><tr><th>Description</th><th>Amount</th></tr>
<tr><td>Consulting</td><td>' . e($data['total']) . '</td></tr></table>
<p class="total">Total: ' . e($data['total']) . '</p>
</body></html>';

$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', false);
$options->setChroot(__DIR__ . '/assets');
$options->setDpi(96);

$dompdf = new Dompdf($options);
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$dompdf->stream('invoice.pdf', ['Attachment' => true]);

Keep the controller free of stray output before stream(). If you need the bytes for storage instead of a download, use $dompdf->output() and write the returned string to a controlled path.

Export with mPDF

Install and render

composer require mpdf/mpdf
<?php
require __DIR__ . '/vendor/autoload.php';

use Mpdf\Mpdf;

$html = render_invoice_template($data); // escape and validate every user value
$mpdf = new Mpdf([
    'format' => 'A4',
    'margin_left' => 18,
    'margin_right' => 18,
    'margin_top' => 22,
    'margin_bottom' => 22,
]);
$mpdf->WriteHTML($html);
$mpdf->Output('invoice.pdf', 'D'); // D = browser download; F = file; S = string

mPDF is designed for UTF-8 HTML. Its manual recommends writing the document in HTML/CSS and passing that markup to mPDF. Headers, footers, page numbering, tables of contents, barcodes and color handling are useful for business documents.

Export with tc-lib-pdf

tc-lib-pdf is the current TCPDF generation: a pure-PHP library for PHP 8.2 and later, installed through Composer. The project supports HTML, CSS and SVG rendering and provides controls for tagged output and PDF/UA-oriented features such as heading structure and alternate text.

composer require tecnickcom/tc-lib-pdf

Follow the package’s Composer setup and API for your chosen document class, pass sanitized HTML or structured content, then return or stream the generated PDF bytes. This route is a good fit when your application standardizes on PHP 8.2+ and needs explicit PDF controls.

When browser-backed rendering is the better choice

Use Chromium through Puppeteer or Browsershot, Snappy with wkhtmltopdf, or a Gotenberg service when the source depends on current CSS, JavaScript, web fonts or browser layout. These choices add an external runtime, binary or service. Pin and patch that runtime separately from PHP. wkhtmltopdf uses an older Qt WebKit engine and was archived upstream in January 2023, so it should not be the default for modern CSS.

Example: render a URL with a browser service

// Node.js worker example (Puppeteer)
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: 'new'});
const page = await browser.newPage();
await page.goto('https://example.com/report', {waitUntil: 'networkidle0'});
await page.pdf({path: 'report.pdf', format: 'A4', printBackground: true});
await browser.close();

For a PHP application, call this worker through a queue or an internal service rather than launching a new browser for every request. Reuse browser processes, cap concurrency and set navigation and PDF timeouts.

Serve a generated PDF from a PHP endpoint

<?php
// After $pdfBytes = $dompdf->output();
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="invoice.pdf"');
header('Content-Length: ' . strlen($pdfBytes));
echo $pdfBytes;

Download that endpoint with cURL

curl -fL https://your-app.example/invoices/1042.pdf -o invoice.pdf

Download it with Python

import requests

r = requests.get('https://your-app.example/invoices/1042.pdf', timeout=90)
r.raise_for_status()
with open('invoice.pdf', 'wb') as f:
    f.write(r.content)

Download it with Node.js

const res = await fetch('https://your-app.example/invoices/1042.pdf');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('invoice.pdf', bytes));

HTML and CSS options that affect PDF output

  • Set <meta charset="utf-8"> and pass UTF-8 explicitly.
  • Use @page for size and margins; set portrait or landscape deliberately.
  • Use print-safe units such as mm, pt and controlled pixel values.
  • Keep tables explicit and test row splitting across pages.
  • Use page-break rules such as break-before, break-after and break-inside where supported by your renderer.
  • Embed and register fonts needed for multilingual output; server fonts are not guaranteed to exist.
  • Inline critical CSS and images when reproducibility matters.
  • For remote images or stylesheets, allow only trusted hosts and set timeouts.

Security checklist

  • Escape every user-controlled value before inserting it into HTML.
  • Sanitize rich HTML more strictly than ordinary browser output.
  • Disable remote URL loading unless the document genuinely needs external resources.
  • Set a filesystem chroot or equivalent boundary where supported.
  • Prevent server-side request forgery by blocking private, loopback and metadata IP ranges in any URL fetcher.
  • Never let users choose arbitrary filesystem paths, shell arguments or renderer flags.
  • Pin Composer and renderer versions, then review upgrades for layout and security changes.

Performance, reliability and cost

  • Cache immutable PDFs by document version and input hash.
  • Queue large reports and browser renders; return a job ID instead of holding an HTTP request open.
  • Reuse Chromium instances and limit parallel pages to protect memory.
  • Set separate timeouts for asset fetches, page loading and PDF generation.
  • Keep representative PDFs as regression fixtures and inspect page breaks, image loading, fonts and Unicode after upgrades.
  • Pure-PHP libraries reduce operational dependencies. Browser-backed rendering adds runtime hosting, memory and patching costs.
  • Measure peak memory with large tables and images; render in smaller batches when necessary.

Troubleshooting

Symptom Likely cause Fix
Blank or partly blank PDF Malformed HTML, blocked resources or a renderer exception Enable HTML5 parsing, validate markup, inspect logs and inline critical assets
Images missing Remote loading disabled, bad paths or unsupported formats Use absolute trusted paths, configure a narrow chroot or embed the image
Fonts or accents are wrong Font not installed or not embedded Register and embed a font with the required glyph coverage
CSS looks different from the browser Pure-PHP renderer does not implement the CSS or JavaScript used Simplify CSS or move to Chromium-backed rendering
Headers already sent Whitespace, warnings or debug output before PDF headers Remove output, disable display_errors for production responses and buffer carefully
Request times out Large document, slow external assets or a browser startup per request Queue the job, cache assets, set bounded timeouts and reuse workers
Pages break in the wrong place Complex table rows, floats or unsupported break rules Reduce layout complexity, add explicit break rules and test the target renderer
PDF is unsafe or fetches internal URLs Untrusted HTML or unrestricted remote-resource loading Sanitize input, disable remote access by default and enforce host/IP allowlists

Or skip the browser setup

If the source is a public URL, ScreenshotNeo can return a PDF from one GET request. See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

For PDF output, request the PDF format using the documented parameter. ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and each response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can PHP itself convert HTML to PDF?

PHP hosts the workflow, but you normally install a Composer renderer or call a browser-backed runtime.

Which library should I start with?

Start with Dompdf for simple layouts, mPDF for UTF-8 business documents, tc-lib-pdf for PHP 8.2+ and explicit controls, and Chromium when browser fidelity is essential.

Why does a PDF differ from the browser?

Pure-PHP engines implement a subset of browser CSS and do not execute JavaScript like Chromium.

How do I make exports repeatable?

Pin versions, embed fonts, control external resources, fix locale and timezone inputs, and keep regression PDFs.