Automatiza el llenado de formularios PDF con Java y PDFtk
Java puede entregar valores de campo estructurados a PDFtk para automatizar el llenado repetido de formularios. Esta guía usa las API XML de Java para generar XFDF en lugar de ensamblar sintaxis PDF a mano, y conserva los archivos existentes mientras comprueba el resultado del conversor.
Introducción a PDFtk y su integración con Java
PDFtk puede llenar campos AcroForm existentes con datos FDF o XFDF. No crea campos rellenables en una página escaneada, y este flujo de trabajo no cubre los formularios XFA. Comienza con una plantilla cuyos nombres de campo, fuentes de apariencia y valores de exportación ya hayas inspeccionado.
Requisitos previos
- Un JDK con mantenimiento activo, con Java 17 o posterior para este ejemplo.
- El comando
pdftkdisponible enPATH. - Una plantilla PDF real con campos AcroForm rellenables.
- Un sistema de archivos de destino que admita enlaces duros.
Configurar PDFtk en tu entorno de Java
En Ubuntu/Debian:
sudo apt-get update
sudo apt-get install pdftk-java
En macOS:
brew install pdftk-java
Para Windows, sigue la documentación de instalación de PDFtk de la distribución que elijas. Verifica el comando y el JDK antes de continuar:
pdftk --version
javac -version
Extraer los campos de datos de formulario de los PDF con PDFtk
Inspecciona los nombres y los valores de exportación en UTF-8:
pdftk template.pdf dump_data_fields_utf8
Un campo de texto puede tener un nombre como Name; los campos de casilla de verificación y de
opción también informan valores FieldStateOption. Usa esos nombres y valores exactos, no la etiqueta visible
ni un valor Yes supuesto. Un comando de PDFtk que se ejecute correctamente todavía puede dejar
sin llenar un campo desconocido.
Automatizar 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 CLI proporciona Name y Date; cambia esas claves para que coincidan con tu plantilla.
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);
}
}
}
Compílalo y ejecútalo:
javac PdfFormFiller.java
java PdfFormFiller template.pdf filled_form.pdf
El serializador XML escapa tanto los nombres de campo como los valores. El transporte en UTF-8 conserva los caracteres, pero las fuentes de apariencia del PDF todavía deben admitirlos. El directorio temporal contiene datos de formulario: mantenlo en un almacenamiento restringido y configura los permisos de directorio o las ACL que correspondan en tu plataforma. La publicación mediante enlaces duros protege contra colisiones de nombres de salida; no cifra el resultado.
flatten=true convierte los campos en contenido de la página. No es un control de acceso ni una
protección contra manipulaciones, y reescribir un PDF firmado invalida su firma. Usa false cuando
necesites campos editables. Inspecciona el resultado visible y, en la salida sin aplanar, inspecciona
también los valores de los campos.
Ejemplos prácticos: automatizar formularios de impuestos y hojas de personaje
Llama a fillForm con un mapa distinto para cada plantilla verificada. Mantén los datos financieros
sensibles fuera del código fuente y de los registros. Usa registros sintéticos para las pruebas; un
tutorial para desarrolladores no es un flujo de trabajo validado de preparación de impuestos. Las
hojas de personaje y los formularios internos son datos de prueba útiles y de bajo riesgo para
comprobar nombres de campo, texto multilínea y valores de selección.
Consejos avanzados para manejar formularios PDF complejos
- Los valores de las casillas de verificación y de 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 mapas escribe un valor de texto sin formato por campo, no texto enriquecido ni una lista de selecciones.
- La cobertura de fuentes, los límites de los campos y la generación de la apariencia afectan el renderizado incluso cuando los valores son correctos.
Solución de problemas comunes
No se encuentra PDFtk
Ejecuta pdftk --version desde el mismo entorno que Java. Corrige PATH o configura una ruta explícita
y de confianza al ejecutable en ProcessBuilder. No aceptes el nombre del ejecutable desde una subida.
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 la publicación falla, no recurras a una copia que sobrescriba; elige un almacenamiento compatible o implementa una alternativa que explícitamente no sobrescriba.
Manejar PDF cifrados
Las plantillas cifradas necesitan un diseño aparte para el manejo de contraseñas. PDFtk admite input_pw,
pero colocar contraseñas en los argumentos del proceso puede exponerlas a la inspección de procesos.
No incluyas contraseñas en el código ni las copies en los registros. Usa un flujo de trabajo seguro
y adecuado para tu entorno en lugar de agregar secretos a la línea de comandos de este tutorial.
Nombres o valores de campo incorrectos
Comprueba los nombres de campo y los valores de exportación exactos con dump_data_fields_utf8. Compara un resultado
sin aplanar con el mapa previsto y luego inspecciona visualmente el resultado aplanado. Los
paréntesis, los ampersands, las comillas y las barras invertidas los maneja el serializador XML, no
una función de escape de FDF escrita a mano. Los glifos faltantes requieren fuentes adecuadas en la
plantilla; no se solucionan cambiando la codificación XML.
Conclusión
Las API XML de Java y PDFtk ofrecen un flujo de trabajo de llenado de formularios pequeño y fácil de probar. Usa una plantilla AcroForm real, mantén las salidas separadas, propaga los fallos y verifica tanto los valores almacenados como la apariencia. Para documentos que no sean de confianza, ejecuta el conversor en un worker restringido, con límites de recursos y sin credenciales de la aplicación.
Para flujos de trabajo de documentos gestionados, explora el servicio de procesamiento de documentos de Transloadit y el Robot 🤖 /document/merge.
