PDF-Verarbeitung in Java mit Ghost4J & Ghostscript
Ghostscript rendert PDF- und PostScript-Dokumente. Dieses Tutorial verwendete ursprünglich Ghost4J, einen Java-Wrapper für die native Bibliothek. Die aktualisierten Beispiele führen Ghostscript in separaten Prozessen aus und verwenden Apache PDFBox zur Schriftprüfung. So vermeiden sie den obsoleten Dependency-Stack von Ghost4J.
Einführung in Ghostscript
Ghostscript kann PDF-Seiten als Bilder rendern und Dokumentformate konvertieren. Der folgende Java-Code startet die Befehlszeilenschnittstelle direkt mit einer Argumentliste. Dateinamen werden niemals zu Shell-Befehlen, und jeder Rendering-Auftrag erhält einen eigenen Prozess und ein eigenes Ausgabeverzeichnis.
Was ist Ghost4J?
Ghost4J bindet Ghostscript über native Bindings ein. Der
Abhängigkeitsgraph von 1.0.1 enthält ältere Bibliotheken wie Log4j
1.x. Ein Java-Executor um einen nativen Singleton macht dessen parallele
Ausführung nicht sicher. Diese Anleitung ersetzt daher die Ghost4J-Abhängigkeit, statt diesen
Software-Stack für neue Anwendungen zu empfehlen.
Separate Prozesse isolieren den Zustand des nativen Interpreters. Sie schaffen keine
Sicherheits-Sandbox: Verarbeiten Sie nicht vertrauenswürdige Dokumente in Workern mit eingeschränktem
Dateisystemzugriff, ohne Netzwerkzugriff und mit verbindlichen Grenzen für Arbeitsspeicher,
CPU-Nutzung und Ausgabegröße. Halten Sie Ghostscript mit Patches aktuell;
-dSAFER ist eine zusätzliche Einschränkung des Interpreters und kein Ersatz
für diese Schutzmaßnahmen.
Ghostscript und PDFBox in Ihrem Java-Projekt einrichten
Verwenden Sie für den Ersatzworkflow JDK 21, eine gepflegte
Ghostscript-Installation und PDFBox 3.0.8. Das Rendering-Beispiel wurde mit
Ghostscript 10.07.1 getestet. Verwenden Sie einen unterstützten Build mit
aktuellen Patches, der zu Ihrer Bereitstellungsumgebung passt. Beachten Sie die
Dokumentation zur Verwendung von Ghostscript
und die PDFBox-Downloads.
Die eigenständige PDFBox-Anwendungs-JAR enthält die Bibliotheken für das Schriftbeispiel:
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
Für Maven-Anwendungen lautet die entsprechende Bibliotheksabhängigkeit
org.apache.pdfbox:pdfbox:3.0.8. Fügen Sie Ghost4J nicht hinzu, um diese Beispiele auszuführen.
PDF in ein Bild konvertieren
Speichern Sie dieses Beispiel als PDFToImage.java. Übergeben Sie den vertrauenswürdigen
Pfad zur ausführbaren Ghostscript-Datei, die Eingabe-PDF und ein neues Ausgabeverzeichnis. Ein festes
relatives Ausgabemuster verhindert, dass Prozentzeichen in Verzeichnisnamen zu
Formatierungsanweisungen für Ghostscript werden.
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]));
}
}
Kompilieren Sie das Beispiel und führen Sie es aus. Setzen Sie dabei den Pfad zu Ihrer installierten ausführbaren Datei ein:
javac PDFToImage.java
java PDFToImage /usr/bin/gs input.pdf rendered
Das Ergebnis enthält ein PNG pro Seite mit 144 DPI. Beim Rendern wird das
Dokument bewusst auf eine Bildebene reduziert; durchsuchbarer Text, Formulare, Signaturen und die
Struktur für Barrierefreiheit bleiben nicht erhalten. Halten Sie bei einem Fehler das
Ausgabeverzeichnis zur Diagnose privat und veröffentlichen Sie keine unvollständigen Ergebnisbilder.
Parallele PDF-Verarbeitung
Speichern Sie dieses Beispiel als ConcurrentPDFProcessing.java. Jeder Worker startet einen eigenen
Ghostscript-Prozess. Die Parallelität des Executors ist begrenzt, sämtliche Ergebnisse werden
abgewartet, und Fehler einer Aufgabe werden an den aufrufenden Befehlszeilenprozess weitergegeben.
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
Schriften in PDF-Dokumenten analysieren
PDFBox 3 lädt Dateien mit Loader.loadPDF(). Schriftnamen in
Ressourcen sind Schlüssel vom Typ COSName; lösen Sie jeden Schlüssel mit
PDResources.getFont() auf. Um diese Ressourcen aufzulisten, müssen Sie keinen
Textextraktor ausführen. Siehe die PDFBox-Migrationsanleitung.
Speichern Sie dieses Beispiel als FontAnalysis.java. Es durchläuft jede Seite und jedes
verschachtelte Form XObject, verhindert Ressourcenzyklen und gibt deklarierte Schriftnamen ohne
Duplikate aus. Dies ist eine Bestandsaufnahme der Ressourcen. Sie belegt weder, dass jede aufgeführte
Schrift sichtbaren Text darstellt, noch stellt sie eine vollständige Prüfung der Schriften innerhalb
von Mustern und Type-3-Glyphen dar.
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);
}
}
Unter macOS oder Linux:
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Leistung und bewährte Verfahren
Die Auflösung bestimmt die Ausgabeabmessungen und den Speicherbedarf. Wählen Sie eine geringe Anzahl an Workern und führen Sie Messungen mit repräsentativen Dateien durch. Prüfen Sie Seitenanzahl, Abmessungen und sichtbare Inhalte der Ausgabe; ein Exit-Status von null allein erkennt kein leeres oder abgeschnittenes Rendering. Testen Sie sowohl fehlerhafte als auch gültige PDFs. Vermeiden Sie es, Dokumentinhalte zu protokollieren oder private Diagnoseprotokolle weiterzugeben.
Fazit
Verwenden Sie separate Ghostscript-Prozesse für natives Rendering und PDFBox zur Dokumentprüfung in Java. Für verwaltete Konvertierungsworkflows bietet Transloadit einen Dienst zur Dokumentenverarbeitung.
