ScreenshotNeo

BlogHTML to image & PDF

How to Convert an HTML Table to PDF in PHP

Render an escaped UTF-8 HTML table to PDF in PHP with Dompdf, mPDF or TCPDF, fix pagination and encoding issues, and automate page capture.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: PHP does not turn arbitrary HTML into a PDF by itself. Build a valid, UTF-8 HTML table, escape every dynamic value, then pass the document to a PDF renderer such as Dompdf, mPDF, TCPDF/tc-lib-pdf, or a browser renderer. Stream the resulting bytes with Content-Type: application/pdf or save them.

1. Choose a renderer

Renderer Best fit Constraints
Dompdf Conventional tables and mostly CSS 2.1 Rows cannot split across pages; flexbox and Grid are unsupported; malformed HTML can break layout.
mPDF UTF-8 reports, headers, footers and page numbers Vet all HTML/CSS carefully; use a browser renderer for state-of-the-art CSS fidelity.
TCPDF/tc-lib-pdf Explicit PHP control and structured page flow Only its supported HTML/CSS subset is rendered.
Headless Chrome Existing pages or modern CSS that must match a browser Requires a browser process or service and deployment operations.

Install one library with Composer. For Dompdf: composer require dompdf/dompdf. Its documentation covers HTML5 parsing, complex tables, remote-resource controls and the limitation that a table row must fit on one page (project documentation). mPDF accepts UTF-8 HTML and provides print features (manual); its project notes direct users needing modern CSS fidelity to headless Chrome (repository). TCPDF documents an HTML API with automatic page and region breaks, spans and repeated table headers (API documentation).

2. Complete Dompdf example

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

use Dompdf\Dompdf;
use Dompdf\Options;

$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$dompdf = new Dompdf($options);

$rows = [
    ['name' => 'Ada', 'total' => '42.00'],
    ['name' => 'Grace', 'total' => '37.50'],
    ['name' => '李雷', 'total' => '19.95'],
];

$e = static fn ($value) => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$html = '<!doctype html><html lang="en"><head><meta charset="utf-8">
<style>
@page { margin: 18mm; }
body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { border: 1px solid #999; padding: 6px; overflow-wrap: anywhere; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr { page-break-inside: avoid; }
</style></head><body>
<h1>Order totals</h1><table><thead><tr><th>Name</th><th>Total</th></tr></thead><tbody>';
foreach ($rows as $row) {
    $html .= '<tr><td>' . $e($row['name']) . '</td><td>' . $e($row['total']) . '</td></tr>';
}
$html .= '</tbody></table></body></html>';

$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait'); // use 'letter' where required
$dompdf->render();
$dompdf->stream('table.pdf', ['Attachment' => false]);

Use Attachment => true to force download. Save instead with $output = $dompdf->output(); file_put_contents($path, $output);. Enable remote resources only when required: $options->set('isRemoteEnabled', true), and keep local files inside the configured chroot. Remote images and fonts must be reachable by the PHP process.

3. mPDF for print-oriented reports

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

$rows = [
    ['name' => 'Ada', 'total' => '42.00'],
    ['name' => 'Grace', 'total' => '37.50'],
];
$e = static fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$html = '<!doctype html><meta charset="utf-8"><style>table{width:100%;border-collapse:collapse}th,td{border:1px solid #999;padding:6px}thead{display:table-header-group}</style><table><thead><tr><th>Name</th><th>Total</th></tr></thead><tbody>';
foreach ($rows as $r) $html .= '<tr><td>'.$e($r['name']).'</td><td>'.$e($r['total']).'</td></tr>';
$html .= '</tbody></table>';

$mpdf = new \\Mpdf\\Mpdf(['format' => 'A4', 'margin_top' => 18, 'margin_bottom' => 18]);
$mpdf->SetHTMLHeader('<div>Order report</div>');
$mpdf->SetHTMLFooter('<div>Page {PAGENO} of {nbpg}</div>');
$mpdf->WriteHTML($html); // trusted, escaped UTF-8 HTML
$mpdf->Output('table.pdf', \\Mpdf\\Output\\Destination::INLINE);

mPDF’s manual warns that it is not intended to receive HTML/CSS from outside users; vet and sanitize such input above normal browser-level sanitization. Supply a font that covers your scripts when the default font is insufficient.

4. TCPDF/tc-lib-pdf

TCPDF’s HTML API uses addHTMLCell() for an HTML block and handles automatic page and region breaks. Its table support includes thead, tbody, tfoot, row and column spans, both border models and repeated headers. The exact package and constructor differ between TCPDF and tc-lib-pdf, so follow the version’s official API documentation and verify the supported CSS subset.

5. Table markup and pagination that survive PDF rendering

  • Use semantic table, caption, thead, tbody and tfoot; semantic headers can repeat on later pages.
  • Set an explicit width, predictable column widths and modest padding. Fixed or percentage widths are safer than content-sized columns.
  • Keep Dompdf rows short: a row must fit on one page. Split exceptionally long notes into separate rows or pre-wrap them.
  • Use page-break-inside: avoid for short rows, but do not expect it to overcome an oversized row.
  • Do not depend on flexbox or CSS Grid with Dompdf. Replace them with tables, blocks and floats.
  • For a deliberate break, place a block with page-break-before: always between tables; avoid breaking inside a row.

6. Encoding, fonts, images and security

Declare <meta charset="utf-8">, pass UTF-8 to the renderer and escape every dynamic cell with htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). Never concatenate untrusted markup into the template. mPDF requires especially careful vetting of outside HTML/CSS. Dompdf includes DejaVu TrueType fonts for useful Unicode coverage; register another font when your data’s script is not covered.

For images, use reachable absolute URLs only when remote access is enabled, or use local files inside Dompdf’s chroot. Confirm the PHP process can resolve DNS, validate TLS and read the file. A missing image should not silently become a broken layout: test it in the same runtime that generates the PDF.

7. Troubleshooting checklist

Symptom Likely cause Fix
Blank or truncated PDF Malformed HTML, fatal error, or output sent before headers Validate the HTML, inspect PHP logs, remove stray output/BOM and set PDF headers before streaming.
Rows overlap or vanish Unsupported CSS or a row taller than a page Simplify CSS, set widths, shorten/split the row, or use a browser renderer.
Header missing on page two Header is outside thead or renderer-specific repetition is absent Put column headings in thead; verify the renderer’s repeat-header support.
Flex/Grid layout ignored Dompdf CSS limitation Use table/block layout or switch to headless Chrome.
Accents or CJK text are boxes Font lacks glyphs or input is not UTF-8 Declare UTF-8 and configure a font covering every script.
Images do not appear Remote access disabled, chroot violation, invalid URL or unreadable file Enable remote access deliberately, allow the path, and test connectivity from PHP.
Unsafe content executes or leaks data Untrusted HTML/CSS passed to a renderer Allow-list markup, sanitize values, and never pass user HTML directly to mPDF or another renderer.
PDF downloads as HTML Wrong content type or an exception page was streamed Set Content-Type: application/pdf, check status/logs and stream only successful renderer output.

8. Performance, reliability and cost

Rendering cost grows with HTML size, row count, images, fonts and page count. Reuse a prepared template, avoid oversized base64 images, constrain remote fetches and cache PDFs when the source data has not changed. Put a timeout around remote assets and queue very large reports so a web request does not exhaust PHP workers. Keep a renderer version pinned, log input identifiers and renderer errors, and regression-test representative long, empty, wide and multilingual tables after upgrades. If exact browser CSS is a requirement, budget for a browser service and its process management rather than forcing a PHP library to emulate unsupported features.

9. Or skip the browser setup

When the source is an existing web page, ScreenshotNeo can return a PDF from one request. It accepts consent banners as a visitor and removes 60+ known consent platforms, newsletter popups and chat widgets before capture; bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the verdict and billing. Its MCP server lets Claude, Cursor and other MCP clients call capture_pdf and related tools.

See the ScreenshotNeo API documentation for all options. cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o table.pdf

Python:

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("table.pdf", "wb").write(r.content)

Node.js:

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(`${res.status} ${await res.text()}`);
require('fs').writeFileSync('table.pdf', Buffer.from(await res.arrayBuffer()));

Use the PDF parameters in the docs for paper size, margins, landscape mode and page ranges. 1,000 shots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

10. FAQ

Can PHP convert a table without a library?

No. PHP must call a renderer or a browser service that produces PDF bytes.

Which library should I start with?

Use Dompdf for simple CSS 2.1 tables, mPDF for UTF-8 print reports, TCPDF for its structured page-flow API, and a browser renderer when modern CSS fidelity matters.

Why does a table row not split?

Dompdf documents rows as non-pageable. Shorten or split the row, or choose a renderer with the pagination behavior you need.

How do I preserve non-ASCII values?

Keep the complete pipeline UTF-8 and use a font containing every required glyph.