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 withpdf.getCatalog()) holds the outline tree (/Outlines), the name trees for named destinations (/Namesand/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 (
PdfAnnotationand 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).
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 rootPdfOutline, andaddOutline(String)creates children. The outline belongs to thePdfDocument; there is no per-writer direct content to hang it from. -
Destinations:
PdfExplicitDestinationfactories (createFit,createXYZ,createFitH,createFitRand the rest), or a string name looked up in the document’s name tree throughPdfAction.createGoTo(String)and the layout methodsetDestination(String). The oldAnchorelement is replaced by the layoutLinkelement for the source and bysetDestinationon 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
PdfAnnotationsubclasses and added withPdfPage.addAnnotation. Form field widgets are annotations too, but they are handled with the form classes, not with this page. -
The old
Chapter/Sectionmachinery 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
PdfExplicitDestinationfactories: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) andcreateFitR(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, withsetDestination("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 |
|---|---|---|
|
The window title shows the document title ( |
Needed for accessible documents; PDF/UA requires it |
|
Resizes the window to the first page |
Small forms and receipts |
|
Hides viewer chrome |
Kiosk-style viewing; use sparingly |
|
Reading order (left to right or right to left) for the page layout of two-up views |
Right-to-left languages |
|
Defaults of the print dialog |
Letters that must print duplex |
|
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 |
|---|---|---|
|
Opens a web address |
Always an absolute URI; |
|
Jumps inside the same file, to an explicit or a named destination |
The string form is the named destination |
|
Jumps to a page or named destination of another PDF |
The file name is relative to the current file; viewers may prompt before opening |
|
Jumps into an embedded file, with a |
Rare; used with portfolios |
|
Runs a viewer command: |
The four navigation commands are standard; other names (such as |
|
Runs Acrobat-style JavaScript |
Supported by Adobe viewers and a few others; ignored by most browser viewers |
|
Form-related actions |
Used with form fields, not covered here |
|
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.createURIproduces/S /URI; theLinkelement becomes a/Linkannotation with that action. -
createNamed(PdfName.NextPage)produces the dictionary<</N/NextPage/S/Named>>.PdfName.NextPageexists as a constant; the other navigation constants arePrevPage,FirstPage,LastPage. -
createGoToR("contract.pdf", 1, true)produces/S /GoToRwith 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/JavaScriptname 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 dedicatedaddJavaScriptmethod onPdfDocument, so older code that called one has to switch to the name tree. -
PdfName.WC(will close) andPdfName.O(page opened) are the keys of the document and page additional-action dictionaries. The other document keys areWS(will save),DS(did save),WP(will print) andDP(did print); the other page key isC(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 |
|---|---|---|
|
|
A clickable rectangle with an action or a destination; the layout |
|
|
A sticky note with an icon ( |
|
|
Review marks over text; they need quad points that cover the text |
|
|
A text box shown directly on the page; the default appearance string chooses font, size and colour |
|
|
The window that opens from a note or markup annotation |
|
|
Approval stamps, attachments and drawing marks |
|
|
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/Annotsarray on every call, a snapshot rather than a live view, soremoveAnnotationduring 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 |
|
Stable, supported by every viewer |
A clickable table of contents |
Layout |
Survives reordering; links can be created before targets |
A link to a web page |
Layout |
Tagged automatically in tagged documents |
A reviewer comment on a value |
|
Editable in any viewer; add |
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" |
|
Matches the numbers printed on the pages |
A script that improves the experience |
Document-level |
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
-
iText Knowledge Base, Chapter 6: Creating actions, destinations, and bookmarks: https://kb.itextpdf.com/itext/chapter-6-creating-actions-destinations-and-bookma
-
iText Knowledge Base, Bookmark examples: https://kb.itextpdf.com/itext/bookmark-examples
-
iText Knowledge Base, Named destinations: https://kb.itextpdf.com/itext/named-destinations
-
iText Knowledge Base, Actions and annotations: https://kb.itextpdf.com/itext/actions-and-annotations
-
iText Knowledge Base, Creating and adding annotations: https://kb.itextpdf.com/itext/creating-and-adding-annotations
-
iText Knowledge Base, Viewer preferences: https://kb.itextpdf.com/itext/viewer-preferences
-
iText 9.8.0 API,
PdfOutline: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/PdfOutline.html -
iText 9.8.0 API,
PdfAction: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/action/PdfAction.html -
iText 9.8.0 API,
PdfExplicitDestination: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/navigation/PdfExplicitDestination.html -
iText 9.8.0 API,
PdfAnnotation: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/annot/PdfAnnotation.html -
iText 9.8.0 API,
PdfLinkAnnotation: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/annot/PdfLinkAnnotation.html -
iText 9.8.0 API,
PdfViewerPreferences: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/PdfViewerPreferences.html -
iText 9.8.0 API,
PdfPage: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/PdfPage.html -
iText 9.8.0 API,
PdfCatalog: https://api.itextpdf.com/iText/java/latest/com/itextpdf/kernel/pdf/PdfCatalog.html -
iText 9.8.0 API,
Link: https://api.itextpdf.com/iText/java/latest/com/itextpdf/layout/element/Link.html -
iText Core on Maven Central: https://central.sonatype.com/artifact/com.itextpdf/itext-core
-
iText licensing, AGPLv3 versus commercial: https://kb.itextpdf.com/itext/what-is-the-difference-between-agpl-and-commercial
-
PDF Association, ISO 32000-2 (PDF 2.0) sponsored access: https://pdfa.org/sponsored-standards/
-
Oracle, Java 21 language updates: https://docs.oracle.com/en/java/javase/21/language/