Processamento de PDF em Java com Ghost4J e Ghostscript
O Ghostscript renderiza documentos PDF e PostScript. Este tutorial originalmente usava o Ghost4J, um wrapper Java para sua biblioteca nativa. Os exemplos atualizados executam o Ghostscript em processos separados e usam o Apache PDFBox para inspecionar fontes, evitando o stack obsoleto de dependências do Ghost4J.
Introdução ao Ghostscript
O Ghostscript pode renderizar páginas PDF como imagens e converter formatos de documentos. O código Java abaixo inicia sua interface de linha de comando diretamente com uma lista de argumentos. Os nomes dos arquivos nunca se tornam comandos do shell, e cada tarefa de renderização recebe seu próprio processo e diretório de saída.
O que é o Ghost4J?
O Ghost4J encapsula o Ghostscript por meio de bindings nativos. Seu
POM 1.0.1 publicado
fixa o Log4j em 1.2.17, a JNA em 4.1.0 e o iText em 2.1.7.
Adicionar um executor Java em torno de um singleton nativo não torna segura sua execução concorrente.
Por isso, este guia substitui a dependência do Ghost4J em vez de recomendar esse stack para novas
aplicações.
Processos separados isolam o estado do interpretador nativo. Eles não criam um ambiente de segurança
isolado: processe documentos não confiáveis em unidades de processamento com acesso restrito ao
sistema de arquivos, sem acesso à rede e com limites efetivos de memória, CPU e tamanho da saída.
Mantenha o Ghostscript atualizado com as correções; -dSAFER é uma restrição
adicional do interpretador, não um substituto para esses controles.
Configurar Ghostscript e PDFBox no seu projeto Java
Instale separadamente um JDK 21
com manutenção ativa, o Ghostscript e curl.
Os comandos abaixo foram testados no Linux com OpenJDK 21.0.12.1, Ghostscript
10.07.1 e PDFBox 3.0.8.
Atualizações de segurança posteriores do JDK 21 são adequadas; use uma
compilação do Ghostscript com suporte e correções aplicadas. O JDK 21 é o
ambiente de execução escolhido para o exemplo, não o requisito mínimo do PDFBox.
Trabalhe em um único diretório que contenha seus arquivos locais input.pdf,
first.pdf e second.pdf. Salve os três programas Java abaixo
nesse diretório. Verifique se java, javac e
gs funcionam antes de baixar o JAR autônomo do aplicativo PDFBox, que
inclui as dependências do exemplo de fontes:
java -version &&
javac -version &&
gs --version &&
curl --fail --location --output pdfbox-app-3.0.8.jar \
https://repo.maven.apache.org/maven2/org/apache/pdfbox/pdfbox-app/3.0.8/pdfbox-app-3.0.8.jar
Para aplicações Maven, a dependência de biblioteca correspondente é org.apache.pdfbox:pdfbox:3.0.8.
Não adicione o Ghost4J para executar estes exemplos. Consulte os
downloads do PDFBox para obter as somas de verificação das versões e a
documentação de uso do Ghostscript para ver as opções de renderização.
Se uma verificação ou um download falhar, corrija esse pré-requisito e execute novamente o bloco de
configuração antes de compilar. Os comandos usam && para que uma falha na
configuração ou na compilação impeça a execução do comando dependente.
Converter PDF em imagem
Salve este programa como PDFToImage.java. Informe o caminho confiável para o
executável do Ghostscript, o PDF de entrada e um novo diretório de saída. Um padrão relativo fixo
para a saída impede que caracteres de porcentagem nos nomes dos diretórios se tornem diretivas de
formatação do Ghostscript.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public final class PDFToImage {
public static void render(Path executable, Path input, Path output) throws Exception {
Path source = input.toRealPath();
if (!Files.isRegularFile(source)) throw new IOException("Input must be a regular file");
Path binary = executable.toRealPath();
Files.createDirectory(output);
Path destination = output.toRealPath();
Process process = new ProcessBuilder(
binary.toString(), "-dSAFER", "-dBATCH", "-dNOPAUSE", "-dNOPROMPT",
"-sDEVICE=png16m", "-r144", "-sOutputFile=page-%03d.png", "-f", source.toString()
).directory(destination.toFile()).redirectErrorStream(true)
.redirectOutput(destination.resolve("ghostscript.log").toFile()).start();
try {
if (!process.waitFor(120, TimeUnit.SECONDS)) {
throw new IOException("Rendering timed out for " + source);
}
if (process.exitValue() != 0) {
throw new IOException("Rendering failed for " + source + "; inspect "
+ destination.resolve("ghostscript.log"));
}
try (var files = Files.list(destination)) {
if (files.noneMatch(path -> path.getFileName().toString().endsWith(".png"))) {
throw new IOException("No pages rendered for " + source);
}
}
} finally {
if (process.isAlive()) {
process.destroyForcibly();
process.waitFor();
}
}
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("gs-path input.pdf output-directory");
render(Path.of(args[0]), Path.of(args[1]), Path.of(args[2]));
}
}
Compile e execute o programa, substituindo o caminho pelo do executável instalado:
javac PDFToImage.java &&
java PDFToImage /usr/bin/gs input.pdf rendered
Abra rendered/page-001.png; as páginas seguintes são numeradas como
page-002.png e assim por diante, com resolução de 144 DPI.
Uma página que mede 144 × 72 pontos PDF produz uma imagem de
288 × 144 pixels. O diretório de saída deve ser novo: uma nova execução com
rendered já existente falha e preserva seus arquivos. Escolha outro diretório
para tentar novamente, mantendo o log da tarefa que falhou e quaisquer imagens parciais para
ajudar no diagnóstico.
A renderização achata intencionalmente o documento; ela não preserva texto pesquisável, formulários, assinaturas ou a estrutura de acessibilidade. Em caso de falha, mantenha o diretório de saída privado para diagnóstico e não publique suas imagens parciais.
Processamento concorrente de PDFs
Salve este programa como ConcurrentPDFProcessing.java. Cada unidade de processamento inicia seu
próprio processo do Ghostscript. O executor aguarda a conclusão das tarefas submetidas, e uma falha
em uma tarefa é propagada para quem fez a chamada pela linha de comando. No máximo dois processos
de renderização são executados ao mesmo tempo; isso limita as tarefas ativas, não a lista de
entradas na fila.
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public final class ConcurrentPDFProcessing {
public static void main(String[] args) throws Exception {
if (args.length < 3) throw new IllegalArgumentException("gs-path new-output-directory PDFs...");
Path output = Files.createDirectory(Path.of(args[1]));
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
var results = new ArrayList<Future<Void>>();
for (int i = 2; i < args.length; i++) {
Path input = Path.of(args[i]);
Path destination = output.resolve("document-" + (i - 2));
results.add(executor.submit((Callable<Void>) () -> {
PDFToImage.render(Path.of(args[0]), input, destination);
return null;
}));
}
for (Future<Void> result : results) result.get();
}
}
}
javac PDFToImage.java ConcurrentPDFProcessing.java &&
java ConcurrentPDFProcessing /usr/bin/gs batch-output first.pdf second.pdf
batch-output/document-0 contém as páginas de first.pdf;
document-1 contém as de second.pdf.
O diretório do lote também deve ser novo. Se alguma tarefa falhar, o comando termina com um código
de saída diferente de zero após a conclusão das demais tarefas submetidas. Tarefas bem-sucedidas
podem deixar imagens completas ao lado da saída parcial de uma tarefa que falhou; inspecione o
arquivo de entrada indicado e o log antes de usar o lote.
Analisar fontes em documentos PDF
O PDFBox 3 carrega arquivos por meio de Loader.loadPDF().
Os nomes das fontes nos recursos são chaves COSName; resolva cada chave com
PDResources.getFont(). Não é necessário executar um extrator de texto para enumerar esses
recursos. Consulte o guia de migração do PDFBox.
Salve este programa como FontAnalysis.java. Ele percorre todas as páginas e os Form
XObjects aninhados, evita ciclos de recursos e lista os nomes de fontes declarados sem duplicatas,
em ordem alfabética, um por linha. Uma fonte sem nome, como uma fonte Type 3 sem nome, é identificada
por sua chave de recurso. Trata-se de um inventário de recursos, não de uma prova de que todas as
fontes listadas desenham texto visível nem de uma auditoria completa das fontes dentro de padrões
e glifos Type 3.
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.TreeSet;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
public final class FontAnalysis {
private static void collect(PDResources resources, Set<COSDictionary> visited,
Set<String> fonts) throws IOException {
if (resources == null || !visited.add(resources.getCOSObject())) return;
for (COSName name : resources.getFontNames()) {
var font = resources.getFont(name);
if (font == null) continue;
String fontName = font.getName();
fonts.add(fontName == null ? "Unnamed font (resource /" + name.getName() + ")" : fontName);
}
for (COSName name : resources.getXObjectNames()) {
if (resources.getXObject(name) instanceof PDFormXObject form) {
collect(form.getResources(), visited, fonts);
}
}
}
public static void main(String[] args) throws Exception {
if (args.length != 1) throw new IllegalArgumentException("input.pdf");
Set<COSDictionary> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Set<String> fonts = new TreeSet<>();
try (PDDocument document = Loader.loadPDF(Path.of(args[0]).toFile())) {
for (var page : document.getPages()) collect(page.getResources(), visited, fonts);
}
for (String font : fonts) System.out.println(font);
}
}
Execute o exemplo de fontes no mesmo diretório:
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java &&
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Considerações de desempenho e boas práticas
A resolução controla as dimensões da saída e os requisitos de memória. Escolha um número reduzido de unidades de processamento e meça o desempenho com arquivos representativos. Verifique o número de páginas, as dimensões e o conteúdo visível da saída; um código de saída igual a zero, por si só, não detecta uma renderização em branco ou com conteúdo cortado. Teste PDFs malformados e válidos, e evite registrar o conteúdo dos documentos em logs ou compartilhar logs de diagnóstico privados.
Conclusão
Se você preferir um fluxo de conversão gerenciado, conheça o serviço de processamento de documentos da Transloadit.
