Processamento de PDF em Java com Ghost4J & Ghostscript
O Ghostscript renderiza documentos PDF e PostScript. Este tutorial usava originalmente o Ghost4J, um wrapper Java em torno da biblioteca nativa do Ghostscript. Os exemplos atualizados executam o Ghostscript em processos separados e usam o Apache PDFBox para inspecionar fontes, evitando o stack de dependências obsoleto do Ghost4J.
Introdução ao Ghostscript
O Ghostscript pode renderizar páginas de PDF como imagens e converter formatos de documentos. O código Java abaixo inicia a interface de linha de comando dele diretamente, com uma lista de argumentos. Os nomes de arquivo nunca viram comandos de 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. O
grafo de dependências 1.0.1 dele inclui bibliotecas legadas, como o Log4j 1.x. Adicionar um executor Java em
torno de um singleton nativo não torna seguro executá-lo de forma 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 sandbox de
segurança: processe documentos não confiáveis em workers com acesso restrito ao sistema de
arquivos, sem acesso à rede e com limites impostos de memória, CPU e tamanho da saída. Mantenha o
Ghostscript atualizado com os patches; -dSAFER é uma restrição adicional do interpretador, não um
substituto para esses controles.
Configure o Ghostscript e o PDFBox no seu projeto Java
Para o fluxo de trabalho substituto, use o JDK 21, uma instalação mantida do Ghostscript e o
PDFBox 3.0.8. O exemplo de renderização foi testado com o Ghostscript 10.07.1; use uma build
suportada e com patches, adequada ao seu deploy. Consulte a
documentação de uso do Ghostscript e os
downloads do PDFBox.
O JAR autônomo da aplicação PDFBox inclui as bibliotecas necessárias para o exemplo de fontes:
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
gs --version
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.
Converter PDF em imagem
Salve isto como PDFToImage.java. Passe o caminho de um executável confiável do Ghostscript, o PDF de
entrada e um novo diretório de saída. Um padrão de saída relativo e fixo impede que caracteres de
porcentagem nos nomes de diretório 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");
if (process.exitValue() != 0) throw new IOException("Rendering failed; inspect ghostscript.log");
try (var files = Files.list(destination)) {
if (files.noneMatch(path -> path.getFileName().toString().endsWith(".png"))) {
throw new IOException("No pages rendered");
}
}
} 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, substituindo pelo caminho do seu executável instalado:
javac PDFToImage.java
java PDFToImage /usr/bin/gs input.pdf rendered
O resultado contém um PNG por página a 144 DPI. A renderização achata o documento
intencionalmente; ela não preserva texto pesquisável, formulários, assinaturas nem a estrutura de
acessibilidade. Em caso de falha, mantenha o diretório de saída privado para diagnóstico e não
publique as imagens parciais dele.
Processamento concorrente de PDFs
Salve isto como ConcurrentPDFProcessing.java. Cada worker inicia seu próprio processo do Ghostscript.
O executor é limitado, todos os resultados são aguardados e a falha de uma tarefa chega a quem
chamou pela linha de comando.
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
Analisar fontes em documentos PDF
O PDFBox 3 carrega arquivos por meio de Loader.loadPDF(). Os nomes de fontes dos recursos são chaves
COSName; resolva cada chave com PDResources.getFont(). Não é necessário executar um text stripper para
enumerar esses recursos. Consulte o guia de migração do PDFBox.
Salve isto como FontAnalysis.java. Ele percorre todas as páginas e os Form XObjects aninhados, evita
ciclos de recursos e informa os nomes de fontes declarados sem repetição. Isto é um inventário de
recursos, não uma prova de que cada fonte listada desenha texto visível, nem 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) fonts.add(font.getName());
}
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);
}
}
No macOS ou no Linux:
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 pequeno de workers e meça arquivos representativos. Verifique a contagem de páginas, as dimensões e o conteúdo visível da saída; um status de saída zero, por si só, não detecta uma renderização em branco ou cortada. Teste PDFs malformados além dos válidos, e evite registrar o conteúdo dos documentos em logs ou compartilhar logs de diagnóstico privados.
Conclusão
Use processos separados do Ghostscript para a renderização nativa e o PDFBox para inspecionar documentos em Java. Para fluxos de trabalho de conversão gerenciados, consulte o serviço de processamento de documentos da Transloadit.
