Automate PDF form filling with Java and PDFtk
Fill an existing PDF form from a Java map, then choose whether its fields remain editable or become page content. The example below generates XFDF with Java’s XML APIs, runs PDFtk Java, and saves a new PDF while refusing to replace an existing destination.
PDFtk’s fill_form operation
accepts FDF or XFDF for existing AcroForm fields. It does not create fields in a scanned page.
Use a trusted, unencrypted AcroForm template for this walkthrough; XFA forms and signed PDFs
require a different workflow.
Prerequisites
- A maintained JDK with
javaandjavaconPATH. - Linux with Bash,
curl, and GNUsha256sum, installed separately from the JDK. - Your existing AcroForm template, with known field names and fonts that cover your text.
- A writable working directory on a filesystem that supports hard links.
The verified runtime is OpenJDK 21.0.12.1 with PDFtk Java 3.3.3 on Linux. These are tested versions, not an intrinsic Java minimum. Use a maintained JDK 21 release, such as Temurin 21.0.12.1, following its archive installation guide. The installation commands below target Linux; macOS and Windows setup are outside this walkthrough.
Setting up PDFtk in your Java environment
PDFtk Java is the Java port of PDFtk. Its versioned installation guide offers a standalone JAR containing its dependencies. Download that JAR into a new working directory so the program selects a known PDFtk version without changing a system installation.
Check the separate command-line prerequisites before creating files. Paste this block in Bash
from a writable parent directory; success leaves your shell inside pdf-form-work:
java -version && javac -version &&
command -v curl && command -v sha256sum &&
mkdir pdf-form-work && cd pdf-form-work
An existing directory stops this block. Keep its contents and choose a new name, or enter the
directory you previously created to recover a partial setup. From inside pdf-form-work, download
and verify the JAR:
curl -fsSLo pdftk-java-3.3.3-all.jar \
https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v3.3.3/pdftk-all.jar &&
printf '%s %s\n' \
a694d49bd03e1edd4c23b3ba808bc221eb8a8ccfe7bfd2a0a884b2b2fb425188 \
pdftk-java-3.3.3-all.jar | sha256sum -c - &&
java -jar pdftk-java-3.3.3-all.jar --version
The checksum matches the publisher’s
package metadata.
Expect the version output to include pdftk port to java 3.3.3. If downloading or verification fails,
rerun the download block in this same owned directory: it replaces only the JAR download.
Do not continue after a failed check. All remaining commands run from pdf-form-work.
Extracting form data fields from PDFs using PDFtk
Place your template in this directory as template.pdf. Inspect its names and export values in UTF-8:
java -jar pdftk-java-3.3.3-all.jar template.pdf dump_data_fields_utf8
A text field may have a name such as Name; checkbox and radio fields additionally report
FieldStateOption values. Use those exact names and values, not the visible label or an assumed
Yes value. Record the names you will use before writing your map. A successful PDFtk command can
still leave an unknown field unfilled.
Automating form filling with Java
Save this complete example as PdfFormFiller.java. fillForm accepts a map of field names and
values. The CLI example supplies Name and Date; change those keys to match your inspected
template. The JAR must remain in the working directory when you run the program.
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
public class PdfFormFiller {
public static void fillForm(Path template, Path output, Map<String, String> values,
boolean flatten) throws Exception {
template = template.toAbsolutePath().normalize();
output = output.toAbsolutePath().normalize();
Path toolkit = Path.of("pdftk-java-3.3.3-all.jar").toAbsolutePath().normalize();
if (!Files.isRegularFile(toolkit)) {
throw new IOException("PDFtk Java JAR must be in the working directory");
}
if (!Files.isRegularFile(template)) {
throw new IOException("Template must be a regular file");
}
if (Files.exists(output, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Output must not exist");
}
if (values.isEmpty() || values.entrySet().stream().anyMatch(
entry -> entry.getKey() == null || entry.getValue() == null)) {
throw new IllegalArgumentException("Supply nonnull field names and values");
}
Path temporary = Files.createTempDirectory(output.getParent(), ".pdf-form-");
Path data = temporary.resolve("data.xfdf");
Path candidate = temporary.resolve("result.pdf");
try {
writeXfdf(data, values);
List<String> arguments = new ArrayList<>(List.of("java", "-jar", toolkit.toString(),
template.toString(),
"fill_form", data.toString(), "output", candidate.toString()));
if (flatten) arguments.add("flatten");
arguments.add("dont_ask");
Process process = new ProcessBuilder(arguments)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD).start();
process.getOutputStream().close();
try {
if (!process.waitFor(120, TimeUnit.SECONDS)) {
throw new IOException("PDFtk conversion timed out");
}
if (process.exitValue() != 0) {
throw new IOException("PDFtk conversion failed");
}
} finally {
if (process.isAlive()) {
process.destroyForcibly();
process.waitFor();
}
}
if (!Files.isRegularFile(candidate) || Files.size(candidate) == 0) {
throw new IOException("PDFtk did not produce a nonempty result");
}
// Same-filesystem publication fails if a destination appeared during conversion.
Files.createLink(output, candidate);
} finally {
Files.deleteIfExists(candidate);
Files.deleteIfExists(data);
Files.deleteIfExists(temporary);
}
}
private static void writeXfdf(Path path, Map<String, String> values) throws Exception {
String namespace = "http://ns.adobe.com/xfdf/";
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElementNS(namespace, "xfdf");
document.appendChild(root);
Element fields = document.createElementNS(namespace, "fields");
root.appendChild(fields);
for (Map.Entry<String, String> entry : values.entrySet()) {
Element field = document.createElementNS(namespace, "field");
field.setAttribute("name", entry.getKey());
Element value = document.createElementNS(namespace, "value");
value.setTextContent(entry.getValue());
field.appendChild(value);
fields.appendChild(field);
}
var transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
try (OutputStream stream = Files.newOutputStream(path)) {
transformer.transform(new DOMSource(document), new StreamResult(stream));
}
}
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("Usage: java -cp . PdfFormFiller <template.pdf> <new-output.pdf>");
System.exit(1);
}
try {
fillForm(Path.of(args[0]), Path.of(args[1]),
Map.of("Name", "Jane Doe", "Date", "2026-09-15"), true);
System.out.println("Filled PDF saved");
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
System.err.println("PDF form filling interrupted");
System.exit(1);
} catch (Exception error) {
System.err.println("PDF form filling failed; check template, fields, destination, and PDFtk");
System.exit(1);
}
}
}
Compile and run it from the directory containing the source, JAR, and template. The && prevents
a failed compilation from running a class left over from an earlier build. The explicit classpath
selects this example even if your enclosing project sets CLASSPATH:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf filled_form.pdf
On success, the program prints Filled PDF saved and creates filled_form.pdf. For a template with
the example’s Name and Date fields, expect Jane Doe and 2026-09-15 in those positions.
Running the command again refuses that destination and leaves its bytes unchanged. Choose a new
output filename for another record.
The temporary directory contains the XFDF values and candidate PDF. Store this working directory
with permissions appropriate for that data. Publication uses
Files.createLink
to refuse a destination that appears during conversion; it requires hard-link support and does not
encrypt the PDF. The finally block removes staging files during normal execution; a killed JVM
may leave them behind.
Choose editable or flattened output
The final true in the example’s fillForm call adds PDFtk’s flatten option, making the field
appearances part of the page. To keep the fields editable, change that argument to false, recompile,
and write a fresh destination:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf editable_form.pdf
Flattening does not provide access control or tamper protection.
Call fillForm with a new map for each verified template. For example, use a checkbox’s reported
export value, such as Accepted, rather than a Boolean string. A multiline field accepts a newline
in its value. Keep synthetic records in code while checking a template, then supply real data from
your application instead of embedding it in source files.
Verify values and appearances
Open filled_form.pdf and compare each visible field with its intended value and position. The
program checks process status and a nonempty output file; it cannot prove that every field was
filled correctly. Review an editable result before committing to flattening a new template.
For the editable variant, also inspect the stored values:
java -jar pdftk-java-3.3.3-all.jar editable_form.pdf dump_data_fields_utf8
Check FieldValue against your map. A flattened PDF has no remaining interactive fields to inspect,
so this command cannot replace reviewing its visible page content.
- Checkbox and radio values must match the template's export values.
- Choice fields can have display labels different from their stored values; inspect both.
- This map-based example writes one plain-text value per field, not rich text or a list of selections.
- UTF-8 and XML escaping preserve text such as
Zoë & <Ada>, but the template’s fonts still need those glyphs. Field bounds can clip long text even when the stored value is correct.
Troubleshooting common issues
PDFtk fails to start
Run the JAR version command from the same working directory as the Java program. Check that the
verified JAR is there and that java is still on PATH. A shell alias for pdftk is unnecessary;
the program launches the JAR directly.
Permission issues
The process needs read access to the template and write access to the destination directory. It also needs hard-link support on that filesystem. If publication fails, do not fall back to an overwriting copy; choose supported storage or implement an explicitly non-overwriting alternative.
Handling encrypted PDFs
Encrypted templates need a separate password-handling design. PDFtk supports input_pw, but placing
passwords in process arguments can expose them to process inspection. Do not hardcode passwords or
copy them into logs. Use an appropriate secure workflow for your environment rather than adding
secrets to this tutorial’s command line. Rewriting a signed PDF also invalidates its signature.
Incorrect field names or values
Check exact field names and export values using dump_data_fields_utf8. Compare an unflattened
result with the intended map, then inspect the flattened result visually. Parentheses, ampersands,
quotes, and backslashes are handled by the XML serializer, not a handwritten FDF escape function.
Missing glyphs require suitable template fonts; they are not fixed by changing the XML encoding.
