Automatiza el llenado de formularios PDF con Java y PDFtk
Rellena un formulario PDF existente desde un mapa de Java y elige si sus campos siguen siendo editables o pasan a formar parte del contenido de la página. El siguiente ejemplo genera XFDF con las API XML de Java, ejecuta PDFtk Java y guarda un PDF nuevo sin reemplazar un archivo de destino existente.
La operación fill_form de PDFtk
acepta FDF o XFDF para campos AcroForm existentes. No crea campos en una página escaneada.
Usa una plantilla AcroForm de confianza y sin cifrar para seguir este tutorial; los formularios XFA
y los PDF firmados requieren un flujo de trabajo diferente.
Requisitos previos
- Un JDK con mantenimiento activo, con
javayjavacenPATH. - Linux con Bash,
curly GNUsha256sum, instalados por separado del JDK. - Tu plantilla AcroForm existente, con nombres de campo conocidos y fuentes que incluyan los caracteres de tu texto.
- Un directorio de trabajo con permisos de escritura en un sistema de archivos compatible con enlaces duros.
El entorno de ejecución verificado es OpenJDK 21.0.12.1 con PDFtk Java 3.3.3 en Linux. Estas son versiones probadas, no un requisito mínimo intrínseco de Java. Usa una versión de JDK 21 con mantenimiento activo, como Temurin 21.0.12.1, y sigue su guía de instalación desde un archivo de distribución. Los comandos de instalación siguientes están dirigidos a Linux; la configuración en macOS y Windows queda fuera del alcance de este tutorial.
Configura PDFtk en tu entorno Java
PDFtk Java es la adaptación de PDFtk a Java. Su guía de instalación para esta versión ofrece un JAR independiente que contiene sus dependencias. Descarga ese JAR en un directorio de trabajo nuevo para que el programa seleccione una versión conocida de PDFtk sin cambiar una instalación del sistema.
Comprueba los requisitos de línea de comandos independientes antes de crear archivos. Pega este
bloque en Bash desde un directorio padre con permisos de escritura; si se ejecuta correctamente,
tu shell quedará dentro de pdf-form-work:
java -version && javac -version &&
command -v curl && command -v sha256sum &&
mkdir pdf-form-work && cd pdf-form-work
Si el directorio ya existe, este bloque se detiene. Conserva su contenido y elige un nombre nuevo,
o entra en el directorio que creaste previamente para recuperar una configuración parcial.
Desde pdf-form-work, descarga y verifica el 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
La suma de comprobación coincide con los
metadatos del paquete proporcionados por su distribuidor.
La salida de versión debe incluir pdftk port to java 3.3.3. Si falla la descarga o la
verificación, vuelve a ejecutar el bloque de descarga en este mismo directorio bajo tu control:
solo reemplaza el JAR descargado.
No continúes si falla una comprobación. Todos los comandos restantes se ejecutan desde
pdf-form-work.
Extrae los campos de datos de formularios PDF con PDFtk
Coloca tu plantilla en este directorio como template.pdf.
Inspecciona sus nombres y valores de exportación en UTF-8:
java -jar pdftk-java-3.3.3-all.jar template.pdf dump_data_fields_utf8
Un campo de texto puede tener un nombre como Name; los campos de casillas
de verificación y botones de opción también indican valores de FieldStateOption.
Usa esos nombres y valores exactos, no la etiqueta visible ni un valor supuesto de
Yes. Anota los nombres que usarás antes de escribir tu mapa. Aunque un
comando de PDFtk se ejecute correctamente, puede dejar sin rellenar un campo desconocido.
Automatiza el llenado de formularios con Java
Guarda este ejemplo completo como PdfFormFiller.java.
fillForm acepta un mapa de nombres de campo y valores.
El ejemplo de la CLI proporciona Name y Date;
cambia esas claves para que coincidan con la plantilla que inspeccionaste. El JAR debe permanecer
en el directorio de trabajo cuando ejecutes el programa.
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);
}
}
}
Compílalo y ejecútalo desde el directorio que contiene el código fuente, el JAR y la plantilla.
&& evita que una compilación fallida ejecute una clase que haya quedado
de una compilación anterior. La ruta de clases explícita selecciona este ejemplo incluso si el
proyecto que lo contiene establece CLASSPATH:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf filled_form.pdf
Si se ejecuta correctamente, el programa imprime Filled PDF saved y crea
filled_form.pdf. En una plantilla con los campos Name
y Date del ejemplo, los valores esperados en esas posiciones son
Jane Doe y 2026-09-15.
Al ejecutar el comando de nuevo, se rechaza ese destino y sus bytes permanecen intactos. Elige un
nombre de archivo de salida nuevo para otro registro.
El directorio temporal contiene los valores XFDF y el PDF provisional. Guarda este directorio de
trabajo con permisos adecuados para esos datos. La publicación usa
Files.createLink
para rechazar un destino que aparezca durante la conversión; requiere compatibilidad con enlaces
duros y no cifra el PDF. El bloque finally elimina los archivos provisionales
durante la ejecución normal; si la JVM termina de forma forzada, puede dejarlos allí.
Elige una salida editable o aplanada
El true final de la llamada a fillForm del ejemplo
añade la opción flatten de PDFtk, que convierte las representaciones visuales
de los campos en parte de la página. Para mantener los campos editables, cambia ese argumento a
false, vuelve a compilar y escribe en un destino nuevo:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf editable_form.pdf
El aplanado no proporciona control de acceso ni protección contra modificaciones.
Llama a fillForm con un mapa nuevo para cada plantilla verificada.
Por ejemplo, usa el valor de exportación indicado por una casilla de verificación, como
Accepted, en lugar de una cadena booleana. Un campo de varias líneas acepta
un salto de línea en su valor. Mantén registros ficticios en el código mientras compruebas una
plantilla; después, proporciona datos reales desde tu aplicación en lugar de incluirlos en los
archivos de código fuente.
Verifica los valores y la apariencia
Abre filled_form.pdf y compara cada campo visible con su valor y posición previstos.
El programa comprueba el estado del proceso y que el archivo de salida no esté vacío; no puede
demostrar que todos los campos se hayan rellenado correctamente. Revisa un resultado editable
antes de decidir aplanar una plantilla nueva.
Para la variante editable, inspecciona también los valores almacenados:
java -jar pdftk-java-3.3.3-all.jar editable_form.pdf dump_data_fields_utf8
Compara FieldValue con tu mapa. Un PDF aplanado ya no tiene campos interactivos
que inspeccionar, por lo que este comando no puede sustituir la revisión del contenido visible
de sus páginas.
- Los valores de las casillas de verificación y los botones de opción deben coincidir con los valores de exportación de la plantilla.
- Los campos de selección pueden tener etiquetas visibles distintas de sus valores almacenados; inspecciona ambos.
- Este ejemplo basado en un mapa escribe un valor de texto sin formato por campo, no texto enriquecido ni una lista de selecciones.
- UTF-8 y el escape XML preservan texto como
Zoë & <Ada>, pero las fuentes de la plantilla deben incluir esos glifos. Los límites del campo pueden recortar textos largos incluso cuando el valor almacenado es correcto.
Resuelve problemas comunes
PDFtk no se inicia
Ejecuta el comando de versión del JAR desde el mismo directorio de trabajo que el programa Java.
Comprueba que el JAR verificado esté allí y que java siga en
PATH. No es necesario un alias de shell para
pdftk; el programa ejecuta el JAR directamente.
Problemas de permisos
El proceso necesita acceso de lectura a la plantilla y acceso de escritura al directorio de destino. También necesita compatibilidad con enlaces duros en ese sistema de archivos. Si falla la publicación, no recurras a una copia que sobrescriba el destino; elige un almacenamiento compatible o implementa una alternativa que explícitamente no sobrescriba archivos.
Manejo de PDF cifrados
Las plantillas cifradas necesitan un diseño independiente para el manejo de contraseñas. PDFtk
admite input_pw, pero incluir contraseñas en los argumentos del proceso puede
exponerlas al inspeccionar procesos. No escribas contraseñas directamente en el código ni las copies
en los registros. Usa un flujo de trabajo seguro y adecuado para tu entorno en lugar de añadir
secretos a la línea de comandos de este tutorial. Reescribir un PDF firmado también invalida su firma.
Nombres de campo o valores incorrectos
Comprueba los nombres de campo y valores de exportación exactos con
dump_data_fields_utf8. Compara un resultado sin aplanar con el mapa previsto y después
inspecciona visualmente el resultado aplanado. El serializador XML maneja los paréntesis, los signos
«&», las comillas y las barras invertidas, no una función de escape FDF escrita manualmente.
Los glifos ausentes requieren fuentes adecuadas en la plantilla; cambiar la codificación XML
no soluciona su ausencia.
