Convert HTML to PDF in PHP Using FPDF

Generating a PDF from HTML is one of those tasks that sounds straightforward until you’re actually doing it. FPDF is a pure PHP class that lets you build PDF documents without any server side extension, no PDFlib, no external binaries. The trade off is that it doesn’t render HTML the way a browser does. It gives you a drawing API, and you bridge the gap yourself.
This guide shows you how to do that correctly, explains what the code is actually doing.
What FPDF Actually Does (and Doesn’t Do)
FPDF generates PDF content by drawing text, lines, rectangles, and images onto a virtual page. It does not contain an HTML or CSS parser. When you see code that “converts HTML to PDF with FPDF,” what’s really happening is that someone wrote a lightweight parser, usually a regex/string split approach that reads a small subset of HTML tags and translates them into FPDF drawing commands.
This means:
- Basic tags like
<b>,<i>,<u>,<a>,<br>,<p>, and<hr>can be supported. - Layout tags like
<div>,<span>, and<table>require extra work to handle properly. - CSS styling is largely ignored unless you write parsing code for it.
- Complex HTML pages cannot be faithfully reproduced with FPDF alone.
If you need full CSS support, responsive layout, or Bootstrap styled content in your PDF, than FPDF may not be the right fit.
We have a separate guide on converting HTML to PDF using Dompdf if Dompdf is a better fit for your project.
Steps to Convert HTML to PDF Document in PHP with fpdf
Step 1: Download FPDF
The official FPDF library is maintained at fpdf.org. The current release as of this writing is FPDF 1.9 (released May 2026), which added WebP image support, switched font definition files to JSON format, and cleared deprecation notices for PHP 8.5.
You can install it via Composer:
composer require setasign/fpdf
Or download the zip directly from fpdf.org/en/download.php and extract it into your project directory.
If you install via Composer, autoloading handles the include automatically. For manual installs, you’ll require the file directly (shown below).
Step 2: Understand the Project Structure
For this tutorial you’ll have three files:
/your-project/ ├── fpdf/ │ └── fpdf.php ← the FPDF library (manual install) ├── HtmlToPdf.php ← your custom HTML-parsing class ├── form.html ← the input form └── generate-pdf.php ← the script that builds and outputs the PDF
Step 3: Create the HTML-Parsing Class
FPDF doesn’t parse HTML, so you extend the FPDF class and add that capability yourself. The approach below handles the most common inline tags (<b>, <i>, <u>, <a>, <br>, <p>, <hr>) and table rows.
Save this as HtmlToPdf.php:
<?php
require_once __DIR__ . '/fpdf/fpdf.php'; // adjust path if using Composer
class HtmlToPdf extends FPDF
{
protected int $bold = 0;
protected int $italic = 0;
protected int $underline = 0;
protected string $href = '';
protected string $align = '';
/**
* Parse a limited subset of HTML and write it to the PDF.
* Only inline tags and simple block tags are supported.
*/
public function writeHtml(string $html): void
{
// Normalize line breaks so the regex doesn't miss tags split across lines
$html = str_replace("\n", ' ', $html);
// Split on HTML tags, capturing the tags themselves
$tokens = preg_split('/<(.+?)>/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($tokens as $index => $token) {
if ($index % 2 === 0) {
// Plain text — render it
if ($this->href !== '') {
$this->putLink($this->href, $token);
} elseif ($this->align === 'center') {
$this->Cell(0, 5, $token, 0, 1, 'C');
} else {
$this->Write(5, $token);
}
} else {
// HTML tag
if ($token[0] === '/') {
$this->closeTag(strtoupper(ltrim($token, '/')));
} else {
// Parse tag name and attributes
$parts = explode(' ', $token, 2);
$tagName = strtoupper($parts[0]);
$attrs = [];
if (isset($parts[1])) {
preg_match_all('/(\w+)=["\']([^"\']*)["\']/', $parts[1], $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs[strtoupper($match[1])] = $match[2];
}
}
$this->openTag($tagName, $attrs);
}
}
}
}
protected function openTag(string $tag, array $attrs): void
{
match ($tag) {
'B', 'I', 'U' => $this->setStyle($tag, true),
'A' => $this->href = $attrs['HREF'] ?? '',
'BR' => $this->Ln(5),
'P' => $this->align = strtolower($attrs['ALIGN'] ?? ''),
'HR' => $this->drawHr($attrs),
default => null,
};
}
protected function closeTag(string $tag): void
{
match ($tag) {
'B', 'I', 'U' => $this->setStyle($tag, false),
'A' => $this->href = '',
'P' => $this->align = '',
default => null,
};
}
protected function setStyle(string $tag, bool $enable): void
{
// Track nesting depth for each style
$this->$tag += $enable ? 1 : -1;
$style = '';
foreach (['B', 'I', 'U'] as $s) {
if ($this->$s > 0) {
$style .= $s;
}
}
$this->SetFont('', $style);
}
protected function putLink(string $url, string $text): void
{
$this->SetTextColor(0, 0, 204);
$this->setStyle('U', true);
$this->Write(5, $text, $url);
$this->setStyle('U', false);
$this->SetTextColor(0);
}
protected function drawHr(array $attrs): void
{
$width = isset($attrs['WIDTH'])
? (float) $attrs['WIDTH']
: $this->w - $this->lMargin - $this->rMargin;
$this->Ln(2);
$x = $this->GetX();
$y = $this->GetY();
$this->SetDrawColor(180, 180, 180);
$this->SetLineWidth(0.4);
$this->Line($x, $y, $x + $width, $y);
$this->SetLineWidth(0.2);
$this->SetDrawColor(0);
$this->Ln(2);
}
}What the important parts do:
writeHtml()splits the HTML string into alternating text/tag tokens usingpreg_split. Even-indexed tokens are plain text; odd-indexed tokens are tag contents.setStyle()tracks bold, italic, and underline nesting with counters rather than booleans, so<b><b>text</b></b>doesn’t accidentally turn bold off on the first closing tag.putLink()sets the text color to blue, applies underline, uses FPDF’s built-inWrite()with a URL parameter (which creates a clickable PDF link), then resets styling.matchexpressions (PHP 8+) replace the originalif/elsechains with cleaner syntax. If you’re on PHP 7, replace them withswitchorifchains.
Step 4: Create the Input Form
This is a basic HTML form. Save it as form.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Generate PDF</title>
</head>
<body>
<h2>Customer Invoice Details</h2>
<form method="post" action="generate-pdf.php">
<label>Name: <input type="text" name="name" required></label><br><br>
<label>Email: <input type="email" name="email" required></label><br><br>
<label>Phone: <input type="text" name="phone" required></label><br><br>
<label>Amount: <input type="text" name="amount" required></label><br><br>
<button type="submit">Generate PDF</button>
</form>
</body>
</html>Step 5: Generate and Output the PDF
Save this as generate-pdf.php:
<?php
require_once __DIR__ . '/HtmlToPdf.php';
// Sanitize POST input — never trust raw user data
$name = htmlspecialchars(trim($_POST['name'] ?? ''), ENT_QUOTES, 'UTF-8');
$email = htmlspecialchars(trim($_POST['email'] ?? ''), ENT_QUOTES, 'UTF-8');
$phone = htmlspecialchars(trim($_POST['phone'] ?? ''), ENT_QUOTES, 'UTF-8');
$amount = htmlspecialchars(trim($_POST['amount'] ?? ''), ENT_QUOTES, 'UTF-8');
if ($name === '' || $email === '') {
http_response_code(400);
exit('Required fields are missing.');
}
$pdf = new HtmlToPdf();
$pdf->SetCreator('CodeFixUp.com');
$pdf->SetAuthor('CodeFixUp');
$pdf->SetTitle('Customer Invoice');
$pdf->SetAutoPageBreak(true, 15);
$pdf->AliasNbPages(); // enables {nb} placeholder for total page count
$pdf->AddPage();
// ── Header ────────────────────────────────────────────────────────────────
$pdf->SetFont('Arial', 'B', 16);
$pdf->Cell(0, 10, 'Customer Invoice', 0, 1, 'C');
$pdf->Ln(4);
// ── Horizontal rule ───────────────────────────────────────────────────────
$pdf->writeHtml('<HR>');
// ── Invoice metadata ──────────────────────────────────────────────────────
$invoiceDate = date('Y-m-d');
$invoiceNumber = 'INV-' . strtoupper(bin2hex(random_bytes(3))); // e.g. INV-A3F91C
$pdf->SetFont('Arial', '', 10);
$pdf->writeHtml("<p>Invoice #: <b>{$invoiceNumber}</b><BR>Date: <b>{$invoiceDate}</b></p>");
$pdf->Ln(4);
// ── Customer details ──────────────────────────────────────────────────────
$pdf->SetFont('Arial', 'B', 11);
$pdf->Cell(0, 8, 'Bill To', 0, 1);
$pdf->SetFont('Arial', '', 10);
$pdf->writeHtml("
<p>
Name: <b>{$name}</b><BR>
Email: <b>{$email}</b><BR>
Phone: <b>{$phone}</b>
</p>
");
$pdf->Ln(4);
// ── Amount row ────────────────────────────────────────────────────────────
$pdf->SetFont('Arial', 'B', 11);
$pdf->Cell(0, 8, 'Payment Summary', 0, 1);
$pdf->SetFont('Arial', '', 10);
$pdf->SetFillColor(240, 240, 240);
$pdf->Cell(100, 8, 'Description', 1, 0, 'L', true);
$pdf->Cell(0, 8, 'Amount', 1, 1, 'R', true);
$pdf->Cell(100, 8, 'Services rendered', 1, 0, 'L');
$pdf->Cell(0, 8, $amount, 1, 1, 'R');
$pdf->Ln(6);
// ── Footer note ───────────────────────────────────────────────────────────
$pdf->SetFont('Arial', 'I', 9);
$pdf->writeHtml('<p>Thank you for your business. Questions? Visit <a href="https://www.codefixup.com">CodeFixUp.com</a></p>');
// ── Output ────────────────────────────────────────────────────────────────
// 'I' streams to the browser. Use 'D' to force a download, 'F' to save to disk.
$pdf->Output('I', 'invoice.pdf');What the important parts do:
htmlspecialchars()encodes characters like<,>, and"before they go into the PDF, preventing injected HTML tags from corrupting the output. This matters even in a PDF context.SetAutoPageBreak(true, 15)tells FPDF to automatically add a new page when content gets within 15mm of the bottom margin.AliasNbPages()lets you use{nb}anywhere in the document as a placeholder for the total page count — useful for headers/footers like “Page 1 of {nb}”.Output('I', 'invoice.pdf')streams the PDF directly to the browser. The first argument controls destination:'I'= inline (view in browser),'D'= download attachment,'F'= save to file,'S'= return as string.- Table cells are drawn using FPDF’s
Cell()directly rather than throughwriteHtml(), because nativeCell()gives precise column control. ThewriteHtml()approach above doesn’t handle<table>layouts.
Step 6: Test Your Output
- Open
form.htmlin a browser. - Fill in the fields and submit.
- The browser should display or prompt to download a PDF.
If nothing appears or you see garbled output, check the troubleshooting section below.
Read this : Simple Ways to Embed PDF in Html Page
Adding a Header and Footer to Every Page
FPDF has built-in Header() and Footer() methods you can override. They run automatically on AddPage() and at the end of each page.
class HtmlToPdf extends FPDF
{
// ... (existing code above)
public function Header(): void
{
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 8, 'CodeFixUp.com — Invoice', 0, 1, 'C');
$this->SetDrawColor(200, 200, 200);
$this->SetLineWidth(0.3);
$this->Line($this->lMargin, $this->GetY(), $this->w - $this->rMargin, $this->GetY());
$this->Ln(4);
}
public function Footer(): void
{
$this->SetY(-15); // 15mm from the bottom
$this->SetFont('Arial', 'I', 8);
$this->Cell(0, 10, 'Page ' . $this->PageNo() . ' of {nb}', 0, 0, 'C');
}
}The {nb} placeholder is replaced with the total page count when you called AliasNbPages() earlier.
Troubleshooting
Headers already sent / blank page
FPDF sends HTTP headers when you call Output(). If anything, even a single space or BOM character was echoed before that call, PHP will throw a “headers already sent” error. Make sure your PHP files have no whitespace before <?php and no output before Output().
Garbled or binary output in the browser
This usually means something was echoed before Output('I', ...). Check for echo, print, var_dump, or print_r calls earlier in the file, and check that your require/include files don’t echo anything.
PDF appears but text is missing
FPDF requires a font to be set before you write any text. If you forgot SetFont(), text won’t appear. Always call SetFont() after AddPage() or inside Header().
Warning: Cannot modify header information
Same root cause as “headers already sent.” Check for BOM markers in your files, some Windows text editors add a UTF-8 BOM (EF BB BF) invisibly. Save all files as UTF-8 without BOM.
Font definition file not found (FPDF 1.9)
FPDF 1.9 changed font definition files from PHP format to JSON. If you upgrade from an older version and have custom fonts, you’ll need to regenerate your font definition files using the updated MakeFont utility bundled with FPDF 1.9. Built-in core fonts (Arial, Helvetica, Times, Courier, Symbol, ZapfDingbats) are unaffected.
Images not loading
FPDF’s Image() method loads images from the filesystem, not from URLs. Pass an absolute server path ($_SERVER['DOCUMENT_ROOT'] . '/images/logo.png'), not a URL. For WebP images, the GD extension must be loaded (new in FPDF 1.9).

Hi,
It is quite simple, appreciate this useful post.
Thanks