How to Generate a PDF from HTML in PHP
Generate PDFs from PHP HTML with Dompdf, mPDF, TCPDF or a headless browser, with code, security controls, troubleshooting and renderer guidance.
To generate a PDF from HTML in PHP, build the document HTML from a template, pass it to a PDF renderer, then stream the result or save its bytes. Dompdf is a practical first choice for conventional HTML and CSS in a PHP-only application. mPDF and tc-lib-pdf/TCPDF are alternatives, while a headless browser is better when the template depends on modern browser layout or JavaScript.
1. Choose a renderer
| Renderer | Use it when | Verify first |
|---|---|---|
| Dompdf | Conventional HTML/CSS and an in-process PHP renderer fit the application. | CSS subset, table pagination, fonts, PHP extensions and resource restrictions. Dompdf documents no flexbox or CSS Grid support, and a table row must fit on one page. Read the project README. |
| mPDF | UTF-8 HTML, headers, footers, page numbering, tables of contents or print-focused behavior matter. | PHP compatibility, CSS coverage, language and font behavior, and output quality for your real template. See the mPDF README and manual. |
| tc-lib-pdf/TCPDF | A PHP PDF library with direct HTML/CSS rendering or documented PDF/UA mapping is relevant. | Exact release, PHP version and HTML/CSS scope. Project site and HTML/CSS documentation. |
| Headless browser | Browser layout, JavaScript, web fonts or client-side rendering is central. | Browser binary deployment, process limits, resource loading, runtime cost and the PHP integration you select. The TCPDF comparison describes browser-driven options such as Browsershot and Snappy. |
There is no universal winner. Render representative short and long documents before committing: include long tables, large images, non-Latin text, headers and footers, page breaks and the fonts used in production.
2. Install Dompdf
composer require dompdf/dompdf
Its README lists PHP 7.1 or newer, DOM and MBString, php-font-lib and php-svg-lib, with GD used for image processing. Requirements can change between releases, so deploy and verify the version Composer resolves instead of copying an old minimum into your infrastructure.
3. Build and render HTML
<?php
require __DIR__ . '/vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
$options = new Options();
$options->set('isRemoteEnabled', false);
$options->set('chroot', __DIR__ . '/pdf-assets');
$dompdf = new Dompdf($options);
$html = '<!doctype html>
<html><head>
<meta charset="UTF-8">
<style>
@page { margin: 24mm 18mm; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11pt; }
h1 { color: #222; }
.total { page-break-inside: avoid; }
</style>
</head><body>
<h1>Invoice</h1>
<p>Generated from a PHP template.</p>
<div class="total">Total: $125.00</div>
</body></html>';
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$dompdf->stream('invoice.pdf', ['Attachment' => true]);
For a download, send the renderer output with PDF headers yourself or use stream(). For a file, write the output bytes to an application-controlled path:
$pdfBytes = $dompdf->output();
file_put_contents(__DIR__ . '/storage/invoices/invoice-123.pdf', $pdfBytes);
Keep template rendering separate from business logic. Escape user-provided text, set the document encoding explicitly, and test both short and very long records.
4. Use a real PHP template
<?php
require __DIR__ . '/vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
$invoice = [
'number' => 'INV-1007',
'customer' => 'Ada Lovelace',
'items' => [
['name' => 'Consulting', 'quantity' => 2, 'price' => 150.00],
['name' => 'Support', 'quantity' => 1, 'price' => 75.00],
],
];
ob_start();
?>
<!doctype html>
<html><head><meta charset="UTF-8">
<style>@page { margin: 20mm; } table { width:100%; border-collapse:collapse; } th,td { border-bottom:1px solid #ddd; padding:6px; text-align:left; }</style>
</head><body>
<h1>Invoice <?= htmlspecialchars($invoice['number'], ENT_QUOTES, 'UTF-8') ?></h1>
<p>Customer: <?= htmlspecialchars($invoice['customer'], ENT_QUOTES, 'UTF-8') ?></p>
<table><tr><th>Item</th><th>Qty</th><th>Price</th></tr>
<?php foreach ($invoice['items'] as $item): ?>
<tr><td><?= htmlspecialchars($item['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= (int) $item['quantity'] ?></td>
<td><?= number_format((float) $item['price'], 2) ?></td></tr>
<?php endforeach; ?>
</table></body></html>
<?php
$html = ob_get_clean();
$options = new Options();
$options->set('isRemoteEnabled', false);
$options->set('chroot', __DIR__ . '/pdf-assets');
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
file_put_contents(__DIR__ . '/storage/invoice.pdf', $dompdf->output());
5. Control paper, margins and page breaks
Set the paper size and orientation with setPaper('A4', 'portrait') or a custom size supported by your renderer. CSS @page margins control printable space. Use page-break-before, page-break-after and page-break-inside: avoid where supported, but validate the result with long content.
Dompdf parses elements onto the active page. Table cells are not pageable, so one oversized row can cause clipping or layout failure; split very large content into multiple rows or choose a renderer with behavior that matches your design. Flexbox and CSS Grid are not supported by Dompdf, so use ordinary block layout and tables for document structure.
6. Fonts, images and other resources
Prefer local, known assets. Put them under the configured chroot directory and reference them with controlled paths. Register or embed fonts according to the renderer’s documentation when non-Latin characters or brand typography matter. Keep image dimensions reasonable; very large raster images increase memory use.
Dompdf keeps remote resources disabled in the example. Enabling them requires isRemoteEnabled plus cURL or allow_url_fopen. Turn it on only when needed, restrict allowed URLs, and never let untrusted HTML fetch arbitrary internal or external resources.
7. Returning a PDF from an HTTP endpoint
$dompdf->render();
$pdf = $dompdf->output();
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="invoice.pdf"');
header('Content-Length: ' . strlen($pdf));
echo $pdf;
Do not print warnings, debug markup or a UTF-8 byte-order mark before these headers. For large files, save them and return a download URL instead of holding every output byte in a long-lived web process.
8. When mPDF, TCPDF or a browser is a better fit
- Choose mPDF when its documented print features, UTF-8 handling, headers, footers or table-of-contents support match your template.
- Choose tc-lib-pdf/TCPDF when its current PHP requirements and HTML/CSS or PDF/UA capabilities fit your project.
- Choose a headless browser when JavaScript, flexbox, Grid, web fonts or browser-specific layout must match a real webpage. Account for the browser binary, process isolation, startup time and memory.
Render the same fixture documents with each candidate and compare pagination, fonts, images, links and output size. A successful one-page sample does not prove production fidelity.
9. Security checklist
- Escape untrusted values before inserting them into HTML.
- Keep remote fetching disabled unless the template requires it.
- Constrain local files with
chrootand application-controlled paths. - Reject arbitrary user-supplied URLs, especially private network addresses and metadata endpoints.
- Limit HTML size, image dimensions, render time and process memory.
- Run browser renderers with appropriate sandboxing and a restricted service account.
- Store generated PDFs with access controls and avoid logging sensitive document contents.
10. Performance, reliability and cost
Rendering cost depends on HTML size, images, fonts, pagination and the renderer. Reuse stable templates, resize images before rendering, avoid unnecessary remote requests and cache identical documents when business rules allow it. Set request and worker timeouts, monitor memory, and retry only failures that are safe to repeat. Persist the input data and template version with each important PDF so it can be reproduced.
Browser rendering generally adds a process and binary dependency; PHP libraries simplify deployment but may require CSS compromises. Measure your own representative documents rather than relying on an abstract benchmark.
11. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or truncated PDF | Fatal error, output before headers, invalid HTML or memory exhaustion. | Inspect PHP logs, remove debug output, validate the template, reduce image size and raise worker memory only after measuring. |
| Images missing | Remote access is disabled, URL is inaccessible, or the path is outside chroot. |
Use controlled local assets, configure the allowed root, or explicitly enable remote resources after a security review. |
| Flexbox or Grid layout breaks | Dompdf does not support those layout systems. | Rewrite the print template with supported block and table layout or use a browser renderer. |
| Rows split or overlap | A table row is taller than a page or contains unbreakable content. | Split the row, allow wrapping, reduce content, or change renderer. |
| Accented or non-Latin text is wrong | Missing font or incorrect encoding. | Use UTF-8 throughout and configure a font that contains the required glyphs. |
| Remote fonts or CSS do not load | Network access, TLS, URL or renderer support problem. | Prefer local assets, verify connectivity from the PHP runtime and inspect renderer logs. |
| Request times out | Large document, slow asset, browser startup or an infinite client-side operation. | Remove remote dependencies, set bounded timeouts, split work into jobs and profile the slow template. |
| PDF differs between environments | Different package versions, fonts, PHP extensions or browser binaries. | Lock dependencies, ship required fonts and compare environment versions during deployment. |
12. Or skip the browser setup
If the source is already a public webpage and you need a PDF capture rather than PHP template rendering, ScreenshotNeo provides a single API endpoint. Its PDF options include paper size, margins, landscape mode and page ranges. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -d format=pdf -o page.pdf
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"}, timeout=90)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com', format: 'pdf' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('page.pdf', Buffer.from(await res.arrayBuffer()));
Cookie and consent banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages and failed loads are never billed, and the response reports the page verdict and billing status in headers. An 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 a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
13. FAQ
Can PHP convert HTML to PDF without a package?
PHP itself does not provide a complete HTML/CSS layout engine. Use a library or a browser process that performs the rendering.
Is Dompdf suitable for invoices?
It can suit conventional invoice markup, provided your CSS, fonts, tables and pagination fit its supported subset. Test long item lists and localized text.
Should I save or stream the PDF?
Stream small, immediate downloads. Save output for asynchronous jobs, repeat downloads, auditing or large documents.
How do I make a browser page match the PDF?
Use a headless browser when JavaScript and modern browser layout are essential; PHP-only renderers intentionally support a narrower HTML/CSS subset.
How can I keep generated PDFs safe?
Escape data, restrict local and remote resources, limit render inputs and isolate browser processes. Treat HTML influenced by users as untrusted.


