Hyperlinks, Bookmarks & Report Books
|
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. |
Baseline date: 2026-10-09. A PDF that people read on a screen is more than its pages. A reader expects the category names in the monthly Bookshelf Billing sales report to be clickable, the book titles to open the store page, the viewer’s outline pane to list the sections, and, when the report is assembled from several documents, a table of contents at the front and one entry per document in the outline. JasperReports covers this with four features that build on each other: hyperlinks and anchors on text fields and images, bookmarks that turn anchors into the PDF outline, report parts that assemble several reports into a single "book" with a generated table of contents, and batch export that concatenates the prints of several independent reports into one PDF.
The examples use JasperReports 7.0.8 and the v7 JRXML syntax (namespace-free, with kind on each element), so
they do not load in 6.x, and the 6.x templates that books and older posts show do not load in 7.0. The template
structure is in JRXML Report Design, the expressions in
Expressions, Parameters, Fields & Variables,
and the grouping used by the sales report in
Groups, Sorting & Filtering.
The exporter settings that surround bookmarks (metadata, PDF/A, encryption) are in
PDF Export Configuration.
The examples were compiled and run with Java 21 and JasperReports 7.0.8, and the resulting PDFs were read back with Apache PDFBox 3.0.8 to check the outline entries, the page each entry points to and the type and target of each link. Statements below that say something fails or behaves in a particular way come from that run and say so.
Setup
The Maven coordinates are the ones used throughout this section: the engine, the PDF exporter, the bundled fonts
and the Eclipse compiler. The jasperreports-pdf artifact brings the OpenPDF producer. Apache PDFBox is only
needed by the small verification program at the end of the page, not by the report code.
<dependencies>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-jdt</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
</dependencies>
Three small Java helpers are shared by the examples. Sale is a plain JavaBean, because JRBeanCollectionDataSource
reads getters and, as the run for this page confirmed, a Java record is not accepted as a bean. ReportCompiler
loads a template from the classpath and compiles it; compile once and reuse the JasperReport in a real service, as
described in JasperReports Architecture & Lifecycle.
SalesData makes thirty fake sales in three categories; a real service reads them from a repository, see
Data Sources & Query Executers.
package bookshelf;
import java.math.BigDecimal;
/** A plain JavaBean: JRBeanCollectionDataSource reads getters, a Java record is not accepted. */
public class Sale {
private final String category;
private final String title;
private final String isbn;
private final Integer quantity;
private final BigDecimal amount;
public Sale(String category, String title, String isbn, Integer quantity, BigDecimal amount) {
this.category = category;
this.title = title;
this.isbn = isbn;
this.quantity = quantity;
this.amount = amount;
}
public String getCategory() { return category; }
public String getTitle() { return title; }
public String getIsbn() { return isbn; }
public Integer getQuantity() { return quantity; }
public BigDecimal getAmount() { return amount; }
}
package bookshelf;
import java.io.IOException;
import java.io.InputStream;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
final class ReportCompiler {
private ReportCompiler() {}
static JasperReport compile(String name) throws JRException, IOException {
try (InputStream jrxml = ReportCompiler.class.getResourceAsStream("/reports/" + name + ".jrxml")) {
return JasperCompileManager.compileReport(jrxml);
}
}
}
package bookshelf;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
final class SalesData {
private SalesData() {}
static List<Sale> sales() {
List<Sale> sales = new ArrayList<>();
String[] categories = {"Fiction", "Poetry", "Science"};
for (String category : categories) {
for (int i = 1; i <= 10; i++) {
sales.add(new Sale(category, category + " title " + i,
"978-0-00-%04d-%d".formatted(sales.size(), i % 10),
1 + i % 3, BigDecimal.valueOf(900 + 37L * i, 2)));
}
}
return sales;
}
}
Anchors and hyperlinks
JasperReports has no "link" element. Text fields, images and charts can be hyperlinks, and text fields and images can also be anchors, because those are the elements that carry the settings. One element can be both an anchor and a hyperlink at the same time.
-
An anchor is a named destination. A text field or image becomes one when it has an
anchorNameExpression, ajava.lang.Stringexpression that is evaluated for every printed copy of the element. -
A hyperlink is an element with a link type other than
None, plus the expression or expressions that the type needs.
In v7 JRXML the link type is the attribute linkType, and the target window is linkTarget. The 6.x JRXML and a
good part of the official sample text call them hyperlinkType and hyperlinkTarget, and the sample README
still shows the old spelling in places, but the 7.0 sample templates use linkType and linkTarget. In the run for
this page a template that spelled the attribute hyperlinkType compiled without any error and the exported PDF
contained no internal or remote link at all, so a wrong attribute name fails silently and the links just vanish.
linkType |
Expressions it evaluates | What the PDF viewer does |
|---|---|---|
|
none |
The element is not a hyperlink. This is the default. |
|
|
Opens an external resource, normally a URL. The PDF carries a URI action. |
|
|
Jumps to the anchor of that name in the same document. |
|
|
Jumps to a one-based page of the same document. |
|
|
Opens another document at the anchor of that name. |
|
|
Opens another document at a one-based page. |
Any other text is a custom link type. The attribute accepts any value, and the engine leaves a custom link to a
hyperlink handler that you register: a JRHyperlinkProducer obtained from a JRHyperlinkProducerFactory that is
set on the exporter, with arbitrary hyperlinkParameter values passed through. Custom types are for links whose
final address depends on the environment at export time, for example a different store host per deployment. The
built-in Swing viewer takes a JRHyperlinkListener for the same purpose.
linkTarget follows the HTML vocabulary: Self (the default), Blank, Parent and Top are the standard
values. Any other value is looked up as a custom target producer (JRHyperlinkTargetProducer), then as the name of
a hyperlink parameter, and finally used as a frame name. The window semantics are an HTML idea, so they matter mainly for the HTML export of the same template, and this page
did not check what a PDF viewer does with them. The optional hyperlinkTooltipExpression stores a tooltip with the link.
The official documentation says the built-in viewer and the HTML exporter show it. This page did not check whether a
particular PDF viewer displays it.
The template below is the monthly sales report of the Bookshelf Billing service, with every kind of link in it.
The title has an anchor named after the report, a local link to the summary, an external link to the store passed as
the parameter $P{STORE_URL}, and a remote link to an anchor in another PDF. Each book title in the detail band
is a reference link whose URL is built from the ISBN field, a local page link in the footer goes back to page 1, and
the last page carries the summary anchor. The bookmarkLevel attributes are explained in the next section. In v7
JRXML the title and pageFooter elements hold their elements directly, with no band child.
<jasperReport name="sales-report" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<parameter name="STORE_URL" class="java.lang.String"/>
<field name="category" class="java.lang.String"/>
<field name="title" class="java.lang.String"/>
<field name="isbn" class="java.lang.String"/>
<field name="quantity" class="java.lang.Integer"/>
<field name="amount" class="java.math.BigDecimal"/>
<group name="category">
<expression><![CDATA[$F{category}]]></expression>
<groupHeader>
<band height="28">
<element kind="textField" x="0" y="4" width="515" height="22" bold="true" fontSize="13"
bookmarkLevel="1">
<expression><![CDATA[$F{category}]]></expression>
<anchorNameExpression><![CDATA[$F{category}]]></anchorNameExpression>
</element>
</band>
</groupHeader>
</group>
<title height="70">
<element kind="textField" x="0" y="0" width="515" height="26" bold="true" fontSize="18"
bookmarkLevel="1">
<expression><![CDATA["Bookshelf sales, September 2026"]]></expression>
<anchorNameExpression><![CDATA["Bookshelf sales"]]></anchorNameExpression>
</element>
<element kind="textField" x="0" y="34" width="250" height="16" forecolor="#0000FF"
linkType="LocalAnchor">
<expression><![CDATA["Go to the summary"]]></expression>
<hyperlinkAnchorExpression><![CDATA["Summary"]]></hyperlinkAnchorExpression>
<hyperlinkTooltipExpression><![CDATA["Jump to the last page"]]></hyperlinkTooltipExpression>
</element>
<element kind="textField" x="260" y="34" width="255" height="16" forecolor="#0000FF"
linkType="Reference" linkTarget="Blank">
<expression><![CDATA["Open the Bookshelf store"]]></expression>
<hyperlinkReferenceExpression><![CDATA[$P{STORE_URL}]]></hyperlinkReferenceExpression>
</element>
<element kind="textField" x="0" y="52" width="515" height="16" forecolor="#0000FF"
linkType="RemoteAnchor">
<expression><![CDATA["Read the terms (anchor in another PDF)"]]></expression>
<hyperlinkReferenceExpression><![CDATA["terms.pdf"]]></hyperlinkReferenceExpression>
<hyperlinkAnchorExpression><![CDATA["Terms and conditions"]]></hyperlinkAnchorExpression>
</element>
</title>
<detail>
<band height="18">
<element kind="textField" x="10" y="0" width="360" height="16" forecolor="#0000FF"
linkType="Reference" linkTarget="Blank" bookmarkLevel="2">
<expression><![CDATA[$F{title}]]></expression>
<anchorNameExpression><![CDATA[$F{title}]]></anchorNameExpression>
<hyperlinkReferenceExpression><![CDATA[$P{STORE_URL} + "/books/" + $F{isbn}]]></hyperlinkReferenceExpression>
<hyperlinkTooltipExpression><![CDATA["ISBN " + $F{isbn}]]></hyperlinkTooltipExpression>
</element>
<element kind="textField" x="380" y="0" width="40" height="16" hTextAlign="Right">
<expression><![CDATA[$F{quantity}]]></expression>
</element>
<element kind="textField" x="430" y="0" width="85" height="16" hTextAlign="Right" pattern="#,##0.00">
<expression><![CDATA[$F{amount}]]></expression>
</element>
</band>
</detail>
<pageFooter height="24">
<element kind="textField" x="0" y="4" width="200" height="16" forecolor="#0000FF"
linkType="LocalPage">
<expression><![CDATA["Back to page 1"]]></expression>
<hyperlinkPageExpression><![CDATA[1]]></hyperlinkPageExpression>
</element>
<element kind="textField" x="315" y="4" width="200" height="16" hTextAlign="Right">
<expression><![CDATA["Page " + $V{PAGE_NUMBER}]]></expression>
</element>
</pageFooter>
<summary height="40">
<element kind="textField" x="0" y="10" width="515" height="20" bold="true" bookmarkLevel="1">
<expression><![CDATA["Summary"]]></expression>
<anchorNameExpression><![CDATA["Summary"]]></anchorNameExpression>
</element>
</summary>
</jasperReport>
The Java side fills the template from a list of beans and exports it with the one-line facade. Nothing in the Java
code mentions links: they are part of the filled JasperPrint and the PDF exporter turns each one into an
annotation.
package bookshelf;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
public class LinkedSalesReport {
public static byte[] render() throws JRException, IOException {
Map<String, Object> parameters = new HashMap<>();
parameters.put("STORE_URL", "https://bookshelf.example.com");
JasperPrint print = JasperFillManager.fillReport(ReportCompiler.compile("sales-report"),
parameters, new JRBeanCollectionDataSource(SalesData.sales()));
return JasperExportManager.exportReportToPdf(print);
}
public static void main(String[] args) throws Exception {
java.nio.file.Files.write(java.nio.file.Path.of("target/linked-sales.pdf"), render());
}
}
Reading the result back, the Reference links became URI actions with the expected addresses, the title’s
LocalAnchor link jumped to the page that contains the summary, the LocalPage link in the footer jumped to
page 1, and the RemoteAnchor link became a remote go-to action naming terms.pdf and the destination
Terms and conditions. A remote link only works when the other file is where the reference expression says, which in
a viewer usually means the same directory; the reference is resolved by the viewer, not by JasperReports.
The example derives from the official
hyperlink sample (hyperlink).
Bookmarks and the PDF outline
PDF has a navigation tree in the viewer’s side pane, called the outline or the bookmarks. JasperReports builds it
from anchors: an anchor becomes an outline entry when its element has a positive bookmarkLevel. Level 1 entries
are at the top, level 2 entries nest under the preceding level 1 entry, and so on. The bookmarkLevel attribute
is available on every hyperlink-capable element. bookmarkLevelExpression is the element child that computes the
level at fill time and, as the official documentation puts it, overrides the attribute.
Three details decided by the run, all of which are easy to miss:
-
The outline entry’s text is the anchor name, not the text that the element displays. The sales report therefore names its anchors with the readable labels (
$F{category},$F{title}), not with technical keys such ascategory-7. The same label feeds the table of contents of the next section. An anchor name should be unique if you link to it, because a link names its destination. -
If a level is skipped (a level 3 entry directly after a level 1 entry), the exporter by default creates an empty entry for the missing level so that the original depth is kept. The property
net.sf.jasperreports.export.pdf.collapse.missing.bookmark.levelsasks it to collapse the level instead. In the 7.0.8 run the empty entry appeared as described, but with the collapse setting on, the second and third entries of the same level were nested under the first one instead of being siblings. Use consecutive levels, and read the outline back with the program at the end of the page before relying on the collapse setting. -
The invoice template of the batch example below uses the
bookmarkLevelExpressionchild element instead of the attribute. Changing its result from 1 to 2 in the run moved the entry one level down and added an empty entry for the missing level, which confirms that the expression takes effect.
Whether the PDF contains an outline at all is a property of the exporter, not of the template.
net.sf.jasperreports.export.pdf.bookmarks.enabled defaults to true (available since 6.20.6), and the same switch
exists as setBookmarksEnabled on SimplePdfReportConfiguration. Setting it to false in the run produced a PDF without an outline. Like every exporter property, it can be given globally in
jasperreports.properties, on the DefaultJasperReportsContext, or as a <property> of the report, as covered in
Scriptlets, Properties & Extensions.
SimplePdfReportConfiguration reportConfiguration = new SimplePdfReportConfiguration();
reportConfiguration.setBookmarksEnabled(false); // anchors and links stay, the outline pane is empty
exporter.setConfiguration(reportConfiguration);
The fill-time counterpart is net.sf.jasperreports.print.create.bookmarks (default false, available since 5.5.2),
which tells the filler to collect the bookmarks into a structure that can be read as a data source. A plain PDF
outline does not need it. A table of contents does, as the next section shows.
The configuration reference documents both properties:
net.sf.jasperreports.export.pdf.bookmarks.enabled,
net.sf.jasperreports.print.create.bookmarks and
net.sf.jasperreports.export.pdf.collapse.missing.bookmark.levels.
Report books and the table of contents
A report part is similar to a subreport, with one difference that matters: the pages that a part generates are
included directly in the generated document, while the pages of a subreport are embedded in the pages of the parent
report. Parts are used in place of bands inside the report sections (the detail, group header and group footer
sections), and a report made of parts is a book. A book must declare sectionType="Part", and parts and bands
cannot be mixed in the same report. Each part has its own page size, so one document can contain pages of different
dimensions, and a part can itself be a part report.
The dataset of the book decides how many times a part is instantiated: a part in the detail section is printed once
per record. A group with a constant expression (a dummy group) places parts that should appear once. Each part can
have a partNameExpression, a printWhenExpression, an evaluationTime and generic properties. The only part
component built in is subreportPart, which fills a report template. It has no data source expression of its own:
a data source or connection is passed with a parameter named REPORT_DATA_SOURCE or REPORT_CONNECTION.
A table of contents part is a part whose data is the bookmarks of the other parts. It needs three things.
-
The book sets
net.sf.jasperreports.print.create.bookmarkstotrue. In the 7.0.8 run, a book without it failed at fill time with aNullPointerExceptioninsidePartReportFiller, because the table of contents part could not find the collected bookmarks. The error does not mention the property. -
The table of contents part has
evaluationTime="Report", so it is filled after the parts it lists. It is placed first in the document, and no later part should have the same evaluation time. -
The part carries the property
net.sf.jasperreports.bookmarks.data.source.parameterwith the name of the parameter that receives the bookmarks. NamingREPORT_DATA_SOURCEmakes the bookmarks the part’s own data source.
The bookmarks arrive as rows with three fields: level (Integer), label (String, the anchor name) and pageIndex
(Integer). The table of contents template turns them into lines: indent by level, make the label a LocalAnchor
link to the anchor with the same name, and print the page. The pageIndex is temporary, because it does not count the
pages that the table of contents itself will occupy. When the table of contents is at the beginning, the final page
is the $V{PAGE_NUMBER} of the table of contents part, evaluated late, plus $F{pageIndex} plus 1. That is
what a text field with evaluationTime="Auto" and the expression $V{PAGE_NUMBER} + $F{pageIndex} + 1
does in the template below. To check it, the same book was filled with a table of contents of one page and with a
longer one of four pages. The printed numbers matched the pages in the outline in both cases (page 2 for the first
category in the short book, page 5 in the long one).
The book itself passes the three compiled templates and the sales data as parameters. The part expression can
return a compiled JasperReport object, as the run confirmed, so no .jasper files on disk are needed. The terms
part receives an empty data source of one record so that its title band is printed; a subreportPart without a
data source produced no pages in the run.
<jasperReport name="sales-book" language="java" sectionType="Part" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<property name="net.sf.jasperreports.print.create.bookmarks" value="true"/>
<parameter name="TOC_REPORT" class="net.sf.jasperreports.engine.JasperReport"/>
<parameter name="SALES_REPORT" class="net.sf.jasperreports.engine.JasperReport"/>
<parameter name="TERMS_REPORT" class="net.sf.jasperreports.engine.JasperReport"/>
<parameter name="SALES_DATA" class="net.sf.jasperreports.engine.JRDataSource"/>
<parameter name="STORE_URL" class="java.lang.String"/>
<group name="once">
<expression><![CDATA[1]]></expression>
<groupHeader>
<part evaluationTime="Report">
<property name="net.sf.jasperreports.bookmarks.data.source.parameter" value="REPORT_DATA_SOURCE"/>
<partNameExpression><![CDATA["Table of contents"]]></partNameExpression>
<component kind="subreportPart">
<expression><![CDATA[$P{TOC_REPORT}]]></expression>
</component>
</part>
<part>
<partNameExpression><![CDATA["Sales"]]></partNameExpression>
<component kind="subreportPart">
<expression><![CDATA[$P{SALES_REPORT}]]></expression>
<parameter name="REPORT_DATA_SOURCE">
<expression><![CDATA[$P{SALES_DATA}]]></expression>
</parameter>
<parameter name="STORE_URL">
<expression><![CDATA[$P{STORE_URL}]]></expression>
</parameter>
</component>
</part>
<part>
<partNameExpression><![CDATA["Terms"]]></partNameExpression>
<component kind="subreportPart">
<expression><![CDATA[$P{TERMS_REPORT}]]></expression>
<parameter name="REPORT_DATA_SOURCE">
<expression><![CDATA[new net.sf.jasperreports.engine.JREmptyDataSource(1)]]></expression>
</parameter>
</component>
</part>
</groupHeader>
</group>
</jasperReport>
<jasperReport name="toc-part" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<field name="level" class="java.lang.Integer"/>
<field name="label" class="java.lang.String"/>
<field name="pageIndex" class="java.lang.Integer"/>
<pageHeader height="40">
<element kind="staticText" x="0" y="0" width="515" height="30" bold="true" fontSize="18">
<text><![CDATA[Table of contents]]></text>
</element>
</pageHeader>
<detail>
<band height="18">
<element kind="textField" x="0" y="0" width="450" height="16" forecolor="#0000FF"
linkType="LocalAnchor" bold="true">
<printWhenExpression><![CDATA[$F{level} == 1]]></printWhenExpression>
<expression><![CDATA[$F{label}]]></expression>
<hyperlinkAnchorExpression><![CDATA[$F{label}]]></hyperlinkAnchorExpression>
</element>
<element kind="textField" x="20" y="0" width="430" height="16" forecolor="#0000FF"
linkType="LocalAnchor">
<printWhenExpression><![CDATA[$F{level} == 2]]></printWhenExpression>
<expression><![CDATA[$F{label}]]></expression>
<hyperlinkAnchorExpression><![CDATA[$F{label}]]></hyperlinkAnchorExpression>
</element>
<element kind="textField" x="455" y="0" width="60" height="16" evaluationTime="Auto"
hTextAlign="Right" linkType="LocalAnchor">
<expression><![CDATA[$V{PAGE_NUMBER} + $F{pageIndex} + 1]]></expression>
<hyperlinkAnchorExpression><![CDATA[$F{label}]]></hyperlinkAnchorExpression>
</element>
</band>
</detail>
</jasperReport>
<jasperReport name="terms" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<title height="120">
<element kind="textField" x="0" y="0" width="515" height="26" bold="true" fontSize="18"
bookmarkLevel="1">
<expression><![CDATA["Terms and conditions"]]></expression>
<anchorNameExpression><![CDATA["Terms and conditions"]]></anchorNameExpression>
</element>
<element kind="staticText" x="0" y="36" width="515" height="60">
<text><![CDATA[Prices include VAT. Books can be returned within 14 days of delivery.]]></text>
</element>
</title>
</jasperReport>
package bookshelf;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
public class SalesBook {
public static byte[] render() throws JRException, IOException {
Map<String, Object> parameters = new HashMap<>();
parameters.put("TOC_REPORT", ReportCompiler.compile("toc-part"));
parameters.put("SALES_REPORT", ReportCompiler.compile("sales-report"));
parameters.put("TERMS_REPORT", ReportCompiler.compile("terms"));
parameters.put("SALES_DATA", new JRBeanCollectionDataSource(SalesData.sales()));
parameters.put("STORE_URL", "https://bookshelf.example.com");
JasperPrint print = JasperFillManager.fillReport(ReportCompiler.compile("sales-book"),
parameters, new JREmptyDataSource(1));
return JasperExportManager.exportReportToPdf(print);
}
public static void main(String[] args) throws Exception {
java.nio.file.Files.write(java.nio.file.Path.of("target/sales-book.pdf"), render());
}
}
The book of three pages (one table of contents page, the sales page and the terms page) had a single outline with the
entries of the sales report followed by Terms and conditions, and all the links of the table of contents pointed to
the right pages. One behaviour is worth knowing: the footer link LocalPage with page 1 of the sales part pointed to
page 1 of the whole book (the table of contents), not to the first page of the part. Page numbers in links are
document pages in a book. Variables help for the other direction:
$V{PAGE_NUMBER} is scoped to the lifecycle of the part, while $V{MASTER_CURRENT_PAGE} and
$V{MASTER_TOTAL_PAGES} give the index in the generated document and its total, and they evaluate to null unless
the text field uses evaluationTime="Master" or "Auto". This is the book version of the page X of Y footer.
This section derives from the official
table of contents sample (tableofcontents)
and book sample (book).
Both samples are available since 6.0.0; the books B2 and B3 predate them and say nothing about parts.
Batch export: several reports in one PDF
A book assembles reports at fill time and shares one table of contents. The alternative is to fill independent
reports, each with its own data, parameters and lifecycle, and concatenate them when exporting. This is batch mode
export: the exporter receives a list of JasperPrint objects and writes one document. The 7.0 exporter API is
built around ExporterInput: SimpleExporterInput.getInstance(List<JasperPrint>) wraps the list into one
ExporterInputItem per print, and each item may carry its own ReportExportConfiguration. The configuration given
with setConfiguration on the exporter applies globally, and a report configuration applies to every report in the
list unless the item overrides it. The 1.x to 3.x exporter parameter style that books show (JRExporterParameter
maps, finding O20) does not exist in this API.
Not every exporter can run in batch mode, but the PDF exporter can. When the list has several prints, the PDF
exporter can add one top-level outline entry per print to mark where each document begins. The entry has the name of
the print, jasperPrint.getName(), which is the name attribute of the report unless you call setName. The setting
is setCreatingBatchModeBookmarks(true) on SimplePdfExporterConfiguration, or the property
net.sf.jasperreports.export.pdf.create.batch.mode.bookmarks, whose default is false, so nothing is added unless
you ask (available since 2.0.1).
The Bookshelf Billing monthly pack puts the invoice, the sales report of the previous sections and the terms in
one PDF. The invoice template is small; its title is a bookmarked anchor, and the level comes from a
bookmarkLevelExpression.
<jasperReport name="invoice" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<parameter name="INVOICE_NUMBER" class="java.lang.String"/>
<field name="title" class="java.lang.String"/>
<field name="quantity" class="java.lang.Integer"/>
<field name="amount" class="java.math.BigDecimal"/>
<title height="40">
<element kind="textField" x="0" y="0" width="515" height="26" bold="true" fontSize="18">
<expression><![CDATA["Invoice " + $P{INVOICE_NUMBER}]]></expression>
<anchorNameExpression><![CDATA["Invoice " + $P{INVOICE_NUMBER}]]></anchorNameExpression>
<bookmarkLevelExpression><![CDATA[$P{INVOICE_NUMBER}.isEmpty() ? 2 : 1]]></bookmarkLevelExpression>
</element>
</title>
<detail>
<band height="18">
<element kind="textField" x="0" y="0" width="360" height="16">
<expression><![CDATA[$F{title}]]></expression>
</element>
<element kind="textField" x="380" y="0" width="40" height="16" hTextAlign="Right">
<expression><![CDATA[$F{quantity}]]></expression>
</element>
<element kind="textField" x="430" y="0" width="85" height="16" hTextAlign="Right" pattern="#,##0.00">
<expression><![CDATA[$F{amount}]]></expression>
</element>
</band>
</detail>
</jasperReport>
The prints are named after what the reader sees in the outline.
package bookshelf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import net.sf.jasperreports.pdf.SimplePdfExporterConfiguration;
public class MonthlyPack {
/** Fills three reports and exports them as one PDF with one top-level bookmark per report. */
public static byte[] render(boolean batchBookmarks) throws JRException, IOException {
Map<String, Object> invoiceParameters = new HashMap<>();
invoiceParameters.put("INVOICE_NUMBER", "2026-0042");
JasperPrint invoice = JasperFillManager.fillReport(ReportCompiler.compile("invoice"),
invoiceParameters, new JRBeanCollectionDataSource(SalesData.sales().subList(0, 5)));
invoice.setName("Invoice 2026-0042");
Map<String, Object> salesParameters = new HashMap<>();
salesParameters.put("STORE_URL", "https://bookshelf.example.com");
JasperPrint sales = JasperFillManager.fillReport(ReportCompiler.compile("sales-report"),
salesParameters, new JRBeanCollectionDataSource(SalesData.sales()));
sales.setName("Sales, September 2026");
JasperPrint terms = JasperFillManager.fillReport(ReportCompiler.compile("terms"),
new HashMap<>(), new JREmptyDataSource(1));
terms.setName("Terms and conditions");
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(SimpleExporterInput.getInstance(List.of(invoice, sales, terms)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
SimplePdfExporterConfiguration configuration = new SimplePdfExporterConfiguration();
configuration.setCreatingBatchModeBookmarks(batchBookmarks);
exporter.setConfiguration(configuration);
exporter.exportReport();
return out.toByteArray();
}
public static void main(String[] args) throws Exception {
java.nio.file.Files.write(java.nio.file.Path.of("target/monthly-pack.pdf"), render(true));
java.nio.file.Files.write(java.nio.file.Path.of("target/monthly-pack-nobatch.pdf"), render(false));
}
}
The outline of the PDF exported with render(true) had three top-level entries, Invoice 2026-0042,
Sales, September 2026 and Terms and conditions, each pointing to the first page of its document, and the
bookmarks created by the reports themselves nested one level below, so the sales report’s own entries were children
of Sales, September 2026. With render(false) the batch entries were missing and the report bookmarks were
at the top level. The LocalPage link in the footer of the sales report, which asks for page 1, pointed to the first
page of the sales report (page 2 of the combined file), unlike in the book. In a batch export page numbers in links are
relative to the print they belong to.
The example derives from the official
batch export sample (batchexport).
Reading the outline back
A cheap regression test for all of the above is to open the exported bytes with PDFBox and print the outline and the link annotations. The program below is what the checks of this page used; run it in a unit test and assert on the titles and pages. It prints each outline title with its page, indented by depth.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
public class OutlineDump {
public static void main(String[] args) throws IOException {
try (PDDocument document = Loader.loadPDF(Files.readAllBytes(Path.of(args[0])))) {
PDOutlineNode root = document.getDocumentCatalog().getDocumentOutline();
System.out.println(root == null ? "no outline" : "pages=" + document.getNumberOfPages());
if (root != null) {
print(root, 0);
}
}
}
static void print(PDOutlineNode node, int depth) throws IOException {
for (PDOutlineItem item : node.children()) {
int page = item.getDestination() instanceof PDPageDestination destination
? destination.retrievePageNumber() + 1 : -1;
System.out.println(" ".repeat(depth) + item.getTitle() + " (page " + page + ")");
print(item, depth + 1);
}
}
}
Choosing between subreports, parts and batch export
| Need | Use | Why |
|---|---|---|
A report inside a band of another report, sharing its pages |
Subreport |
Its pages are embedded in the parent’s pages. See Subreports, Datasets, Tables & Lists. |
One document made of sections with their own page size, with a generated table of contents |
Book ( |
The parts are filled in one run, share the bookmarks, and page numbers in links are document pages. |
Documents produced separately (different data, different times) delivered as one file |
Batch export |
Each print stays independent; the exporter adds one outline entry per print. |
Only a navigable outline over one report |
|
No book, no table of contents part and no fill-time bookmark collection are needed. |
For the iText equivalents of links, destinations and outlines in documents built with the layout API, see Navigation, Annotations & Actions.
What changed since the books
The two JasperReports books that were consulted for this section, B2 (2007, JasperReports 1.3.x) and B3 (2002), were
used only as references for the topic order and never as a source for the code. Both predate report parts, the table of
contents part (6.0.0) and the Jackson JRXML of 7.0. Their templates use the DTD-based JRXML (finding O15), where the
link attributes have different spellings and the file does not load in 7.0. Their export code uses the
JRExporterParameter style (finding O20); the current equivalent is the SimpleExporterInput and
SimplePdfExporterConfiguration shown above. Every example on this page was written against the current official
documentation and run on 7.0.8, and where the official sample README and the 7.0 sample templates disagree about an
attribute name, the templates and the run decide.
References
-
Configuration reference:
net.sf.jasperreports.export.pdf.bookmarks.enabled -
Configuration reference:
net.sf.jasperreports.export.pdf.create.batch.mode.bookmarks -
Configuration reference:
net.sf.jasperreports.print.create.bookmarks -
JRXML Report Design on this site
-
Groups, Sorting & Filtering on this site
-
Subreports, Datasets, Tables & Lists on this site
-
PDF Export Configuration on this site
-
Navigation, Annotations & Actions on this site