Add links and text annotations
At a glance
Section titled “At a glance”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.
Install
Section titled “Install”composer require nextpdf/core:^3No 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.
Conceptual overview
Section titled “Conceptual overview”Internal links use a three-call pattern, which supports forward references:
addLink()reserves a link identifier (an int).link($x, $y, $w, $h, $id)places a clickable rectangle bound to that id.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.
API surface
Section titled “API surface”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.
Code sample — Quick start
Section titled “Code sample — Quick start”<?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');Code sample — Production
Section titled “Code sample — Production”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";Edge cases & gotchas
Section titled “Edge cases & gotchas”pageIndexis 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 fromaddLink(). 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 withsetLink()has no destination. The rectangle is clickable but inert. Bind it beforesave(). - The clickable area is the rectangle, not the text.
link()takes explicitx, 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.
Performance
Section titled “Performance”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.
Security notes
Section titled “Security notes”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.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_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.
Commercial context
Section titled “Commercial context”Not applicable. Links and text annotations are Core capabilities with no Premium gate.