Navigation, Annotations & Actions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

A PDF that is only a stack of pages is hard to use once it has more than a handful of them. Readers expect a bookmark panel, a clickable table of contents, links to web sites, sticky notes from reviewers, and a viewer that opens at the right page with the right title. All of that lives in the PDF catalog, in the page dictionaries and in annotation objects, not in the page content streams described in How a PDF File Works. This page covers the iText 9.8 API for it: outlines, destinations, actions, annotations, viewer preferences, page labels and JavaScript. Versions are stated as of 2026-10-09: iText Core 9.8.0 on Java 21. Every example below was compiled and run against exactly that version, and the resulting files were inspected for the catalog entries, actions and annotations they are supposed to create.

The running scenario is the Bookshelf Billing invoice. The invoice has a clickable contents list, a bookmark tree with the annexes nested under it, a link to the store’s web site, a reviewer note on the total, and an action that fires when the file is opened. The layout classes used to put text on the page are in Layout Elements, the document and page life cycle is in Document Lifecycle, and the module that supplies the classes of this page (kernel, with layout for the Link element) is described in iText Architecture & Modules.

Where the pieces live

Five different structures are involved, and knowing which one holds what avoids a lot of confusion:

  • The document catalog (PdfCatalog, reached with pdf.getCatalog()) holds the outline tree (/Outlines), the name trees for named destinations (/Names and /Dests) and document-level JavaScript, the page labels (/PageLabels), the viewer preferences (/ViewerPreferences), the open action (/OpenAction), the page mode and layout, and the document-level additional actions (/AA).

  • A page dictionary (PdfPage) holds the list of annotations (/Annots) and page-level additional actions (/AA).

  • An annotation (PdfAnnotation and its subclasses) is an interactive object placed on a rectangle of a page: a link, a note, a highlight, a stamp, a form widget. Annotations are not part of the page content, so a viewer can show, hide, edit or delete them without touching the content stream.

  • A destination says where to go: a page plus a view (fit the page, scroll to a point, zoom to a rectangle).

  • An action says what to do: go to a destination, open a URI, run JavaScript, jump to another file, execute a named viewer command. Links, bookmarks, buttons and the open action all just carry an action or a destination.

The diagram below is the data model in one picture. A bookmark or a link either points directly at a destination or wraps an action, and a destination is either explicit (a page object and a view) or named (a string looked up in a name tree).

flowchart LR CAT["PdfCatalog"] OUT["Outline tree PdfOutline"] NAMES["Name trees: Dests, JavaScript"] OPEN["Open action"] PAGE["PdfPage"] ANNOT["Annotations: link, text, markup"] ACT["PdfAction: URI, GoTo, JavaScript"] DEST["Destination: explicit or named"] CAT --> OUT CAT --> NAMES CAT --> OPEN CAT --> PAGE PAGE --> ANNOT OUT --> DEST OUT --> ACT ANNOT --> ACT ANNOT --> DEST OPEN --> ACT OPEN --> DEST ACT --> DEST NAMES --> DEST

What changed since iText 1 and 2

Books written for iText 1 and 2, including the classic 2006 text iText in Action (consulted for the order of concepts on this page and not reproduced), create bookmarks by hanging PdfOutline objects from the writer’s direct content, build destinations from a PdfWriter, mark link sources and targets with the Anchor element, and set viewer preferences with integer bit masks on the writer. None of that compiles against iText 7, 8 or 9, which are a rewrite of the library. The current approach:

  • Bookmarks: pdf.getOutlines(false) returns the root PdfOutline, and addOutline(String) creates children. The outline belongs to the PdfDocument; there is no per-writer direct content to hang it from.

  • Destinations: PdfExplicitDestination factories (createFit, createXYZ, createFitH, createFitR and the rest), or a string name looked up in the document’s name tree through PdfAction.createGoTo(String) and the layout method setDestination(String). The old Anchor element is replaced by the layout Link element for the source and by setDestination on any layout element for the target.

  • Viewer preferences are an object, PdfViewerPreferences, with one chained setter per entry instead of an integer flag set.

  • Page labels are set per page with PdfPage.setPageLabel(PageLabelNumberingStyle, String prefix, int firstPageNumber).

  • Annotations are created as PdfAnnotation subclasses and added with PdfPage.addAnnotation. Form field widgets are annotations too, but they are handled with the form classes, not with this page.

  • The old Chapter/Section machinery that produced bookmarks automatically does not exist any more; you build the outline tree yourself or from your own data model (see the Java 21 idioms section below).

Because iText 7 and later is AGPLv3 or commercial, as opposed to the MPL/LGPL of iText 1 and 2, porting old bookmark code also means checking the licence of the result, a topic covered in Introduction & Approaches.

Maven coordinates

Everything on this page is in the kernel and layout modules, which the itext-core aggregator pulls in. The Bouncy Castle adapter is only needed for encryption and signing; it is restated here so the coordinates are complete on the first example. iText is released under the AGPLv3 or under a commercial licence.

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>9.8.0</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>9.8.0</version>
</dependency>

The Spring Boot plumbing that returns generated bytes to a client is in Getting Started with Spring Boot.

Outlines, destinations and page labels

An outline (what viewers show as the bookmark panel) is a tree of titles, each one with a destination or an action. The root is pdf.getOutlines(false): the boolean tells the document whether to rebuild the in-memory tree from the catalog first, which only matters when a file was modified behind its back; false is right for a document you are creating. Each addOutline(title) returns the child; addDestination sets where it goes; setOpen(true) makes the viewer show its children expanded; addAction attaches an action instead of a destination. Setting the catalog’s page mode to PdfName.UseOutlines makes the viewer open the bookmark panel when the file is opened.

There are two kinds of destinations:

  • Explicit destinations, created with the PdfExplicitDestination factories: createFit(page) (the whole page), createFitH(page, top) (full width starting at a y coordinate), createXYZ(page, left, top, zoom) (a point and a zoom factor, 0 keeps the current zoom) and createFitR(page, ...) for a rectangle. They reference a page object, so they only work once the page exists, and they can be used from bookmarks, link annotations and the open action.

  • Named destinations: a string key mapped to a destination in the catalog’s name tree. A link or bookmark that uses the name stays valid when pages are reordered, because the name is resolved at click time, and links can be written before the target page exists, which is what a table of contents needs. They are added with PdfDocument.addNamedDestination(String, PdfObject) or, in the layout API, with setDestination("name") on any element, which registers the destination at the element’s position on the page where it is finally placed. Names are text strings, so other documents can target them with a remote GoTo action.

The example builds a contents page whose links use named destinations, one page per section whose headings register those names, a bookmark tree with a nested entry, page labels (the first page numbered with lowercase Roman numerals and the others as "Page 1", "Page 2"), an open action that lands on the second page, and the viewer preferences described in the next section.

package com.example.billing;

import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfOutline;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.navigation.PdfExplicitDestination;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PageLabelNumberingStyle;
import com.itextpdf.kernel.pdf.PdfViewerPreferences;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.AreaBreak;
import com.itextpdf.layout.element.Link;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.AreaBreakType;
import com.itextpdf.kernel.colors.ColorConstants;

import java.io.IOException;
import java.util.List;

public class NavigationDemo {

    record Section(String name, String destination) { }

    public static void main(String[] args) throws IOException {
        List<Section> sections = List.of(
                new Section("Invoice lines", "lines"),
                new Section("Taxes", "taxes"),
                new Section("Payment terms", "terms"));

        try (PdfDocument pdf = new PdfDocument(new PdfWriter("navigation.pdf"));
             Document document = new Document(pdf)) {
            PdfFont bold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);

            pdf.getCatalog().setLang(new PdfString("en-US"));
            pdf.getCatalog().setPageMode(PdfName.UseOutlines);
            pdf.getCatalog().setViewerPreferences(new PdfViewerPreferences()
                    .setDisplayDocTitle(true)
                    .setFitWindow(true)
                    .setHideToolbar(false)
                    .setDirection(PdfViewerPreferences.PdfViewerPreferencesConstants.LEFT_TO_RIGHT));
            pdf.getDocumentInfo().setTitle("Invoice 2026-0001");

            // Table of contents page: links are written before the targets exist
            document.add(new Paragraph("Contents").setFont(bold).setFontSize(18));
            for (Section s : sections) {
                Link link = new Link(s.name(), PdfAction.createGoTo(s.destination()));
                link.setFontColor(ColorConstants.BLUE).setUnderline();
                document.add(new Paragraph(link));
            }

            PdfOutline root = pdf.getOutlines(false);
            for (Section s : sections) {
                document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
                PdfPage page = pdf.getLastPage();
                // Named destination, resolved through the catalog name tree
                Paragraph heading = new Paragraph(s.name()).setFont(bold).setFontSize(16)
                        .setDestination(s.destination());
                document.add(heading);
                document.add(new Paragraph("Body of the section " + s.name() + "."));

                PdfOutline entry = root.addOutline(s.name());
                entry.addDestination(PdfExplicitDestination.createFit(page));
            }
            // Nested bookmark
            PdfOutline taxes = root.getAllChildren().get(1);
            PdfOutline vat = taxes.addOutline("VAT breakdown");
            vat.addDestination(PdfExplicitDestination.createXYZ(pdf.getPage(3), 36, 700, 1.0f));
            vat.setOpen(false);
            taxes.setOpen(true);

            // Page labels: the cover is "i", the rest is "Page 1", "Page 2", ...
            pdf.getPage(1).setPageLabel(PageLabelNumberingStyle.LOWERCASE_ROMAN_NUMERALS, null);
            pdf.getPage(2).setPageLabel(PageLabelNumberingStyle.DECIMAL_ARABIC_NUMERALS, "Page ", 1);

            // Open the document at the second page when it is opened
            pdf.getCatalog().setOpenAction(PdfExplicitDestination.createFit(pdf.getPage(2)));
        }
    }
}

The Paragraph that wraps the Link is a layout element; the PdfAction it carries is a kernel object. The layout Link is converted to a link annotation when the page is drawn. Source: the iText Knowledge Base page on creating actions, destinations and bookmarks, with the bookmark and named-destination variants in Bookmark examples and Named destinations. The knowledge base tutorials show some calls from earlier 7.x releases; the call shapes above are the ones that exist in 9.8.0, in particular PdfPage.setPageLabel for page labels (the 9.8.0 kernel has no public page-label builder class; the numbering styles are in the PageLabelNumberingStyle enum) and pdf.getOutlines(false) for the root outline.

Two details are worth knowing. First, a viewer shows the page label in its page box and uses it in "go to page", but the physical page index is still what iText’s getPage(int) takes. Second, page labels and outlines are catalog entries, so they stay with the file when you stamp an existing document (see the page on manipulating existing PDFs of this section), but they belong to the source document, not to its pages. When you copy pages into a new document, check the result and rebuild the tree or the labels if you need them.

Viewer preferences, page mode and open action

PdfViewerPreferences controls how a viewer presents the file, without changing the content. The chained setters in the example above map one-to-one to the entries of the PDF specification:

Setter Effect Typical use

setDisplayDocTitle(true)

The window title shows the document title (PdfDocumentInfo.setTitle) instead of the file name

Needed for accessible documents; PDF/UA requires it

setFitWindow(true)

Resizes the window to the first page

Small forms and receipts

setHideToolbar, setHideMenubar, setHideWindowUI

Hides viewer chrome

Kiosk-style viewing; use sparingly

setDirection(...)

Reading order (left to right or right to left) for the page layout of two-up views

Right-to-left languages

setPrintScaling, setDuplex, setNumCopies, setPickTrayByPDFSize

Defaults of the print dialog

Letters that must print duplex

setNonFullScreenPageMode

Which panel to show when leaving full screen

Presentations

Two other catalog entries decide what the reader sees first. PdfCatalog.setPageMode(PdfName) chooses the panel (UseOutlines for bookmarks, UseThumbs, UseNone, FullScreen, UseOC for layers, UseAttachments), and PdfCatalog.setPageLayout(PdfName) chooses how pages flow (SinglePage, OneColumn, TwoColumnLeft and so on). The open action is either a destination (as in the example) or a full PdfAction: setOpenAction(PdfDestination) or setOpenAction(PdfAction). Viewers differ in how many of these preferences they honour. Browser viewers ignore most of the window-related ones, so treat them as hints, not guarantees. Source: the Viewer preferences knowledge base page and the PdfViewerPreferences API.

Actions

A PdfAction is created by a static factory; there is no public constructor to remember. The ones that matter for a back-office service:

Factory What it does Notes

createURI(String)

Opens a web address

Always an absolute URI; createURI(String, boolean) adds the image-map flag

createGoTo(PdfDestination) and createGoTo(String)

Jumps inside the same file, to an explicit or a named destination

The string form is the named destination

createGoToR(String file, int page, boolean newWindow) and variants

Jumps to a page or named destination of another PDF

The file name is relative to the current file; viewers may prompt before opening

createGoToE(...)

Jumps into an embedded file, with a PdfTarget

Rare; used with portfolios

createNamed(PdfName)

Runs a viewer command: NextPage, PrevPage, FirstPage, LastPage, Print

The four navigation commands are standard; other names (such as Print) depend on the viewer

createJavaScript(String)

Runs Acrobat-style JavaScript

Supported by Adobe viewers and a few others; ignored by most browser viewers

createSubmitForm, createResetForm, createHide

Form-related actions

Used with form fields, not covered here

createLaunch

Opens an external application

Blocked by most viewers; avoid

Actions can be chained with PdfAction.next(PdfAction), so a single click can both go to a page and run a script. Actions can be attached to a link annotation (setAction), to an outline entry, to a form widget, to the catalog as the open action, and to the additional-action dictionaries described below.

The example below shows an invoice with the five most useful link styles, a document-level script, an open action, and page and catalog additional actions. It then writes the file and reads back the entries it wrote.

package com.example.billing;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.navigation.PdfExplicitDestination;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.AreaBreak;
import com.itextpdf.layout.element.Link;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.AreaBreakType;

import java.io.IOException;
import java.net.URI;

public class ActionsDemo {

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("actions.pdf"));
             Document document = new Document(pdf)) {

            // Document-level JavaScript runs once when the file is opened, before any page is shown
            pdf.getCatalog().getNameTree(PdfName.JavaScript).addEntry("init",
                    PdfAction.createJavaScript("var invoiceNumber = '2026-0001';").getPdfObject());

            // Open action: a JavaScript action instead of a destination
            pdf.getCatalog().setOpenAction(PdfAction.createJavaScript(
                    "app.alert('Invoice ' + invoiceNumber + ' opened');"));

            // Additional action of the document: WC runs when the document is about to close
            pdf.getCatalog().setAdditionalAction(PdfName.WC,
                    PdfAction.createJavaScript("console.println('closing');"));

            document.add(new Paragraph("Page one"));
            document.add(new Paragraph(new Link("Visit the store", PdfAction.createURI(
                    URI.create("https://example.com/bookshelf").toString()))));
            document.add(new Paragraph(new Link("Next page (named action)",
                    PdfAction.createNamed(PdfName.NextPage))));
            document.add(new Paragraph(new Link("Print this document",
                    PdfAction.createJavaScript("this.print({bUI: true, bSilent: false});"))));
            document.add(new Paragraph(new Link("Open the contract",
                    PdfAction.createGoToR("contract.pdf", 1, true))));

            document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
            document.add(new Paragraph("Page two"));
            // Page-level additional action: O runs when the page is opened (C when it is closed)
            pdf.getPage(2).setAdditionalAction(PdfName.O,
                    PdfAction.createJavaScript("console.println('page two opened');"));
            document.add(new Paragraph(new Link("Back to page one",
                    PdfAction.createGoTo(PdfExplicitDestination.createFit(pdf.getPage(1))))));
        }
    }
}

Notes on what the example creates, as inspected in the output file:

  • PdfAction.createURI produces /S /URI; the Link element becomes a /Link annotation with that action.

  • createNamed(PdfName.NextPage) produces the dictionary <</N/NextPage/S/Named>>. PdfName.NextPage exists as a constant; the other navigation constants are PrevPage, FirstPage, LastPage.

  • createGoToR("contract.pdf", 1, true) produces /S /GoToR with a /D [0 /FitH 10000] array (the first page, scrolled to the top) and /NewWindow true. The target must be in the same directory when the file is opened from disk.

  • The document-level script is stored in the /Names /JavaScript name tree, and the open action is an action dictionary rather than a destination array.

  • getNameTree(PdfName.JavaScript) is the way to add document-level scripts in 9.8.0: there is no dedicated addJavaScript method on PdfDocument, so older code that called one has to switch to the name tree.

  • PdfName.WC (will close) and PdfName.O (page opened) are the keys of the document and page additional-action dictionaries. The other document keys are WS (will save), DS (did save), WP (will print) and DP (did print); the other page key is C (page closed).

Source: the iText Knowledge Base page Actions and annotations, and the PdfAction API reference, which lists every factory. When a tutorial shows an older method name, the compiled example above is the truth.

JavaScript in PDFs

PDF JavaScript is the Acrobat JavaScript dialect, a small API (app, this as the document, console, util, event for form fields) layered on ECMAScript 3 plus some later features, defined by Adobe’s reference for the Acrobat SDK. It can run in four places: as the action of a link or a button (user-triggered), as the open action, as a document-level script in the /JavaScript name tree (runs once when the document is opened, and defines functions the other scripts can call), and in additional actions on the catalog, on pages and on form fields (calculation and validation scripts, covered by the forms pages). The JavaScript code is a PDF string, so quotes and backslashes in it are escaped by iText when it is written; in the Java source, a text block keeps multi-line scripts readable, as in the Java 21 section below.

Practical limits matter more than the syntax:

  • Support is uneven. Adobe Acrobat and Reader run the full API. Browser-built-in viewers and many mobile and command-line viewers run little or none of it, so test the viewers your readers actually use. Never make a business rule depend on a PDF script; use it to improve the experience for readers who have it, and make the document correct without it.

  • Security tooling blocks it. Email gateways, document-management systems and archives often reject or strip PDFs with JavaScript. PDF/A forbids it, as described in PDF Standards & Conformance, so a document that must be archived as PDF/A cannot use any of the actions in this section that run scripts.

  • An open-action script runs automatically, which is what security teams dislike. Prefer a document-level function that a visible button calls.

The Acrobat JavaScript API reference is hosted by Adobe and its URL has moved more than once; search for "Acrobat JavaScript API reference" on Adobe’s developer site rather than relying on a bookmark.

Annotations

An annotation is an object in the page’s /Annots array that a viewer draws and manages on top of the page content. The PdfAnnotation class has one subclass per annotation subtype. The ones used in business documents:

Class Subtype Use

PdfLinkAnnotation

/Link

A clickable rectangle with an action or a destination; the layout Link element creates these for you

PdfTextAnnotation

/Text

A sticky note with an icon (Note, Comment, Help, Insert, Key, NewParagraph, Paragraph) and a popup

PdfTextMarkupAnnotation

/Highlight, /Underline, /StrikeOut, /Squiggly

Review marks over text; they need quad points that cover the text

PdfFreeTextAnnotation

/FreeText

A text box shown directly on the page; the default appearance string chooses font, size and colour

PdfPopupAnnotation

/Popup

The window that opens from a note or markup annotation

PdfStampAnnotation, PdfFileAttachmentAnnotation, PdfLineAnnotation, PdfSquareAnnotation, PdfCircleAnnotation, PdfInkAnnotation, PdfPolyGeomAnnotation

/Stamp, /FileAttachment, /Line and so on

Approval stamps, attachments and drawing marks

PdfWidgetAnnotation

/Widget

The visible part of a form field; handled by the forms API

Every annotation has a rectangle (the Rectangle argument is x, y, width, height in points, with the origin at the bottom left of the page as explained in the page on the PDF file structure), optional contents (the text shown in the note), a title (the author name), a colour, and flags. PdfAnnotation.PRINT makes the annotation print with the page. Without it, many viewers hide the annotation when printing. PdfAnnotation.HIDDEN, NO_VIEW, READ_ONLY and LOCKED complete the set. Markup annotations (PdfMarkupAnnotation and its subclasses) additionally have a subject, a creation date, an opacity, a popup, and a reply thread (setInReplyTo).

The example creates a link with a visible blue border, a sticky note with a popup, a highlight over the total, and a free text box, and then reopens the file in stamping mode to list and remove the note and its popup. Removing a note without its popup would leave an orphan popup annotation, so the example removes both.

package com.example.billing;

import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.colors.DeviceRgb;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfTextAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfTextMarkupAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfFreeTextAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfPopupAnnotation;
import com.itextpdf.kernel.pdf.annot.da.AnnotationDefaultAppearance;
import com.itextpdf.kernel.pdf.annot.da.StandardAnnotationFont;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;

import java.io.IOException;
import java.util.ArrayList;

public class AnnotationsDemo {

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("annotations.pdf"))) {
            PdfPage page = pdf.addNewPage(PageSize.A4);
            PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
            PdfCanvas canvas = new PdfCanvas(page);
            canvas.beginText().setFontAndSize(font, 14).moveText(36, 780)
                    .showText("Invoice 2026-0001 - total 128.50 EUR").endText();
            canvas.beginText().setFontAndSize(font, 12).moveText(36, 740)
                    .showText("Visit the Bookshelf web site").endText();

            // Link annotation with a URI action and a visible border
            PdfLinkAnnotation link = new PdfLinkAnnotation(new Rectangle(36, 736, 160, 16));
            link.setAction(PdfAction.createURI("https://example.com/bookshelf"));
            link.setBorder(new PdfArray(new float[] {0, 0, 1}));
            link.setColor(ColorConstants.BLUE);
            page.addAnnotation(link);

            // Text (sticky note) annotation with a popup
            PdfTextAnnotation note = new PdfTextAnnotation(new Rectangle(500, 780, 20, 20));
            note.setIconName(PdfName.Note).setOpen(false);
            note.setTitle(new PdfString("Accounting"))
                    .setContents("Check the VAT rate before sending")
                    .setColor(ColorConstants.YELLOW)
                    .setFlags(PdfAnnotation.PRINT);
            PdfPopupAnnotation popup = new PdfPopupAnnotation(new Rectangle(400, 600, 160, 100));
            note.setPopup(popup);
            page.addAnnotation(note);
            page.addAnnotation(popup);

            // Highlight markup over the total: quad points go top-left, top-right, bottom-left, bottom-right
            Rectangle total = new Rectangle(36, 776, 260, 18);
            float[] quad = {total.getLeft(), total.getTop(), total.getRight(), total.getTop(),
                    total.getLeft(), total.getBottom(), total.getRight(), total.getBottom()};
            PdfTextMarkupAnnotation highlight = PdfTextMarkupAnnotation.createHighLight(total, quad);
            highlight.setColor(ColorConstants.YELLOW).setContents("Total to be paid");
            page.addAnnotation(highlight);

            // Free text annotation that shows a text box on the page
            PdfFreeTextAnnotation free = new PdfFreeTextAnnotation(new Rectangle(36, 640, 200, 40),
                    new PdfString("Paid by bank transfer"));
            free.setDefaultAppearance(new AnnotationDefaultAppearance()
                    .setFont(StandardAnnotationFont.Helvetica).setFontSize(12)
                    .setColor(new DeviceRgb(0.7f, 0.1f, 0.1f)));
            page.addAnnotation(free);
        }

        // Reading and removing the annotations of an existing file in stamping mode
        try (PdfDocument stamper = new PdfDocument(new PdfReader("annotations.pdf"),
                new PdfWriter("annotations-clean.pdf"))) {
            for (int i = 1; i <= stamper.getNumberOfPages(); i++) {
                PdfPage p = stamper.getPage(i);
                for (PdfAnnotation a : new ArrayList<>(p.getAnnotations())) {
                    System.out.println(a.getSubtype() + " " + a.getContents());
                    if (PdfName.Text.equals(a.getSubtype()) || PdfName.Popup.equals(a.getSubtype())) {
                        p.removeAnnotation(a);
                    }
                }
            }
        }
    }
}

Things the example does on purpose:

  • The popup is added to the page like any other annotation, and it is also referenced from the note with setPopup.

  • The highlight’s rectangle and its quad points describe the same area. A highlight whose quad points are outside its rectangle is drawn in some viewers and not in others. Quad points are eight numbers per quadrilateral; the example lists the corners as top-left, top-right, bottom-left and bottom-right, the order that mainstream viewers expect even though the specification text describes counterclockwise corners.

  • The free-text annotation sets a default appearance with a standard font (StandardAnnotationFont.Helvetica) and an RGB colour; the appearance of the annotation is generated by the viewer from its dictionary entries.

  • page.getAnnotations() builds a new list from the page’s /Annots array on every call, a snapshot rather than a live view, so removeAnnotation during the loop does not disturb the iteration. The extra copy in the loop is only a defensive habit and is not required.

Source: the iText Knowledge Base page Creating and adding annotations and Actions and annotations, and the PdfAnnotation API reference. The knowledge base examples are written for earlier 7.x releases and use some deprecated constructors; the example above compiles against 9.8.0 as written.

Annotations interact with the standards in two ways. In a PDF/UA document link annotations must be part of the structure tree and described. The layout Link element takes part in tagging when the document is tagged, while a hand-made PdfLinkAnnotation added directly to a page is your responsibility. A PDF/A document also restricts annotations (for instance, they need an appearance stream and the print flag in some levels). The conformance classes, tagging and alternative text are covered in the PDF/A and PDF/UA pages of this section.

Java 21 idioms for navigation code

Navigation data is naturally a tree of small immutable values, which is what records, sealed interfaces and pattern matching are for. This is finding O28 of this section’s source analysis: the books consulted for the order of concepts here target Java 1.3 to 5 and use anonymous classes, raw collections and integer flag constants, none of which is needed today. The example models bookmarks as a record tree, link targets as a sealed interface so that the compiler checks the switch that turns them into PdfAction`s is exhaustive, and the JavaScript as a text block. A `try-with-resources block closes the PdfDocument and the layout Document in the right order (the layout Document first, which flushes the pending content, then the PdfDocument), so the file is complete even when an exception interrupts the code afterwards.

package com.example.billing;

import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfOutline;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.navigation.PdfExplicitDestination;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.AreaBreak;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.AreaBreakType;

import java.io.IOException;
import java.util.List;

public class ModernNavigation {

    /** What happens when the reader activates a link. A closed set, so the switch below is exhaustive. */
    sealed interface Target permits Web, Page, Script { }

    record Web(String uri) implements Target { }

    record Page(int number) implements Target { }

    record Script(String code) implements Target { }

    /** A node of the bookmark tree: a title, the page it points to and its children. */
    record Bookmark(String title, int page, List<Bookmark> children) {
        Bookmark(String title, int page) {
            this(title, page, List.of());
        }
    }

    static PdfAction toAction(Target target, PdfDocument pdf) {
        return switch (target) {
            case Web w -> PdfAction.createURI(w.uri());
            case Page p -> PdfAction.createGoTo(PdfExplicitDestination.createFit(pdf.getPage(p.number())));
            case Script s -> PdfAction.createJavaScript(s.code());
        };
    }

    static void addOutlines(PdfOutline parent, List<Bookmark> bookmarks, PdfDocument pdf) {
        for (Bookmark b : bookmarks) {
            PdfOutline outline = parent.addOutline(b.title());
            outline.addDestination(PdfExplicitDestination.createFit(pdf.getPage(b.page())));
            outline.setOpen(!b.children().isEmpty());
            addOutlines(outline, b.children(), pdf);
        }
    }

    public static void main(String[] args) throws IOException {
        List<Bookmark> tree = List.of(
                new Bookmark("Invoice", 1),
                new Bookmark("Annexes", 2, List.of(
                        new Bookmark("Delivery note", 2),
                        new Bookmark("Terms of sale", 3))));

        // Text block: the JavaScript stays readable instead of being a concatenation of quoted lines
        String welcomeScript = """
                if (this.numPages > 1) {
                    app.alert("This invoice has " + this.numPages + " pages");
                }
                """;

        try (PdfDocument pdf = new PdfDocument(new PdfWriter("modern-navigation.pdf"));
             Document document = new Document(pdf)) {
            PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
            document.setFont(font);
            for (int i = 1; i <= 3; i++) {
                if (i > 1) {
                    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
                }
                document.add(new Paragraph("Page " + i));
            }
            addOutlines(pdf.getOutlines(false), tree, pdf);
            pdf.getCatalog().setPageMode(PdfName.UseOutlines);
            // Runs when the file is opened in a viewer that allows JavaScript
            pdf.getCatalog().setOpenAction(toAction(new Script(welcomeScript), pdf));
            System.out.println(toAction(new Web("https://example.com/terms"), pdf).getPdfObject());
        }
    }
}

The Bookmark record has a convenience constructor for the common leaf case, and addOutlines recurses over children(), so a service can build the tree from a JPA projection or a Spring configuration instead of writing addOutline calls one by one. Adding a new link target means adding a record to the permits list, and the compiler then flags every switch that does not handle it. Source: the same iText knowledge base pages as above for the API, and the Java 21 language guide for the language features.

Choosing the right mechanism

Need Use Why

A bookmark panel for a long document

PdfOutline with explicit destinations

Stable, supported by every viewer

A clickable table of contents

Layout Link with PdfAction.createGoTo(String) and setDestination on the headings

Survives reordering; links can be created before targets

A link to a web page

Layout Link with PdfAction.createURI

Tagged automatically in tagged documents

A reviewer comment on a value

PdfTextAnnotation plus PdfPopupAnnotation

Editable in any viewer; add PRINT if it must print

Visible text that must not be lost on print

Page content, not a free-text annotation

Annotations may be hidden or flattened by workflows

Page numbers shown to the reader as "i, ii, 1, 2"

PdfPage.setPageLabel

Matches the numbers printed on the pages

A script that improves the experience

Document-level /JavaScript entry plus a button or link action

Easier to disable and audit than an open action

A document that must pass PDF/A

No JavaScript or launch actions; links and tagged annotations only

Forbidden by the standard

Summary

Navigation, annotations and actions all hang off the catalog and the page dictionaries and are separate from the page content. Outlines use pdf.getOutlines(false) and addOutline; destinations are explicit (PdfExplicitDestination) or named; the layout Link plus PdfAction creates link annotations; PdfPage.setPageLabel sets page labels; PdfViewerPreferences and setPageMode choose how the viewer starts; document-level JavaScript goes in the /JavaScript name tree. Annotations are objects you add to and remove from a page and must carry the PRINT flag to print. Three pitfalls repeat: JavaScript is not portable, named destinations are looked up by exact string, and removing a markup annotation means removing its popup too.

References