How to Render HTML to PDF in PHP
Compare PHP PDF renderers, choose the right architecture, and generate reliable PDFs from HTML with runnable PHP examples.
To render HTML to PDF in PHP, choose a renderer that matches your HTML. Use Dompdf, mPDF, or tc-lib-pdf when a PHP-native engine supports your CSS and deployment; use a Chromium-based renderer when you need modern CSS or close visual parity with a web page. Then load the HTML, configure resources and page settings, render, and stream or save the PDF.
Render representative documents before production. Check fonts, images, tables, page breaks, headers, footers, and every CSS feature your template depends on. PHP PDF libraries implement different subsets of HTML and CSS, so there is no universal best package.
Choose a rendering approach
| Approach | Good fit | Important constraints |
|---|---|---|
| Dompdf | Simple to moderately complex documents, PHP-only deployment, invoices and reports with tables. | Mostly CSS 2.1; no flexbox or CSS Grid. Table rows must fit on one page. Remote assets need explicit configuration. |
| mPDF | UTF-8 documents needing headers, footers, page numbers, tables of contents, barcodes, or print-oriented controls. | For state-of-the-art CSS or close rendering of an existing page, its manual recommends headless Chrome. |
| tc-lib-pdf | PHP 8.2+ projects wanting a current pure-PHP PDF library and a documented HTML/CSS subset. | It is not a browser engine. Validate layout and pagination against your real templates. |
| Browsershot/Chromium | Modern CSS, JavaScript-rendered pages, and pixel-level similarity to a browser. | PHP invokes Node, Puppeteer, and Chromium. You must install, update, and operate that runtime. |
| Gotenberg | A separate HTTP service for Chromium or LibreOffice rendering. | Operate or reach the service and account for network failures and renderer updates. |
| Snappy/wkhtmltopdf | Existing systems already verified against its output. | Upstream was archived in January 2023 and its Qt WebKit engine predates much of CSS3; treat it as a legacy choice. |
The key decision axes are CSS fidelity, whether rendering stays inside PHP, external runtime operations, document features, and output stability as engines change. See the Dompdf documentation, mPDF manual, and the tc-lib-pdf project for the current release requirements and APIs.
1. Prepare HTML that can be paginated
Start with a complete HTML document and explicit print styles. Use absolute or correctly rooted asset URLs, declare UTF-8, and keep layout rules within the renderer’s supported subset.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 18mm 16mm; }
body { font-family: DejaVu Sans, sans-serif; color: #222; font-size: 11pt; }
h1 { font-size: 22pt; margin: 0 0 8mm; }
h2 { font-size: 14pt; page-break-after: avoid; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 0.2mm solid #bbb; padding: 2mm; vertical-align: top; }
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
.page-break { page-break-before: always; }
</style>
</head>
<body>
<h1>Monthly report</h1>
<p>Generated at <?= htmlspecialchars($generatedAt, ENT_QUOTES, 'UTF-8') ?></p>
<table>
<thead><tr><th>Item</th><th>Amount</th></tr></thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= number_format($row['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body>
</html>
Escape untrusted text with htmlspecialchars. Keep CSS in the template while diagnosing layout so you can see which rule causes a difference. A browser renderer may support flexbox, Grid, web fonts, and JavaScript that a PHP-native library ignores.
2. Render with Dompdf
Install Dompdf with Composer:
composer require dompdf/dompdf
This complete example renders a string and sends it as a download:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
$options = new Options();
$options->set('isRemoteEnabled', true); // Only when remote assets are required.
$options->set('defaultFont', 'DejaVu Sans');
$options->setChroot([__DIR__ . '/public']); // Permit local files only below this path.
$dompdf = new Dompdf($options);
$html = file_get_contents(__DIR__ . '/templates/report.php');
if ($html === false) {
throw new RuntimeException('Template could not be read');
}
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$dompdf->stream('report.pdf', ['Attachment' => true]);
Resource access and security
Dompdf requires isRemoteEnabled plus cURL or allow_url_fopen for remote files. Local files must be under configured chroot paths. Do not enable unrestricted filesystem or network access for user-supplied HTML. Prefer downloading approved assets yourself, storing them in a controlled directory, and passing local paths.
Dompdf limitations to design around
- It does not support CSS flexbox.
- It does not support CSS Grid.
- Table rows must fit on one page; a very tall row can overflow instead of splitting cleanly.
- Do not reuse one Dompdf instance for multiple unrelated HTML documents; create a new instance for each render.
3. Render with mPDF
Install mPDF:
composer require mpdf/mpdf
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Mpdf\Mpdf;
$mpdf = new Mpdf([
'format' => 'A4',
'orientation' => 'P',
'margin_left' => 16,
'margin_right' => 16,
'margin_top' => 18,
'margin_bottom' => 18,
'default_font' => 'dejavusans',
]);
$html = file_get_contents(__DIR__ . '/templates/report.html');
if ($html === false) {
throw new RuntimeException('Template could not be read');
}
$mpdf->SetTitle('Monthly report');
$mpdf->WriteHTML($html);
$mpdf->Output(__DIR__ . '/storage/report.pdf', 'F');
// Use 'D' to download or 'I' to display inline.
mPDF accepts UTF-8 HTML and provides document-oriented features such as headers, footers, page numbering, tables of contents, barcodes, and print color controls. Its manual says to consider headless Chrome when you need state-of-the-art CSS support or want to mirror an existing HTML page.
4. Render with tc-lib-pdf
tc-lib-pdf describes itself as the current generation of TCPDF and requires PHP 8.2 or later according to its current project information. Its HTML/CSS support is a documented subset, so check the installed version’s API and examples before wiring it into an application.
Use the same workflow: build the HTML, create the PDF object, configure page and font settings, write the supported HTML, and save or stream the result. Keep the integration behind a small application service so replacing the renderer does not require rewriting controllers and templates.
5. Use Chromium when browser fidelity matters
A browser-backed renderer is usually the better fit when the source page relies on flexbox, Grid, modern selectors, web fonts, JavaScript, lazy-loaded content, or styles already validated in Chrome. Browsershot invokes Node/Puppeteer and Chromium; a separate service such as Gotenberg exposes rendering over HTTP.
Plan for a browser executable in every deployment environment, sandbox and container settings, font packages, process timeouts, and version changes. Browser updates can change line wrapping, pagination, and generated output. Pin versions where reproducibility matters and keep a sample-document regression set.
6. Control page size, margins, fonts, and assets
Page geometry
Set paper size and orientation explicitly. Use @page for CSS-aware engines and the library’s page API where available. Keep margins in one place so a template does not fight application settings.
Fonts and character sets
Declare UTF-8, choose a font available to the renderer, and embed or install the font when licensing permits. Test accented characters, non-Latin scripts, emoji, and right-to-left text separately. Missing glyphs often appear as empty boxes or fallback symbols.
Images and remote resources
Prefer local, controlled assets for PHP-native renderers. Verify MIME types, image dimensions, and readable permissions. Remote URLs introduce DNS, TLS, authentication, and timeout failures. For browser rendering, wait until images and web fonts have finished loading before printing.
Pagination
Use page-break-before, page-break-after, and page-break-inside where supported. Repeat table headers with thead. Avoid placing an entire invoice or a very tall table row inside an unbreakable container.
7. Save, stream, and serve the PDF safely
<?php
$pdfBytes = $dompdf->output();
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="report.pdf"');
header('Content-Length: ' . strlen($pdfBytes));
echo $pdfBytes;
For large documents, write to a temporary file and move it into durable storage after a successful render. Generate filenames from trusted identifiers, not raw user input. If the PDF contains private data, authorize every download and avoid exposing temporary paths.
8. Test the document before production
- Render the smallest valid document first.
- Test long paragraphs, long words, wide tables, nested lists, and forced page breaks.
- Test missing images, slow remote resources, and invalid HTML.
- Inspect fonts, accented characters, right-to-left text, and fallback glyphs.
- Compare output from every supported renderer if your application offers more than one.
- Keep representative PDFs or page images for visual regression checks after package or browser upgrades.
9. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank PDF | HTML exception, invalid template, or renderer received an empty string. | Log the template result, fail when it is empty, and render a minimal document to isolate the input. |
| Images missing | Remote access disabled, URL not reachable, unsupported format, or local path outside the chroot. | Use approved local assets, configure remote access deliberately, verify permissions and MIME types, and inspect renderer logs. |
| Flexbox or Grid collapses | PHP-native renderer does not implement those layout systems. | Rewrite the print stylesheet with tables, blocks, floats, or switch to Chromium. |
| Fonts show as boxes | Font is unavailable, not embedded, or lacks the required glyphs. | Install or embed a font supported by the renderer and verify UTF-8 input. |
| Table content is cut off | A row is taller than a page or marked unbreakable. | Allow row splitting where supported, reduce padding, or split the data into smaller sections. |
| Styles work in a browser but not in the PDF | Unsupported CSS, relative asset URLs, or JavaScript-dependent content. | Check the renderer’s supported subset, make URLs resolvable, or use a browser engine. |
| Render times out | Large images, slow remote resources, runaway JavaScript, or too many pages. | Resize assets, remove unnecessary network calls, set bounded timeouts, and queue large jobs. |
| Output changes after an upgrade | Package, font, or browser engine changed line wrapping or pagination. | Pin versions, record renderer metadata, and review visual regression output before rollout. |
10. Performance, reliability, and cost
Rendering cost is driven by HTML size, image decoding, page count, font work, and browser startup. Reuse a long-lived browser process for batches when your architecture supports it, but isolate jobs and enforce per-document time and memory limits. PHP-native renderers avoid browser startup but may require more template work to achieve the same layout.
Cache immutable PDFs or a hash of the input data and template version. Do not cache documents containing user-specific or sensitive data without an authorization strategy. Queue large or user-triggered batches so web requests do not hold workers open. Record renderer version, template version, duration, page count, and failure reason.
For reliability, make resource URLs deterministic, bundle required fonts, avoid unnecessary third-party requests, and fail clearly when an asset is unavailable. A browser service adds a network dependency; a colocated browser adds process and image-maintenance work. Choose the failure boundary your team can operate.
Or skip the browser setup
ScreenshotNeo can capture a URL as PNG, JPEG, WebP, or PDF with one request. The API accepts the URL and options, and the documentation lists the current parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
<?php
$url = 'https://api.screenshotneo.com/v1/shot';
$query = http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => 'https://stripe.com',
]);
$context = stream_context_create(['http' => ['timeout' => 90]]);
$bytes = file_get_contents($url . '?' . $query, false, $context);
if ($bytes === false) {
throw new RuntimeException('Screenshot request failed');
}
file_put_contents(__DIR__ . '/shot.webp', $bytes);
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', body);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and whether the shot was billed. ScreenshotNeo also supports PDF output, custom CSS and JavaScript, waiting rules, headers and cookies, device presets, full-page capture, element capture, caching, asynchronous jobs, bulk capture, signed links, and an MCP server for AI agents.
There are 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account.
FAQ
Can PHP render any HTML page as a PDF?
No. Each library implements a subset of HTML and CSS. A page that depends on modern CSS or JavaScript may require Chromium.
Which library should I start with?
Start with Dompdf for straightforward PHP templates, mPDF for document features, tc-lib-pdf for a current PHP 8.2+ pure-PHP option, and Chromium when browser fidelity is the priority.
Should I use wkhtmltopdf for a new project?
Usually no. Its upstream has been archived since January 2023 and its WebKit engine is old. Use it only when an existing, verified system depends on it.
How do I make PDF output reproducible?
Pin package, browser, and font versions; bundle assets; avoid nondeterministic remote content; and review representative output after upgrades.


