API per applicazioni

V parte: Apache PDFBox, una libreria Java per file PDFdi

Nella nostra rassegna di librerie Java, è questa volta il turno di Apache PDFBox, utile strumento in grado di realizzare le azioni più comuni sul formato PDF. Ma PDFBox, prodotto open targato Apache, può anche compiere alcune operazioni avanzate che possono risultare davvero utili nei più svariati contesti.

Uno sguardo a PDFBox

Apache PDFBox  è uno strumento Java open source per lavorare su documenti PDF. La libreria di questo progetto permette di creare nuovi documenti PDF, modificare quelli esistenti e di estrarre il contenuto di questi documenti. Essa include inoltre anche diverse utility richiamabili sotto forma di riga di comando.

Funzionalità

PDFBox è in grado di svolgere le più comuni operazioni che possono riguardare i PDF:

  • estrarre testo all'interno di un PDF per l'utilizzo in altre applicazioni;
  • unire più file PDF in un solo documento o dividere un singolo file PDF in più file;
  • estrarre dati da moduli o precompilare un modulo PDF;
  • convalidare PDF con lo standard ISO per l'archiviazione (PDF/A);
  • stampare un file PDF dalle stampanti che supportano API Java Printing;
  • convertire un PDF in un file immagine;
  • creare un PDF;
  • firmare un PDF.

Struttura della libreria

PDFBox è formato da tre componenti chiamati pdfbox, fontbox e jempbox. Il package di tutti i componenti PDFBox è org.apache.pdfbox.

Requisiti

Il principale componente di PDFBox, pdfbox, ha una forte dipendenza dalla libreria commons-logging. Common Logging è un generico wrapper intorno a diversi framework di logging. Così si è reso necessario l'uso di librerie di logging come log4j che permette di interfacciarsi con lo standard API java.util.logging incluso nella piattaforma Java.

Download e importazione della libreria

Andiamo sul sito di Apache PDFBox [1] e clicchiamo alla voce download sulla sinistra.

 

 

Figura 1 - Il sito del progetto PDFBox, da cui è possibile scaricare la libreria.

 

Fatto ciò scarichiamo l'ultima versione della libreria e importiamola nel nostro progetto, così saremo pronti a creare il nostro primo documento PDF in Java con PDFBox.

 

 

Figura 2 - La pagina di download.

 

Creazione di documento vuoto

Nel piccolo esempio che segue, viene illustrato come creare un nuovo documento PDF utilizzando PDFBox.

// Creo documento vuoto
PDDocument document = new PDDocument();
// Creo una nuova pagina bianca da aggiungere al documento
PDPage blankPage = new PDPage();
document.addPage(blankPage);
// Salvo il documento
document.save("BlankPage.pdf");
//Chiudo il documento
document.close();

Scrivere su un documento

Di seguito viene illustrato come creare un nuovo documento e stampare il testo "Ciao mondo" utilizzando una delle font di base PDF.

// Creo un nuovo documento con una pagina
PDDocument document = new PDDocument();
PDPage page = new PDPage();
document.addPage(page);
          
// Creo in nuovo oggetto font e imposto il carattere
PDFont font = PDType1Font.HELVETICA_BOLD;
          
// Instanzio un nuovo stream che contiene l'oggetto creato
PDPageContentStream contentStream 
  = new PDPageContentStream(document, page);
          
// Definisco un flusso di contenuti di testo utilizzando 
// il carattere selezionato, spostando il cursore 
// e disegnando il testo "Ciao mondo"
          
contentStream.beginText();
contentStream.setFont(font, 12);
contentStream.moveTextPositionByAmount(100, 700);
contentStream.drawString("Ciao mondo");
contentStream.endText();
          
// Chiudo lo stream
contentStream.close();
          
// Salvo il documento
document.save("Ciao mondo.pdf");
document.close();

Estrarre un testo

Una delle caratteristiche principali della libreria PDFBox è la sua capacità di estrarre rapidamente e con precisione il testo da una varietà di documenti PDF. Questa funzionalità è incapsulata nella classe org.apache.pdfbox.util.PDFTextStripper e può essere facilmente eseguita su riga di comando con org.apache.pdfbox.ExtractText.

Estrazione di testo avanzata

Non c'è però solo il caso del semplice recupero di testo da un documento. Alcune applicazioni  hanno complessi requisiti di estrazione di testo e non supportano applicazioni a riga di comando: in questo caso, è possibile utilizzare o estendere la classe PDFTextStripper per venire incontro ad alcuni di questi requisiti

Definire i limiti del testo da estrarre

Esistono svariate modalità con le quali delimitare il testo che viene recuperato durante il processo di estrazione. Il più semplice è quello di specificare l'intervallo di pagine che si desidera estrarre. Ad esempio, per estrarre solo il testo dalla seconda e terza pagina del documento PDF, si può fare in questo modo:

PDFTextStripper stripper = new PDFTextStripper();
stripper.setStartPage(2);
stripper.setEndPage(3);
stripper.writeText(...);

Ma non c'è solo la possibilità di estrarre specifiche pagine: è anche possibile limitare il testo da estrarre a quello compreso tra due segnalibri nella pagina. La cosa è ben spiegata nella documentazione ufficiale presente sulla pagina del progetto [1], e funziona basandosi sulle proprietà startBookmark ed endBookmark di PDFTextStripper. Nella tabella di figura 3 sono riportati gli stati possibili dei segnalibri.

 

 

Figura 3 - I possibili stati dei segnalibri di PDFTextStripper.

 

Lista dei glifi

Una delle caratteristiche importanti del formato PDF rispetto ai file di testo semplici è quella di poter inglobare o fare riferimento a liste di font. Grazie alla codifica Unicode, ormai il numero di caratteri che è possibile mappare in una font è molto superiore al vecchio standard di 128 o 256 glifi inizialmente previsti nelle versioni del vecchio standard ASCII. Questo consente oggi di avere font che contengono al loro interno anche mille glifi, comprendendo le diverse varianti di un determinato carattere, le "legature", i caratteri simbolici, le lettere di alfabeti non latini come il cirillico o l'arabo e così via.

Per favorire un'uniformazione del modo in cui le varie applicazioni "riconoscono" i diversi caratteri, è stata messa a punto una lista di corrispondenza tra glifo e valore Unicode, mantenuta da Adobe e che si chiama Adobe Glyph List [2].  In tal modo, i file PDF possono mappare una corrispondenza tra i nomi dei glifi e i valori Unicode durante l'estrazione di testo. PDFBox viene fornito con un elenco di Adobe Glyph, ma è possibile utilizzare il proprio file di corripondenza, fornendo il nome del file per la proprietà glyphlist_ext JVM.

Testo scritto da destra a sinistra

Alcune lingue scrivono da destra a sinistra, ad esempio l'arabo o l'ebraico. Questo va tenuto presente quando si estrae il testo dal documento. PDFBox può normalizzare questa situazione e invertire l'ordine di lettura del testo; per tale funzionalità, è necessario che il file JAR ICU4J sia stato immesso sul classpath: si tratta di una dipendenza opzionale. Per garantire un output preciso è necessario consentire lo smistamento sia con org.apache.pdfbox.util.PDFTextStripper che con org.apache.pdfbox.ExtractText.

Utilizzo di font

Le specifiche dei file PDF stabiliscono che deve essere sempre dispobibile un set standard di 14 differenti caratteri. In PDFBox tali caratteri sono sono definiti come costanti nella classe PDType1Font. Di seguito, riportiamo la lista dei caratteri standard e la corrispondente costante nella classe.

  • Times normale = PDType1Font.TIMES_ROMAN
  • Times grassetto = PDType1Font.TIMES_BOLD
  • Times corsivo = PDType1Font.TIMES_OBLIQUE
  • Times corsivo grassetto = PDType1Font.TIMES_BOLD_OBLIQUE
  • Helvetica normale = PDType1Font.HELVETICA
  • Helvetica grassetto = PDType1Font.HELVETICA_ BOLD
  • Helvetica corsivo = PDType1Font.HELVETICA_OBLIQUE
  • Helvetica corsivo grassetto = PDType1Font.HELVETICA_BOLD_OBLIQUE
  • Courier normale = PDType1Font.COURIER
  • Courier grassetto = PDType1Font.COURIER_ BOLD
  • Courier corsivo = PDType1Font.COURIER_OBLIQUE
  • Courier corsivo grassetto = PDType1Font.COURIER_BOLD_OBLIQUE
  • Simboli = PDType1Font.SYMBOL
  • Dingbats = PDType1Font.ZAPF_DINGBATS

Esempio con font di base

// Creo documento con una pagina
PDDocument document = new PDDocument();
PDPage page = new PDPage();
document.addPage(page);
          
// Creo oggetto font
PDFont font = PDType1Font.HELVETICA_BOLD;
          
//Creo lo stream che contiene il documento PDF
PDPageContentStream contentStream 
    = new PDPageContentStream(document, page);
          
// Definisco il testo e le sue caratteristiche nello stream
contentStream.beginText();
contentStream.setFont(font, 12);
contentStream.moveTextPositionByAmount(100, 700);
contentStream.drawString("Hello World");
contentStream.endText();
          
// Chiudo lo stream
contentStream.close();
          
// Salvo il PDF
document.save("Hello World.pdf");
document.close();

Esempio con font non di base

PDDocument document = new PDDocument();
PDPage page = new PDPage();
document.addPage(page);
          
// Richiamo font esterno
PDFont font = PDTrueTypeFont.loadTTF(document, "Arial.ttf");
          
PDPageContentStream contentStream 
    = new PDPageContentStream(document, page);
          
contentStream.beginText();
contentStream.setFont(font, 12);
contentStream.moveTextPositionByAmount(100, 700);
contentStream.drawString("Hello World");
contentStream.endText();
          
contentStream.close();
          
document.save("Hello World.pdf");
document.close();

Esempio con font PostScript

PDDocument document = new PDDocument();
PDPage page = new PDPage();
document.addPage(page);
          
// Creo un nuovo font con PostScript
PDFont font = new PDType1AfmPfbFont(doc,"cfm.afm");
          
PDPageContentStream contentStream 
    = new PDPageContentStream(document, page);
          
contentStream.beginText();
contentStream.setFont(font, 12);
contentStream.moveTextPositionByAmount(100, 700);
contentStream.drawString("Hello World");
contentStream.endText();
          
contentStream.close();
          
document.save("Hello World.pdf");
document.close();

Metadati

I documenti PDF possono contenere informazioni che descrivono il documento stesso o alcuni attributi all'interno del documento, come l'autore del documento, o la data di creazione. Le informazioni di base possono essere  recuperate utilizzando l'oggetto PDDocumentInformation.

PDDocumentInformation info = document.getDocumentInformation();
System.out.println("Page Count=" + document.getNumberOfPages());
System.out.println("Title=" + info.getTitle());
System.out.println("Author=" + info.getAuthor());
System.out.println("Subject=" + info.getSubject());
System.out.println("Keywords=" + info.getKeywords());
System.out.println("Creator=" + info.getCreator());
System.out.println("Producer=" + info.getProducer());
System.out.println("Creation Date=" + info.getCreationDate());
System.out.println("Modification Date=" + info.getModificationDate());
System.out.println("Trapped=" + info.getTrapped());

Vediamo ora come accedere ai metadati.

PDDocument doc = PDDocument.load(...);
PDDocumentCatalog catalog = doc.getDocumentCatalog();
PDMetadata metadata = catalog.getMetadata();
          
//Leggere l'XML dei metadati
InputStream xmlInputStream = metadata.createInputStream();
          
// Scrivere l'xml dei metadati 
InputStream newXMPData =...;
PDMetadata newMetadata = new PDMetadata(doc, newXMLData, false);
catalog.setMetadata(newMetadata);

Aggiungere allegati al PDF

I documenti PDF possono contenere file allegati a cui si accede dal menù "documento - file allegati". PDFBox permette di aggiungere ed estrarre allegati da documenti PDF.

PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode();
          
// Creo prima la specifica del file, che contiene il file incorporato
PDComplexFileSpecification fs = new PDComplexFileSpecification();
fs.setFile("Test.txt");
InputStream is =...;
PDEmbeddedFile ef = new PDEmbeddedFile(doc, is);
          
// Imposto gli attributi
ef.setSubtype("test/plain");
ef.setSize(data.length);
ef.setCreationDate(new GregorianCalendar());
fs.setEmbeddedFile(ef);
          
// Creo la mappa in cui metterò gli allegati
Map efMap = new HashMap();
efMap.put("My first attachment", fs);
efTree.setNames(efMap);
          
// Gli allegati vengono memorizzati come parte 
// del dizionario "nomi" nel catalogo del documento
PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog());
names.setEmbeddedFiles(efTree);
doc.getDocumentCatalog().setNames(names);

File per l'archiviazione: PDF/A

Le API di Apache PDFBox possono essere utilizzate per creare un file PDF/A. Il PDF/A (dove la lettera "A" sta per "Archive") è un file PDF con alcuni vincoli per garantire la sua lunga conservazione nel tempo in modo che siano leggibili anche in futuro: tali vincoli sono descritti nella norma ISO-19005. I PDF/A servono per archiviare documenti di vario tipo che potranno essere necessari anche tra molti anni.

L'esempio che segue mostra ciò che dovrebbe essere aggiunto durante la creazione di un file PDF per trasformarla in un PDF/A valido [3].

// Carico tutte le font utilizzate nel documento
InputStream fontStream 
= CreatePDFA.class.getResourceAsStream("/org/apache/pdfbox/resources/ttf/ArialMT.ttf");
.Carattere PDFont = PDTrueTypeFont loadTTF (doc, fontStream);
          
// Includo i metadati XMP
InputStream fontStream 
= CreatePDFA.class.getResourceAsStream("/org/apache/pdfbox/resources/ttf/ArialMT.ttf");
.Carattere PDFont = PDTrueTypeFont loadTTF (doc, fontStream);
          
// Specifico il profilo colore
InputStream colorProfile 
= CreatePDFA.class.getResourceAsStream(
        "/org/apache/pdfbox/resources/pdfa/sRGB Color Space Profile.icm");
PDOutputIntent oi = new PDOutputIntent(doc, colorProfile); 
oi.setInfo("sRGB IEC61966-2.1"); 
oi.setOutputCondition("sRGB IEC61966-2.1"); 
oi.setOutputConditionIdentifier("sRGB IEC61966-2.1"); 
oi.setRegistryName("http://www.color.org"); 
cat.addOutputIntent(oi);

Validazione PDF/A

La libreria Apache Preflight è uno strumento Java che implementa un parser compatibile con le specifiche ISO-19005 per creare file PDF usati per l'archiviazione di documenti (PDF/A-1). Vediamo come è possibile utilizzare questo strumento.

ValidationResult result = null;
FileDataSource fd = new FileDataSource(args[0]);
PreflightParser parser = new PreflightParser(fd);
try {
/ * Analizzare il file PDF con PreflightParser 
  * che eredita dalla NonSequentialParser.
  * Alcuni controlli addizionali sono presenti 
  * per verificare una serie di requisiti PDF/A.
  * (Consistenza lunghezza Stream, EOL dopo qualche Keyword...)
  * /
  parser.parse();
  / * Una volta che la convalida di sintassi è fatta,
    * Il parser può fornire un PreflightDocument
    * (Che eredita da PDDocument)
    * Questo documento viene lavorato alla fine della convalida PDF / A 
    * /
  PreflightDocument document = parser.getPreflightDocument();
  document.validate();
            
   // Ottieni il risultato della validazione
   result = document.getResult();
   document.close();
 }
 catch (SyntaxValidationException e){
  * Il metodo parse può lanciare una SyntaxValidationException
  * Se il file PDF non può essere analizzato.
  * In questo caso, l'eccezione contiene un'istanza di ValidationResult
  * /
  result = e.getResult();
}
          
// mostra risultato della validazione
if (result.isValid()){
  System.out.println("The file " + args[0] 
          + " is a valid PDF/A-1b file");
}
else
{
  System.out.println("The file" + args[0] + " is not valid, error(s) :");
  for (ValidationError error : result.getErrorsList()) {
    System.out.println(error.getErrorCode() + " : " + error.getDetails());
  }
}

Categorie di errori di convalida

Se la convalida non va a buon fine, l'oggetto ValidationResult contiene tutte le cause del fallimento. Per aiutare nella comprensione del fallimento, tutti i codici di errore hanno il seguente formato x[.y [.z]] dove:

  • x è la categoria, per esempio "errore di convalida font";
  • y è una sottosezione della categoria, per esempio "font con errore di glifi";
  • z è la causa dell'errore, per esempio "carattere con un glifo mancante".

Categoria (y) e causa (z) potrebbero mancare a seconda della difficoltà di individuare il dettaglio dell'errore. Nel dettaglio, i codici numerici x corrispondenti alle categorie d'errore, sono i seguenti:

  • 1 = errore di sintassi
  • 2 = errore negli elementi grafici
  • 3 = errore di font
  • 4 = errore nelle trasparenze
  • 5 = errore nelle annotazioni
  • 6 = errore nelle azioni
  • 7 = errore di metadati

Conclusione

PDFBox è una libreria Java completa per quanto riguarda i PDF. Ci permette di crearli e modificarli, usare tutti i caratteri che vogliamo, aggiungere oggetti, immagini, allegati e per di più creare documenti complessi come PDF/A che rispondono a certi standard precisi.

 

 

 

Riferimenti

[1] PDFBox, il sito della libreria

https://pdfbox.apache.org/index.html

 

[2] Adobe Glyph List

https://github.com/adobe-type-tools/agl-aglfn

 

[3] L'esempio completo di creazione di PDF/A si trova nella directory src/main/java/org/apache/pdfbox/examples/pdfa/CreatePDFA.java

 

Condividi

Pubblicato nel numero
205 aprile 2015
Yuri Cervoni si è laureato alla facoltà di Ingegneria Informatica dell’Università “La Sapienza” di Roma nel maggio 2009 con la tesi: “Implementazione e realizzazione di un metodo per l’animazione dell’algoritmo di Dijkstra”. Dall’Aprile 2009, prima come stagista e poi come sviluppatore software, lavora nella Società degli Studi di Settore. Nel…
Articoli nella stessa serie
Ti potrebbe interessare anche