Editable Forms with JasperReports & PDFBox
|
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. |
An editable PDF form is a normal page description with an extra layer on top: a tree of fields (the data model) and
one or more widget annotations per field (the rectangles a viewer draws and a user clicks). That layer is called an
AcroForm, and it is the only form technology to use today (see the section on outdated points below). This page covers the two
tools that suit a Java service that already generates reports with JasperReports: the JasperReports PDF exporter, which
can turn selected report elements into fields (since 6.12.0), and Apache PDFBox 3.0.8, which can open the exported file,
fill it, flatten it, add the field types the exporter cannot produce and, when needed, build a form from nothing. The
running example is the editable customer onboarding form of the Bookshelf Billing scenario: the back office renders a
form with the customer number already printed, the customer types a name and an address, chooses a country and a plan,
and the file is later flattened or signed. Versions are stated as of 2026-10-09: JasperReports 7.0.8 (with
jasperreports-pdf and jasperreports-jdt) and Apache PDFBox 3.0.8 on Java 21. Every Java example and the JRXML template on
this page were compiled and run against exactly those versions, and the resulting PDFs were inspected with PDFBox and
the Python library pypdf and rendered to images with PDFBox. What was not tested is listed in the section
What was run, and what was not.
The concepts of an AcroForm (field types, flags, the value and appearance model, XFA, flattening and form-signing order) are explained in Editable Forms Fundamentals; the same task with iText instead of PDFBox is in Editable Forms with iText. How the exporter itself is configured is in PDF Export Configuration, the v7 template syntax is in JRXML Report Design, and the Spring Boot plumbing that returns bytes to a client is in Getting Started with Spring Boot. Signing a form after it has been filled is covered in Signing with PDFBox and EU DSS and Signing with iText.
Who does what
Two different moments are involved, and the tools split along that line.
-
Design time, in the report template. JasperReports decides which elements become fields, what their names are and what choices a list offers. It does this through element properties on the JRXML element, read by the PDF exporter. The exporter writes the field, the widget and the appearance in one pass.
-
Run time, on the exported bytes. PDFBox opens the file with
Loader.loadPDF, reads and writes values, flattens the form into page content, and adds what the exporter cannot create. JasperReports itself has no API to fill a field after export, and the properties that set an initial value are the only way to pre-fill from inside a report.
PDFBox is Apache-2.0 licensed, so using it in a closed-source service needs no licence purchase. JasperReports Community is LGPL-3.0 licensed. Both are used here in their free editions; the form export described below is part of the Community engine (tagged PDF, in contrast, needs the Professional edition in 7.0.8, see PDF Export Configuration).
What changed since the books
The consulted books on JasperReports and iText predate form export in JasperReports, and some of them do not mention PDF/A, tagging, form fields or signing at all (finding O27 of this section’s source analysis). Four points follow from that and are stated here once.
-
JasperReports can write AcroForm fields, since version 6.12.0. Before it, an element that looked like a text box was only a drawing, and a form meant a second tool. The properties are documented in the configuration reference with
Since: 6.12.0, and the sample that demonstrates them is the official forms sample. -
JasperReports cannot sign, and it cannot read or fill a form it has produced. Signing, like filling after export, is a post-processing step on the bytes.
-
PDFBox 3 is not PDFBox 1 or 2.
PDDocument.load(...)is gone andLoader.loadPDF(...)replaces it; thePDType1Fontconstants such asPDType1Font.HELVETICAare replaced bynew PDType1Font(Standard14Fonts.FontName.HELVETICA); andPDAcroForm.flatten()and its overloads are the supported flattening API. Old snippets that callsetNeedAppearancesas the only way to show a value are no longer the recommended approach:setValueon a field regenerates its appearance. The migration guide at https://pdfbox.apache.org/3.0/migration.html lists the removed methods. -
XFA is not the answer. XFA forms are deprecated in PDF 2.0 and forbidden by PDF/A. Both tools on this page work with AcroForms only.
Dependencies
The PDF exporter is not in the core jasperreports artifact, and a template compiler has to be on the class path
(Eclipse JDT in this example, as in the other JasperReports pages). PDFBox 3 needs one artifact.
<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-jdt</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.8</version>
</dependency>
</dependencies>
pdfbox brings pdfbox-io and fontbox transitively. On the first run PDFBox scans the operating system fonts and
logs a message while it builds its font cache, which is harmless. Derived from the
PDFBox dependencies page and the other JasperReports pages of this
section.
Declaring fields in the template
A field is declared by properties on a report element in the JRXML. The names start with
net.sf.jasperreports.export.pdf.field. and every one of them has scope Element (a few can also be set globally,
see below). The element is what the user sees in a non-form export, so the properties only add behaviour: size,
position, background, font and text colour come from the element, and the exporter reuses them for the field.
| Property | Meaning | Applies to |
|---|---|---|
|
The kind of field: |
every field |
|
The field name, unique within the form. If missing, the name is generated from the element UUID. Radio options that share a name become one radio group. |
every field |
|
The initial text of a text field, or the initially selected choice of a list or combo. If missing, the text content of the element is used. |
Text, Combo, List |
|
The choices of a list or combo, separated by |
Combo, List |
|
The characters that separate the choices. Default |
Combo, List |
|
A flag: when true the combo box is editable, so the user can type a value that is not in the list. Default false. |
Combo |
|
The glyph of a check box or radio option: |
Check, Radio |
|
A flag: the box or the option is selected when the file opens. |
Check, Radio |
|
|
every field |
|
A flag: the field cannot be edited. The reference states that this is not applicable to radio groups. |
every field except Radio |
|
A flag: a text field accepts several lines. Default false. |
Text |
Derived from the JasperReports
configuration reference and the forms sample.
The configuration reference states that the type, choices, separators and combo properties exist in 6.12.0 or later and
documents the Java constants for them on net.sf.jasperreports.pdf.JRPdfExporter (PDF_FIELD_TYPE, PDF_FIELD_NAME
and so on). The sample README states "Since: 6.12.0" for the whole feature.
The template
The template below is a complete v7 JRXML document for the onboarding form. It uses static texts as the empty text
fields (the Text type reads the element content, which is empty here), a text field for the customer number, which is
printed from a report parameter and made read-only, a combo box, a list, a check box and a radio group of two options.
Inside an expression or an attribute, a $P{...} reference is the JasperReports parameter syntax; the lines that
declare the fields are the property elements.
<jasperReport name="onboarding-form" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
<parameter name="CUSTOMER_ID" class="java.lang.String"/>
<title height="440">
<element kind="staticText" x="0" y="0" width="515" height="30" fontSize="18" bold="true">
<text><![CDATA[Bookshelf customer onboarding]]></text>
</element>
<element kind="staticText" x="0" y="45" width="140" height="22">
<text><![CDATA[Customer number]]></text>
</element>
<element kind="textField" x="150" y="45" width="200" height="22" backcolor="#EEEEEE" mode="Opaque">
<expression><![CDATA[$P{CUSTOMER_ID}]]></expression>
<property name="net.sf.jasperreports.export.pdf.field.type" value="Text"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="customerId"/>
<property name="net.sf.jasperreports.export.pdf.field.read.only" value="true"/>
</element>
<element kind="staticText" x="0" y="80" width="140" height="22">
<text><![CDATA[Full name]]></text>
</element>
<element kind="staticText" x="150" y="80" width="365" height="22">
<text><![CDATA[]]></text>
<property name="net.sf.jasperreports.export.pdf.field.type" value="Text"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="fullName"/>
<box>
<pen lineWidth="1.0" lineColor="#444444"/>
</box>
</element>
<element kind="staticText" x="0" y="115" width="140" height="22">
<text><![CDATA[Billing address]]></text>
</element>
<element kind="staticText" x="150" y="115" width="365" height="60">
<text><![CDATA[]]></text>
<property name="net.sf.jasperreports.export.pdf.field.type" value="Text"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="billingAddress"/>
<property name="net.sf.jasperreports.export.pdf.field.text.multiline" value="true"/>
<property name="net.sf.jasperreports.export.pdf.field.border.style" value="Beveled"/>
<box>
<pen lineWidth="1.0" lineColor="#444444"/>
</box>
</element>
<element kind="staticText" x="0" y="190" width="140" height="22">
<text><![CDATA[Country]]></text>
</element>
<element kind="staticText" x="150" y="190" width="200" height="22">
<text><![CDATA[]]></text>
<property name="net.sf.jasperreports.export.pdf.field.type" value="Combo"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="country"/>
<property name="net.sf.jasperreports.export.pdf.field.choices" value="Spain|France|Portugal|Other"/>
<property name="net.sf.jasperreports.export.pdf.field.value" value="Spain"/>
<box>
<pen lineWidth="1.0" lineColor="#444444"/>
</box>
</element>
<element kind="staticText" x="0" y="225" width="140" height="22">
<text><![CDATA[Interests]]></text>
</element>
<element kind="staticText" x="150" y="225" width="200" height="70">
<text><![CDATA[]]></text>
<property name="net.sf.jasperreports.export.pdf.field.type" value="List"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="interests"/>
<property name="net.sf.jasperreports.export.pdf.field.choice.separators" value=","/>
<property name="net.sf.jasperreports.export.pdf.field.choices" value="Fiction,Poetry,Science,History"/>
<box>
<pen lineWidth="1.0" lineColor="#444444"/>
</box>
</element>
<element kind="staticText" x="0" y="310" width="140" height="22">
<text><![CDATA[Newsletter]]></text>
</element>
<element kind="rectangle" x="150" y="310" width="20" height="20">
<property name="net.sf.jasperreports.export.pdf.field.type" value="Check"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="newsletter"/>
<property name="net.sf.jasperreports.export.pdf.field.check.type" value="Cross"/>
<property name="net.sf.jasperreports.export.pdf.field.checked" value="true"/>
</element>
<element kind="staticText" x="0" y="345" width="140" height="22">
<text><![CDATA[Plan]]></text>
</element>
<element kind="rectangle" x="150" y="345" width="20" height="20">
<property name="net.sf.jasperreports.export.pdf.field.type" value="Radio"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="plan"/>
<property name="net.sf.jasperreports.export.pdf.field.check.type" value="Circle"/>
<property name="net.sf.jasperreports.export.pdf.field.checked" value="true"/>
</element>
<element kind="staticText" x="175" y="345" width="60" height="20">
<text><![CDATA[Basic]]></text>
</element>
<element kind="rectangle" x="250" y="345" width="20" height="20">
<property name="net.sf.jasperreports.export.pdf.field.type" value="Radio"/>
<property name="net.sf.jasperreports.export.pdf.field.name" value="plan"/>
<property name="net.sf.jasperreports.export.pdf.field.check.type" value="Circle"/>
</element>
<element kind="staticText" x="275" y="345" width="60" height="20">
<text><![CDATA[Premium]]></text>
</element>
</title>
</jasperReport>
A few details of the template are worth stating.
-
A check box or radio option needs an element with a shape. A rectangle works and an ellipse is what the official sample uses. The exporter draws the glyph named by
check.type, so the element itself only provides the position and size. The sample also puts a radio option on a text element, which is accepted too. -
A radio group is made of several elements that share the same
field.name. Each element is one option, andfield.checkedselects the one that is selected when the file opens. Without the shared name the options would be separate groups. -
A list or combo ignores the element text. The element can contain anything, because the displayed text comes from the choices and the value.
-
A border comes from the
<box>pen of the element. Without a pen the field has no visible border, which is why the read-only customer number uses a grey background instead. -
A multi-line text field in the official sample also sets
net.sf.jasperreports.print.keep.full.textto true on elements whose content is long, so that the full text and not only the part that fits the element is exported. It is unnecessary for an empty field.
The properties are set per element here. The reference lists the choice separators and the combo edit flag with a wider
scope (global, context, report and element), so a default for them can be configured in a jasperreports.properties
file, but the name, type, value, choices, checked and read-only properties are element-level only.
Exporting the form
Exporting is the same code as any PDF export, as described in
PDF Export Configuration: there is no
form-specific option on the exporter, and the fields appear because the properties are present. Two details are
easy to miss: the parameter map must be mutable because the fill manager adds its own entries (an immutable Map.of(...)
failed with an UnsupportedOperationException in the run used for this page), and a new JRPdfExporter is created
for each call.
package bookshelf;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
/** Fills the onboarding template and exports it to a PDF that carries AcroForm fields. */
public final class OnboardingFormExport {
private OnboardingFormExport() {
}
public static void export(Path target, String customerId) throws JRException, IOException {
try (InputStream template = OnboardingFormExport.class
.getResourceAsStream("/reports/onboarding-form.jrxml")) {
JasperReport report = JasperCompileManager.compileReport(template);
// The fill manager adds its own entries to the map, so it must be mutable: Map.of(...) fails.
Map<String, Object> parameters = new HashMap<>();
parameters.put("CUSTOMER_ID", customerId);
JasperPrint print = JasperFillManager.fillReport(report, parameters, new JREmptyDataSource(1));
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
try (var out = Files.newOutputStream(target)) {
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();
}
}
}
public static void main(String[] args) throws Exception {
export(Path.of(args.length > 0 ? args[0] : "onboarding-form.pdf"), "BK-000123");
}
}
Derived from the forms sample and the
JRPdfExporter API.
Reading the result with PDFBox
Before filling anything, look at what the exporter wrote. PDAcroForm.getFieldTree() walks every field, and a field is
either a non-terminal field (a group, with children and no widget) or a terminal field, which is the one that
holds a value and one or more widgets.
package bookshelf;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
/** Prints every field of a PDF form: the fully qualified name, the class, the value and the widget count. */
public final class FormInspector {
private FormInspector() {
}
public static void print(Path pdf) throws IOException {
try (PDDocument document = Loader.loadPDF(pdf.toFile())) {
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
if (form == null) {
System.out.println("no AcroForm");
return;
}
System.out.println("NeedAppearances=" + form.getNeedAppearances()
+ " XFA=" + form.hasXFA());
for (PDField field : form.getFieldTree()) {
if (field instanceof PDNonTerminalField) {
System.out.println(" group " + field.getFullyQualifiedName());
} else {
PDTerminalField terminal = (PDTerminalField) field;
System.out.printf(" %-8s %-22s value=%-14s widgets=%d%n",
field.getFieldType(), field.getFullyQualifiedName(),
field.getValueAsString(), terminal.getWidgets().size());
}
}
}
}
public static void main(String[] args) throws IOException {
print(Path.of(args[0]));
}
}
Run on the exported file, with the radio group UUIDs shortened here, the output was:
NeedAppearances=false XFA=false
Tx customerId value=BK-000123 widgets=1
Tx fullName value= widgets=1
Tx billingAddress value= widgets=1
Ch country value=[Spain] widgets=1
Ch interests value=[Fiction] widgets=1
Btn newsletter value=checked widgets=1
Btn plan value=FIELD_<uuid> widgets=2
What this shows, and what is a result of this run rather than of the official documentation:
-
The types map directly:
Txfor text,Chfor the combo and the list,Btnfor the check box and the radio group.PDField.getFieldType()returns these PDF names, and the Java class isPDTextField,PDComboBox,PDListBox,PDCheckBoxorPDRadioButton, as the next section uses. -
The radio options became one field with two widgets, as intended. Their on-states are named
FIELD_followed by the UUID of each element, and they are different on every export. Code that fills the group must read them from the file (the example below does) and must not hard-code a value. -
The check box on-state is named
checked, and the exporter choseSpainas the combo value fromfield.value. When nofield.valueis given for a list, the first choice is selected in this run (Fictionabove). -
The
customerIdfield has the read-only flag (bit 1,/Ff1) andbillingAddresshas the multiline flag (/Ff4096);countryhas the combo flag (131072) and the radio group has the radio flag (32768) with the no-toggle-to-off flag (16384). -
The widgets have no
/Pentry (the reference from the widget to its page). PDFBox logs a warning for each widget when it builds its page map during flattening, and it still finds the widget by scanning the page annotations. The warning is harmless but noisy; the next sections show where it appears. -
NeedAppearancesis false: the exporter writes an appearance stream for every widget, so a viewer does not have to regenerate them.
Derived from the PDAcroForm Javadoc.
Filling a form
Filling is form.getField(name), a cast to the field class, and setValue. PDFBox regenerates the appearance of the
field when a value is set, so the result shows in viewers that do not regenerate it themselves.
package bookshelf;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
/** Fills the form exported by JasperReports and writes a filled copy and a flattened copy. */
public final class FillAndFlatten {
private FillAndFlatten() {
}
public static void fill(Path source, Path filled) throws IOException {
try (PDDocument document = Loader.loadPDF(source.toFile())) {
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
((PDTextField) form.getField("fullName")).setValue("Ada Example");
((PDTextField) form.getField("billingAddress")).setValue("1 Fictitious Street\nSampletown");
((PDComboBox) form.getField("country")).setValue("Portugal");
// The list that JasperReports writes is single-select: a second value is rejected.
((PDListBox) form.getField("interests")).setValue("Science");
PDCheckBox newsletter = (PDCheckBox) form.getField("newsletter");
newsletter.unCheck();
// JasperReports names each radio option after its element (FIELD_ and a UUID that changes on every
// export), so read the "on" state of the second widget instead of hard-coding a value.
PDRadioButton plan = (PDRadioButton) form.getField("plan");
plan.setValue(onState(plan.getWidgets().get(1)));
document.save(filled.toFile());
}
}
private static String onState(PDAnnotationWidget widget) {
return widget.getAppearance().getNormalAppearance().getSubDictionary().keySet().stream()
.map(COSName::getName)
.filter(name -> !"Off".equals(name))
.findFirst()
.orElseThrow();
}
public static void flatten(Path filled, Path flat) throws IOException {
try (PDDocument document = Loader.loadPDF(filled.toFile())) {
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
// Flatten every field of the tree (the same as flatten()), refreshing each appearance first.
List<PDField> toFlatten = new ArrayList<>();
form.getFieldTree().forEach(toFlatten::add);
form.flatten(toFlatten, true);
document.save(flat.toFile());
}
}
public static void main(String[] args) throws IOException {
fill(Path.of(args[0]), Path.of(args[1]));
flatten(Path.of(args[1]), Path.of(args[2]));
}
}
The behaviour of each field type, as observed in the run, is as follows.
-
Text and multi-line text:
setValue(String). A line break in the value is kept (the address above shows on two lines). The text is clipped to the widget rectangle, and a font size of 0 in the default appearance means automatic size. -
Combo:
setValue(String)with one of the options. A value that is not one of the options is meant for an editable combo (thecombo.editproperty); that case was not exercised. -
List: the list written by JasperReports is single select, and PDFBox rejects more than one value with an
IllegalArgumentException("The list box does not allow multiple selections."). The properties above have no multi-select flag, so a multi-select list needs the field flags changed with PDFBox (PDChoice.setMultiSelect(true)) before values are set; that was not exercised. -
Check box:
check()andunCheck(), orsetValuewith the on-state name. In the run the check box was cleared withunCheck()and read back asOff. -
Radio group:
setValuewith the on-state of the chosen option. Read it from the widget (the helper reads the keys of the normal appearance dictionary and dropsOff) because the JasperReports names are random. -
Read-only fields can still be changed by code. The read-only flag only affects what the user can do in a viewer, and PDFBox does not enforce it, so a server that fills
customerIdwould not be stopped. Do not treat a read-only flag as a security control (see Encryption and Permissions for what is and is not enforced).
Derived from the PDFBox example
FillFormField.java
and the PDAcroForm Javadoc.
Rendering and pre-filling in one service
A Spring Boot controller usually wants bytes in and bytes out. The same two tools can be chained entirely in memory:
export into a ByteArrayOutputStream, load the bytes with Loader.loadPDF(byte[]), set the values and save into another
ByteArrayOutputStream. The compiled report is immutable and can be a singleton bean; the exporter and the
PDDocument are per call. A controller then returns the bytes with the content type application/pdf (see
Getting Started with Spring Boot and
JasperReports Spring Boot Integration).
package bookshelf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
/** Renders the form in memory and pre-fills it, so a controller can return the bytes directly. */
public final class PrefilledFormService {
private final JasperReport report; // compile once, reuse: the compiled report is immutable
public PrefilledFormService() throws JRException, IOException {
try (InputStream template = getClass().getResourceAsStream("/reports/onboarding-form.jrxml")) {
this.report = JasperCompileManager.compileReport(template);
}
}
public byte[] render(String customerId, String fullName) throws JRException, IOException {
Map<String, Object> parameters = new HashMap<>();
parameters.put("CUSTOMER_ID", customerId);
ByteArrayOutputStream exported = new ByteArrayOutputStream();
JRPdfExporter exporter = new JRPdfExporter(); // a new exporter per call
exporter.setExporterInput(new SimpleExporterInput(
JasperFillManager.fillReport(report, parameters, new JREmptyDataSource(1))));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(exported));
exporter.exportReport();
try (PDDocument document = Loader.loadPDF(exported.toByteArray())) {
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
((PDTextField) form.getField("fullName")).setValue(fullName);
ByteArrayOutputStream result = new ByteArrayOutputStream();
document.save(result);
return result.toByteArray();
}
}
}
The service was run once and the returned bytes were inspected with the inspector above; no controller was run. The
Loader.loadPDF overloads without a stream cache argument already use IOUtils.createMemoryOnlyStreamCache(), so the
scratch data of the document stays on the heap. For large files pass IOUtils.createTempFileOnlyStreamCache(), or
MemoryUsageSetting.setupMixed(maxMainMemoryBytes).streamCache to keep a bounded amount in memory and spill the rest
to a temporary file, to the Loader.loadPDF overload that takes a stream cache function (all of them exist in
pdfbox-io 3.0.8); that overload was not exercised here.
Flattening
Flattening turns the fields into ordinary page content and removes the form, so a recipient can no longer edit
anything and a text extractor sees the values as text. It is the right last step for an invoice or an archived form. In
the example, flatten(fields, true) takes the list of fields to flatten and a flag that says to refresh their
appearance first. The no-argument flatten() flattens every field, and passing a subset leaves the others
editable. After the full flatten of the example the field tree is empty: the inspector printed only its header line for the
flattened file.
Observed after flattening the filled file: the text extracted by pypdf contained the typed name, the address on two lines, the country and the four list options, and the rendered page showed the selected list entry highlighted, the chosen radio option marked and the cleared check box empty. Two consequences are worth knowing.
-
A flattened list box keeps the whole list visible, with the selected line highlighted. It does not collapse to the selected value as a combo does.
-
Flatten after the customer has finished, and before a signature that must cover the final content. A signed file that is later flattened loses the signature, and a form that is meant to stay editable after signing needs the incremental-save approach of the signing pages. The order fill, flatten, sign is the one used in Signing with iText, and the same applies when the signer is PDFBox or EU DSS.
The PDFBox warnings about missing /P entries described above appear while flattening a JasperReports file. They can
be silenced through the logging configuration of org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm.
Derived from the PDAcroForm Javadoc
and the PDFBox example
FillFormField.java.
What the exporter cannot produce
The JasperReports property list above is closed: the documented field properties cover text, combo, list, check box and radio fields, and nothing else. In particular there is no property for push buttons, for signature fields, for required fields, for a maximum length, for calculated fields or for JavaScript actions. A report that needs one of them has to add it after the export, with PDFBox. The most common case on the onboarding form is the signature field, an empty field the customer signs later.
package bookshelf;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
/** Adds the one thing the JasperReports exporter cannot produce: an empty signature field. */
public final class AddSignatureField {
private AddSignatureField() {
}
public static void add(Path source, Path target) throws IOException {
try (PDDocument document = Loader.loadPDF(source.toFile())) {
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
PDPage page = document.getPage(0);
PDSignatureField signature = new PDSignatureField(form);
signature.setPartialName("customerSignature");
PDAnnotationWidget widget = signature.getWidgets().get(0);
widget.setRectangle(new PDRectangle(150, 250, 200, 60));
widget.setPage(page);
widget.setPrinted(true);
page.getAnnotations().add(widget);
form.getFields().add(signature);
// SignaturesExist (1) | AppendOnly (2): later signatures must be incremental updates.
form.setSignaturesExist(true);
form.setAppendOnly(true);
document.save(target.toFile());
}
}
public static void main(String[] args) throws IOException {
add(Path.of(args[0]), Path.of(args[1]));
}
}
setSignaturesExist(true) and setAppendOnly(true) set the two /SigFlags bits that tell a viewer that signatures
exist and that changes must be made as incremental updates. The field has no appearance, so it is invisible until a
signature is applied or an appearance is drawn; a visible placeholder needs an appearance stream like the one in the
next section. The signing itself (PDFBox SignatureInterface, EU DSS) is in
Signing with PDFBox and EU DSS. The run confirmed
that the field is present in the form and reported as Sig; no signature was applied on this page.
Because the JasperReports properties write one widget per element, a field that appears in several places (a customer number repeated on every page, for example) cannot be declared in the template with the same name and be a single field with several widgets. The recommended way is to create the field once with PDFBox and add the widgets, as shown next.
Creating a form with PDFBox
PDFBox can also build a form from nothing, which is useful when there is no report template or when the fields must
be added to a PDF made by another tool. The model is the same as in the inspector: a PDAcroForm attached to the
catalog, a list of fields, and one PDAnnotationWidget per visual location, added to the page annotations. Four rules
decide whether the result works.
-
Set default resources and a default appearance on the form. The default appearance string such as
/Helv 0 Tf 0 gnames a font that must exist in the form’s/DRresources, otherwise setting a value fails. -
A widget must be linked in both directions:
widget.setPage(page)andpage.getAnnotations().add(widget), and for a field with several widgetswidget.setParent(field)as well. -
A check box or radio option needs appearance streams for its states: an "on" state with a name and an
Offstate. PDFBox does not draw them for you, so the example draws a cross and a filled square. -
A radio widget is invisible until its
/ASentry names a state. PDFBox sets the selected option’s state when the value is set; the unselected widget needsOffset explicitly, which the example does.
package bookshelf;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
/** Builds a two-page form from nothing: text field, check box, radio group and a field with two widgets. */
public final class CreateForm {
private CreateForm() {
}
public static void create(Path target) throws IOException {
try (PDDocument document = new PDDocument()) {
PDPage page1 = new PDPage(PDRectangle.A4);
PDPage page2 = new PDPage(PDRectangle.A4);
document.addPage(page1);
document.addPage(page2);
PDAcroForm form = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(form);
// The default resources name the font that the default appearance string refers to.
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
PDResources resources = new PDResources();
resources.put(COSName.getPDFName("Helv"), helvetica);
form.setDefaultResources(resources);
form.setDefaultAppearance("/Helv 0 Tf 0 g");
// 1. A text field with a single widget.
PDTextField name = new PDTextField(form);
name.setPartialName("fullName");
form.getFields().add(name);
addWidget(name.getWidgets().get(0), page1, new PDRectangle(150, 700, 250, 22));
name.setValue("Ada Example");
// 2. A check box: the two appearance states are drawn by hand.
PDCheckBox newsletter = new PDCheckBox(form);
newsletter.setPartialName("newsletter");
form.getFields().add(newsletter);
PDAnnotationWidget box = newsletter.getWidgets().get(0);
addWidget(box, page1, new PDRectangle(150, 660, 18, 18));
box.setAppearance(appearance(document, 18, "Yes", true));
newsletter.check();
// 3. A radio group: one field, two widgets, one export value per widget.
PDRadioButton plan = new PDRadioButton(form);
plan.setPartialName("plan");
plan.setRadiosInUnison(true);
form.getFields().add(plan);
plan.setExportValues(List.of("basic", "premium"));
PDAnnotationWidget basic = new PDAnnotationWidget();
PDAnnotationWidget premium = new PDAnnotationWidget();
addWidget(basic, page1, new PDRectangle(150, 620, 18, 18));
addWidget(premium, page1, new PDRectangle(250, 620, 18, 18));
basic.setAppearance(appearance(document, 18, "basic", false));
premium.setAppearance(appearance(document, 18, "premium", false));
plan.setWidgets(List.of(basic, premium));
plan.setValue("premium");
// A radio widget is invisible until its /AS names a state: set the unselected one explicitly.
basic.getCOSObject().setName(COSName.AS, "Off");
// 4. One field, two widgets on two pages: both always show the same value.
PDTextField phone = new PDTextField(form);
phone.setPartialName("contactPhone");
form.getFields().add(phone);
// Two fresh widgets, both pointing at the field through /Parent (do not reuse the merged one).
PDAnnotationWidget first = new PDAnnotationWidget();
PDAnnotationWidget second = new PDAnnotationWidget();
first.setParent(phone);
second.setParent(phone);
addWidget(first, page1, new PDRectangle(150, 580, 150, 22));
addWidget(second, page2, new PDRectangle(150, 780, 150, 22));
phone.setWidgets(List.of(first, second));
phone.setValue("+34 600 000 000");
try (PDPageContentStream cs = new PDPageContentStream(document, page1)) {
cs.beginText();
cs.setFont(helvetica, 12);
cs.newLineAtOffset(40, 706);
cs.showText("Full name");
cs.newLineAtOffset(0, -40);
cs.showText("Newsletter");
cs.newLineAtOffset(0, -40);
cs.showText("Plan (basic, premium)");
cs.newLineAtOffset(0, -40);
cs.showText("Contact phone");
cs.endText();
}
document.save(target.toFile());
}
}
private static void addWidget(PDAnnotationWidget widget, PDPage page, PDRectangle rectangle)
throws IOException {
widget.setRectangle(rectangle);
widget.setPage(page);
widget.setPrinted(true);
page.getAnnotations().add(widget);
}
/** Normal appearance with an "on" state named onName and an empty Off state. */
private static PDAppearanceDictionary appearance(PDDocument document, float size, String onName,
boolean cross) throws IOException {
COSDictionary states = new COSDictionary();
states.setItem(COSName.getPDFName(onName), state(document, size, true, cross));
states.setItem(COSName.OFF, state(document, size, false, cross));
PDAppearanceDictionary dictionary = new PDAppearanceDictionary();
dictionary.setNormalAppearance(new PDAppearanceEntry(states));
return dictionary;
}
private static PDAppearanceStream state(PDDocument document, float size, boolean on, boolean cross)
throws IOException {
PDAppearanceStream stream = new PDAppearanceStream(document);
stream.setBBox(new PDRectangle(size, size));
stream.setResources(new PDResources());
try (PDPageContentStream cs = new PDPageContentStream(document, stream)) {
cs.setLineWidth(1);
cs.addRect(0.5f, 0.5f, size - 1, size - 1);
cs.stroke();
if (on && cross) {
cs.moveTo(3, 3);
cs.lineTo(size - 3, size - 3);
cs.moveTo(3, size - 3);
cs.lineTo(size - 3, 3);
cs.stroke();
} else if (on) {
cs.addRect(4, 4, size - 8, size - 8);
cs.fill();
}
}
return stream;
}
public static void main(String[] args) throws IOException {
create(Path.of(args[0]));
}
}
The result, read back with the inspector, is one text field, one check box (Yes), one radio group with two widgets
(value premium) and one text field contactPhone with two widgets, one on each of the two pages:
Tx fullName value=Ada Example widgets=1
Btn newsletter value=Yes widgets=1
Btn plan value=premium widgets=2
Tx contactPhone value=+34 600 000 000 widgets=2
Multi-widget fields
A field with several widgets stores its value once, so editing it on one page changes it on the other, which is what a
customer number or a name repeated on every page needs. The two widgets can have different positions, sizes and colours.
The pitfall found while writing the example: reusing the widget that new PDTextField(form) already created and adding a
second one with setWidgets made PDFBox log "Child field is same object as parent" and turned the field into a group in the
field tree. Creating two new widgets and giving each setParent(field), as the official example does, avoids it. The
radio group above is the other kind of multi-widget field: each widget is one option, and the value says which one is on.
Derived from the PDFBox examples CreateSimpleForm.java, CreateCheckBox.java, CreateRadioButtons.java and CreateMultiWidgetsForm.java.
Choosing between the two tools
| Need | JasperReports properties | PDFBox |
|---|---|---|
Fields that follow the layout of a data-driven report |
Yes: position, font and colours come from the element |
Manual: every rectangle is computed in code |
Text, combo, list, check box, radio |
Yes |
Yes |
Push button, signature field, required flag, max length, JavaScript |
No |
Yes (fields and flags); actions through the annotation and action classes |
One field in several places |
No (one widget per element; a shared name only groups radio options) |
Yes (several widgets with the same parent) |
Fill, read back and flatten an existing file |
No |
Yes |
Licence |
LGPL-3.0 (Community) |
Apache-2.0 |
For the Bookshelf onboarding form the two are combined: JasperReports lays out the page and declares the fields, PDFBox pre-fills the values that the back office knows, adds the signature field and, after the customer has returned the file, reads the values back with the inspector’s approach. iText (AGPL or commercial) can do the same end to end with a single library, as shown in Editable Forms with iText.
What was run, and what was not
-
The JRXML template and the six Java classes on this page were compiled with
maven.compiler.release21 against JasperReports 7.0.8,jasperreports-pdf7.0.8,jasperreports-jdt7.0.8 and PDFBox 3.0.8, and run on a local JDK. -
The exported form, the filled form, the flattened form, the generated form and the form with a signature field were opened with PDFBox, the field values and flags were read back, and three of them were also read with pypdf and rendered to PNG with PDFBox to check how they look.
-
Not tested: opening the files in Adobe Acrobat, a browser viewer or a mobile viewer (appearance of the generated glyphs, auto font size and JavaScript-free behaviour can differ); a multi-select list; a signature applied to a form; PDF/A or tagged output of a form (JasperReports tagging needs the Professional edition in 7.0.8 and PDF/A forbids some field features, see PDF Standards & Conformance); accessibility of the fields (field labels as tooltips,
/TU, are not set by the exporter properties above). -
The check box on-state name, the radio on-state names and the first-choice default of a list are observations of this run, not documented behaviour.
Outdated points to avoid
-
Do not use
PDDocument.loadorPDDocument.loadNonSeq. PDFBox 3 loads withLoader.loadPDF(File | byte[] | RandomAccessRead). -
Do not rely on
setNeedAppearances(true)to make values appear. It asks a viewer to regenerate appearances, and many viewers and printers do not.setValuealready writes the appearance, and a flattened file needs it. -
Do not build the radio value from a template UUID. The on-state names JasperReports generates change on each export; read them from the widget.
-
Do not assume JasperReports can produce every field. Push buttons and signature fields are added with PDFBox, and the properties exist only since 6.12.0 (finding O27).
-
Do not use XFA for new forms. It is deprecated in PDF 2.0 and not a target of this page.
-
Do not treat a read-only flag as protection. Any code can still change the value.
References
-
JasperReports, PDF Forms Sample and configuration reference, net.sf.jasperreports.export.pdf.field.type.
-
JasperReports API, JRPdfExporter.
-
Apache PDFBox, Migration to PDFBox 3.0, dependencies and the PDAcroForm Javadoc.
-
PDFBox examples, CreateSimpleForm, FillFormField, CreateCheckBox, CreateRadioButtons and CreateMultiWidgetsForm.
-
Licences, Apache-2.0.
-
In this section: Editable Forms Fundamentals, Editable Forms with iText, PDF Export Configuration, JRXML Report Design, Signing with PDFBox and EU DSS and PDF Standards & Conformance.