Java PDF processing with Ghost4J & Ghostscript
Ghostscript renders PDF and PostScript documents. This tutorial originally used Ghost4J, a Java wrapper around its native library. The updated examples run Ghostscript in separate processes and use Apache PDFBox for font inspection, avoiding Ghost4J’s obsolete dependency stack.
Introduction to Ghostscript
Ghostscript can render PDF pages to images and convert document formats. The Java code below launches its command-line interface directly with an argument list. Filenames never become shell commands, and each rendering job gets its own process and output directory.
What is Ghost4J?
Ghost4J wraps Ghostscript through native bindings. Its
1.0.1 dependency graph includes legacy libraries such as Log4j 1.x. Adding a Java executor around
a native singleton does not make it safe to run concurrently. This guide therefore replaces the
Ghost4J dependency rather than recommending that stack for new applications.
Separate processes isolate native interpreter state. They do not create a security sandbox: process
untrusted documents in workers with restricted filesystem access, no network access, and enforced
memory, CPU, and output-size limits. Keep Ghostscript patched; -dSAFER is an additional interpreter
restriction, not a substitute for those controls.
Set up Ghostscript and PDFBox in your Java project
For the replacement workflow, use JDK 21, a maintained Ghostscript installation, and PDFBox
3.0.8. The rendering example was exercised with Ghostscript 10.07.1; use a supported, patched
build appropriate to your deployment. Check the Ghostscript usage documentation
and the PDFBox downloads.
The standalone PDFBox application JAR includes the libraries needed by the font example:
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
For Maven applications, the corresponding library dependency is org.apache.pdfbox:pdfbox:3.0.8.
Do not add Ghost4J to run these examples.
Convert PDF to image
Save this as PDFToImage.java. Pass the trusted Ghostscript executable path, input PDF, and a new
output directory. A fixed relative output pattern prevents percent characters in directory names
from becoming Ghostscript formatting directives.
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 and run it, substituting your installed executable path:
javac PDFToImage.java
java PDFToImage /usr/bin/gs input.pdf rendered
The result contains one PNG per page at 144 DPI. Rendering intentionally flattens the document;
it does not preserve searchable text, forms, signatures, or accessibility structure. On failure,
keep the output directory private for diagnosis and do not publish its partial images.
Concurrent PDF processing
Save this as ConcurrentPDFProcessing.java. Each worker launches its own Ghostscript process.
The executor is bounded, all results are awaited, and a task failure reaches the command-line caller.
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
Analyze fonts in PDF documents
PDFBox 3 loads files through Loader.loadPDF(). Resource font names are COSName keys; resolve
each key with PDResources.getFont(). Running a text stripper is unnecessary for enumerating these
resources. See the PDFBox migration guide.
Save this as FontAnalysis.java. It visits every page and nested Form XObject, prevents resource
cycles, and reports unique declared font names. This is a resource inventory, not proof that every
listed font draws visible text or a complete audit of fonts inside patterns and Type 3 glyphs.
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);
}
}
On macOS or Linux:
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Performance considerations and best practices
Resolution controls output dimensions and memory requirements. Choose a small worker count and measure representative files. Verify output page count, dimensions, and visible content; a zero exit status alone cannot detect a blank or clipped rendering. Test malformed PDFs as well as valid ones, and avoid logging document content or sharing private diagnostic logs.
Conclusion
Use separate Ghostscript processes for native rendering and PDFBox for Java document inspection. For managed conversion workflows, see Transloadit’s document processing service.
