Dompdf turns a controlled HTML document into a PDF inside a PHP application. It is useful for invoices, booking confirmations, delivery notes and reports whose layout can be expressed with conservative HTML and CSS. A successful implementation needs more than a call to a rendering method: money must be calculated consistently, assets must resolve on the server, page breaks must survive long content, and the renderer must not become a way to read private files or contact arbitrary URLs.

This guide builds an original invoice example, then explains how to extend and operate it. The examples target PHP 8.2 or later and Dompdf 3.1.6 or a later compatible 3.x release. Use a maintained PHP release with current patches. The invoice data is fictional; the tax rate and rounding policy are teaching examples, not a specification for legally compliant invoicing.

What you will build

You will create a small command-line project that loads trusted invoice data, builds an escaped HTML template and saves a PDF in a private directory. Keeping the first example outside an HTTP request makes it easier to distinguish rendering problems from authentication, buffering and response-header problems.

By the end, you should be able to:

1. Understand the rendering model

Dompdf parses HTML and CSS, computes a paginated layout and writes PDF drawing instructions. It does not run your page as a normal web browser would. A React component tree, a chart that appears only after JavaScript executes, or a layout built entirely around browser CSS features cannot simply be handed to it and expected to render identically.

Use server-rendered HTML with explicit data. Prefer a straightforward document structure, predictable widths and CSS supported by the engine. Tables suit tabular invoice data. Avoid using a large table as the container for the entire document: it makes pagination harder and can turn one oversized row into a rendering failure.

If the business requirement is to print an existing browser interface with its JavaScript and modern layout intact, evaluate a browser-based PDF workflow separately. For an invoice template maintained alongside a PHP backend, Dompdf is often a practical fit.

See the Dompdf project (opens in a new tab) and its CSS compatibility reference (opens in a new tab) for the supported rendering features.

2. Install with Composer and verify PHP

Create a project directory outside the public document root. This example uses the following structure:

invoice-demo/
  composer.json
  composer.lock
  vendor/
  assets/
    fonts/
  var/
    font-cache/
    tmp/
    invoices/
  invoice.php

Run these commands from that directory:

php -v
php -m
composer require "dompdf/dompdf:^3.1.6"
composer show dompdf/dompdf
composer check-platform-reqs
composer audit

The version constraint accepts compatible 3.x updates; commit the lock file so development and production install the same resolved dependencies. Deploy with Composer's install workflow, rather than resolving new versions on each server. Dompdf 3.1.6 includes security fixes affecting resource handling, so use a patched release rather than copying an old tutorial's dependency version. Recheck advisories before deploying.

Dompdf requires PHP's DOM and Mbstring extensions. Image processing may require GD, depending on the asset and operation. A successful Composer installation on a developer machine does not prove that the production PHP process has the same extensions. The command-line PHP binary and Apache or PHP-FPM can load different configuration files. Check both environments when PDF generation moves behind a web endpoint.

If an installation fails, fix the actual extension or PHP-version mismatch. Do not use a platform-requirement override as a substitute for installing a working runtime.

References: Dompdf requirements (opens in a new tab), 3.1.6 release notes (opens in a new tab), and Composer's installation and lock-file workflow (opens in a new tab).

3. Build a complete invoice generator

Save the following as invoice.php. Run it from the command line with php invoice.php. It creates the required local directories and writes var/invoices/INV-2026-0042.pdf.

The asset directory is deliberately separate from both application code and private output. No image or custom font is required for this first run: it uses Dompdf's bundled DejaVu Sans font.

<?php
declare(strict_types=1);

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

use Dompdf\Dompdf;
use Dompdf\Options;

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

function money(int $minor): string
{
    if ($minor < 0) {
        throw new InvalidArgumentException('Negative amounts are not supported in this demo.');
    }
    $whole = intdiv($minor, 100);
    $fraction = str_pad((string) ($minor % 100), 2, '0', STR_PAD_LEFT);
    return 'EUR ' . number_format($whole, 0, '.', ',') . '.' . $fraction;
}

$base = __DIR__;
foreach (['assets', 'var/tmp', 'var/font-cache', 'var/invoices'] as $relative) {
    $directory = $base . '/' . $relative;
    if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
        throw new RuntimeException('Cannot create a required directory.');
    }
}
$assetRoot = realpath($base . '/assets');
if ($assetRoot === false) {
    throw new RuntimeException('Asset directory cannot be resolved.');
}

$invoice = [
    'number' => 'INV-2026-0042',
    'issued' => '2026-10-16',
    'seller' => 'Harbor Studio — Demo Company',
    'customer' => 'Northwind Learning — Demo Customer',
    'email' => 'billing@example.test',
    'lines' => [
        ['description' => 'Workshop facilitation', 'quantity' => 2, 'unitMinor' => 12500],
        ['description' => 'Printed learning packs', 'quantity' => 3, 'unitMinor' => 3500],
    ],
];

$subtotal = 0;
$rows = '';
foreach ($invoice['lines'] as $line) {
    $quantity = $line['quantity'];
    $unitMinor = $line['unitMinor'];
    if (!is_int($quantity) || $quantity < 1 || $quantity > 10000
        || !is_int($unitMinor) || $unitMinor < 0 || $unitMinor > 1000000) {
        throw new InvalidArgumentException('Invalid invoice line.');
    }
    $lineMinor = $quantity * $unitMinor;
    if ($subtotal > 100000000 - $lineMinor) {
        throw new InvalidArgumentException('Demo invoice total exceeds its limit.');
    }
    $subtotal += $lineMinor;
    $rows .= '<tr><td>' . escapeHtml($line['description']) . '</td>'
        . '<td class="numeric">' . escapeHtml($quantity) . '</td>'
        . '<td class="numeric">' . escapeHtml(money($unitMinor)) . '</td>'
        . '<td class="numeric">' . escapeHtml(money($lineMinor)) . '</td></tr>';
}

// Teaching policy: one 20% tax, rounded half up on the invoice subtotal.
$taxPercent = 20;
$tax = intdiv($subtotal * $taxPercent + 50, 100);
$total = $subtotal + $tax;

$number = escapeHtml($invoice['number']);
$issued = escapeHtml($invoice['issued']);
$seller = escapeHtml($invoice['seller']);
$customer = escapeHtml($invoice['customer']);
$email = escapeHtml($invoice['email']);
$subtotalText = escapeHtml(money($subtotal));
$taxText = escapeHtml(money($tax));
$totalText = escapeHtml(money($total));

$html = <<<HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Invoice {$number}</title>
<style>
@page { margin: 20mm 15mm 22mm; }
body { font-family: "DejaVu Sans", sans-serif; font-size: 10pt; color: #182230; }
h1 { font-size: 23pt; margin: 0 0 4mm; }
.meta { margin-bottom: 7mm; line-height: 1.6; }
.items { width: 100%; border-collapse: collapse; table-layout: fixed; }
.items th, .items td { padding: 3mm 2mm; border-bottom: 1px solid #d8dde3; }
.items th { background: #edf1f5; text-align: left; }
.items thead { display: table-header-group; }
.items tr { page-break-inside: avoid; }
.items .description { width: 46%; }
.items .quantity { width: 10%; }
.numeric { text-align: right; }
.summary { width: 58%; margin: 6mm 0 0 auto; border-collapse: collapse; }
.summary td { padding: 2mm; }
.summary .total { font-weight: bold; border-top: 1px solid #182230; }
.note { margin-top: 8mm; font-size: 9pt; color: #475467; }
</style>
</head>
<body>
<h1>Invoice</h1>
<div class="meta">
<strong>{$seller}</strong><br>
Invoice: {$number}<br>
Issued: {$issued}<br>
Bill to: {$customer}<br>
Contact: {$email}
</div>
<table class="items">
<thead><tr><th class="description">Description</th><th class="quantity">Qty</th>
<th>Unit price</th><th>Amount</th></tr></thead>
<tbody>{$rows}</tbody>
</table>
<table class="summary">
<tr><td>Subtotal</td><td class="numeric">{$subtotalText}</td></tr>
<tr><td>Illustrative tax (20%)</td><td class="numeric">{$taxText}</td></tr>
<tr class="total"><td>Total</td><td class="numeric">{$totalText}</td></tr>
</table>
<p class="note">Fictional demonstration invoice. Thank you for learning with us.</p>
</body>
</html>
HTML;

$options = new Options();
$options->set('chroot', $assetRoot);
$options->set('tempDir', $base . '/var/tmp');
$options->set('fontDir', $base . '/var/font-cache');
$options->set('fontCache', $base . '/var/font-cache');
$options->set('isRemoteEnabled', false);
$options->set('isPhpEnabled', false);
$options->set('isJavascriptEnabled', false);

$dompdf = new Dompdf($options);
$dompdf->setPaper('A4', 'portrait');
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->render();

$canvas = $dompdf->getCanvas();
$font = $dompdf->getFontMetrics()->getFont('DejaVu Sans', 'normal');
if ($font === null) {
    throw new RuntimeException('Page-number font is unavailable.');
}
$canvas->page_text(42, 810, 'Page {PAGE_NUM} of {PAGE_COUNT}', $font, 9, [0.3, 0.3, 0.3]);

$pdf = $dompdf->output();
$destination = $base . '/var/invoices/INV-2026-0042.pdf';
$written = file_put_contents($destination, $pdf, LOCK_EX);
if ($written === false || $written !== strlen($pdf)) {
    throw new RuntimeException('PDF output was not fully written.');
}
echo 'Created invoice: ' . basename($destination) . PHP_EOL;

Read the pipeline before changing it

The calculations happen before HTML is constructed. This prevents a display template from becoming the place where totals depend on formatting or a user's browser. Each variable inserted as text is escaped. The $rows variable is trusted HTML assembled by the application; each value inside it has already passed through the escaping function.

The subtotal is 35,500 cents: two units at 12,500 plus three at 3,500. The illustrative tax is 7,100 cents, producing a total of 42,600 cents, displayed as EUR 426.00. Currency formatting is presentation only. The money() helper divides an integer into its whole and fractional parts instead of using floating-point arithmetic to compute prices.

The limits on quantities, unit values and total are deliberately small enough for this demo's arithmetic. A production billing service also needs rules for refunds, fractional quantities, discounts, multiple tax rates and currency precision. Decide whether tax is rounded per line or per invoice, and persist the result with the invoice rather than recalculating historical documents under a future rule.

For application input, validate the complete structure before reaching this code. Load invoice records through an authorized server-side query. Do not trust a submitted total, customer identity or price simply because a field arrived as JSON.

Finally, render() performs the expensive layout work; output() returns bytes. A PDF is a binary response or file, not HTML that can be mixed with debugging output. The usage reference (opens in a new tab) documents the rendering lifecycle. PHP's htmlspecialchars documentation (opens in a new tab) explains the text escaping used here.

4. Load local images reliably

A relative browser URL such as /images/logo.png refers to a web origin when viewed in a browser. In a command-line renderer there may be no corresponding origin. Resolve local assets to a known absolute location inside the asset directory.

Place a small, valid PNG at assets/logo.png. The following helper converts an existing absolute local path to a file URI on Unix-like systems and Windows. Add it to the generator before assembling the HTML, then create a fixed asset URI:

<?php
function localFileUri(string $absolutePath): string
{
    $normalized = str_replace('\\', '/', $absolutePath);
    if (str_starts_with($normalized, '//')) {
        throw new InvalidArgumentException('Network share paths are outside this example.');
    }
    $encoded = implode('/', array_map('rawurlencode', explode('/', $normalized)));
    if (preg_match('/^[A-Za-z]:\//', $normalized) === 1) {
        $encoded = substr($normalized, 0, 2) . substr($encoded, 4);
        return 'file:///' . $encoded;
    }
    if (!str_starts_with($normalized, '/')) {
        throw new InvalidArgumentException('An absolute local path is required.');
    }
    return 'file://' . $encoded;
}

// Uses $assetRoot and escapeHtml() from the complete generator.
$logoPath = realpath($assetRoot . '/logo.png');
if ($logoPath === false) {
    throw new RuntimeException('The demo logo is missing.');
}
$logoMarkup = '<img src="' . escapeHtml(localFileUri($logoPath))
    . '" alt="Harbor Studio" style="width: 40mm; height: auto;">';

Insert $logoMarkup into the trusted HTML template where the logo belongs. The path is derived from a fixed application filename; no request parameter selects it. The helper is a URI formatter, not an authorization check. For several selectable assets, map a small set of business identifiers to approved files and verify their resolved paths remain inside the approved directory, including after symbolic links are resolved.

The directory configured as chroot is the local resource boundary for the renderer. Keep it narrow. Setting it to the application's parent directory merely to make an image work can expose configuration, uploads or private documents to resource resolution.

Use an image size appropriate for its printed dimensions. A tiny company logo does not need a huge camera image. If images are supplied by users, validate and normalize them through a separate upload pipeline; a filename extension is not evidence of a valid image. For untrusted SVG content, rasterization in an isolated, controlled pipeline is often simpler than exposing a complex vector input directly to the renderer.

See the resource options in the versioned source (opens in a new tab) and PHP's rawurlencode reference (opens in a new tab).

5. Fonts, Unicode and language support

A UTF-8 HTML document and a suitable font solve different problems. Encoding tells the parser which characters are present. The font must also contain glyphs for those characters. A missing square in a PDF can therefore be a font coverage problem even when the source string is valid UTF-8.

The main example uses DejaVu Sans, which is a useful starting point for many European-language documents. Do not assume that it covers every writing system, emoji sequence or complex shaping requirement. Test representative names, addresses, symbols and punctuation from your actual audience. A sample containing only plain ASCII can conceal production failures.

For a custom font, obtain a licensed TrueType font that allows the intended embedding and distribution. Put the regular and bold files in assets/fonts/. Resolve their paths with the same local URI helper used for images, escape the URIs for the quoted template context, and insert them into trusted CSS:

<?php
// Add before the $html heredoc; requires the previous helper functions.
$regularPath = realpath($assetRoot . '/fonts/InvoiceSans-Regular.ttf');
$boldPath = realpath($assetRoot . '/fonts/InvoiceSans-Bold.ttf');
if ($regularPath === false || $boldPath === false) {
    throw new RuntimeException('Copy both licensed font files into assets/fonts first.');
}
$regularUri = escapeHtml(localFileUri($regularPath));
$boldUri = escapeHtml(localFileUri($boldPath));

Insert these declarations at the beginning of the existing template's style element. The variables are expanded by the PHP heredoc:

@font-face {
  font-family: 'InvoiceSans';
  font-style: normal;
  font-weight: normal;
  src: url('{$regularUri}') format('truetype');
}
@font-face {
  font-family: 'InvoiceSans';
  font-style: normal;
  font-weight: bold;
  src: url('{$boldUri}') format('truetype');
}
body {
  font-family: 'InvoiceSans', 'DejaVu Sans', sans-serif;
}

Use real files for each weight you depend on. A custom body font does not automatically change the separate Canvas font used for page numbers. Leave that footer in DejaVu Sans or explicitly retrieve the registered custom family after rendering.

The registered-font directory, font cache and temporary directory must be writable by the rendering process, while remaining inaccessible as public downloads. The example places both registered fonts and their cache under var/font-cache, so custom font registration does not require writing into vendor/. Do not solve a permissions issue by making the whole application writable. Keep fonts under application control and do not let request data supply font URLs or CSS declarations. The font registration implementation (opens in a new tab) shows how these files are stored.

Font support and document layout limits are described in the Dompdf documentation (opens in a new tab).

6. CSS, pagination and long invoices

Design for a physical page early. A4 portrait has fixed dimensions; the usable rectangle is smaller after margins. A layout that looks comfortable in a wide browser window may leave an unusable amount of space for the description column in print.

The sample uses millimetres for page geometry and points for typography. The Canvas page-number coordinates are points, measured from the top-left corner of the page. The hard-coded 810 footer position is chosen for A4 portrait. Recalculate it if you change the page size, orientation or bottom margin.

Choose layout features deliberately

Requirement Practical Dompdf approach What to verify
Invoice line items A real table with thead and tbody Column widths and repeated headings
Short totals block A separate compact table Position when the preceding table spans pages
Side-by-side metadata Simple supported table or block layout Long company names and addresses
Page numbering Canvas text after rendering Footer coordinates and reserved margin
New report section page-break-before: always No unnecessary blank first page
Long narrative text Flowing paragraphs outside table rows Natural splitting over pages

Dompdf does not support browser Flexbox or CSS Grid as substitutes for these layouts. Avoid transporting an entire application stylesheet into the PDF template. A small dedicated stylesheet is easier to inspect, faster to adjust and less likely to depend on unsupported behavior.

Know what a page-break rule cannot fix

A table row must fit on a page. Setting page-break-inside: avoid helps express a preference, but it cannot make an extremely tall row fit into the available area. A row containing a two-page product description needs a different document structure, not a stronger CSS declaration.

Keep concise descriptions in the line-item table. Move extended terms, technical notes or service reports into paragraphs after it. If the business requirement permits splitting a long logical item into several rows, do so explicitly and preserve the relationship between those rows. Do not silently discard text to make the page look correct.

Test with at least these fixtures:

  1. A short invoice with two lines.
  2. Enough normal lines to span several pages.
  3. A long customer address and a long but valid product description.
  4. Unicode names and currency formatting representative of real customers.
  5. A near-limit line count, handled within a defined resource budget.

Inspect the final PDF rather than relying on a browser preview of the HTML. Check repeated headings, clipped text, overlapping footers, totals placement and any final page containing only a few lines. The compatibility table (opens in a new tab) is useful when narrowing a CSS problem.

7. Keep resource access under application control

Think of rendering as a server process reading a document that can reference other resources. An image, stylesheet or font can introduce a file read or network request. Escaping customer names alone does not make arbitrary HTML safe to render.

Start with a trusted template

The invoice template belongs to the application. Customers supply validated text fields, never the complete HTML document, arbitrary CSS, a document URL, or an executable template fragment. Escape text at insertion time and use context-appropriate handling for the rare dynamic URL or attribute.

For example, the literal customer name A&B <Training> should appear as readable text in the PDF. It should not create an HTML element. Conversely, a server-owned table row is intended HTML; escaping the entire assembled template would print angle brackets instead of an invoice. Preserve that distinction between trusted structure and untrusted values.

If a product truly needs user-authored rich text, design a separate sanitization policy with an explicit allowlist and independent resource restrictions. Do not pass raw editor output into the renderer just because it looked harmless in a browser.

Keep the three execution-related options explicit

Calling $canvas->page_text() from trusted server-side PHP does not require enabling embedded document PHP. Keep the setting disabled when adding page numbers.

Treat local and remote boundaries separately

chroot restricts allowed local resources. It does not provide a network allowlist and does not replace operating-system isolation. A remote-host restriction does not protect a private file on disk. Keep the rendering user's permissions narrow, keep secrets outside the asset tree, and store generated customer documents outside public hosting directories.

If a fixed remote resource is indispensable, consider fetching it ahead of rendering through a controlled application service and storing a validated local copy. That makes rendering independent of the remote server's latency and availability. Cache only resources you are allowed to retain, and refresh them through a known process.

If you enable remote loading, Dompdf provides an allowedRemoteHosts option. An approved hostname list is one layer, not a complete SSRF defense: redirects, DNS resolution, internal address ranges and network egress still need attention. Never enable unrestricted remote access to accommodate a user-provided URL. An attacker-controlled reference can target infrastructure that is reachable from the server even when it is unreachable from the attacker's browser.

Limit the work as well as the permissions

An authenticated request can still consume too much memory or CPU. Bound line counts, image dimensions, input bytes and execution time. Authorize access before starting expensive work. For costly documents, enqueue a job with concurrency limits and enforce a retention policy for the resulting private files.

Log an internal job identifier, template version, elapsed time, page count and outcome where available. Avoid logging full customer HTML, invoice addresses or private asset paths. Return a generic failure to the requester and keep diagnostic detail in controlled server logs.

Review the official security guidance (opens in a new tab) and the release notes (opens in a new tab) when maintaining a production integration.

8. Serve a PDF through an authenticated endpoint

Once the CLI generator works, integrate it into an authorized application service. The sequence should be: authenticate the requester, authorize the invoice record, load its trusted data, generate or retrieve the private PDF, then return a binary response.

An invoice identifier in a URL is a lookup key, not permission to download that invoice. Check ownership or an explicit staff entitlement before retrieving bytes. A cached PDF needs the same access check as a newly generated one.

For a plain PHP endpoint, the following is the response fragment after those checks and after obtaining $pdf as a byte string. The complete CLI generator must not be included unchanged in an endpoint because its final echo prints a status message. Remove that CLI message and use exactly one binary-response mechanism:

<?php
// Response fragment: $pdf must already contain an authorized PDF document.
if (headers_sent()) {
    throw new RuntimeException('Response output started before PDF headers.');
}
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="invoice.pdf"');
header('Cache-Control: private, no-store');
header('X-Content-Type-Options: nosniff');
echo $pdf;
exit;

Use the framework's binary-response abstraction if it manages responses itself. Do not mix that mechanism with Dompdf's stream() or direct echo calls. With direct streaming, a UTF-8 byte-order mark, an accidental space before the PHP opening tag, a warning printed to the response or a debug dump can corrupt the download. Configure production errors to be logged rather than displayed in document responses.

The fixed response filename avoids putting unvalidated input into a header. If filenames must be customized, build them through the framework's safe content-disposition support. Keep PDF output out of public shared caches; changing a filename does not protect a document from an unauthorized request.

For a generated-file workflow, use an application-owned opaque storage identifier. The fixed demo destination deliberately replaces its one example file; it is not a multi-customer storage scheme. Real jobs need collision-free destinations, controlled file permissions and a reliable way to expose only complete files to readers, such as writing to a temporary private file and atomically moving it into place.

9. Diagnose common failures systematically

Start with a minimal template and reproduce the failure using the same PHP runtime, Dompdf version and resource paths as the failing job. Change one variable at a time. A random combination of remote access, broad directory permissions and bigger memory limits can hide the cause while increasing exposure.

Symptom Likely area First useful check
Composer refuses to install Runtime or extensions PHP version, DOM, Mbstring and platform requirements
Logo is absent Path or resource permissions Resolved local file URI, approved asset root and file readability
Local image works but remote image fails Remote loading or network Whether remote access is intentionally disabled; prefer a local copy
Accented text is wrong Encoding or font coverage Source UTF-8, HTML charset, selected font and representative glyphs
Browser layout changes in PDF Unsupported or different CSS Remove framework CSS and test a small supported layout
A long row overflows or fails Pagination constraints Whether the row can fit on one physical page
PDF opens as damaged Response bytes Warnings, debug output, BOM and mixed streaming methods
Font loading fails only on server Paths or writable cache Installed files, license/deployment process and process permissions
Memory rises sharply Input size or rendering complexity Image pixels, large tables, repeated resources and bounded line count
A customer receives another PDF Application authorization or cache key Record ownership, private storage mapping and cache isolation

A worked debugging example

Suppose an invoice works locally but its production logo is missing. The first temptation is to enable remote loading. Instead, inspect how the logo is referenced. The browser page may use /assets/logo.png, while the worker has no web origin and the deployed logo actually lives under a different absolute directory.

Resolve the deployed asset root, confirm the file exists there, and test readability as the rendering user. Generate the URI from that fixed file. Check that the file falls within the configured chroot. Then render a minimal document containing only the logo. If that succeeds, restore the invoice template. If it fails, investigate the image format, decoder availability and file content before changing CSS.

This sequence separates path discovery, permission checks, image decoding and layout. It also avoids using a production hostname as a workaround for a local filesystem problem.

A second example: totals move onto a new page

An invoice with five lines looks correct, while one with forty lines puts the totals on a new page. This can be legitimate pagination. First verify that nothing is clipped or overlapping. If the business requires a heading next to the totals, group only that small heading and totals block, then test its page-break behavior. Do not attempt to keep the entire invoice table and summary together; that forces an impossible requirement for long invoices.

If one line contains a huge paragraph, shorten the table's description to a meaningful summary and move the full text into a flowing appendix. The original data must remain available and clearly associated with the relevant item.

The official troubleshooting guide (opens in a new tab) offers additional starting points.

10. Performance and repeatable document generation

Separate stable business data from rendering. An issued invoice should refer to an immutable snapshot of line descriptions, prices, tax results and customer details appropriate to that issuance. Generating the same invoice later from today's product catalog can silently change its meaning.

Use a new Dompdf instance for each document. Reusing a renderer across unrelated documents risks carrying rendering state between jobs. Reuse trusted templates, immutable data and approved local assets instead.

For moderate traffic, a synchronous endpoint may be sufficient if its maximum work is small and bounded. For long reports or bursts of invoices, a queue keeps web requests responsive and lets you set a sensible number of concurrent renderers. More workers are not automatically faster when they compete for memory.

Measure realistic fixtures before changing limits. Record generation time and peak memory for short, medium and maximum-size documents. Reduce oversized images and unnecessary HTML first. Repeated full-resolution images and very large tables can dominate costs even when the final PDF file looks small.

If you cache generated documents, key them by the correct business snapshot and template version. Keep the cache private and enforce authorization at download time. Define expiry and deletion rules rather than accumulating customer PDFs indefinitely.

11. Knowledge check: 12 original MCQs with explanations

Choose one answer for each question. These questions test the implementation decisions in this guide; they are original practice material, not an official certification exam.

Question 1: Choosing the renderer

A report is rendered by React, depends on client-side chart execution and uses CSS Grid throughout. What should the team do first?

A. Enable isJavascriptEnabled and assume browser-equivalent rendering. B. Assess a browser-based PDF workflow or build a separate supported server-side document template. C. Rename the HTML file to .pdf. D. Increase the font cache size.

Answer: B. The rendering model must match the document requirements. A is incorrect because the PDF JavaScript option does not provide a browser runtime for React. C changes a filename, not the content format. D cannot supply missing layout or scripting capabilities.

Question 2: Money calculations

Why does the demo keep prices in integer cents until formatting?

A. Integers automatically implement every country's invoice rules. B. Dompdf cannot print decimal numbers. C. It keeps the chosen arithmetic policy separate from display formatting and avoids binary floating-point price calculations. D. It eliminates the need to validate quantities.

Answer: C. Integer minor units make the chosen calculations explicit. A is false because currency precision and tax rules still need business decisions. B is false: decimal text can be rendered normally. D is false because a quantity can be invalid regardless of how prices are represented.

Question 3: Customer-controlled text

A customer name contains <img src="...">. What should a trusted invoice template do with that name?

A. Escape it as text at the insertion point. B. Pass it directly into the document because customers are authenticated. C. Enable embedded PHP so the renderer can validate it. D. Escape the entire finished HTML document instead of individual values.

Answer: A. The name is data, not document structure. B confuses authentication with trust in submitted markup. C adds an unrelated execution capability. D would turn the intended template tags into visible text and fails to preserve the structure/data distinction.

Question 4: Missing local logo

A local logo fails to render. Which first response is most appropriate?

A. Set chroot to the filesystem root. B. Turn on unrestricted remote loading. C. Make the entire application world-writable. D. Verify the fixed resolved path, allowed asset root, readability and file format.

Answer: D. Those checks target the resource actually being used. A expands file exposure, B does not fix a local path, and C weakens permissions without explaining the failure.

Question 5: A row longer than a page

One table row contains several pages of narrative. What is the best structural correction?

A. Repeat page-break-inside: avoid with a stronger selector. B. Move the narrative into flowing paragraphs or explicitly split the logical item into appropriate rows. C. Remove the document's UTF-8 declaration. D. Enable PDF JavaScript.

Answer: B. The content needs a structure that can paginate. A cannot make an oversized row fit. C changes encoding interpretation, and D concerns a different feature; neither solves the physical page constraint.

Question 6: Remote host controls

An application enables remote resources for an approved hostname. Which statement is correct?

A. The host allowlist removes all SSRF concerns. B. chroot alone controls HTTP requests. C. The allowlist is one layer; redirects, DNS, internal networks and egress policy still matter. D. Any URL submitted by an authenticated customer can now be rendered safely.

Answer: C. Network access needs controls beyond a hostname comparison. A overstates the guarantee. B applies a local resource boundary to the wrong type of access. D ignores both resource authorization and attacker-controlled destinations.

Question 7: Page numbers

Does trusted PHP code calling the Canvas page-number API require isPhpEnabled: true?

A. No. Embedded document PHP and application-side Canvas calls are different mechanisms. B. Yes. Every PHP method call requires that option. C. Only when the document is more than one page. D. Only when using A4 paper.

Answer: A. The option governs PHP embedded in the document, not normal execution of the application. B incorrectly extends its scope. C and D invent page-count and paper-size conditions.

Question 8: A damaged download

A generated PDF is valid when saved by the CLI but damaged when returned by an endpoint. What should be checked first?

A. The invoice's CSS colors. B. Whether all text is bold. C. The number of decimal places in the price display. D. Extra output, displayed warnings, a BOM, and mixed response/streaming methods.

Answer: D. The different delivery path makes response-byte contamination a strong first hypothesis. A and B affect appearance. C affects displayed values. None normally explains why a valid binary file becomes damaged only during HTTP delivery.

Question 9: Missing glyphs

An accented customer name displays as boxes even though the original string is valid UTF-8. Which explanation should be investigated?

A. Integer arithmetic cannot handle accented text. B. The selected font may lack the required glyphs or may not have loaded. C. Remote loading is mandatory for every Unicode character. D. The invoice needs a larger tax percentage.

Answer: B. Character encoding and font coverage are separate concerns. A and D relate to money, not text rendering. C is false because appropriate locally available fonts can supply glyphs.

Question 10: Cached document authorization

A PDF has already been generated and cached. What must happen before a user downloads it?

A. Skip authorization because generation has already completed. B. Trust the invoice identifier in the URL. C. Authorize access to the invoice and resolve its private stored document. D. Place it in a public directory with an obscure filename.

Answer: C. A stored document retains the same access requirements as a freshly generated one. A ignores the download boundary. B treats knowledge of an identifier as permission. D relies on obscurity and public serving instead of an access check.

Question 11: Sudden memory growth

A small invoice becomes expensive after adding a logo. What should be measured first?

A. The image's pixel dimensions and repeated decoding or embedding costs. B. Whether the filename contains the word "logo". C. The number of characters in the invoice number. D. The number of installed JavaScript packages on a separate frontend project.

Answer: A. Image processing can be costly even if the image appears small on the printed page. B and C are poor explanations for the observed change. D is unrelated to this PHP rendering pipeline.

Question 12: Reproducing an old invoice

A company regenerates a six-month-old invoice using today's product prices and customer profile. What is the underlying problem?

A. It should have used a landscape page. B. It is rendering mutable current data instead of the issued invoice's snapshot. C. It needs to disable HTML escaping. D. It should load its fonts from an arbitrary customer URL.

Answer: B. Historical documents must preserve the business facts that were issued. A changes layout. C introduces markup risk. D adds resource risk. None corrects the data versioning problem.

12. Final implementation checklist

Official references

Technical references checked on October 16, 2026. Recheck releases and security guidance before deploying or updating the integration.