Procesamiento de PDF en Java con Ghost4J y Ghostscript
Ghostscript renderiza documentos PDF y PostScript. Este tutorial usaba originalmente Ghost4J, una capa de Java que envuelve su biblioteca nativa. Los ejemplos actualizados ejecutan Ghostscript en procesos separados y usan Apache PDFBox para inspeccionar fuentes, evitando el conjunto 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 ejecuta 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 envuelve Ghostscript mediante código de integración nativa. Su
grafo de dependencias de 1.0.1 incluye bibliotecas heredadas como Log4j
1.x. 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 reemplaza la dependencia de
Ghost4J en lugar de recomendar ese conjunto de componentes para aplicaciones nuevas.
Los procesos separados aíslan el estado del intérprete nativo. No crean un entorno aislado de
seguridad: procesa los documentos que no sean de confianza en procesos de trabajo con acceso
restringido al sistema de archivos, sin acceso a la red y con límites obligatorios de memoria, CPU y
tamaño de salida. Mantén Ghostscript actualizado con los parches disponibles;
-dSAFER es una restricción adicional del intérprete, no un sustituto de esos
controles.
Configura Ghostscript y PDFBox en tu proyecto Java
Para el flujo de trabajo que lo reemplaza, usa JDK 21, una instalación de
Ghostscript que reciba mantenimiento y PDFBox 3.0.8. El ejemplo de renderizado
se probó con Ghostscript 10.07.1; usa una compilación con soporte y los parches
aplicados que sea adecuada para tu despliegue. Consulta la
documentación de uso de Ghostscript y las
descargas de PDFBox.
El JAR de la aplicación independiente de PDFBox incluye las bibliotecas necesarias para el ejemplo de fuentes:
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
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.
Convierte PDF a imagen
Guarda este código como PDFToImage.java. Proporciona la ruta del ejecutable de
Ghostscript de confianza, el PDF de entrada y un nuevo directorio de salida. Un patrón relativo fijo
para la salida 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");
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]));
}
}
Compílalo y ejecútalo, sustituyendo la ruta por la del ejecutable instalado:
javac PDFToImage.java
java PDFToImage /usr/bin/gs input.pdf rendered
El resultado contiene un PNG por página a 144 DPI. El renderizado aplana
el documento de forma intencional; no conserva texto en el que se puedan realizar búsquedas,
formularios, firmas ni la estructura de accesibilidad. Si se produce un error, mantén privado el
directorio de salida para el diagnóstico y no publiques sus imágenes parciales.
Procesamiento concurrente de PDF
Guarda este código como ConcurrentPDFProcessing.java. Cada proceso de trabajo inicia su propio
proceso de Ghostscript. El ejecutor tiene límites, se espera a que lleguen todos los resultados y
los errores de las tareas se propagan a quien realiza la llamada desde la línea de comandos.
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
Analiza las fuentes de 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 código como FontAnalysis.java. Recorre cada página y cada Form XObject anidado,
evita ciclos de recursos e informa de los nombres de fuente declarados sin duplicados. Se trata de
un inventario de recursos, no de una prueba de que cada fuente enumerada dibuje texto visible ni de
una auditoría completa de las fuentes dentro de patrones y glifos 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) 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);
}
}
En macOS o Linux:
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 un número reducido de procesos de trabajo y realiza mediciones con archivos representativos. Verifica el número de páginas de salida, sus dimensiones y su contenido visible; un código de salida de cero por sí solo no permite 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 de diagnóstico privados.
Conclusión
Usa procesos separados de Ghostscript para el renderizado nativo y PDFBox para inspeccionar documentos en Java. Para flujos de trabajo de conversión gestionados, consulta el servicio de procesamiento de documentos de Transloadit.
