Automatize o preenchimento de formulários PDF com Java e PDFtk
O Java pode fornecer valores estruturados de campos ao PDFtk para automatizar o preenchimento repetido de formulários. Este guia usa as APIs XML do Java para gerar XFDF em vez de montar a sintaxe PDF manualmente, além de preservar os arquivos existentes enquanto verifica o resultado do conversor.
Introdução à integração entre PDFtk e Java
O PDFtk pode preencher campos AcroForm existentes usando dados FDF ou XFDF. Ele não cria campos preenchíveis em uma página digitalizada, e este fluxo de trabalho não abrange formulários XFA. Comece com um modelo cujos nomes de campos, fontes de aparência e valores de exportação você já tenha inspecionado.
Pré-requisitos
- Um JDK com manutenção ativa, com Java 17 ou mais recente para este exemplo.
- O comando
pdftkdisponível noPATH. - Um modelo PDF real com campos AcroForm preenchíveis.
- Um sistema de arquivos de destino com suporte a links físicos (hard links).
Configuração do PDFtk no seu ambiente Java
No Ubuntu/Debian:
sudo apt-get update
sudo apt-get install pdftk-java
No macOS:
brew install pdftk-java
No Windows, siga a documentação de instalação do PDFtk para a distribuição escolhida. Verifique o comando e o JDK antes de continuar:
pdftk --version
javac -version
Extração dos campos de dados de formulários de PDFs com o PDFtk
Inspecione os nomes e os valores de exportação em UTF-8:
pdftk template.pdf dump_data_fields_utf8
Um campo de texto pode ter um nome como Name; campos de caixa de seleção e de opção também
informam valores FieldStateOption. Use exatamente esses nomes e valores, e não o rótulo visível nem um
valor Yes presumido. Um comando do PDFtk bem-sucedido ainda pode deixar um campo
desconhecido sem preenchimento.
Automação do preenchimento de formulários com Java
Salve este exemplo completo como PdfFormFiller.java. fillForm aceita um mapa de nomes de campos e
valores. O exemplo de CLI fornece Name e Date; altere essas chaves para corresponder ao seu
modelo.
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 e execute:
javac PdfFormFiller.java
java PdfFormFiller template.pdf filled_form.pdf
O serializador XML faz o escape tanto dos nomes quanto dos valores dos campos. O transporte em UTF-8 preserva os caracteres, mas as fontes de aparência do PDF ainda precisam oferecer suporte a eles. O diretório temporário contém dados do formulário: mantenha-o em armazenamento restrito e defina permissões de diretório ou ACLs adequadas para sua plataforma. A publicação por link físico protege contra colisões de nomes de saída; ela não criptografa o resultado.
flatten=true renderiza os campos no conteúdo da página. Isso não é controle de acesso nem proteção
contra adulteração, e reescrever um PDF assinado invalida sua assinatura. Use false quando
precisar de campos editáveis. Inspecione o resultado visível e, para saídas não achatadas, inspecione
também os valores dos campos.
Exemplos práticos: automação de formulários fiscais e fichas de personagem
Chame fillForm com um mapa diferente para cada modelo verificado. Mantenha dados financeiros
sensíveis fora do código-fonte e dos logs. Use registros sintéticos nos testes; um tutorial para
desenvolvedores não é um fluxo de trabalho validado de preparação de declarações de impostos. Fichas
de personagem e formulários internos são fixtures úteis e de baixo risco para verificar nomes de
campos, texto multilinha e valores de seleção.
Dicas avançadas para lidar com formulários PDF complexos
- Os valores de caixas de seleção e de opção precisam 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.
- A cobertura de fontes, os limites dos campos e a geração de aparência afetam a renderização mesmo quando os valores estão corretos.
Solução de problemas comuns
PDFtk não encontrado
Execute pdftk --version no mesmo ambiente do Java. Corrija o PATH ou configure um caminho explícito
para um executável confiável em ProcessBuilder. Não aceite o nome do executável a partir de um upload.
Problemas de permissão
O processo precisa de acesso de leitura ao modelo e de acesso de gravação ao diretório de destino. Ele também precisa de suporte a links físicos nesse sistema de arquivos. Se a publicação 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 nada.
Como lidar com PDFs criptografados
Modelos criptografados exigem um design separado para o tratamento de senhas. O PDFtk oferece suporte
a input_pw, mas colocar senhas nos argumentos do processo pode expô-las à inspeção de processos.
Não deixe senhas fixas no código nem as copie para logs. Use um fluxo de trabalho seguro adequado ao
seu ambiente em vez de adicionar segredos à linha de comando deste tutorial.
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, e comerciais, aspas e barras invertidas são tratados pelo serializador XML, não por uma
função de escape de FDF escrita à mão. Glifos ausentes exigem fontes adequadas no modelo; eles não
são corrigidos com a alteração da codificação XML.
Conclusão
As APIs XML do Java e o PDFtk oferecem um fluxo de trabalho de preenchimento de formulários pequeno e testável. Use um modelo AcroForm real, mantenha as saídas separadas, propague as falhas e verifique tanto os valores armazenados quanto a aparência. Para documentos não confiáveis, execute o conversor em um worker restrito, com limites de recursos e sem credenciais da aplicação.
Para fluxos de trabalho gerenciados de documentos, conheça o serviço de processamento de documentos da Transloadit e o Robot 🤖 /document/merge.
