Automatizar o preenchimento de formulários PDF com Java e PDFtk
Preencha um formulário PDF existente a partir de um mapa Java e escolha se os campos continuarão editáveis ou se tornarão conteúdo da página. O exemplo abaixo gera XFDF com as APIs XML do Java, executa o PDFtk Java e salva um novo PDF, recusando-se a substituir um arquivo de destino existente.
A operação fill_form do PDFtk
aceita FDF ou XFDF para campos AcroForm existentes. Ela não cria campos em uma página digitalizada.
Use um modelo de AcroForm confiável e não criptografado neste tutorial; formulários XFA e PDFs
assinados exigem um fluxo de trabalho diferente.
Pré-requisitos
- Um JDK com manutenção ativa, com
javaejavacemPATH. - Linux com Bash,
curle GNUsha256sum, instalados separadamente do JDK. - Seu modelo de AcroForm existente, com nomes de campos conhecidos e fontes que incluam os caracteres do seu texto.
- Um diretório de trabalho com permissão de escrita em um sistema de arquivos que suporte links físicos.
O ambiente de execução verificado é OpenJDK 21.0.12.1 com PDFtk Java 3.3.3 no Linux. Essas são versões testadas, não um requisito mínimo inerente ao Java. Use uma versão do JDK 21 com manutenção ativa, como o Temurin 21.0.12.1, seguindo seu guia de instalação a partir de um arquivo de distribuição. Os comandos de instalação abaixo são para Linux; a configuração no macOS e no Windows está fora do escopo deste tutorial.
Configurar o PDFtk no seu ambiente Java
O PDFtk Java é a implementação do PDFtk em Java. Seu guia de instalação específico para a versão oferece um JAR independente que contém suas dependências. Baixe esse JAR em um novo diretório de trabalho para que o programa selecione uma versão conhecida do PDFtk sem alterar uma instalação do sistema.
Verifique os pré-requisitos de linha de comando instalados separadamente antes de criar arquivos.
Cole este bloco no Bash a partir de um diretório pai com permissão de escrita; ao concluir com
sucesso, o shell estará em pdf-form-work:
java -version && javac -version &&
command -v curl && command -v sha256sum &&
mkdir pdf-form-work && cd pdf-form-work
Se o diretório já existir, este bloco será interrompido. Preserve seu conteúdo e escolha um novo
nome, ou entre no diretório que você criou anteriormente para retomar uma configuração parcial.
De dentro de pdf-form-work, baixe e verifique o 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
A soma de verificação corresponde à informada nos
metadados do pacote pelo responsável pela publicação.
A saída de versão deve incluir pdftk port to java 3.3.3. Se o download ou a verificação falhar,
execute novamente o bloco de download nesse mesmo diretório sob seu controle: ele substitui apenas
o JAR baixado. Não continue após uma verificação malsucedida. Todos os comandos restantes devem ser
executados a partir de pdf-form-work.
Extrair campos de dados de formulários de PDFs com PDFtk
Coloque seu modelo neste diretório como template.pdf. Inspecione seus nomes e valores
de exportação em UTF-8:
java -jar pdftk-java-3.3.3-all.jar template.pdf dump_data_fields_utf8
Um campo de texto pode ter um nome como Name; campos de caixa de seleção e
botão de opção também informam valores de FieldStateOption. Use esses nomes e valores
exatos, em vez do rótulo visível ou de um valor presumido de Yes.
Anote os nomes que você usará antes de escrever seu mapa. Mesmo um comando PDFtk concluído com
sucesso pode deixar um campo desconhecido sem preenchimento.
Automatizar o preenchimento de formulários com Java
Salve este exemplo completo como PdfFormFiller.java. fillForm aceita
um mapa de nomes e valores de campos. O exemplo de CLI fornece Name e
Date; altere essas chaves para corresponder ao modelo que você inspecionou.
O JAR deve permanecer no diretório de trabalho quando você executar o 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);
}
}
}
Compile e execute o programa a partir do diretório que contém o código-fonte, o JAR e o modelo.
O operador && impede que uma compilação malsucedida execute uma classe
restante de uma compilação anterior. O classpath explícito seleciona este exemplo mesmo que o
projeto que o contém defina CLASSPATH:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf filled_form.pdf
Ao concluir com sucesso, o programa imprime Filled PDF saved e cria
filled_form.pdf. Para um modelo com os campos Name e
Date do exemplo, os valores esperados nessas posições são
Jane Doe e 2026-09-15.
Executar o comando novamente recusa esse destino e mantém seus bytes inalterados. Escolha um novo
nome de arquivo de saída para outro registro.
O diretório temporário contém os valores XFDF e o PDF candidato. Armazene esse diretório de trabalho
com permissões adequadas para esses dados. A gravação final usa
Files.createLink
para recusar um destino que surja durante a conversão; ela exige suporte a links físicos e não
criptografa o PDF. O bloco finally remove os arquivos temporários durante a
execução normal; uma JVM encerrada à força pode deixá-los para trás.
Escolher uma saída editável ou achatada
O valor final true na chamada fillForm do exemplo
adiciona a opção flatten do PDFtk, tornando a aparência dos campos parte da
página. Para manter os campos editáveis, altere esse argumento para false,
recompile e grave em um novo destino:
javac -encoding UTF-8 -cp . PdfFormFiller.java &&
java -cp . PdfFormFiller template.pdf editable_form.pdf
O achatamento não fornece controle de acesso nem proteção contra adulteração.
Chame fillForm com um novo mapa para cada modelo verificado. Por exemplo, use o
valor de exportação informado por uma caixa de seleção, como Accepted, em vez
de uma string booleana. Um campo multilinha aceita uma quebra de linha no seu valor. Mantenha
registros sintéticos no código enquanto verifica um modelo e depois forneça dados reais a partir
da sua aplicação, em vez de incorporá-los aos arquivos de código-fonte.
Verificar valores e aparência
Abra filled_form.pdf e compare cada campo visível com seu valor e sua posição
pretendidos. O programa verifica o status do processo e se o arquivo de saída não está vazio; ele
não pode comprovar que todos os campos foram preenchidos corretamente. Revise um resultado editável
antes de optar pelo achatamento de um novo modelo.
Para a variante editável, inspecione também os valores armazenados:
java -jar pdftk-java-3.3.3-all.jar editable_form.pdf dump_data_fields_utf8
Compare FieldValue com seu mapa. Um PDF achatado não tem mais campos interativos
para inspecionar, portanto este comando não substitui a revisão do conteúdo visível da página.
- Os valores de caixas de seleção e botões de opção devem corresponder aos valores de exportação do modelo.
- Campos de escolha podem ter rótulos de exibição diferentes dos valores armazenados; inspecione ambos.
- Este exemplo baseado em mapa grava um valor de texto simples por campo, não texto formatado nem uma lista de seleções.
- UTF-8 e o escape XML preservam textos como
Zoë & <Ada>, mas as fontes do modelo ainda precisam conter esses glifos. Os limites dos campos podem cortar textos longos mesmo quando o valor armazenado está correto.
Solucionar problemas comuns
O PDFtk não inicia
Execute o comando de versão do JAR a partir do mesmo diretório de trabalho do programa Java.
Verifique se o JAR verificado está lá e se java ainda está em
PATH. Um alias de shell para pdftk é desnecessário;
o programa inicia o JAR diretamente.
Problemas de permissão
O processo precisa de acesso de leitura ao modelo e de escrita ao diretório de destino. Ele também precisa de suporte a links físicos nesse sistema de arquivos. Se a gravação final falhar, não recorra a uma cópia que sobrescreva arquivos; escolha um armazenamento compatível ou implemente uma alternativa que explicitamente não sobrescreva arquivos.
Lidar com PDFs criptografados
Modelos criptografados precisam de um projeto separado para o tratamento de senhas. O PDFtk suporta
input_pw, mas colocar senhas nos argumentos do processo pode expô-las à
inspeção de processos. Não inclua senhas diretamente no código nem as copie para logs. Use um fluxo
de trabalho seguro e adequado ao seu ambiente, em vez de adicionar segredos à linha de comando
deste tutorial. Regravar um PDF assinado também invalida sua assinatura.
Nomes ou valores de campos incorretos
Verifique os nomes exatos dos campos e os valores de exportação usando
dump_data_fields_utf8. Compare um resultado não achatado com o mapa pretendido e depois
inspecione visualmente o resultado achatado. Parênteses, sinais de e comercial, aspas e barras
invertidas são tratados pelo serializador XML, não por uma função de escape FDF escrita
manualmente. Glifos ausentes exigem fontes adequadas no modelo; alterar a codificação XML não
resolve esse problema.
