Pdf — High-Level Builder
The Pdf class is a cursor-based document builder. You add content in order, and it handles page breaks, word wrap, margins, and font management automatically.
Basic usage
Section titled “Basic usage”use Phpdftk\Pdf\Writer\Alignment;use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\TextStyle;
$pdf = new Pdf();
$pdf->addHeading('Annual Report', 1);$pdf->addText('Fiscal year 2026 was a year of disciplined growth across every line of business.');$pdf->addSpacer(12);
$pdf->addHeading('Revenue', 2);$pdf->addText('Total revenue reached $4.2M, up 23% year over year.');$pdf->addSpacer(8);$pdf->addRule();$pdf->addSpacer(8);
$pdf->addText( 'Prepared by Finance · Confidential', new TextStyle(alignment: Alignment::Center, italic: true),);
$pdf->save('annual-report.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Defaults
Section titled “Defaults”| Setting | Default |
|---|---|
| Page size | Letter (612 x 792 pt) |
| Margins | 72 pt (1 inch) all sides |
| Font | Helvetica 11pt |
| Line height | 1.4x font size |
Headings
Section titled “Headings”Six heading levels with decreasing font sizes:
$pdf->addHeading('Title', 1); // 24pt bold$pdf->addHeading('Section', 2); // 20pt bold$pdf->addHeading('Subsection', 3); // 16pt bold// ... through level 6Auto-generated outline (bookmarks)
Section titled “Auto-generated outline (bookmarks)”enableOutline() turns headings into PDF bookmarks. Each addHeading()
call registers an OutlineItem with a destination pointing at that
heading’s location; heading levels (1–6) drive parent/child nesting.
Viewers display the result in their bookmarks panel.
use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Auto-generated outline');
// Turn on auto-outline before the first heading. Every subsequent// addHeading call registers an OutlineItem; heading levels (1–6)// drive parent/child nesting.$pdf->enableOutline();
$pdf->addHeading('Part One: Foundations', 1);$pdf->addText('Introductory material covering the basics.');
$pdf->addHeading('Chapter 1: Origins', 2);$pdf->addText('The story begins with a single decision.');
$pdf->addHeading('A first principle', 3);$pdf->addText('Three-level headings nest correctly under their level-2 parent.');
$pdf->addHeading('A second principle', 3);$pdf->addText('Sibling at the same level chains via /Prev and /Next.');
$pdf->addHeading('Chapter 2: Aftermath', 2);$pdf->addText('Returning to level 2 closes the deeper level-3 chain — ' . 'the next level-3 heading would start a fresh chain under this chapter.');
$pdf->addHeading('Part Two: Practice', 1);$pdf->addText('Returning to level 1 starts a new top-level branch.');
$pdf->addHeading('Chapter 3: Application', 2);$pdf->addText('All headings since enableOutline() show up in the viewer\'s bookmarks panel.');
$pdf->save('auto-outline.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Disable later in the document by calling enableOutline(false). Only
headings added between the on/off transitions are recorded.
Multi-column layout
Section titled “Multi-column layout”setColumns(N, gutter) splits the body region into N equal-width
columns separated by gutter points. Flow content fills the current
column from top to bottom, then advances to the next column;
page breaks only occur after the last column on a page overflows.
All existing flow methods (addText, addHeading, addList,
addTable, addQuote, addCallout, addImage, addRule) honour
the column geometry without code changes. Switch back to single-column
flow with setColumns(1).
use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Multi-column layout demo');
// Single-column intro for the heading + abstract.$pdf->addHeading('Multi-column layout', 1);$pdf->addText( 'When setColumns(N, gutter) is active, body content fills the current ' . 'column from top to bottom, then advances to the next column. A page ' . 'break only happens after the last column on a page overflows.',);
// Switch to two columns for the body.$pdf->setColumns(2, gutter: 16.0);
$pdf->addHeading('Two-column body', 2);
$paragraph = 'The quick brown fox jumps over the lazy dog while a thoughtful ' . 'narrator describes the scene in tremendous detail. Lorem ipsum dolor ' . 'sit amet, consectetur adipiscing elit. Praesent vitae quam vel sapien ' . 'ornare suscipit. Curabitur eu nulla sit amet ipsum facilisis dapibus.';
for ($i = 1; $i <= 8; $i++) { $pdf->addText("Paragraph {$i} — {$paragraph}");}
// Back to a single column for the closing note.$pdf->setColumns(1);$pdf->addHeading('Single-column footer', 2);$pdf->addText( 'Calling setColumns(1) restores the full-width body for any further content. ' . 'Headers, footers and watermarks always render on the full page, ' . 'regardless of the body\'s column configuration.',);
$pdf->save('multi-column.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Headers, footers, watermarks, and page numbering always span the full page — they’re independent of the body’s column configuration.
Barcodes
Section titled “Barcodes”addBarcode(Symbology, $data, ?$options, $align) renders a barcode at
the current cursor. The phpdftk/barcode package implements the
encoders; the writer wraps them in a flow / positioned / template
adapter.
Supported symbologies:
| Symbology | Notes |
|---|---|
Code128 | Subset B — printable ASCII (32-127) |
Code39 | Digits + uppercase + a few symbols, with * start/stop |
Codabar | Digits + -$:/.+, A-D start/stop sentinels |
ITF | Interleaved 2 of 5 — digit pairs, even length |
EAN13 | 12 digits + computed checksum (or 13 with checksum verified) |
EAN8 | 7 digits + checksum (or 8 verified) |
UPCA | 11 digits + checksum (or 12 verified) — North America |
QR | Versions 1-40, ECC levels L/M/Q/H, numeric/alphanumeric/byte modes |
DataMatrix | ECC 200, 24 square sizes (10×10 to 144×144), ASCII mode with digit-pair compression |
PDF417 | ISO/IEC 15438. Byte Compaction mode (any 8-bit payload), auto-selected rows/columns (3–90 × 1–30), Reed-Solomon over GF(929) with auto ECC level. Full 2,787-entry codeword pattern table from the AIM USS-PDF417 specification. |
Aztec | ISO/IEC 24778, Compact + Full formats with high-level encoding. Compact (1–4 layers, 15×15 to 27×27) for ≤ 64 data codewords; Full (1–32 layers, 19×19 to 151×151, reference grid for L ≥ 5) for up to 2 048 data codewords. High-level encoder picks the optimal mix of Upper / Lower / Mixed / Punct / Digit sub-alphabets + Binary Shift via Dijkstra-style state search — typical ASCII payloads encode at ~30–40% smaller symbols than raw byte mode. Reed-Solomon over GF(64) (L ≤ 2), GF(256) (L ≤ 8), GF(1024) (L ≤ 22), or GF(4096) (L ≤ 32); mode message uses GF(16). Auto-selected format + layer count with ~23% ECC. |
use Phpdftk\Barcode\BarcodeOptions;use Phpdftk\Barcode\Symbology;use Phpdftk\Pdf\Writer\Alignment;use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Barcode demo — all supported symbologies');
$pdf->addHeading('Linear (1D) symbologies', 1);
$pdf->addHeading('Code 128 (printable ASCII)', 2);$pdf->addBarcode(Symbology::Code128, 'HELLO WORLD');
$pdf->addHeading('Code 39 (digits + uppercase + a few symbols)', 2);$pdf->addBarcode(Symbology::Code39, 'CODE-39 123');
$pdf->addHeading('Codabar (digits + symbols with A-D sentinels)', 2);$pdf->addBarcode(Symbology::Codabar, 'A1234-5678B');
$pdf->addHeading('Interleaved 2 of 5 (digit pairs)', 2);$pdf->addBarcode(Symbology::ITF, '12345678');
$pdf->newPage();$pdf->addHeading('Retail symbologies (EAN / UPC)', 1);
$pdf->addHeading('EAN-13', 2);$pdf->addText('Pass 12 digits (the checksum is computed for you) or 13 (verified).');$pdf->addBarcode(Symbology::EAN13, '590123412345');
$pdf->addHeading('EAN-8', 2);$pdf->addBarcode(Symbology::EAN8, '1234567');
$pdf->addHeading('UPC-A (North America)', 2);$pdf->addBarcode(Symbology::UPCA, '72527273070');
$pdf->newPage();$pdf->addHeading('Matrix (2D) symbologies', 1);
$pdf->addHeading('QR code', 2);$pdf->addText('QR auto-selects the smallest version that fits at the requested ECC level. ' . 'Numeric, alphanumeric, and byte modes are chosen automatically based on the input.');$pdf->addBarcode( Symbology::QR, 'https://phpdftk.dev/', new BarcodeOptions(moduleWidth: 3.0, quietZoneModules: 4), align: Alignment::Center,);
$pdf->addHeading('Data Matrix', 2);$pdf->addText('Data Matrix (ISO/IEC 16022, ECC 200) auto-selects from 24 square sizes. ' . 'ASCII-mode encoding includes 2-digit pair compression that packs digit pairs into a single codeword.');$pdf->addBarcode( Symbology::DataMatrix, 'ORDER-12345-ABCDEF', new BarcodeOptions(moduleWidth: 4.0, quietZoneModules: 2), align: Alignment::Center,);
$pdf->addHeading('PDF417', 2);$pdf->addText('Multi-row stacked 2D symbology (ISO/IEC 15438). Byte Compaction mode encodes ' . 'arbitrary 8-bit payloads. Row count / column count / ECC level are auto-selected.');$pdf->addBarcode( Symbology::PDF417, 'https://phpdftk.dev/', new BarcodeOptions(moduleWidth: 1.5, quietZoneModules: 2), align: Alignment::Center,);
$pdf->addHeading('Aztec', 2);$pdf->addText('Compact format only (1–4 layers, 15×15 to 27×27). Byte (Binary Shift) compaction ' . 'encodes arbitrary 8-bit payloads. Layer count is auto-selected to fit payload + 23% ECC.');$pdf->addBarcode( Symbology::Aztec, 'https://phpdftk.dev/', new BarcodeOptions(moduleWidth: 3.0, quietZoneModules: 2), align: Alignment::Center,);
$pdf->addHeading('Reusable template', 2);$pdf->addText('createBarcode() returns a FormXObject you can stamp on every page without rebuilding.');$template = $pdf->doc()->createBarcode(Symbology::QR, 'REUSE-ME', new BarcodeOptions(moduleWidth: 2.0));$page = $pdf->doc()->addPage();$page->drawTemplate($template, 72.0, 720.0);$page->drawTemplate($template, 200.0, 720.0);$page->drawTemplate($template, 328.0, 720.0);
$pdf->save('barcodes.pdf');📄 View the sample PDF · View the full script on GitHub ↗
For positioned placement, use Writer\Page::drawBarcode($symbology, $data, $x, $y).
For multi-document reuse, build a FormXObject template once with
PdfDoc::createBarcode() and stamp it with Writer\Page::drawTemplate().
Text alignment
Section titled “Text alignment”use Phpdftk\Pdf\Writer\Alignment;
$pdf->addText('Left aligned (default)');$pdf->addText('Centered text', Alignment::Center);$pdf->addText('Right aligned', Alignment::Right);Inline links
Section titled “Inline links”Pass a TextStyle with a link URI to make a paragraph clickable.
Every wrapped line of the linked text gets its own link annotation, so
multi-line linked paragraphs are fully clickable — including
continuation lines after a page break.
use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\TextStyle;
$pdf = new Pdf();$pdf->setTitle('Inline links demo');$pdf->addHeading('Inline links', 1);
$pdf->addText( 'Mixing plain paragraphs with linked paragraphs is as simple as passing a TextStyle ' . 'with the link field set. Each wrapped line of the linked text gets its own link ' . 'annotation, so the whole block is clickable.',);
$pdf->addText( 'Click this paragraph to visit phpdftk.dev — the official documentation site for the project.', new TextStyle( color: [0.1, 0.4, 0.8], link: 'https://phpdftk.dev/', ),);
$pdf->addText('Back to plain text in between.');
$pdf->addText( 'Long linked paragraphs wrap naturally and the entire wrapped region is clickable. ' . 'Every line that the paragraph occupies is covered by its own annotation rectangle, ' . 'including continuation lines on the next page when the paragraph spans a page break.', new TextStyle( bold: true, color: [0.6, 0.0, 0.0], link: 'https://example.com/long-url-here', ),);
$pdf->save('inline-links.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Text decoration
Section titled “Text decoration”TextStyle carries underline and strikethrough flags. Both render
as graphics lines drawn in the same colour as the surrounding text;
they apply per rendered line, so a wrapped paragraph gets one
underline / strikethrough segment per line.
use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\TextStyle;
$pdf = new Pdf();$pdf->setTitle('Text decoration demo');
$pdf->addHeading('Text decoration', 1);
$pdf->addText('Plain body text for comparison.');
$pdf->addText('Underlined paragraph.', new TextStyle(underline: true));
$pdf->addText('Struck-through paragraph.', new TextStyle(strikethrough: true));
$pdf->addText( 'A paragraph with both underline and strikethrough applied.', new TextStyle(underline: true, strikethrough: true),);
$pdf->addText( 'Decoration colour follows the text fill colour, so coloured paragraphs ' . 'render coloured decoration lines too. Each wrapped line gets its own line.', new TextStyle( color: [0.1, 0.4, 0.8], underline: true, ),);
$pdf->addText( 'Decoration also works with the link field — the strike line is drawn over ' . 'the same text region as the link annotation.', new TextStyle( link: 'https://example.com/', underline: true, color: [0.6, 0.0, 0.0], ),);
$pdf->save('text-decoration.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Blockquotes
Section titled “Blockquotes”addQuote() renders an italic, indented body with a coloured vertical
bar down the left edge. Multi-line quotes wrap at the column width and
auto-paginate; the bar is drawn once per page the quote occupies.
use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\TextStyle;use Phpdftk\Pdf\Writer\Theme;
$pdf = new Pdf();$pdf->setTitle('Blockquote demo');
$pdf->addHeading('Blockquotes', 1);
$pdf->addText('Default blockquote — italic body, indented under a light-grey bar:');$pdf->addSpacer(4);
$pdf->addQuote( 'The greatest enemy of knowledge is not ignorance — it is the illusion of knowledge.',);
$pdf->addSpacer(8);$pdf->addText('Long quotes wrap naturally at the column width:');$pdf->addSpacer(4);
$pdf->addQuote( 'When you have eliminated the impossible, whatever remains, however improbable, must be the truth. ' . 'This is the principle that underlies all reasonable investigation, and it is one that any honest ' . 'observer will return to repeatedly when the facts begin to seem strange or contradictory.',);
$pdf->addSpacer(8);$pdf->addText('Override font / colour / alignment via TextStyle (italic stays the default unless you set it explicitly):');$pdf->addSpacer(4);
$pdf->addQuote( 'Plain upright body, in a muted colour — sometimes you want the visual signature of a quote without italic.', new TextStyle(italic: false, color: [0.3, 0.3, 0.3]),);
$pdf->save('blockquote.pdf');
// region: example-themed// Customize the bar via the Theme.$themed = new Pdf( theme: new Theme( quoteIndent: 24.0, quoteBarWidth: 4.0, quoteBarColor: [0.1, 0.4, 0.8], ),);$themed->addQuote( 'A themed blockquote with a thicker, blue bar.',);$themed->save('blockquote-themed.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Override the indent, bar width, and bar colour on the Theme to give
quotes a different visual identity:
$pdf = new Pdf(theme: new Theme( quoteIndent: 24.0, quoteBarWidth: 4.0, quoteBarColor: [0.1, 0.4, 0.8],));Callouts
Section titled “Callouts”addCallout() renders a panel with a coloured left bar, a tinted
background, and an optional title row. Pick a CalloutType —
Note, Tip, Warning, or Danger — to get a sensible default
palette, or pass a CalloutStyle to override any of the bar colour,
background, label text, or title visibility.
Callouts auto-advance to a new page if they would overflow the current one, but they don’t split across pages — bodies longer than a page throw.
use Phpdftk\Pdf\Writer\CalloutStyle;use Phpdftk\Pdf\Writer\CalloutType;use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Callouts demo');$pdf->addHeading('Callouts', 1);$pdf->addText('Four built-in callout types — each with its own bar / background palette:');$pdf->addSpacer(4);
$pdf->addCallout( 'A neutral aside or contextual reminder. Notes are the safe choice when you ' . 'want to draw attention without alarm.', CalloutType::Note,);
$pdf->addCallout( 'A helpful suggestion or shortcut that improves on the default approach.', CalloutType::Tip,);
$pdf->addCallout( 'Something the reader needs to know but that is not yet a hard failure. ' . 'Examples: deprecation notice, potentially confusing edge case, opt-in ' . 'risk that the reader can accept.', CalloutType::Warning,);
$pdf->addCallout( 'Stop and read this carefully — this content describes a hazard, data-loss ' . 'risk, or other situation that absolutely must be acknowledged before ' . 'continuing.', CalloutType::Danger,);
$pdf->addHeading('Customizing a callout', 2);$pdf->addText('Pass a CalloutStyle to override the label, colours, or to hide the title row:');$pdf->addSpacer(4);
$pdf->addCallout( 'Body content rendered without a title row.', CalloutType::Note, new CalloutStyle(showLabel: false),);
$pdf->addCallout( 'The label can be replaced with any string — useful for localized docs.', CalloutType::Warning, new CalloutStyle(labelOverride: 'Heads up'),);
$pdf->addCallout( 'Bar and background colours are fully replaceable.', CalloutType::Note, new CalloutStyle( barColor: [0.5, 0.0, 0.5], // purple bar bgColor: [0.97, 0.94, 0.99], // very light purple tint ),);
$pdf->save('callouts.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Images
Section titled “Images”$pdf->addImage('photo.jpg', width: 300);$pdf->addImage('logo.png', width: 150, alignment: Alignment::Center);Horizontal rules
Section titled “Horizontal rules”$pdf->addRule(); // default thin gray line$pdf->addSpacer(24); // vertical whitespaceTables
Section titled “Tables”addTable() renders tabular data with optional header rows, explicit
column widths, per-column alignment, and automatic row-by-row
pagination. The header row repeats at the top of every page the table
occupies. Cells longer than their column wrap onto multiple lines and
the row grows to fit.
use Phpdftk\Pdf\Writer\Alignment;use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\TableStyle;
$pdf = new Pdf();$pdf->setTitle('Tables demo');$pdf->addHeading('Quarterly results', 1);$pdf->addText('A simple table with a repeating header row, explicit column widths, and per-column alignment:');$pdf->addSpacer(8);
$pdf->addTable( rows: [ ['Q1 2026', 'Widgets', '12,340', '$148,080.00'], ['Q2 2026', 'Widgets', '14,520', '$174,240.00'], ['Q3 2026', 'Widgets', '11,890', '$142,680.00'], ['Q4 2026', 'Widgets', '17,200', '$206,400.00'], ['Q1 2026', 'Gadgets', '3,210', '$80,250.00'], ['Q2 2026', 'Gadgets', '4,140', '$103,500.00'], ['Q3 2026', 'Gadgets', '3,990', '$99,750.00'], ['Q4 2026', 'Gadgets', '5,820', '$145,500.00'], ], columnWidths: [80.0, 120.0, 100.0, 140.0], headerRow: ['Period', 'Product', 'Units', 'Revenue'], style: new TableStyle( cellAlignments: [Alignment::Left, Alignment::Left, Alignment::Right, Alignment::Right], ),);
$pdf->addSpacer(16);$pdf->addHeading('Long content auto-wraps', 2);$pdf->addText('Cells that overflow the column width wrap onto multiple lines, and the row grows to fit the tallest cell:');$pdf->addSpacer(8);
$pdf->addTable( rows: [ ['Short', 'Cells stay on a single line when they fit.'], ['Longer cell', 'This cell contains enough text that it overflows the narrower column and wraps onto a second and likely third line.'], ['Multi-paragraph', "Explicit newlines are honoured too.\nThe second paragraph gets its own line break inside the cell."], ], columnWidths: [100.0, 320.0],);
$pdf->save('tables.pdf');📄 View the sample PDF · View the full script on GitHub ↗
For positioned table rendering (no flow), drop to Level 2 with
$page->drawTable($table, $x, $y, $bodyFont, $headerFont).
addList() produces a bullet list and addNumberedList() produces a
running 1./2./3. list. Items are plain strings; long items wrap at the
column width and the gap between items is controlled by
ListStyle::itemSpacing. Lists paginate item-by-item.
use Phpdftk\Pdf\Writer\ListStyle;use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Lists demo');
$pdf->addHeading('Bullet list', 2);$pdf->addList([ 'Espresso — concentrated extraction under pressure', 'Pour-over — slow gravity drip through a paper filter', 'French press — full-immersion brew with a metal mesh', 'Cold brew — long extraction in cold water, low acidity',]);
$pdf->addHeading('Numbered list', 2);$pdf->addNumberedList([ 'Weigh the beans (15g for a single shot).', 'Grind to fine consistency.', 'Tamp with consistent pressure.', 'Pull the shot in roughly 25 to 30 seconds.',]);
$pdf->addHeading('Customized markers', 2);$pdf->addText('Lists accept a ListStyle to override indent, bullet glyphs, item spacing, and the numbering suffix:');$pdf->addSpacer(6);
$pdf->addNumberedList( [ 'First option', 'Second option', 'Third option', ], new ListStyle( indent: 24.0, itemSpacing: 6.0, numberSuffix: ')', ),);
$pdf->addHeading('Wrapping long items', 2);$pdf->addList([ 'Short item.', 'A much longer item that contains enough text to overflow the available column width, ' . 'causing the renderer to wrap it onto subsequent lines while keeping the bullet at the ' . 'item start position and indenting the continuation lines to align with the first line of text.', 'Another short item to show how the gap between items respects ListStyle::itemSpacing.',]);
$pdf->save('lists.pdf');📄 View the sample PDF · View the full script on GitHub ↗
For positioned list rendering, use Level 2’s $page->drawList().
Themes
Section titled “Themes”Customize fonts, colors, margins, and reserved header/footer regions:
use Phpdftk\Pdf\Writer\Theme;
$theme = new Theme( margin: 36.0, // half-inch margins family: 'Courier', headerHeight: 30.0, // reserve top area for setHeader closures footerHeight: 30.0, // reserve bottom area for setFooter closures);
$pdf = new Pdf(theme: $theme);When headerHeight / footerHeight are non-zero, the body cursor starts below the reserved header and stops above the reserved footer — flow content never overlaps the decorator regions.
Document metadata
Section titled “Document metadata”Fluent chainable setters create the Info dict lazily and populate fields:
$pdf->setTitle('My Report') ->setAuthor('Finance Team') ->setSubject('Q4 numbers') ->setKeywords('quarterly, finance') ->setCreator('phpdftk');These forward to the underlying PdfDoc. For raw XMP control or Info object manipulation, drop to Level 2 via $pdf->doc().
Headers, footers, watermarks
Section titled “Headers, footers, watermarks”Register closures invoked on every page during a deferred pass after all flow content is placed — totalPages is reliably set when the closure runs, so page numbering and any “X of Y” labels work without manual two-pass logic.
use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Writer\PageContext;use Phpdftk\Pdf\Writer\Pdf;use Phpdftk\Pdf\Writer\Theme;
// Reserve 28pt at the top and bottom of every page for the decorator// closures registered below — body content automatically respects the// reserve and never overlaps headers or footers.$theme = new Theme( margin: 48.0, headerHeight: 28.0, footerHeight: 28.0,);
$pdf = new Pdf(theme: $theme);$pdf->setTitle('Quarterly Report');
// The header closure renders text and needs a font handle. The footer// uses showPageNumbers() below, which resolves fonts itself.$boldFont = $pdf->writer()->addFont(new Type1Font(StandardFont::HelveticaBold));
// Header: document title, left-aligned in the reserved top band.$pdf->setHeader(function (PageContext $ctx) use ($boldFont): void { $ctx->page->drawText( 'Quarterly Report — FY2026', $ctx->theme->margin, $ctx->pageHeight - 22.0, $boldFont, 9.0, );});
// Footer: "Page X of Y" centered. `showPageNumbers()` is sugar over// setFooter() that uses the deferred totalPages — equivalent to writing// the closure by hand, just shorter.$pdf->showPageNumbers();
// Watermark: the string form renders centered, diagonal, light-grey text.$pdf->setWatermark('DRAFT', opacity: 0.18, angleDeg: 30.0);
// Now build the body — addText paginates automatically and never// overlaps the header / footer regions.$pdf->addHeading('Executive summary', 1);$pdf->addText( 'This document demonstrates the per-page render hooks: setHeader, setFooter, ' . 'and setWatermark. The hooks fire in a single deferred pass after all flow ' . 'content is placed, which is why the footer can show the correct total page ' . 'count even though it draws at the top-down build time the total was unknown.',);
for ($i = 1; $i <= 3; $i++) { $pdf->addHeading("Section {$i}", 2); for ($p = 0; $p < 4; $p++) { $pdf->addText( 'The quick brown fox jumps over the lazy dog. ' . str_repeat( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 6, ), ); }}
$pdf->save('headers-footers-watermark.pdf');📄 View the sample PDF · View the full script on GitHub ↗
use Phpdftk\Pdf\Writer\PageContext;
$pdf->setHeader(function (PageContext $ctx) use ($headerFont): void { $ctx->page->drawText( 'CONFIDENTIAL', 72.0, $ctx->pageHeight - 24.0, $headerFont, 10.0, );});
$pdf->setFooter(function (PageContext $ctx) use ($footerFont): void { $ctx->page->drawText( "Page {$ctx->pageNumber} of {$ctx->totalPages}", 72.0, 24.0, $footerFont, 10.0, );});
// Simple watermark: string form renders centered diagonal grey text.$pdf->setWatermark('DRAFT');
// Or full control via closure receiving a PageContext.$pdf->setWatermark(function (PageContext $ctx): void { // ... custom rendering});The decorator pass runs exactly once per document — calling toBytes() / save() / writeTo() multiple times will not re-fire closures, and pages added after the first output are not retroactively decorated.
Page numbers shortcut
Section titled “Page numbers shortcut”For the common “Page X of Y” footer, use showPageNumbers() — it wraps setFooter() with a sprintf-formatted closure and uses the same deferred totalPages mechanism:
$pdf->showPageNumbers(); // "Page 1 of 5", centered$pdf->showPageNumbers('— %d / %d —', Alignment::Right);Pair with Theme(footerHeight: 30.0) to reserve a strip below the body content for the number.
Escape hatches
Section titled “Escape hatches”Drop to Level 2 (PdfDoc) for friendly wrappers over annotations, navigation, file attachments, and viewer preferences without leaving the flow-builder context:
$doc = $pdf->doc();$doc->addLink($page, $rect, 'https://example.com/');Drop to Level 1 (PdfWriter) when you need byte/resource control — custom fonts, encryption, signing, conformance profiles:
$writer = $pdf->writer(); // equivalent to $pdf->doc()->writer()$writer->addFont(\Phpdftk\Pdf\Core\Font\TrueTypeFont::fromFile('/path/to/font.ttf'));Output modes
Section titled “Output modes”$pdf->save('/path/to/file.pdf'); // Write to file$bytes = $pdf->toBytes(); // Get as string$pdf->writeTo($stream); // Write to stream resource