Automate PDF form filling with Java and PDFtk
Java can supply structured field values to PDFtk to automate repeated form filling. This guide uses Java's XML APIs to generate XFDF rather than assembling PDF syntax by hand, and preserves existing files while checking the converter's result.
Introduction to PDFtk and Java integration
PDFtk can fill existing AcroForm fields using FDF or XFDF data. It does not create fillable fields in a scanned page, and this workflow does not cover XFA forms. Start with a template whose field names, appearance fonts, and export values you have inspected.
Prerequisites
- A maintained JDK, with Java 17 or newer for this example.
- The
pdftkcommand available onPATH. - A real PDF template with fillable AcroForm fields.
- A destination filesystem that supports hard links.
Setting up PDFtk in your Java environment
On Ubuntu/Debian:
sudo apt-get update
sudo apt-get install pdftk-java
On macOS:
brew install pdftk-java
For Windows, follow the PDFtk installation documentation for your chosen distribution. Verify the command and JDK before continuing:
pdftk --version
javac -version
Extracting form data fields from PDFs using PDFtk
Inspect names and export values in UTF-8:
pdftk 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. 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 template.
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();
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("pdftk", 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 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:
javac PdfFormFiller.java
java PdfFormFiller template.pdf filled_form.pdf
The XML serializer escapes both field names and values. UTF-8 transport preserves characters, but the PDF's appearance fonts must still support them. The temporary directory contains form data: keep it on restricted storage, and set appropriate directory permissions or ACLs for your platform. Hard-link publication protects against output-name collisions; it does not encrypt the result.
flatten=true renders fields into page content. It is not access control or tamper protection,
and rewriting a signed PDF invalidates its signature. Use false when you need editable fields.
Inspect the visible result and, for unflattened output, inspect the field values as well.
Practical examples: automating tax forms and character sheets
Call fillForm with a different map for each verified template. Keep sensitive financial data out
of source code and logs. Use synthetic records for tests; a developer tutorial is not a validated
tax-preparation workflow. Character sheets and internal forms are useful low-risk fixtures for
checking field names, multiline text, and selection values.
Advanced tips for handling complex PDF forms
- 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.
- Font coverage, field bounds, and appearance generation affect rendering even when values are correct.
Troubleshooting common issues
PDFtk not found
Run pdftk --version from the same environment as Java. Correct PATH, or configure an explicit
trusted executable path in ProcessBuilder. Do not accept the executable name from an upload.
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.
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.
Conclusion
Java's XML APIs and PDFtk provide a small, testable form-filling workflow. Use a real AcroForm template, keep outputs separate, propagate failures, and verify both stored values and appearance. For untrusted documents, run the converter in a restricted worker with resource limits and no application credentials.
For managed document workflows, explore Transloadit's Document Processing service and 🤖 /document/merge Robot.
