Traitement PDF en Java avec Ghost4J & Ghostscript
Ghostscript effectue le rendu de documents PDF et PostScript. Ce tutoriel utilisait initialement Ghost4J, une interface Java pour sa bibliothèque native. Les exemples mis à jour exécutent Ghostscript dans des processus distincts et utilisent Apache PDFBox pour inspecter les polices, évitant ainsi les dépendances obsolètes de Ghost4J.
Introduction à Ghostscript
Ghostscript peut convertir des pages PDF en images et convertir des formats de documents. Le code Java ci-dessous lance directement son interface en ligne de commande avec une liste d’arguments. Les noms de fichiers ne deviennent jamais des commandes shell, et chaque tâche de rendu dispose de son propre processus et de son propre répertoire de sortie.
Qu’est-ce que Ghost4J ?
Ghost4J fournit une interface à Ghostscript via des liaisons natives. Son
POM 1.0.1 publié
fixe les versions de Log4j à 1.2.17, de JNA à
4.1.0 et d’iText à 2.1.7.
Ajouter un exécuteur Java autour d’un singleton natif ne rend pas son exécution concurrente sûre.
Ce guide remplace donc la dépendance Ghost4J plutôt que de recommander cet ensemble de dépendances
pour de nouvelles applications.
Des processus distincts isolent l’état de l’interpréteur natif. Ils ne constituent pas un bac à
sable de sécurité : traitez les documents non fiables dans des processus de travail avec un accès
restreint au système de fichiers, sans accès réseau et avec des limites imposées de mémoire, de
CPU et de taille de sortie. Maintenez Ghostscript à jour avec les correctifs ;
-dSAFER est une restriction supplémentaire de l’interpréteur,
qui ne remplace pas ces mesures de contrôle.
Configurer Ghostscript et PDFBox dans votre projet Java
Installez séparément un JDK 21
maintenu, Ghostscript et curl.
Les commandes ci-dessous ont été testées sous Linux avec OpenJDK
21.0.12.1, Ghostscript 10.07.1 et PDFBox
3.0.8.
Les mises à jour de sécurité ultérieures du JDK 21 conviennent ;
utilisez une version de Ghostscript prise en charge et à jour avec les correctifs.
Le JDK 21 est l’environnement d’exécution choisi pour cet exemple,
et non la version minimale requise par PDFBox.
Travaillez dans un seul répertoire contenant vos fichiers locaux input.pdf,
first.pdf et second.pdf. Enregistrez-y les trois programmes
Java ci-dessous. Vérifiez que java, javac et
gs s’exécutent avant de télécharger le JAR de l’application autonome
PDFBox, qui inclut les dépendances de l’exemple sur les polices :
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
Pour les applications Maven, la dépendance de bibliothèque correspondante est
org.apache.pdfbox:pdfbox:3.0.8.
N’ajoutez pas Ghost4J pour exécuter ces exemples. Consultez les
téléchargements PDFBox pour les sommes de contrôle des versions
et la documentation d’utilisation de Ghostscript
pour les options de rendu. Si une vérification ou un téléchargement échoue, corrigez ce prérequis
et réexécutez le bloc de configuration avant de compiler. Les commandes utilisent
&& pour qu’un échec de configuration ou de compilation empêche
l’exécution de la commande qui en dépend.
Convertir un PDF en image
Enregistrez ce programme sous PDFToImage.java. Fournissez le chemin de l’exécutable
Ghostscript de confiance, le PDF d’entrée et un nouveau répertoire de sortie. Un motif de sortie
relatif fixe empêche les caractères de pourcentage des noms de répertoires de devenir des
directives de formatage 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]));
}
}
Compilez et exécutez le programme en remplaçant le chemin par celui de votre exécutable installé :
javac PDFToImage.java &&
java PDFToImage /usr/bin/gs input.pdf rendered
Ouvrez rendered/page-001.png ; les pages suivantes sont numérotées
page-002.png, et ainsi de suite, à 144 DPI.
Une page de 144 × 72 points PDF produit une image de
288 × 144 pixels. Le répertoire de sortie doit être nouveau : si vous
réexécutez le programme alors que rendered existe déjà, l’exécution échoue
et ses fichiers sont conservés. Choisissez un autre répertoire pour réessayer, en conservant le
journal de la tâche échouée et les éventuelles images partielles pour le diagnostic.
Le rendu aplatit volontairement le document ; il ne conserve ni le texte recherchable, ni les formulaires, ni les signatures, ni la structure d’accessibilité. En cas d’échec, gardez le répertoire de sortie privé pour le diagnostic et ne publiez pas ses images partielles.
Traitement concurrent de PDF
Enregistrez ce programme sous ConcurrentPDFProcessing.java. Chaque thread de travail lance
son propre processus Ghostscript. L’exécuteur attend la fin des tâches soumises, et l’échec d’une
tâche est transmis à l’appelant en ligne de commande. Au maximum, deux processus de rendu
s’exécutent simultanément ; cette limite porte sur les tâches actives, pas sur la liste des
entrées en attente.
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 contient les pages de first.pdf ;
document-1 contient celles de second.pdf.
Le répertoire du lot doit lui aussi être nouveau. Si une tâche échoue, la commande se termine
avec un code de sortie non nul après la fin des autres tâches soumises. Les tâches réussies
peuvent laisser des images complètes à côté des sorties partielles d’une tâche échouée ;
inspectez l’entrée nommée et le journal avant d’utiliser le lot.
Analyser les polices des documents PDF
PDFBox 3 charge les fichiers via Loader.loadPDF().
Les noms des ressources de polices sont des clés COSName ; résolvez chaque
clé avec PDResources.getFont(). Il est inutile d’exécuter un extracteur de texte pour
énumérer ces ressources. Consultez le guide de migration de PDFBox.
Enregistrez ce programme sous FontAnalysis.java. Il parcourt chaque page et chaque
XObject Form imbriqué, évite les cycles de ressources et affiche les noms de polices déclarés
uniques dans l’ordre de tri, un par ligne. Une police sans nom, telle qu’une police de type 3
sans nom, est désignée par sa clé de ressource. Il s’agit d’un inventaire des ressources, qui ne
prouve pas que chaque police répertoriée produit du texte visible et ne constitue pas un audit
complet des polices présentes dans les motifs et les glyphes de 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);
}
}
Exécutez l’exemple sur les polices depuis le même répertoire :
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java &&
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Considérations sur les performances et bonnes pratiques
La résolution détermine les dimensions de sortie et les besoins en mémoire. Choisissez un petit nombre de threads de travail et effectuez des mesures sur des fichiers représentatifs. Vérifiez le nombre de pages produites, leurs dimensions et leur contenu visible ; un code de sortie nul ne suffit pas à détecter un rendu vide ou tronqué. Testez des PDF malformés ainsi que des PDF valides, et évitez de consigner le contenu des documents ou de partager des journaux de diagnostic privés.
Conclusion
Si vous préférez un flux de travail de conversion géré, consultez le service de traitement de documents de Transloadit.
