Skip to content

Navigation: annotations, links, outlines, actions, and attachments

The Navigation module builds the Portable Document Format (PDF) interactive layer. It covers annotations, link and Uniform Resource Locator (URL) annotations, the document outline (bookmarks), the table of contents, actions and trigger chains, page transitions, and embedded-file attachments with their associated-file relationships.

Terminal window
composer require nextpdf/core:^3

ISO 32000-2 §12 defines a PDF document’s interactive features: annotations, actions, destinations, and the document outline. This module encodes that layer for the engine. Manager classes collect intent; value objects carry the data those managers emit.

AnnotationManager is the broadest entry point. It adds text, free-text, line, square, circle, polygon, polyline, ink, and text-markup annotations (highlight and underline). It is hardened for hostile input: an unknown annotation subtype falls back to a safe default, an icon name that is not a valid PDF name token is replaced, and text-markup QuadPoints must arrive as a multiple of eight floats or they are dropped. These checks are PDF-injection defenses, not validation conveniences. LinkManager handles internal links, named destinations, and URL annotations. It pre-allocates annotation objects so the Writer can reference them before serialization.

BookmarkManager and TocBuilder build the navigation hierarchy. The document outline is the bookmark tree a viewer shows in its sidebar. TocBuilder renders an on-page table of contents and can use FontMetrics for leader/width layout. OutlineAutoGenerator derives an outline from document structure.

The Action hierarchy under NextPDF\Navigation\Action models trigger-driven behavior: GoTo (remote and embedded), launch, named, hide, reset/submit form, and set optional-content state. The §13 screen-annotation rendition action is deferred; it is future work and is not yet wired. Do not rely on it as a supported action. Action::withNext() builds the action chain that runs for a single trigger event. PageTransition models a presentation transition. FileAttachment embeds a file from a path or a byte string and tags it with an AFRelationship (the associated-file relationship enum). It returns a FileAttachmentResult and writes through writeAttachments(). The core managers are @since 1.0.0. The action hierarchy is @since 2.1.0. The FileAttachment constructor and AFRelationship arrived at @since 1.8.0 / @since 1.1.0.

ClassKey membersRole
AnnotationManageraddAnnotation(), addFreeText(), addLine(), addSquare(), addCircle(), addPolygon(), addInk(), addHighlight(), addUnderline()Annotation builder with injection-safe inputs (@since 1.0.0)
LinkManageraddLink(), setLink(), setDestination(), addLinkAnnotation(), addUrlAnnotation(), preallocateAnnotationObjects()Internal links, named destinations, URL annotations (@since 1.0.0)
BookmarkManageraddBookmark(), hasBookmarks(), write()Document outline (bookmark) tree (@since 1.0.0)
TocBuilderaddEntry(), hasEntries(), render(), getEntries()On-page table of contents (@since 1.0.0)
Action (interface)subtype(), toDictionary(), withNext(), nextChain()Trigger action + action chain (@since 2.1.0)
PageTransitiontoDict(), toInlineDict()Presentation page transition (@since 1.2.0)
FileAttachmentembedFile(), embedFileFromString(), hasAttachments(), getCount(), writeAttachments()Embedded-file attachments (@since 1.0.0)
AFRelationship (enum)toPdfName()Associated-file relationship (@since 1.1.0)
AnnotationFlagswith(), without(), has(), toInt()Immutable annotation flag set (@since 1.2.0)

Run composer docs:generate-api-php -- --module=Navigation to generate the full PHPDoc table.

examples/12-bookmarks-and-toc.php builds the document outline through the high-level facade that wraps BookmarkManager. To use the manager directly:

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Navigation\BookmarkManager;
$bookmarks = new BookmarkManager();
$bookmarks->addBookmark(title: 'Chapter 1', level: 0, pageIndex: 0);
$bookmarks->addBookmark(title: '1.1 Overview', level: 1, pageIndex: 0);
$bookmarks->addBookmark(title: 'Chapter 2', level: 0, pageIndex: 4);
if ($bookmarks->hasBookmarks()) {
// The Writer calls $bookmarks->write(...) during catalog serialization.
}

Embed an attachment with an explicit associated-file relationship, then check the result before finalizing the document.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Navigation\AFRelationship;
use NextPDF\Navigation\FileAttachment;
$attachments = new FileAttachment();
$attachments->embedFile(
path: '/srv/invoices/INV-2026-0042.xml',
description: 'Structured invoice source (Factur-X)',
afRelationship: AFRelationship::Source,
);
if ($attachments->hasAttachments()) {
// writeAttachments() is invoked by the Writer with a live ObjectRegistry;
// getCount() lets the application assert the expected attachment count.
assert($attachments->getCount() === 1);
}
  • AnnotationManager silently normalizes hostile input: an invalid subtype becomes the default, a non-name icon becomes the default icon, and malformed QuadPoints are dropped. The annotation is still produced; validate inputs upstream if you need them honored exactly.
  • LinkManager::preallocateAnnotationObjects() must run before the Writer serializes references, or link targets resolve to nothing.
  • Action::withNext() returns a new action with the chain attached; the interface supports immutable chaining, not in-place mutation.
  • FileAttachment::writeAttachments() requires a live ObjectRegistry from the Writer. Called in isolation, the manager accumulates intent but writes nothing until the Writer drives it.
  • AnnotationFlags is an immutable flag set. with()/without() return a new instance; the original is unchanged.

Annotation, link, and bookmark accumulation is O(n) in the number of items and performs no reflow. Embedded-file attachment cost is dominated by the embedded byte size, not by manager overhead. The default reference workload stays inside the 1500 ms wall / 64 MB peak budget. The reproducibility profile is structural: object numbers and the trailer /ID differ between runs. Two documents with the same navigation intent are structurally equal but not byte-identical.

AnnotationManager treats annotation subtype, icon, and QuadPoints as untrusted. It validates each value against an allow-list or pattern and replaces a bad value instead of writing it through. This closes a PDF name-injection vector. LinkManager::addUrlAnnotation() encodes a URL into a link action. The URL remains the consumer’s trust boundary: a hostile URL can still be valid, so sanitize destinations before adding them. FileAttachment embeds arbitrary bytes. Bound the embedded size, and treat any user-supplied attachment source as untrusted. See the engine threat model in /modules/core/security/.

The structures this module emits follow ISO 32000-2 §12 for annotations, actions, destinations, and the document-outline hierarchy. Per-feature clause references (annotation icons §12.5.6.4, text-markup QuadPoints §12.5.6.10, actions §12.6) are documented inline in src/Navigation/ and exercised by tests/Unit/Navigation/. These are implementation facts, not a statement of end-to-end PDF 2.0 conformance. Full-document conformance is validated by the oracle and golden suites described in /modules/core/conformance/.