Procesamiento de PDF en Java con Ghost4J y Ghostscript
Ghostscript renderiza documentos PDF y PostScript. Este tutorial usaba originalmente Ghost4J, un wrapper de Java para su biblioteca nativa. Los ejemplos actualizados ejecutan Ghostscript en procesos separados y usan Apache PDFBox para inspeccionar fuentes, evitando el stack obsoleto de dependencias de Ghost4J.
Introducción a Ghostscript
Ghostscript puede renderizar páginas PDF como imágenes y convertir formatos de documentos. El código Java que aparece a continuación inicia directamente su interfaz de línea de comandos con una lista de argumentos. Los nombres de archivo nunca se convierten en comandos de shell, y cada tarea de renderizado tiene su propio proceso y directorio de salida.
¿Qué es Ghost4J?
Ghost4J encapsula Ghostscript mediante código de integración nativo. Su
POM publicado de 1.0.1
fija las versiones de Log4j 1.2.17, JNA 4.1.0
e iText 2.1.7. Añadir un ejecutor de Java alrededor de un singleton nativo
no hace que sea seguro ejecutarlo de forma concurrente. Por eso, esta guía sustituye la dependencia
de Ghost4J en lugar de recomendar ese stack para aplicaciones nuevas.
Los procesos separados aíslan el estado del intérprete nativo. No crean un sandbox de seguridad:
procesa los documentos no confiables en workers con acceso restringido al sistema de archivos, sin
acceso a la red y con límites de memoria, CPU y tamaño de salida que se apliquen de forma efectiva.
Mantén Ghostscript actualizado con los parches; -dSAFER es una restricción
adicional del intérprete, no un sustituto de esos controles.
Configura Ghostscript y PDFBox en tu proyecto Java
Instala un JDK 21 con mantenimiento activo,
Ghostscript y curl por separado. Los comandos
siguientes se probaron en Linux con OpenJDK 21.0.12.1,
Ghostscript 10.07.1 y PDFBox 3.0.8.
Las actualizaciones de seguridad posteriores de JDK 21 son adecuadas;
usa una versión de Ghostscript con soporte y con los parches aplicados.
JDK 21 es el entorno de ejecución elegido para el ejemplo, no el requisito
mínimo de PDFBox.
Trabaja en un único directorio que contenga tus archivos locales input.pdf,
first.pdf y second.pdf. Guarda allí los tres programas Java
que aparecen a continuación. Comprueba que java,
javac y gs se ejecuten antes de descargar el JAR
de la aplicación independiente de PDFBox, que incluye las dependencias del ejemplo de fuentes:
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
Para las aplicaciones Maven, la dependencia de biblioteca correspondiente es
org.apache.pdfbox:pdfbox:3.0.8.
No añadas Ghost4J para ejecutar estos ejemplos. Consulta las
descargas de PDFBox para obtener las sumas de verificación de las
versiones y la documentación de uso de Ghostscript
para conocer las opciones de renderizado. Si una comprobación o descarga falla, resuelve ese
requisito previo y vuelve a ejecutar el bloque de configuración antes de compilar. Los comandos usan
&& para que un fallo de configuración o compilación detenga el comando
que depende de él.
Convierte PDF en imágenes
Guarda este programa como PDFToImage.java. Pasa la ruta del ejecutable confiable de
Ghostscript, el PDF de entrada y un directorio de salida nuevo. Un patrón de salida relativo fijo
evita que los caracteres de porcentaje de los nombres de directorio se conviertan en directivas
de formato de 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]));
}
}
Compílalo y ejecútalo, sustituyendo la ruta por la de tu ejecutable instalado:
javac PDFToImage.java &&
java PDFToImage /usr/bin/gs input.pdf rendered
Abre rendered/page-001.png; las páginas posteriores se numeran
page-002.png, y así sucesivamente, a 144 DPI.
Una página que mide 144 × 72 puntos PDF produce una imagen de
288 × 144 píxeles. El directorio de salida debe ser nuevo: volver a ejecutar el
programa cuando rendered ya existe provoca un fallo y conserva sus archivos.
Elige otro directorio para volver a intentarlo y conserva el registro de la tarea fallida y cualquier
imagen parcial para el diagnóstico.
El renderizado aplana el documento de forma intencional; no conserva el texto en el que se pueden realizar búsquedas, los formularios, las firmas ni la estructura de accesibilidad. Si se produce un fallo, mantén privado el directorio de salida para el diagnóstico y no publiques sus imágenes parciales.
Procesamiento concurrente de PDF
Guarda este programa como ConcurrentPDFProcessing.java. Cada worker inicia su propio proceso de
Ghostscript. El ejecutor espera a que termine el trabajo enviado, y el fallo de una tarea se comunica
a quien invoca el programa desde la línea de comandos. Como máximo, dos procesos de renderizado se
ejecutan a la vez; esto limita las tareas activas, no la lista de entradas en espera.
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 contiene las páginas de first.pdf;
document-1 contiene las de second.pdf.
El directorio del lote también debe ser nuevo. Si alguna tarea falla, el comando termina con un
código distinto de cero después de que finalicen las demás tareas enviadas. Las tareas exitosas
pueden dejar imágenes completas junto a la salida parcial de una tarea fallida; inspecciona el
archivo de entrada indicado y el registro antes de usar el lote.
Analiza fuentes en documentos PDF
PDFBox 3 carga archivos mediante Loader.loadPDF().
Los nombres de fuente de los recursos son claves COSName; resuelve cada clave
con PDResources.getFont(). No es necesario ejecutar un extractor de texto para enumerar estos
recursos. Consulta la guía de migración de PDFBox.
Guarda este programa como FontAnalysis.java. Recorre cada página y cada Form XObject
anidado, evita ciclos de recursos y muestra los nombres declarados de fuentes sin duplicados, en
orden, uno por línea. Una fuente sin nombre, como una fuente de tipo 3 que carece de nombre, se
identifica por su clave de recurso. Esto es un inventario de recursos, no una prueba de que cada
fuente enumerada dibuje texto visible ni una auditoría completa de las fuentes dentro de patrones
y glifos de tipo 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);
}
}
Ejecuta el ejemplo de fuentes desde el mismo directorio:
javac -cp pdfbox-app-3.0.8.jar FontAnalysis.java &&
java -cp '.:pdfbox-app-3.0.8.jar' FontAnalysis input.pdf
Consideraciones de rendimiento y buenas prácticas
La resolución controla las dimensiones de salida y los requisitos de memoria. Elige una cantidad pequeña de workers y realiza mediciones con archivos representativos. Verifica la cantidad de páginas de salida, sus dimensiones y el contenido visible; un código de salida de cero por sí solo no puede detectar un renderizado en blanco o recortado. Prueba tanto PDF malformados como válidos y evita registrar el contenido de los documentos o compartir registros privados de diagnóstico.
Conclusión
Si prefieres un flujo de trabajo de conversión gestionado, consulta el servicio de procesamiento de documentos de Transloadit.
