Skip to content

Add links and text annotations

Use this recipe to add three interactive elements: an internal link that jumps to another page, an external link that opens a Uniform Resource Locator (URL), and a text annotation, also called a sticky note. It follows examples/17-links.php and examples/29-annotations.php.

A clickable link is an ISO 32000-2 link annotation: a hypertext link to a destination or an action. A sticky note is a text annotation. It appears as an icon when closed and as a popup when open.

Terminal window
composer require nextpdf/core:^3

No optional extension is required. The link and annotation application programming interface (API) has been stable since 1.0.0 and runs on the 8.1–8.4 backport matrix.

Internal links use a three-call pattern, which supports forward references:

  1. addLink() reserves a link identifier (an int).
  2. link($x, $y, $w, $h, $id) places a clickable rectangle bound to that id.
  3. setLink($id, $pageIndex, $y) binds the id to a destination page (zero-based) and Y.

Call step 3 after step 2 when you link to a page that does not exist yet. The destination uses an explicit destination form. ISO 32000-2 §12.3.2.2 defines [page /XYZ left top zoom], where a null component retains the reader’s current value.

For an external link, pass a URL string to link() instead of an int. NextPDF then emits a Uniform Resource Identifier (URI) action whose URI is a required UTF-8 ASCII string. As a shortcut, write($height, $text, $link) draws inline text with a URL attached, and annotation($x, $y, $w, $h, $text) places a Text subtype sticky note. ISO 32000-2 requires SubtypeLink for a link annotation and defines the text-annotation icon and popup behavior.

The API surface is generated from PHPDoc. This recipe relies on these methods:

  • addLink(): int — reserve an internal link identifier.
  • setLink(int $linkId, int $pageIndex = -1, float $y = 0): static — bind an id to a destination page (zero-based) and Y.
  • link(float $x, float $y, float $w, float $h, string|int $link): static — create a clickable rectangle; an int id is internal, and a string is an external URL.
  • write(float $height, string $text, string $link = ''): static — write inline text with an optional URL.
  • annotation(float $x, float $y, float $w, float $h, string $text, string $subtype = 'Text'): static — add a sticky-note annotation.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$jump = $doc->addLink(); // 1. reserve an id (forward reference)
$doc->addPage();
$doc->setFont('helvetica', 'B', 12);
$x = $doc->getX();
$y = $doc->getY();
$doc->cell(60, 10, 'Go to page 2', newLine: true);
$doc->link($x, $y, 60, 10, $jump); // 2. clickable rectangle -> id
$doc->link($doc->getX(), $doc->getY(), 80, 10, 'https://nextpdf.dev'); // external
$doc->addPage();
$doc->setLink($jump, pageIndex: 1, y: 0); // 3. bind id to page 2 (index 1)
$doc->cell(0, 10, 'Destination (page 2).', newLine: true);
$doc->annotation(x: 180, y: 20, w: 10, h: 10, text: 'A reviewer note.');
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/links.pdf');

This is the complete, harness-ready example. It honors NEXTPDF_COOKBOOK_OUTPUT and adds no entropy of its own.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Links and Annotations');
// Reserve the internal-link id before its destination page exists.
$linkToPage3 = $doc->addLink();
// Page 1 — source of the internal and external links.
$doc->addPage();
$doc->setFont('helvetica', 'B', 20);
$doc->cell(0, 14, 'Links and Annotations', newLine: true);
$doc->ln(6);
$doc->setFont('helvetica', 'B', 12);
$doc->setTextColor(0, 51, 153);
$linkX = $doc->getX();
$linkY = $doc->getY();
$doc->cell(60, 10, 'Go to Page 3', newLine: true);
$doc->link($linkX, $linkY, 60, 10, $linkToPage3); // internal: int id
$doc->setTextColor(0);
$doc->ln(6);
$doc->setFont('helvetica', 'B', 12);
$doc->setTextColor(0, 102, 204);
$doc->write(10, 'Visit https://nextpdf.dev', link: 'https://nextpdf.dev');
$doc->setTextColor(0);
$doc->ln(6);
$urlX = $doc->getX();
$urlY = $doc->getY();
$doc->cell(80, 10, 'NextPDF on GitHub', newLine: true);
$doc->link($urlX, $urlY, 80, 10, 'https://github.com/nextpdf-labs/nextpdf');
// Page 2 — intermediate.
$doc->addPage();
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'Internal links can jump across pages; this page is '
. 'skipped by the link on page 1.');
// Page 3 — destination + a sticky note.
$doc->addPage();
$doc->setLink($linkToPage3, pageIndex: 2, y: 0); // bind id to page 3 (index 2)
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 14, 'Page 3 — Link Target', newLine: true);
$doc->ln(4);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'You arrived via the internal link on page 1.');
$doc->annotation(
x: 185, y: 40, w: 10, h: 10,
text: 'Sticky note: appears as an icon; click to read this text.',
);
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/links.pdf';
$doc->save($out);
echo "Created links.pdf\n";
  • pageIndex is zero-based. setLink($id, pageIndex: 2, …) targets the third page. Off-by-one errors here are the most common mistake.
  • String vs. int on link(). An int is an internal destination id from addLink(). A string is an external URL. If you pass the wrong type, you get the wrong kind of link with no error.
  • Bind every reserved id. An addLink() id that you never bind with setLink() has no destination. The rectangle is clickable but inert. Bind it before save().
  • The clickable area is the rectangle, not the text. link() takes explicit x, y, w, h. Size it to cover the visible text. The engine does not measure glyphs for you.
  • External links are not validated. NextPDF stores the URI verbatim. It does not verify that the target resolves or is safe. The reader resolves it.

Each link or annotation adds one annotation dictionary to the page. The cost is O(1) per element. Hundreds per page stay well within the 2000 ms / 64 MB budget.

External link targets are stored verbatim and resolved by the reader, not the library. Treat user-supplied URLs as untrusted. Allow-list the scheme, which is typically https. Reject javascript: and file: before passing them to link(). Annotation text appears in the reader user interface (UI), so length-bound and sanitize user-controlled note content. No input parsing or network access occurs in this recipe.

StatementSpecClausereference_id
A link annotation is a hypertext link to a destination or an action.ISO 32000-2§12.5.6.5
Subtype is Link for a link annotation.ISO 32000-2§12.5.6.5
A URI action’s URI is a required UTF-8 ASCII string.ISO 32000-2§12.6.4.8
A text annotation is a sticky note (closed = icon, open = popup).ISO 32000-2§12.5.6.4
Explicit destination [page /XYZ left top zoom]; null retains the current value.ISO 32000-2§12.3.2.2

Reproducibility profile — structural. The trailer /ID and date atoms vary on each save. The harness strips those atoms and then compares the qpdf-normalized structure. This recipe describes how NextPDF produces the structure. It does not assert ISO 32000-2 conformance as a blanket claim.

Not applicable. Links and text annotations are Core capabilities with no Premium gate.