In un articolo precedente abbiamo già presentato iText, la libreria Java per la creazione e la manipolazione di documenti, dedicato a diversi formati, ma con una predilezione per i PDF.
In questo articolo sfrutteremo un caso di studio realistico, per prendere maggior confidenza le molte funzionalità di iText. Vedremo come implementare moduli di gestione automatizzata dei report aziendali, in termini, principalmente, di:
- conversione in PDF
- manipolazione dei documenti in formato PDF
Osserveremo come le funzionalità di gestione e conversione dei PDF facciano uso di una logica necessaria a tresformare le informazioni, memorizzate in un formato semplice di base, in un PDL (Page Definition Language) come PDF.
L'esempio
Supponiamo che un sistema informativo aziendale possieda un repository di documenti, ben strutturato, per conservare e catalogare diversi report. Come spesso accade, al repository arrivano le informazioni in forma semplice, come file di testo, che dovranno essere opportunamente formattati e convertite in PDF.
Tipicamente le esigenze di formattazione dei report constano di:
- una prima pagina di presentazione, che contiene il titolo, l'autore e altre informazioni sul documento
- sezioni e sottosezioni opportunamente rilevate dal documento di testo originale
Dobbiamo dunque prevedere un modulo del sistema che svolga le operazioni di formattazione e conversione in PDF dei report semplici che arrivano al repository.
La figura 1 mostra il processo trattamento dei report, i quali arrivano al repository in forma di file semplici, vengono elaborati dal modul predisposto alla formattazione e conversione e vengono restituiti in formato PDF. È utile sottolineare che, alla fine del processo, il repository permette di prelevare solo documenti in formato PDF, evitando l'accesso ai documenti semplici e garantendo l'uniformità della raccolta.
Il formato PDF del report può, però, non essere sufficiente a soddisfare le esigenze di consultazione dell'utenza. Per questo è necessario prevedere un ulteriore modulo a supporto del repository che permetta:
- la suddivisione di un PDF composito (split), secondo una logica ben definita
- la fusione (merge) di più report in un PDF unico, siano essi provenienti da precedenti split o da report semplici
Questo modulo permette, quindi, la manipolazione di PDF preesistenti, secondo diverse necessità e utilizzando diverse logiche.
In figura 2 si propone un modulo di manipolazione che, acquisito un PDF dal repository (ad esempio, generato dal modulo di formattazione e conversione), effettua uno split di quest'ultimo ed un successivo merge, aggiungendo nuove porzioni al PDF, prese da altri report, per generare un nuovo documento più ricco.
Le funzionalità che abbiamo mostrato sono piuttosto ricorrenti nei casi reali. Per il nostro esempio realizzeremo le seguenti funzioni:
- formattazione di documenti semplici provenienti da qualche fonte
- conversione dei documenti in formato PDF
- selezione di PDF singoli da PDF compositi presenti nel repository
- fusione di due o più PDF a realizzare un PDF composito
I punti appena descritti fissano i requisiti del caso di studio che implementiamo le prosieguo dell'articolo, sfruttando la libreria iText per la creazione e manipolazione dei documenti PDF.
Modulo di Formattazione e Conversione in PDF
Iniziamo realizzando il modulo di formattazione e conversione di report semplici in documenti PDF. Rappresentiamo il modulo con una classe che chiamiamo Creator
.
Il metodo più importante esposto da Creator
è convertToPDF
, che consente, dato un documento semplice (report importato nel caso specifico come un documento TXT), di realizzare un report in PDF opportunamente formattato. La formattazione consta di:
- inserimento della prima pagina (title page)
- disposizione del body del report in apposite sezioni (autonomamente riconosciute)
- inserimento di watermark che accoppiano il logo desiderato al numero di pagina corrente
Il passo fondamentale nell'utilizzo delle API di iText consta della creazione di una nuova instanza di Document rispetto alla quale saranno compiute le successive elaborazioni. Chapter e Section, sono infatti componenti del Document che, gerarchicamente rappresenta il livello più alto dell'albero del documento.
Document | +-- Chapter | +-- Section
Nel seguente listato esaminiamo il codice che implementa la conversione.
public void convertToPDF(String sourceFile, String pdfFile)
{
// crea una nuova istanza di Document
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
// crea una nuova istanza del PdfWriter
this.writeToPdf(document, pdfFile);
Paragraph par = null;
document.open();
// aggiunge metadati al documento
this.addMetaData(document);
try
{
// aggiunge la pagina di presentazione al corpo del Document
this.addTitlePage(document);
// apertura di un buffered reader (input stream)
BufferedReader br = new BufferedReader( new FileReader(this.txtBasePath +"/" +sourceFile));
String text = "";
// crea un'istanza di Paragraph
// (parte fondamentale di Document)
par = new Paragraph("Questo è un Chapternn", this.chapterFont);
// crea un'istanza di Chapter appartenente al
// Paragraph definito
Chapter chap = new Chapter(par,1);
chap.setNumberDepth(0);
// imposta i bookmark
chap.setBookmarkOpen(true);
par = new Paragraph("Questa è una Sectionnn", this.sectionFont);
Section sec = chap.addSection(par);
// cicla sulle linee di testo del documento
// "semplice" in input
int lines = 0;
while(text != null)
{
lines++;
try
{
// legge una linea di testo
text = br.readLine();
if(text != null)
{
par = new Paragraph(text,this.bodyFont);
// giustifica il testo
par.setAlignment(Element.ALIGN_JUSTIFIED);
sec.add(par);
System.out.println("(@ convertToPDF) : the Text Readed and Converted [" +lines+ ", " +text +"]");
}
}
catch (IOException e) { e.printStackTrace(); }
}
try
{
// aggiunge il Chapter al Document
document.add(chap);
}
catch (DocumentException e) { e.printStackTrace(); }
// chiude e finalizza l'istanza di Document
document.close();
}
catch (FileNotFoundException e) { e.printStackTrace(); }
catch (DocumentException e) { e.printStackTrace(); }
}
Una buona parte del codice proposto è stato tratto da un ottimo tutorial di Lars Vogel, che approfondisce anche l'utilizzo di tabelle e altri elementi che non tratteremo in questo articolo.
In particolare esaminiamo alcuni dei metodi ausiliari come writeToPDF che realizza la scrittura su file system del documento PDF che man mano si definisce arricchendo il body dell'istanza di Document. Esso utilizza un'istanza statica del PDFWriter
di iText che consente l'esportazione in PDF con estrema semplicità (i soli parametri da passare sono l'istanza del documento su cui si sta lavorando ed il rispettivo output stream).
private void writeToPdf(Document document, String pdfFile)
{
try
{
// recupera l'istanza statica di PdfWriter
PdfWriter.getInstance(document, new FileOutputStream (this.pdfBasePath +"/" +pdfFile));
}
catch (FileNotFoundException e1) { e1.printStackTrace(); }
catch (DocumentException e1) { e1.printStackTrace(); }
}
Altro metodo ausiliario è addMetaData, che consente di aggiungere al documento le meta-informazioni; queste meta-informazioni saranno visualizzabili accendendo alle proprietà del PDF finale. Anche quest'ultimo metodo sfrutta l'istanza di lavoro della classe Document
.
private void addMetaData(Document document)
{
document.addTitle(this.title);
document.addSubject(this.subject);
document.addKeywords(this.keyword);
document.addAuthor(this.author);
document.addCreator(this.creator);
}
Ancora, un altro metodo ausiliario è addTitlePage, che, invocato sull'istanza di Document
su cui si lavorerà anche successivamente, consente di inserire, in testa al body del documento, una pagina di presentazione del report; quest'ultima, modificando opportunamente la firma del metodo, può essere personalizzata con informazioni dinamiche generate di volta in volta.
private void addTitlePage(Document document) throws DocumentException
{
Paragraph preface = new Paragraph();
// aggiunge un rigo vuoto
addEmptyLine(preface, 1);
// aggiunge un'intestazione (intesa come titolo)
preface.add(new Paragraph("Titolo del Documento", this.chapterFont));
addEmptyLine(preface, 1);
// aggiunge informazioni riguardanti la fonte
preface.add(new Paragraph("Report generato da: "
+System.getProperty("user.name") + ", "
+ new Date(), this.sectionFont));
addEmptyLine(preface, 3);
preface.add(new Paragraph("Questo documento contiene informazioni di vitale importanza per l'azienda ", this.sectionFont));
addEmptyLine(preface, 8);
preface.add(new Paragraph("Questo documento è una versione provvisora - HTML.it", this.coloredFont));
if(document.isOpen())
{
document.add(preface);
// crea una nuova pagina
document.newPage();
}
else
{
System.out.println("È impossibile aggiungere la title page: lo stream di Document non è aperto!");
}
}
Aggiungere un WaterMark
Nel prossimo listato esaminiamo un metodo che consente di aggiungere al PDF, come watermark, un logo ed un testo che contiene il numero di pagina corrente del report, dopo l'inserzione della title page.
Le classi che entrano in scena in questo frangente sono PdfReader
, PdfStamper
, PdfContentByte
, che ci permettono di scrivere direttamente sullo stream di uscita del PDF finale. Esamineremo queste classi più avanti, essendo di uso specifico per la manipolazione dei PDF, quando tratteremo l'implementazione del modulo di manipolazione dei PDF.
public void addLogoAndPageNumber(String imgFile, String pdfFile)
{
try
{
// crea un'istanza di PdfReader: consente la lettura del PDF
PdfReader reader = new PdfReader(this.pdfBasePath +"/" +pdfFile);
int n = reader.getNumberOfPages();
// crea un'istanza del PdfStamper: consente la copia della
// pagina in un nuovo stream
PdfStamper stamp = new PdfStamper(reader,
new FileOutputStream(this.pdfBasePath +"/"
+pdfFile.replaceAll(".pdf", "")
+"_watermarked"+".pdf"));
int i = 1;
PdfContentByte over;
// carica l'immagine usata come Logo
Image img = Image.getInstance(this.imgBasePath +"/" +imgFile);
BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA,
BaseFont.WINANSI,
BaseFont.EMBEDDED);
// imposta la posizione assoluta del logo
img.setAbsolutePosition(200, 20);
img.setAlignment(0);
while (i <= n)
{
// recupera il contenuto in byte del nuovo PDF...
under = stamp.getUnderContent(i);
under.addImage(img);
under.beginText();
// inizializza la text matrix nella quale si inseriranno i contenuti
under.setTextMatrix(280,25);
under.setFontAndSize(bf, 10);
under.showText(" - Pagina " + i +" di " +n);
under.endText();
System.out.println("(@ addLogoAndPageNumber) : Under Content ["+under.toString().replaceAll("n", "")+ "]");
i++;
}
// chiude e finalizza lo stream dell'istanza di PdfStamper
stamp.close();
}
catch(Exception e) { e.printStackTrace(); }
}
Abbiamo ulteriori esempi di inserimento delle immagini e gestione del formato PDF con iText nel tutorial iText by Example.
L'analisi del codice proposto nei listati sinora discussi, mette immediatamente in evidenza la potenza delle API di iText: la logica necessaria alla creazione dei documenti si limita, nella maggior parte dei casi, al controllo e all'apertura o chiusura di Java stream.
Altra cosa interessante da notare è il ciclo di vita dell'oggetto Document. L'istanza di Document
va 'inizializzata' per subire modifiche mediante l'invocazione del metodo open
, il quale dà a tutti gli effetti il via alle operazioni di modifica del body del documento; la 'finalizzazione' delle operazioni di modifica viene, invece, definita richiamando sull'istanza di Document il metodo close
: dopo l'invocazione di tale metodo non sarà più possibile operare modifiche al body del documento.
Modulo di Manipolazione dei PDF
Il modulo di manipolazione di report già resi in formato PDF è stato implementato con un'apposita classe Java denominata Manipulator
. I principali metodi di Manipulator
sono:
splitPDFs
, che sfuttando le API di iText, consente di estrapolare i PDF desiderati rispetto ad un dato range di paginemergePDFs
, che implementa la fusione di più report PDF in un unico documento PDF
La combinazione dei metodi 1 e 2 può produrre risultati particolarmente interessanti: è possibile mettere insieme in un unico report informazioni provenienti da report di fonti diverse e, all'occorrenza, dal report unico estrapolare particolari documenti e combinarli con altri. Le combinazioni a disposizione di un ipotetico modulo utente sono innumerevoli e, per motivi di brevità, riduciamo la descrizione al caso illustato.
Nel prossimo listato vediamo l'implementazione del metodo splitPDFs
. La classe Document
è ancora una volta essenziale, infatti il PdfWriter
prende come parametro, appunto, un'istanza di Document
.
L'istanza del PdfReader
, al pari dell'istanza di PdfWriter
, è molto importante. Essa ci permette di prelevare il contenuto delle pagine desiderate e di riversarle direttamente nello stream di output (del PDF finale), mediante l'utilizzo della classe PdfContentByte
in combinazione con PdfImportedPage
.
public void splitPDFs(String sourceFile, String destFile, int fromPage, int toPage)
{
Document document = new Document();
try
{
// creazione degli stream necessary per
// 1. Input dal file system (fis)
// 2. Output al file system (fos)
this.fis = new FileInputStream(this.repositoryBasePath +"/" +sourceFile);
PdfReader inputPDF = new PdfReader(this.fis);
this.fos = new FileOutputStream(this.repositoryBasePath +"/" +destFile);
PdfWriter writer = PdfWriter.getInstance(document, this.fos);
// recupera il numero di pagine del PDF aperto
int totNOfPages = inputPDF.getNumberOfPages();
// effettua semplici controlli sugli input (definiscono
// l'intervallo di selezione):
// 1. L'intervallo di pagine selezionato deve essere ben format;
// 2. Non è possible selezionare più pagine di quelle a disposizione.
if(fromPage > toPage ) { fromPage = toPage; }
if(toPage > totNOfPages) { toPage = totNOfPages; }
document.open();
// consente di scrivere direttamente sul byte output stream
PdfContentByte cb = writer.getDirectContent();
PdfImportedPage page;
while(fromPage <= toPage)
{
document.newPage();
// preleva la pagina corrente (fromPage) dal PDF originale
page = writer.getImportedPage(inputPDF, fromPage);
// aggiunge la pagina prelevata al nuovo PDF col template
// specificato ('0,0' sta per no-caling e no-rotation)
cb.addTemplate(page, 0, 0);
fromPage++;
}
// chiusura di documento e stream aperti
this.fos.flush();
document.close();
this.fos.close();
}
catch (Exception e) { e.printStackTrace(); }
finally
{
this.closeOpenedDocument(document);
}
}
La combinazione delle classi PdfReader
, PdfWriter
, PdfImportedPage
e PdfCententByte
per realizzare lo split prevede che:
- dall'istanza del
reader
sia prelevata la pagina desiderata (mediante chiamata agetImportedPage
sull'istanza diwriter
) - la pagina desiderata sia appoggiata momentaneamente in un'istanza di
PdfImportedPage
addTemplate
) che, a sua volta, ha accesso diretto allo stream di output del PDF finale (grazie alla chiamata del metodogetDirectContent
sull'istanza delwriter
)
l'istanza di PdfImportedPage
sia aggiunta all'istanza di PdfContentByte
(grazie al metodo
Questi tre passaggi, sono fondamentali per leggere da uno stream di un PDF, selezionare il contenuto da importare, ed infine riversare il contenuto selezione nello stream di un PDF finale.
Nell'ultimo listato esaminiamo il metodo mergePDFs
. Qui è possibile osservare che le classi appena descritte (PdfReader
, PdfWriter
, PdfContentByte
, PdfImportedPage
e Document
) sono ancora una volta fondamentali per implementare la logica che consente di fondere più PDF in un unico PDF finale.
public void mergePDFs(List<String> sourcePdfFiles, String destFile, boolean paginate)
{
Document document = new Document();
try
{
// apre un output stream diretto al repository
this.fos = new FileOutputStream(this.repositoryBasePath +"/" +destFile);
// istanzia una lista di PdfReader
List<PdfReader> readers = new ArrayList<PdfReader>();
int totalNOfPages = 0;
Iterator<String> iteratorPdfs = sourcePdfFiles.iterator();
// crea un'istanza di PdfReader di appoggio per l'algoritmo
PdfReader pdfReader = null;
while (iteratorPdfs.hasNext())
{
// apre un input stream rispetto al PDF restituito dall'iteratore
this.fis = new FileInputStream(this.repositoryBasePath +"/" +iteratorPdfs.next());
pdfReader = new PdfReader(this.fis);
// aggiorna la lista dei documenti letti
readers.add(pdfReader);
// aggiorna il numero totale di pagine sommando le pagine
// del documento corrente
totalNOfPages += pdfReader.getNumberOfPages();
System.out.println("(@ mergerPDFs) : I'm adding [" +this.fis.toString()+ "]");
}
// crea un'istanza di PdfWriter
PdfWriter writer = PdfWriter.getInstance(document, this.fos);
document.open();
PdfContentByte cb = writer.getDirectContent();
PdfImportedPage page = null;
int docCurrentPageNumber = 0;
int pageOfCurrentReaderPdf = 0;
Iterator<PdfReader> iteratorPdfReader = readers.iterator();
PdfReader reader = null;
// cicla sui documenti letti
while (iteratorPdfReader.hasNext())
{
reader = iteratorPdfReader.next();
System.out.println("(@ mergerPDFs) : I'm merging [" +reader.toString()+ "]");
// per ogni pagina del document letto crea una nuova pagina nel
// PDF di destinazione
while (pageOfCurrentReaderPdf < reader.getNumberOfPages())
{
document.newPage();
pageOfCurrentReaderPdf++;
docCurrentPageNumber++;
// passi di prelievo e aggiunta di pagine (come descritto prima)
page = writer.getImportedPage(reader, pageOfCurrentReaderPdf);
cb.addTemplate(page, 0, 0);
// aggiunge nell'angolo in bassa a destra la paginazione
this.paginateNewDocument(cb,
docCurrentPageNumber,
totalNOfPages, paginate);
}
pageOfCurrentReaderPdf = 0;
}
// chiusura di documento e stream aperti
this.fos.flush();
document.close();
this.fos.close();
}
catch (Exception e) { e.printStackTrace(); }
finally
{
this.closeOpenedDocument(document);
}
}
Conclusioni
Assunto il caso di studio descritto nella prima parte dell'articolo, è stato possibile mostrare: come usare le API di iText, quali e quante sono le entità di primaria importanza messe a disposizione dal core della libreria ed infine come accoppiare queste ultime con una logica che consenta di implementare servizi di manipolazione di PDF diffusamente usati nei moderni sistemi informativi.
Abbiamo anche acquisito gli strumenti necessari per implementare un servizio di creazione e manipolazione dei PDF e abbiamo un pattern di riferimento per l'interazione repository-moduli di gestione PDF.