PdfWriter — Object Model
PdfWriter is the ergonomic builder for the PDF object model. It handles object registration, resource naming, and page tree wiring, while giving you precise control over what goes where.
End-to-end example
Section titled “End-to-end example”use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Writer\PdfWriter;
$writer = new PdfWriter();
$page = $writer->addPage(612, 792); // US Letter, 8.5 x 11 in$font = $writer->addFont(new Type1Font(StandardFont::Helvetica));
$cs = $writer->addContentStream($page);$cs->beginText() ->setFont($font, 24) ->moveTextPosition(72, 720) ->showText('Quarterly Report') ->endText();
$cs->beginText() ->setFont($font, 12) ->moveTextPosition(72, 690) ->showText('Q4 2026 — prepared by Finance') ->endText();
$writer->save('quarterly-report.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Creating pages
Section titled “Creating pages”use Phpdftk\Pdf\Writer\PdfWriter;use Phpdftk\Pdf\Writer\PageSize;
$writer = new PdfWriter();
// Explicit dimensions (points)$page = $writer->addPage(612, 792);
// Or use a named size$page = $writer->addPage(PageSize::A4);$page = $writer->addPage(PageSize::Legal);use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Core\Font\StandardFont;
// Standard 14 fonts (no embedding needed)$helvetica = $writer->addFont(new Type1Font(StandardFont::Helvetica));$courier = $writer->addFont(new Type1Font(StandardFont::Courier));
// TrueType font (auto-embedded and subsetted)$custom = $writer->addFont( \Phpdftk\Pdf\Core\Font\TrueTypeFont::fromFile('/path/to/font.ttf'));Text encoding
Section titled “Text encoding”Standard Type1 fonts (Helvetica, Times, Courier) and embedded TrueType fonts use WinAnsi encoding. Pass the Font handle returned by addFont() to ContentStream::setFont() and the stream will encode UTF-8 input to the right bytes for you:
$cs->beginText() ->setFont($helvetica, 12) // pass the handle, not the resource name ->showText('café — résumé · 20×20') ->endText();If a codepoint has no WinAnsi mapping it is substituted with ?; inspect $writer->getEncodingWarnings() to see what was lost. Passing the resource name string ($helvetica->getResourceName()) instead disables the encoder so you can emit pre-encoded bytes by hand.
For Unicode beyond WinAnsi (CJK, Cyrillic, mathematical symbols), use addCompositeFont() — see the Fonts gallery for the full pattern. The example below subsets a TrueType font down to the glyphs an accented Latin string actually needs:
use Phpdftk\FontParser\TrueTypeParser;use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Writer\PdfWriter;
// Any TTF on disk works. DejaVu ships with mpdf and has broad Unicode coverage,// so it's a convenient choice for showcase examples that run anywhere.$fontPath = __DIR__ . '/../../vendor/mpdf/mpdf/ttfonts/DejaVuSans.ttf';$ttData = (new TrueTypeParser($fontPath))->parse();
$writer = new PdfWriter();$page = $writer->addPage();
// Headline in a built-in standard font for contrast.$caption = $writer->addFont(new Type1Font(StandardFont::HelveticaBold))->getResourceName();
// Embed DejaVu Sans as a Type 0 (CID) font, subset to just the glyphs we use.$sample = "Custom font subsetting — embedded DejaVu Sans with accented Latin: " . "café · résumé · naïve · jalapeño · København · groß";$codepoints = array_values(array_unique(array_map('mb_ord', mb_str_split($sample))));$dejavu = $writer->addCompositeFont($ttData, $codepoints);$dejavuName = $dejavu->getResourceName();
// CID fonts use hex glyph IDs in the content stream rather than literal text.// Use the post-subset Unicode → GID map from the font handle; the map on// the parsed font data points at glyphs that no longer exist in the subset.$unicodeToGid = $dejavu->getUnicodeToGidMap();$gidHex = '';foreach (mb_str_split($sample) as $char) { $gid = $unicodeToGid[mb_ord($char)] ?? 0; $gidHex .= sprintf('%04X', $gid);}
$cs = $writer->addContentStream($page);$cs->beginText() ->setFont($caption, 18) ->moveTextPosition(72, 720) ->showText('Custom Font Embedding') ->endText();
$cs->beginText() ->setFont($dejavuName, 14) ->moveTextPosition(72, 680) ->showTextHex($gidHex) ->endText();
$writer->save('custom-font.pdf');📄 View the sample PDF · View the full script on GitHub ↗
The returned Font handle is opaque — pass it to drawing methods:
$page->drawText('Hello', 72, 720, $helvetica, 12);Drawing on pages
Section titled “Drawing on pages”The Page object provides fluent drawing methods. Each call is isolated in its own graphics state (q/Q):
$page->drawText('Hello World', 72, 720, $font, 12);$page->drawText('Red text', 72, 700, $font, 14, color: new RgbColor(1, 0, 0));Shapes
Section titled “Shapes”use Phpdftk\Color\RgbColor;
// Rectangle with fill and stroke$page->drawRectangle(72, 600, 200, 100, fill: new RgbColor(0.9, 0.9, 1.0), stroke: new RgbColor(0, 0, 0.5), strokeWidth: 2.0,);
// Circle$page->drawCircle(300, 400, 50, fill: new RgbColor(1, 0.8, 0));
// Line with dash patternuse Phpdftk\Pdf\Writer\DashPattern;$page->drawLine(72, 500, 540, 500, color: new RgbColor(0.5, 0.5, 0.5), dash: DashPattern::dashed(),);Images
Section titled “Images”$page->drawImage('photo.jpg', 72, 300, width: 200);Custom paths
Section titled “Custom paths”$page->drawPath( function ($p) { $p->moveTo(100, 100) ->lineTo(200, 200) ->curveTo(250, 250, 300, 200, 300, 150) ->close(); }, fill: new RgbColor(0.2, 0.6, 1.0),);Catalog conveniences moved to PdfDoc
Section titled “Catalog conveniences moved to PdfDoc”Methods that operate on the document catalog (setOutline, addOutlineItem, setPageLabels, setNamedDestinations, setInfo, setMetadata, syncInfoToMetadata) now live on the new PdfDoc class. They remain on PdfWriter as @deprecated forwarders for one minor release.
New code should call them on a PdfDoc:
use Phpdftk\Pdf\Writer\PdfDoc;
$doc = PdfDoc::wrap($writer); // or new PdfDoc() to start fresh$doc->setOutline($outline);$doc->setPageLabels($labels);The examples below use the legacy $writer->... path (still functional during the deprecation window).
Bookmarks
Section titled “Bookmarks”setOutline() registers an Outline root and wires it to the catalog; addOutlineItem() registers each entry and returns a reference for cross-linking. Each item carries a /Dest array (or named destination) describing where the bookmark jumps to.
use Phpdftk\Pdf\Core\Document\Outline;use Phpdftk\Pdf\Core\Document\OutlineItem;use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Core\PdfArray;use Phpdftk\Pdf\Core\PdfName;use Phpdftk\Pdf\Core\PdfNumber;use Phpdftk\Pdf\Core\PdfReference;use Phpdftk\Pdf\Writer\PdfWriter;
$writer = new PdfWriter();$body = $writer->addFont(new Type1Font(StandardFont::Helvetica))->getResourceName();$bold = $writer->addFont(new Type1Font(StandardFont::HelveticaBold))->getResourceName();
$chapters = ['Introduction', 'Background', 'Implementation', 'Conclusions'];
// One page per chapter — keep the page reference so the bookmarks can target it.$pageRefs = [];foreach ($chapters as $i => $title) { $page = $writer->addPage(); $pageRefs[$i] = new PdfReference($page->corePage()->objectNumber);
$cs = $writer->addContentStream($page); $cs->beginText()->setFont($bold, 22)->moveTextPosition(72, 720) ->showText($title)->endText(); $cs->beginText()->setFont($body, 11)->moveTextPosition(72, 690) ->showText(sprintf('Chapter %d of %d.', $i + 1, count($chapters)))->endText();}
// One OutlineItem per chapter, with an /XYZ destination pointing at the page.$items = [];foreach ($chapters as $i => $title) { $item = new OutlineItem($title); $item->dest = new PdfArray([ $pageRefs[$i], new PdfName('XYZ'), new PdfNumber(0), new PdfNumber(792), new PdfNumber(0), ]); $writer->addOutlineItem($item); // assigns objectNumber $items[] = $item;}
// Wire the doubly-linked sibling chain.foreach ($items as $i => $item) { if ($i > 0) { $item->prev = new PdfReference($items[$i - 1]->objectNumber); } if ($i < count($items) - 1) { $item->next = new PdfReference($items[$i + 1]->objectNumber); }}
// Register the Outline root and point every item's /Parent at it.$outline = new Outline();$writer->setOutline($outline);$outline->first = new PdfReference($items[0]->objectNumber);$outline->last = new PdfReference(end($items)->objectNumber);$outline->count = count($items);
foreach ($items as $item) { $item->parent = new PdfReference($outline->objectNumber);}
$writer->save('outline.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Page labels
Section titled “Page labels”setPageLabels() overrides the displayed page numbers — useful for front matter in roman numerals followed by arabic body pages and a prefixed appendix.
use Phpdftk\Pdf\Core\Document\PageLabel;use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Core\PdfName;use Phpdftk\Pdf\Core\PdfString;use Phpdftk\Pdf\Writer\PdfWriter;
$writer = new PdfWriter();$body = $writer->addFont(new Type1Font(StandardFont::Helvetica))->getResourceName();$bold = $writer->addFont(new Type1Font(StandardFont::HelveticaBold))->getResourceName();
$toRoman = static function (int $n): string { $map = ['m' => 1000, 'cm' => 900, 'd' => 500, 'cd' => 400, 'c' => 100, 'xc' => 90, 'l' => 50, 'xl' => 40, 'x' => 10, 'ix' => 9, 'v' => 5, 'iv' => 4, 'i' => 1]; $out = ''; foreach ($map as $r => $v) { while ($n >= $v) { $out .= $r; $n -= $v; } } return $out;};
// 3 front-matter pages numbered i, ii, iiifor ($i = 1; $i <= 3; $i++) { $page = $writer->addPage(); $cs = $writer->addContentStream($page); $cs->beginText()->setFont($bold, 20)->moveTextPosition(72, 720) ->showText('Front Matter')->endText(); $cs->beginText()->setFont($body, 11)->moveTextPosition(72, 690) ->showText('Table of contents, preface, acknowledgements.')->endText(); $cs->beginText()->setFont($body, 10)->moveTextPosition(296, 36) ->showText($toRoman($i))->endText();}
// 5 main-content pages numbered 1..5for ($i = 1; $i <= 5; $i++) { $page = $writer->addPage(); $cs = $writer->addContentStream($page); $cs->beginText()->setFont($bold, 18)->moveTextPosition(72, 720) ->showText(sprintf('Chapter %d', $i))->endText(); $cs->beginText()->setFont($body, 11)->moveTextPosition(72, 690) ->showText('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')->endText(); $cs->beginText()->setFont($body, 10)->moveTextPosition(296, 36) ->showText((string) $i)->endText();}
// 2 appendix pages with "App-A", "App-B"for ($i = 0; $i < 2; $i++) { $page = $writer->addPage(); $cs = $writer->addContentStream($page); $cs->beginText()->setFont($bold, 16)->moveTextPosition(72, 720) ->showText(sprintf('Appendix %s', chr(65 + $i)))->endText(); $cs->beginText()->setFont($body, 11)->moveTextPosition(72, 690) ->showText('Supplementary material and references.')->endText(); $cs->beginText()->setFont($body, 10)->moveTextPosition(290, 36) ->showText('App-' . chr(65 + $i))->endText();}
// Wire up the three label ranges$frontMatter = new PageLabel();$frontMatter->s = new PdfName('r'); // lowercase roman
$mainContent = new PageLabel();$mainContent->s = new PdfName('D'); // decimal
$appendix = new PageLabel();$appendix->s = new PdfName('A'); // uppercase alpha$appendix->p = new PdfString('App-');
$writer->setPageLabels([ 0 => $frontMatter, // pages 0-2 → i, ii, iii 3 => $mainContent, // pages 3-7 → 1..5 8 => $appendix, // pages 8-9 → App-A, App-B]);
$writer->save('page-labels.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Linearization (fast web view)
Section titled “Linearization (fast web view)”Linearized PDFs front-load the first page’s objects so a viewer can render it before the rest of the file finishes downloading. Toggle it with setLinearized(true).
use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Writer\PdfWriter;
$writer = new PdfWriter();$body = $writer->addFont(new Type1Font(StandardFont::Helvetica));$bold = $writer->addFont(new Type1Font(StandardFont::HelveticaBold));
// First page rendered immediately on web display; subsequent pages stream in.$first = $writer->addPage();$cs = $writer->addContentStream($first);$cs->beginText()->setFont($bold, 26)->moveTextPosition(72, 720) ->showText('Fast Web View Demo')->endText();$cs->beginText()->setFont($body, 12)->moveTextPosition(72, 686) ->showText('This PDF is linearized — the first page renders before the')->endText();$cs->beginText()->setFont($body, 12)->moveTextPosition(72, 670) ->showText('rest of the file finishes downloading (ISO 32000-2 Annex F).')->endText();
// A handful more pages so the linearization layout is meaningful.for ($i = 2; $i <= 10; $i++) { $page = $writer->addPage(); $cs = $writer->addContentStream($page); $cs->beginText()->setFont($bold, 18)->moveTextPosition(72, 720) ->showText(sprintf('Page %d', $i))->endText(); $cs->beginText()->setFont($body, 11)->moveTextPosition(72, 690) ->showText('Loaded after the first page reaches the viewer.')->endText();}
$writer->setLinearized(true);$writer->save('linearized.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Digital signatures
Section titled “Digital signatures”$writer->setSigner($signatureValue, $pkcs7Signer);See Core → Interactive → Signature fields for the full object graph.
Encryption
Section titled “Encryption”PdfEncryptor::aes256() encrypts the document with a user/owner password and a permission mask. The version is automatically bumped to PDF 2.0 (the minimum AES-256 requires).
use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Core\Security\PdfEncryptor;use Phpdftk\Pdf\Writer\PdfWriter;
$writer = new PdfWriter();$page = $writer->addPage();$font = $writer->addFont(new Type1Font(StandardFont::Helvetica));$bold = $writer->addFont(new Type1Font(StandardFont::HelveticaBold));
$cs = $writer->addContentStream($page);$cs->beginText()->setFont($bold, 20)->moveTextPosition(72, 720) ->showText('Confidential — Internal Use Only')->endText();$cs->beginText()->setFont($font, 12)->moveTextPosition(72, 690) ->showText('This document is encrypted with AES-256.')->endText();$cs->beginText()->setFont($font, 12)->moveTextPosition(72, 670) ->showText('User password: "open-sesame" — owner password: "owner"')->endText();$cs->beginText()->setFont($font, 12)->moveTextPosition(72, 650) ->showText('Printing is allowed; copying and modification are not.')->endText();
// AES-256 needs a stable 16-byte file id. Use any deterministic source.$fileId = md5('phpdftk-encrypted-showcase', true);
$encryptor = PdfEncryptor::aes256( userPassword: 'open-sesame', ownerPassword: 'owner', fileId: $fileId, permissions: PdfEncryptor::PERM_PRINT, // copy/modify implicitly denied);$writer->setEncryption($encryptor);
$writer->save('encrypted.pdf');📄 View the sample PDF · View the full script on GitHub ↗
For public-key (X.509) encryption, see Core → Security & Files.
Escape hatches
Section titled “Escape hatches”// Drop to Level 0 — raw ContentStream operators$cs = $page->contentStream();$cs->beginText() ->setFont($font->getResourceName(), 12) ->moveTextPosition(72, 720) ->showText('Raw operator access') ->endText();
// Access the core Page object for annotations, resources, etc.$corePage = $page->corePage();
// Access the PdfFileWriter for byte-level control$fw = $writer->fileWriter();