Skip to content

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.

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');
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')
);

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');

The returned Font handle is opaque — pass it to drawing methods:

$page->drawText('Hello', 72, 720, $helvetica, 12);

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));
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 pattern
use Phpdftk\Pdf\Writer\DashPattern;
$page->drawLine(72, 500, 540, 500,
color: new RgbColor(0.5, 0.5, 0.5),
dash: DashPattern::dashed(),
);
$page->drawImage('photo.jpg', 72, 300, width: 200);
$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),
);

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).

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');

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, iii
for ($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..5
for ($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');

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');
$writer->setSigner($signatureValue, $pkcs7Signer);

See Core → Interactive → Signature fields for the full object graph.

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');

For public-key (X.509) encryption, see Core → Security & Files.

// 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();