PdfDoc — Friendly Document API
PdfDoc sits between PdfWriter (Level 1, byte/resource concerns) and Pdf (Level 3, flow-layout builder). It provides one method per “thing a user wants to put in a document” — bookmarks, page labels, named destinations, link annotations, document metadata — wrapped over the underlying PDF object model so you never need register() or manual Catalog wiring for built-in PDF features.
End-to-end example
Section titled “End-to-end example”A two-page document that uses every Phase 0 / Phase 1 PdfDoc API in one place: fluent metadata setters, XMP sync, a URI link annotation, and an internal-navigation link via an inline Destination.
use Phpdftk\Geometry\Rectangle;use Phpdftk\Pdf\Core\Annotation\BorderStyle;use Phpdftk\Pdf\Core\Document\Destination;use Phpdftk\Pdf\Core\Font\StandardFont;use Phpdftk\Pdf\Core\Font\Type1Font;use Phpdftk\Pdf\Core\PdfName;use Phpdftk\Pdf\Core\PdfNumber;use Phpdftk\Pdf\Core\PdfReference;use Phpdftk\Pdf\Writer\PdfDoc;
$doc = new PdfDoc();
// Fluent metadata setters create the /Info dict lazily.$doc->setTitle('PdfDoc demo') ->setAuthor('phpdftk') ->setSubject('Level 2 — friendly catalog API') ->setKeywords('pdfdoc, level 2, links, metadata') ->setCreator('examples/writer/pdf-doc.php');
// Mirror Info into XMP — required for PDF/A and good practice anywhere.$doc->syncInfoToMetadata();
// Register a font on the underlying PdfWriter so the two pages have// something to render with.$writer = $doc->writer();$bodyFont = $writer->addFont(new Type1Font(StandardFont::Helvetica));$boldFont = $writer->addFont(new Type1Font(StandardFont::HelveticaBold));
// ---- Page 1 — outbound URI link ----$cover = $doc->addPage();$cover->drawText('PdfDoc — friendly catalog API', 72.0, 720.0, $boldFont, 22.0);$cover->drawText('Click the box below to visit the project homepage.', 72.0, 680.0, $bodyFont, 12.0);
$uriRect = new Rectangle(72.0, 640.0, 200.0, 18.0);$border = new BorderStyle();$border->s = new PdfName('S'); // solid$border->w = new PdfNumber(0.75);
$doc->addLink($cover, $uriRect, 'https://phpdftk.dev/', $border);$cover->drawText('phpdftk.dev', 78.0, 645.0, $bodyFont, 12.0);
// Internal navigation: link cover → details page via an inline Destination.$detailsPage = $doc->addPage();$detailsRef = new PdfReference($detailsPage->corePage()->objectNumber);$internalRect = new Rectangle(72.0, 600.0, 200.0, 18.0);$doc->addLink($cover, $internalRect, Destination::fit($detailsRef));$cover->drawText('Jump to page 2 →', 78.0, 605.0, $bodyFont, 12.0);
// ---- Page 2 — destination of the internal link ----$detailsPage->drawText('Page 2 — details', 72.0, 720.0, $boldFont, 22.0);$detailsPage->drawText( 'This page was opened via an inline /Dest array on the link annotation.', 72.0, 680.0, $bodyFont, 12.0,);
$doc->writer()->save('pdf-doc.pdf');📄 View the sample PDF · View the full script on GitHub ↗
When to use which class
Section titled “When to use which class”| Class | Use when | Examples |
|---|---|---|
Pdf (Level 3) | You want auto-layout, headings, paragraphs | reports, articles, letters |
PdfDoc (Level 2) | You want explicit page positioning plus friendly wrappers for catalog / annotation / form features | invoices with links, forms, attachments |
PdfWriter (Level 1) | You need precise byte/resource control — custom fonts, encryption, signing, conformance profiles | font subsetting, signed documents, PDF/A |
You can always drop one level. Pdf::doc() returns the PdfDoc; PdfDoc::writer() returns the PdfWriter; PdfWriter::fileWriter() returns the PdfFileWriter.
Creating a document
Section titled “Creating a document”use Phpdftk\Pdf\Writer\PdfDoc;
$doc = new PdfDoc();$page = $doc->addPage(); // returns a Writer\Page for explicit drawingIf you already have a configured PdfWriter (with a signer, encryption, or conformance profile), wrap it instead:
use Phpdftk\Pdf\Writer\PdfWriter;use Phpdftk\Pdf\Conformance\Profile\PdfAProfile;
$writer = new PdfWriter();$writer->setConformance(PdfAProfile::A2b);$doc = PdfDoc::wrap($writer);Document metadata
Section titled “Document metadata”Fluent setters lazily create an Info dict and populate fields. Each returns $this for chaining:
$doc->setTitle('Annual Report 2026') ->setAuthor('Finance Team') ->setSubject('Year-end summary') ->setKeywords('annual, report, 2026') ->setCreator('phpdftk');To mirror those values into XMP metadata (required for PDF/A and recommended for general use):
$doc->syncInfoToMetadata();For full control, pass a constructed Info:
use Phpdftk\Pdf\Core\Document\Info;use Phpdftk\Pdf\Core\PdfString;
$info = new Info();$info->title = new PdfString('Explicit Title');$info->producer = new PdfString('My App 1.2');$doc->setInfo($info);Raw XMP:
$doc->setMetadata($xmpXmlString);Link annotations
Section titled “Link annotations”addLink() accepts three target forms:
use Phpdftk\Geometry\Rectangle;
// URI link — builds an inline /A action dict$rect = new Rectangle(72.0, 700.0, 200.0, 14.0);$doc->addLink($page, $rect, 'https://example.com/');
// Internal navigation — pass a Destination (fit, xyz, fitH, …)use Phpdftk\Pdf\Core\Document\Destination;use Phpdftk\Pdf\Core\PdfReference;
$pageRef = new PdfReference($page->corePage()->objectNumber);$doc->addLink($page, $rect, Destination::fit($pageRef));
// Named destination — pass a PdfReference into the name tree$doc->addLink($page, $rect, $namedDestinationRef);Optional border:
use Phpdftk\Pdf\Core\Annotation\BorderStyle;use Phpdftk\Pdf\Core\PdfName;use Phpdftk\Pdf\Core\PdfNumber;
$border = new BorderStyle();$border->s = new PdfName('D'); // dashed$border->w = new PdfNumber(1.5);
$doc->addLink($page, $rect, 'https://example.com/', $border);Bookmarks (outline)
Section titled “Bookmarks (outline)”use Phpdftk\Pdf\Core\Document\Outline;use Phpdftk\Pdf\Core\Document\OutlineItem;
$outline = new Outline();$doc->setOutline($outline);
$intro = new OutlineItem('Introduction');$intro->parent = new PdfReference($outline->objectNumber);$ref = $doc->addOutlineItem($intro);
$outline->first = $ref;$outline->last = $ref;$outline->count = 1;Page labels
Section titled “Page labels”Override the page-number labels shown in the viewer’s UI. Useful for front matter in lowercase roman, followed by arabic body pages:
use Phpdftk\Pdf\Core\Document\PageLabel;use Phpdftk\Pdf\Core\PdfName;
$front = new PageLabel();$front->s = new PdfName('r'); // lowercase roman: i, ii, iii…
$main = new PageLabel();$main->s = new PdfName('D'); // decimal: 1, 2, 3…
$doc->setPageLabels([ 0 => $front, // page index 0 starts roman labels 3 => $main, // page index 3 starts arabic labels]);Named destinations
Section titled “Named destinations”Build a name → destination map for inter-page navigation. Other annotations can then reference these by name (or by PdfReference into the tree).
$pageRef = new PdfReference($page->corePage()->objectNumber);$doc->setNamedDestinations([ 'intro' => Destination::fit($pageRef), 'chapter1' => Destination::xyz($pageRef, 72.0, 750.0, 1.0),]);Annotation builders
Section titled “Annotation builders”Wrap PDF annotation types without register() plumbing. Every method
attaches the annotation to the page’s /Annots array, registers it as
an indirect object, and returns the typed annotation for further
customization.
use Phpdftk\Geometry\Rectangle;
// Sticky note + popup$doc->addStickyNote($page, 72, 720, 'Reviewer comment goes here');
// Text-markup annotations operate over an array of quad Rectangles// (one per highlighted line):$doc->addHighlight($page, [ new Rectangle(72, 700, 200, 14), new Rectangle(72, 680, 180, 14),]);
// Shape annotations$doc->addSquare($page, new Rectangle(72, 500, 200, 80));$doc->addCircleAnnotation($page, new Rectangle(72, 400, 100, 100));
// Line and free-form ink$doc->addLineAnnotation($page, 100, 200, 300, 250);$doc->addInk($page, [[100, 100, 150, 120, 200, 110]]);
// Polygon / polyline take a list of [x, y] pairs$doc->addPolygon($page, [[100, 100], [200, 100], [150, 200]]);
// Standard rubber-stamp$doc->addStamp($page, new Rectangle(72, 100, 180, 60), 'Approved');Other annotation types: addFreeText, addUnderlineAnnotation,
addSquiggly, addStrikeout, addCaret, addPolyline,
addWatermarkAnnotation.
File attachments
Section titled “File attachments”attachFile() embeds a file from disk; attachFileBytes() accepts
in-memory bytes. Both register a FileSpec + EmbeddedFile and append
the FileSpec reference to the catalog’s /AF (Associated Files) array,
which is what ZUGFeRD / Factur-X invoices expect for the embedded XML.
$doc->attachFile( '/path/to/data.csv', description: 'Source spreadsheet', mimeType: 'text/csv',);
$doc->attachFileBytes( 'invoice.xml', $xmlBytes, relationship: 'Alternative', // ZUGFeRD wiring);Viewer preferences
Section titled “Viewer preferences”Set how viewers should display the document (window flags, page mode,
direction). Closure form gives you a fresh ViewerPreferences to
mutate; pre-built form passes one in directly.
use Phpdftk\Pdf\Core\Document\ViewerPreferences;
$doc->setViewerPreferences(function (ViewerPreferences $vp): void { $vp->displayDocTitle = true; $vp->fitWindow = true;});Layers (optional content)
Section titled “Layers (optional content)”addLayer() registers an OCG and adds it to the catalog’s
/OCProperties. Use Writer\Page::inLayer() to scope drawing
operations to the layer — viewers will toggle the wrapped content on
or off when the user shows / hides the layer in the bookmarks panel.
$markup = $doc->addLayer('Reviewer markup', visible: true);
$page->inLayer($markup, function ($p): void { $p->drawLine(72, 200, 540, 200, color: new RgbColor(0.8, 0.2, 0.2));});Actions
Section titled “Actions”Use the Action factory to build action objects, then attach them via
setOpenAction() (for document-open behavior) or directly on
annotations.
use Phpdftk\Pdf\Writer\Action;
$doc->setOpenAction(Action::uri('https://example.com/'));// Other shortcuts: Action::goTo, Action::javascript, Action::launch,// Action::namedAction('NextPage'), Action::resetForm([...]).Writer\Page transforms, opacity, page boxes
Section titled “Writer\Page transforms, opacity, page boxes”The page handle returned by addPage() exposes a fluent graphics-state
API. withTransform() scopes any transforms or opacity changes inside
a q ... Q save/restore pair so they don’t leak into subsequent
drawing.
$page->withTransform(function ($p): void { $p->translate(300, 300); $p->rotate(15); $p->setOpacity(0.4); $p->drawRectangle(0, 0, 120, 60, fill: new RgbColor(0.2, 0.5, 0.9));});
// Rotation, crop / bleed / trim / art boxes.$page->setRotation(90);$page->setTrimBox(new Rectangle(36, 36, 540, 720));Form fields
Section titled “Form fields”PdfDoc::addTextField, addCheckbox, addChoiceField, and
addSignatureField build an AcroForm lazily and attach a Widget
annotation to the page. The AcroForm sets /NeedAppearances so
viewers regenerate widget appearances at open time — no need to
pre-build the /AP dict for each field.
use Phpdftk\Pdf\Writer\Form\{TextFieldOptions, CheckboxOptions, ChoiceFieldOptions};
$doc->addTextField('name', $page, new Rectangle(72, 700, 200, 22), new TextFieldOptions(required: true, maxLength: 80));
$doc->addCheckbox('agree', $page, new Rectangle(72, 670, 14, 14), new CheckboxOptions(onValue: 'Yes', defaultChecked: false));
$doc->addChoiceField('country', $page, new Rectangle(72, 640, 200, 22), new ChoiceFieldOptions( choices: [['us', 'United States'], ['ca', 'Canada']], combo: true, sort: true, ));
$doc->addSignatureField('signature', $page, new Rectangle(72, 100, 200, 50));Push buttons and radio groups are planned but not in v1.
Gradients
Section titled “Gradients”Build a ShadingPattern with addLinearGradient or
addRadialGradient, then attach it to the page via
Writer\Page::useGradient() to get the resource name. The content
stream uses standard cs/scn operators:
use Phpdftk\Geometry\Point;
$g = $doc->addLinearGradient( new Point(0, 0), new Point(200, 0), [1, 0, 0], [0, 0, 1],);$name = $page->useGradient($g);$page->contentStream() ->setFillColorSpace('Pattern') ->setFillColor("/{$name} scn") ->rectangle(72, 600, 200, 80) ->fill();addRadialGradient takes two circles (inner / outer) for the same
two-stop pattern.
Spot colors
Section titled “Spot colors”registerSpotColor builds a Separation color space with the given
CMYK approximation. The returned SpotColor handle attaches to a page
via Writer\Page::useSpotColor():
$spot = $doc->registerSpotColor('Pantone 185 C', [0.0, 0.85, 0.6, 0.0]);$name = $page->useSpotColor($spot);$page->contentStream() ->setFillColorSpace($name) ->setFillColor(1.0) // full tint ->rectangle(72, 600, 200, 80) ->fill();Form XObject templates
Section titled “Form XObject templates”createTemplate captures a closure’s drawing into a reusable
FormXObject. Place it on any page with Writer\Page::drawTemplate(),
optionally scaling via $w/$h parameters. Repeated placements
reuse a single page-level XObject resource.
$badge = $doc->createTemplate( new Rectangle(0, 0, 100, 30), function ($cs): void { $cs->setFillColorRGB(0.2, 0.6, 0.2) ->rectangle(0, 0, 100, 30) ->fill(); },);
$page->drawTemplate($badge, 72, 500);$page->drawTemplate($badge, 240, 500); // same template, different positionMultimedia + 3D annotations (legacy)
Section titled “Multimedia + 3D annotations (legacy)”Sound and Movie annotations are deprecated in PDF 2.0 (replaced by
Rich Media); the wrappers are provided for legacy workflows. The
caller constructs the underlying Sound / Movie / ThreeDStream,
and PdfDoc registers + attaches the annotation:
$sound = new Sound(44100.0, $samples);$doc->addSoundAnnotation($page, $rect, $sound);
$movie = new Movie(new FileSpec('clip.mp4'));$doc->addMovieAnnotation($page, $rect, $movie);
$threeD = new ThreeDStream($u3dBytes);$doc->add3DAnnotation($page, $rect, $threeD);Phase 4 end-to-end examples
Section titled “Phase 4 end-to-end examples”use Phpdftk\Geometry\Rectangle;use Phpdftk\Pdf\Core\Document\ViewerPreferences;use Phpdftk\Pdf\Writer\Action;use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();
// 4.10 — Viewer preferences. Closure form mutates a fresh instance.$pdf->setViewerPreferences(function (ViewerPreferences $vp): void { $vp->displayDocTitle = true; $vp->fitWindow = true;});
// 4.8 — Open action. Jump straight to a URL the first time the doc opens// (most viewers respect this with a security prompt).$pdf->setOpenAction(Action::uri('https://phpdftk.dev/'));
$pdf->setTitle('Phase 4 overview');$pdf->addHeading('Phase 4 — Level 2 wrappers', 1);$pdf->addText('This document exercises the Phase 4 wrappers exposed on PdfDoc and Writer\\Page.');
// 4.3 — File attachments. Pass-through bytes (no file on disk needed).$pdf->doc()->attachFileBytes( 'invoice.xml', "<?xml version=\"1.0\"?>\n<invoice><id>I-001</id></invoice>", description: 'Embedded ZUGFeRD-style invoice (demo only)', mimeType: 'application/xml', relationship: 'Alternative',);
// 4.1 — Annotation builders.$pdf->newPage();$pdf->addHeading('Annotations', 2);$page = $pdf->doc()->addPage(); // explicit, positioned drawing surface$pdf->doc()->addStickyNote($page, 72, 720, 'A sticky note attached at (72, 720).');$pdf->doc()->addSquare($page, new Rectangle(72, 600, 200, 80));$pdf->doc()->addCircleAnnotation($page, new Rectangle(300, 600, 100, 80));$pdf->doc()->addLineAnnotation($page, 72, 540, 540, 540);$pdf->doc()->addStamp($page, new Rectangle(72, 460, 180, 60), 'Approved');
// 4.4 — Graphics state transforms + opacity. Scoped to a single withTransform block.$page->withTransform(function ($p): void { $p->translate(300, 300); $p->rotate(15); $p->setOpacity(0.4); $p->drawRectangle(0, 0, 120, 60, fill: new \Phpdftk\Color\RgbColor(0.2, 0.5, 0.9), );});
// 4.6 — Optional content (layers).$layer = $pdf->doc()->addLayer('Markup', visible: true);$page->inLayer($layer, function ($p): void { $p->drawLine(72, 200, 540, 200, color: new \Phpdftk\Color\RgbColor(0.8, 0.2, 0.2), );});
// 4.7 — Page rotation + boxes. Set a TrimBox slightly inside the MediaBox.$page->setTrimBox(new Rectangle(36, 36, 540, 720));
$pdf->save('phase4-overview.pdf');📄 View the sample PDF · View the full script on GitHub ↗
use Phpdftk\Geometry\Point;use Phpdftk\Geometry\Rectangle;use Phpdftk\Pdf\Writer\Form\CheckboxOptions;use Phpdftk\Pdf\Writer\Form\ChoiceFieldOptions;use Phpdftk\Pdf\Writer\Form\TextFieldOptions;use Phpdftk\Pdf\Writer\Pdf;
$pdf = new Pdf();$pdf->setTitle('Phase 4 batch 2 — fields, gradients, templates, spot colors');
$pdf->addHeading('Form fields', 1);$page = $pdf->doc()->addPage();
// 4.2 — Form fields. NeedAppearances tells viewers to render widgets// at open time, so we don't have to pre-build their appearance dicts.$pdf->doc()->addTextField( 'name', $page, new Rectangle(72, 720, 250, 22), new TextFieldOptions(defaultValue: 'Jane Doe', required: true),);$pdf->doc()->addTextField( 'notes', $page, new Rectangle(72, 640, 250, 60), new TextFieldOptions(multiline: true),);$pdf->doc()->addCheckbox( 'newsletter', $page, new Rectangle(72, 610, 14, 14), new CheckboxOptions(defaultChecked: true),);$pdf->doc()->addChoiceField( 'country', $page, new Rectangle(72, 570, 200, 22), new ChoiceFieldOptions( choices: [['us', 'United States'], ['ca', 'Canada'], ['mx', 'Mexico']], defaultValue: 'us', sort: true, ),);$pdf->doc()->addSignatureField( 'signature', $page, new Rectangle(72, 500, 200, 50),);
// 4.5 — Gradients. The shading pattern is registered on the document// and attached to the page via useGradient(), which returns the// resource name the content stream needs.$page = $pdf->doc()->addPage();$linear = $pdf->doc()->addLinearGradient( new Point(72, 720), new Point(540, 720), [0.95, 0.4, 0.4], [0.4, 0.4, 0.95],);$gradName = $page->useGradient($linear);$page->contentStream() ->saveGraphicsState() ->setFillColorSpace('Pattern') ->setFillColor('/' . $gradName . ' scn') ->rectangle(72, 660, 468, 80) ->fill() ->restoreGraphicsState();
$radial = $pdf->doc()->addRadialGradient( new Point(300, 480), 0.0, new Point(300, 480), 120.0, [1.0, 1.0, 1.0], [0.1, 0.1, 0.6],);$radName = $page->useGradient($radial);$page->contentStream() ->saveGraphicsState() ->setFillColorSpace('Pattern') ->setFillColor('/' . $radName . ' scn') ->rectangle(180, 360, 240, 240) ->fill() ->restoreGraphicsState();
// 4.11 — Spot colors. CMYK approximation for viewers without the ink.$page = $pdf->doc()->addPage();$spot = $pdf->doc()->registerSpotColor('Pantone 185 C', [0.0, 0.85, 0.6, 0.0]);$csName = $page->useSpotColor($spot);$page->contentStream() ->setFillColorSpace($csName) ->setFillColor(1.0) // full tint ->rectangle(72, 700, 468, 40) ->fill();
// 4.12 — Form XObject template, reused on multiple pages.$badge = $pdf->doc()->createTemplate(new Rectangle(0, 0, 100, 30), function ($cs): void { $cs->setFillColorRGB(0.2, 0.6, 0.2) ->rectangle(0, 0, 100, 30) ->fill() ->setFillColorRGB(1, 1, 1) ->beginText() ->setFont('/F1', 12) ->moveTextPosition(20, 10) ->showText('VERIFIED') ->endText();});// The badge content stream refers to '/F1' — register Helvetica on// the placing pages so the resource is in scope.$font = $pdf->writer()->addFont(new \Phpdftk\Pdf\Core\Font\Type1Font(\Phpdftk\Pdf\Core\Font\StandardFont::Helvetica));$page->drawTemplate($badge, 72, 200);$page->drawTemplate($badge, 240, 200);$page->drawTemplate($badge, 408, 200);
$pdf->save('phase4-batch2.pdf');📄 View the sample PDF · View the full script on GitHub ↗
Escape hatches
Section titled “Escape hatches”// Drop to Level 1 — full PdfWriter API: addFont, addImage, signing, encryption, conformance$writer = $doc->writer();
// Drop to Level 0 — raw PdfFileWriter byte assembly$fileWriter = $doc->writer()->fileWriter();Migration from PdfWriter
Section titled “Migration from PdfWriter”Several methods previously on PdfWriter now live on PdfDoc. They remain on PdfWriter as @deprecated forwarders for one minor release. Update call sites:
| Before | After |
|---|---|
$writer->setOutline($outline) | $doc->setOutline($outline) |
$writer->addOutlineItem($item) | $doc->addOutlineItem($item) |
$writer->setPageLabels($labels) | $doc->setPageLabels($labels) |
$writer->setNamedDestinations($dests) | $doc->setNamedDestinations($dests) |
$writer->setInfo($info) | $doc->setInfo($info) |
$writer->setMetadata($xmp) | $doc->setMetadata($xmp) |
$writer->syncInfoToMetadata() | $doc->syncInfoToMetadata() |
From a Pdf instance, the new path is $pdf->doc()->... — the existing $pdf->writer()->... still works during the deprecation window.