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.