PDF-Verarbeitung in Java mit Ghost4J & Ghostscript
Ghostscript rendert PDF- und PostScript-Dokumente. Dieses Tutorial nutzte ursprünglich Ghost4J, einen Java-Wrapper für die native Bibliothek von Ghostscript. Die aktualisierten Beispiele führen Ghostscript in separaten Prozessen aus und verwenden Apache PDFBox zur Schriftanalyse. So vermeiden sie den obsoleten Abhängigkeitsstack von Ghost4J.
Einführung in Ghostscript
Ghostscript kann PDF-Seiten als Bilder rendern und Dokumentformate konvertieren. Der folgende Java-Code startet die Kommandozeilenschnittstelle von Ghostscript direkt mit einer Argumentliste. Dateinamen werden nie zu Shell-Befehlen, und jeder Rendering-Auftrag erhält einen eigenen Prozess und ein eigenes Ausgabeverzeichnis.
Was ist Ghost4J?
Ghost4J kapselt Ghostscript mithilfe nativer Bindings. Die
veröffentlichte POM-Datei für 1.0.1
schreibt Log4j 1.2.17, JNA 4.1.0 und
iText 2.1.7 fest. Ein Java-Executor um ein natives Singleton macht dessen
parallele Ausführung nicht sicher. Diese Anleitung ersetzt deshalb die Ghost4J-Abhängigkeit, statt
den 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 durchgesetzten Limits für Arbeitsspeicher, CPU 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
Installieren Sie ein gepflegtes JDK 21,
Ghostscript und curl separat.
Die folgenden Befehle wurden unter Linux mit OpenJDK 21.0.12.1,
Ghostscript 10.07.1 und PDFBox 3.0.8 getestet.
Spätere Sicherheitsupdates innerhalb der JDK-Linie 21 sind geeignet;
verwenden Sie einen unterstützten Ghostscript-Build mit eingespielten Patches.
JDK 21 ist die für das Beispiel gewählte Laufzeitumgebung, nicht die
Mindestanforderung von PDFBox.
Arbeiten Sie in einem Verzeichnis, das Ihre lokalen Dateien input.pdf,
first.pdf und second.pdf enthält. Speichern Sie dort
alle drei folgenden Java-Programme. Prüfen Sie, ob java,
javac und gs laufen, bevor Sie das
eigenständige PDFBox-Anwendungs-JAR herunterladen, das die Abhängigkeiten des Beispiels zur
Schriftanalyse enthält:
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
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.
Prüfsummen der Releases finden Sie unter PDFBox-Downloads.
Rendering-Optionen beschreibt die
Nutzungsdokumentation von Ghostscript. Wenn eine Prüfung oder ein
Download fehlschlägt, beheben Sie das Problem und führen Sie den Einrichtungsblock vor dem
Kompilieren erneut aus. Die Befehle verwenden &&, sodass der abhängige
Befehl nicht ausgeführt wird, wenn die Einrichtung oder der Build fehlschlägt.
PDF in ein Bild umwandeln
Speichern Sie dies als PDFToImage.java. Übergeben Sie den vertrauenswürdigen Pfad zur
Ghostscript-Programmdatei, die PDF-Eingabedatei 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 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]));
}
}
Kompilieren Sie das Programm und führen Sie es aus. Setzen Sie dabei den Pfad zu Ihrer installierten Programmdatei ein:
javac PDFToImage.java &&
java PDFToImage /usr/bin/gs input.pdf rendered
Öffnen Sie rendered/page-001.png; die folgenden Seiten sind als
page-002.png und so weiter nummeriert. Die Auflösung beträgt
144 DPI. Eine Seite, die 144 × 72 PDF-Punkte misst,
ergibt ein Bild mit 288 × 144 Pixeln. Das Ausgabeverzeichnis muss neu sein:
Wenn rendered bereits vorhanden ist, schlägt eine erneute Ausführung fehl;
die Dateien bleiben erhalten. Wählen Sie für einen neuen Versuch ein anderes Verzeichnis und
bewahren Sie das Protokoll des fehlgeschlagenen Auftrags sowie alle teilweise erzeugten Bilder zur
Diagnose auf.
Das Dokument wird beim Rendern bewusst gerastert; durchsuchbarer Text, Formulare, Signaturen und die Struktur für Barrierefreiheit bleiben dabei nicht erhalten. Halten Sie bei einem Fehler das Ausgabeverzeichnis für die Diagnose privat und veröffentlichen Sie die teilweise erzeugten Bilder nicht.
Parallele PDF-Verarbeitung
Speichern Sie dies als ConcurrentPDFProcessing.java. Jeder Worker startet seinen eigenen
Ghostscript-Prozess. Der Executor wartet, bis die eingereichten Aufgaben abgeschlossen sind.
Schlägt eine Aufgabe fehl, wird der Fehler an den Aufrufer auf der Kommandozeile weitergegeben.
Höchstens zwei Rendering-Prozesse laufen gleichzeitig; dies begrenzt aktive Aufträge, nicht die
Liste der wartenden Eingaben.
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 enthält die Seiten aus first.pdf;
document-1 enthält die Seiten aus second.pdf.
Auch das Batch-Verzeichnis muss neu sein. Wenn ein Auftrag fehlschlägt, endet der Befehl mit einem
Exit-Code ungleich null, nachdem die übrigen eingereichten Aufträge abgeschlossen sind.
Erfolgreiche Aufträge können vollständige Bilder neben der Teilausgabe eines fehlgeschlagenen
Auftrags hinterlassen. Prüfen Sie die angegebene Eingabedatei und das Protokoll, bevor Sie die
Batch-Ergebnisse verwenden.
Schriften in PDF-Dokumenten analysieren
PDFBox 3 lädt Dateien mit Loader.loadPDF().
Die Namen der Schriftressourcen sind Schlüssel vom Typ COSName;
lösen Sie jeden Schlüssel mit PDResources.getFont() auf. Zum Auflisten dieser Ressourcen
ist es nicht nötig, einen Text-Extraktor auszuführen. Weitere Hinweise finden Sie im
PDFBox-Migrationsleitfaden.
Speichern Sie dies als FontAnalysis.java. Das Programm besucht jede Seite und jedes
verschachtelte Form XObject, verhindert Ressourcenzyklen und gibt die deklarierten Schriftnamen
ohne Duplikate in sortierter Reihenfolge aus, einen pro Zeile. Eine unbenannte Schrift, etwa eine
Type-3-Schrift ohne Namen, wird mit ihrem Ressourcenschlüssel gekennzeichnet. Dies ist eine
Bestandsaufnahme der Ressourcen. Sie belegt nicht, dass jede aufgelistete Schrift sichtbaren Text
zeichnet, und ist keine vollständige Prüfung der Schriften in Mustern und Type-3-Glyphen.
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);
}
}
Führen Sie das Beispiel zur Schriftanalyse im selben Verzeichnis aus:
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java &&
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Leistungsaspekte und bewährte Verfahren
Die Auflösung bestimmt die Ausgabeabmessungen und den Speicherbedarf. Wählen Sie wenige Worker und führen Sie Messungen mit repräsentativen Dateien durch. Prüfen Sie Seitenzahl, Abmessungen und sichtbaren Inhalt der Ausgabe; ein Exit-Code von null reicht allein nicht aus, um ein leeres oder abgeschnittenes Rendering zu erkennen. Testen Sie sowohl fehlerhaft aufgebaute als auch gültige PDFs und vermeiden Sie es, Dokumentinhalte zu protokollieren oder private Diagnoseprotokolle weiterzugeben.
Fazit
Wenn Sie einen verwalteten Konvertierungsablauf bevorzugen, sehen Sie sich den Dienst für Dokumentenverarbeitung von Transloadit an.
