|
|
PDFs mit Java erstellen und bearbeitenJava-AkrobatikBernhard Bablok |
Der Erfolg des PDF-Formats hat zwei Gründe: Zum einen gab es immer kostenlose Acrobat Reader, zum anderen bietet das Format einige Vorteile: Hyperlinks, eingebettete Bilder, Formularverarbeitung, Metadaten, Verschlüsselung und gute Drucktreue der Dokumente. Das Projekt I-Text[1] des Belgiers Bruno Lowagie stellt ein Java-API für die Erzeugung von PDF-Dateien bereit. Es handelt sich um eine Bibliothek, nicht um ein Satz- oder Layoutsystem.
Damit eignet es sich für die programmgesteuerte Ausgabe von strukturierten Daten, weniger als Allroundwerkzeug für PDF-Dateien. Wer volle Kontrolle über die Ausgabe haben möchte, wird mit I-Text zwar Erfolg haben, aber nicht unbedingt glücklich werden. Dieser Coffee-Shop führt das I-Text-API anhand einfacher Beispiele ein. Wer tiefer einsteigen will, kommt nicht darum herum, das recht gute Tutorial zu studieren. Außerdem gibt es verschiedene Tutorials zu I-Text ([2], [3]), die sich zum Beispiel damit beschäftigen, wie man mit Servlets PDF-Dateien erzeugt.
Das I-Text-Paket an sich ist relativ kompakt (705 KByte Quellen). Die Bibliothek besteht aus zwei Jar-Dateien (»itext-1.02b.jar« und »iTextHYPH.jar«) mit zusammen 1,1 MByte. Wer die Javadoc-Dokumentation nicht selbst generieren möchte, findet die fertigen HTML-Seiten (1,2 MByte) ebenfalls online. Weiterhin ist ein Paket mit dem Tutorial (91 KByte) und den zugehörigen Beispielen (1,7 MByte) verfügbar. All dies gibt es auf der Homepage[1] des I-Text-Projekts (wobei die eigentlichen Dateien auf Sourceforge liegen). Dort findet sich auch eine experimentelle XML-Unterstützung. Ein praktisches All-in-one-Paket fehlt leider.
Die Installation ist denkbar einfach: Die Jar-Dateien gehören in den »CLASSPATH« der Anwendung. Bei der Installation des Tutorials muss man etwas Acht geben, sonst stimmen die relativen Links nicht:
mkdir -p /usr/local/iText/tutorial mkdir -p /usr/local/iText/examples tar -xvzf itext-tutorial-0.94.tar.gz -C /usr/local/iText/tutorial tar -xvzf itext-examples.tar.gz -C /usr/local/iText/examples
Für Selbstkompilierer ist allerdings Handarbeit angesagt, da das Paket weder Makefile noch Ant-Datei enthält. Die erforderlichen Schritte zeigt Listing 1. Alternativ findet sich ein Ant-Buildfile »build.xml« auf der Projekt-Homepage. Damit lässt sich sogar der Code herunterladen:
ant download.site
Jar-Files erzeugt man mit »ant jar« respektive »ant jarWithXML« für die Version mit XML-Unterstützung. Das I-Text-Paket steht unter einer dualen Lizenz: Zur Wahl stehen die LGPL oder die Mozilla Public License.
Listing 1: I-Text selbst bauen |
01 mkdir -p /tmp/iText/src 02 mkdir -p /tmp/iText/build 03 tar -xvzf itext-src-1.02b.tar.gz -C /tmp/iText/src 04 cd /tmp/iText 05 javac -d build `find src -type f -name *.java` 06 cd src 07 find . -name *.afm -exec cp -v --parents {} ../build \; 08 cd .. 09 jar -cf itext.jar -C build . |
Den Einstieg in die I-Text-Programmierung verschafft ein erweitertes Hello-World-Beispiel, siehe Listing 2. Das Programm liest von der Standardeingabe und übergibt den daraus erzeugten String an I-Text. Wer zum Beispiel per Pipe das GPL-Dokument ins Programm schreibt, erhält das in Abbildung 1 dargestellte Ergebnis.
Listing 2: »SimplePdf.java« |
22 import java.io.*; 23 24 import com.lowagie.text.*; 25 import com.lowagie.text.pdf.*; 26 34 public class SimplePdf { 35 38 public static void main(String[] args) { 39 try { 40 SimplePdf pdf = new SimplePdf(); 41 Document document = new Document(); 42 PdfWriter.getInstance(document,new FileOutputStream("SimplePdf.pdf")); 43 document.open(); 44 document.add(new Paragraph(pdf.getLines())); 45 document.close(); 46 } catch (Exception e) { 47 e.printStackTrace(); 48 } 49 } 50 51 ///////////////////////////////////////////////////// 52 57 private String getLines() throws IOException { 58 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 59 StringBuffer result = new StringBuffer(); 60 String line; 61 while((line = reader.readLine()) != null) 62 result.append(line).append("\n"); 63 return result.toString(); 64 } 65 } |
Vom notwendigen Drumherum einmal abgesehen passiert das Interessante in den Zeilen 41 bis 45. Etwas ungewöhnlich und erklärungsbedürftig ist dabei die Zeile 42. Dieser Methodenaufruf verknüpft das Dokument mit einer »PdfWriter«-Instanz. Eine »Document.addWriter()«- oder »PdfWriter.setDocument()«-Methode wäre klarer.
Für die wenigen Codezeilen sieht das Ergebnis schon ganz passabel aus. Wie es sich noch verfeinern lässt, zeigen die folgenden Beispiele.
Metadaten wie Titel, Thema oder Schlüsselwörter sind zum Beispiel dann nützlich, wenn das Dokument in ein Archiv aufgenommen werden soll und man die entsprechenden Daten für die Suche verwenden möchte. Die »Document«-Klasse besitzt dafür Methoden (siehe Listing 4), die vor dem Öffnen des Dokuments aufzurufen sind. Der Acrobat-Reader zeigt diese Metadaten in den PDF-Eigenschaften an (siehe Abbildung 2).
Listing 4: Hinzufügen von Metainformationen |
75 private void addMetaInfo(Document doc) { 76 doc.addTitle("A Simple PDF-Document"); 77 doc.addSubject("PDF-Creation from Java"); 78 doc.addKeywords("PDF Java Linux-Magazin Coffee-Shop"); 79 doc.addAuthor("Bernhard Bablok"); 80 doc.addCreator("Bernhard Bablok"); 81 } |
Funktional wichtiger als Metadaten sind aber andere Eigenschaften des Dokuments, etwa die Papiergröße, die Ausrichtung oder die Ränder.
Neben dem Default-Konstruktor (Listing 2, Zeile 41) von »Document« gibt es noch zwei weitere Konstruktoren, mit denen sich Papiergröße und Ränder definieren lassen. Ersteres erfolgt über ein »Rectangle«-Objekt (eine Klasse in »com.lowagie.text«, nicht zu verwechseln mit der Klasse »java.awt.Rectangle«), Letzteres über absolute Größenangaben der vier Ränder. Gemessen wird jeweils in Points, wobei ein Inch (2,54 cm) 72 Points entspricht.
Um fehleranfälliges und redundantes Rechnen zu verhindern, empfiehlt es sich, die vordefinierten, statischen Objekte aus der »PageSize«-Klasse zu verwenden. So entsteht ein Dokument im A4-Seitenformat mit Rändern der Größe 0,5 Inch:
Document doc = new Document(PageSize.A4)
»PageSize« hat eine Reihe nützlicher Methoden wie »setBackgroundColor()« oder »rotate()«. Letztere definiert eine Seite im Querformat.
Grundlegend in der I-Text-Architektur ist das »Element«-Interface, das von einfachen Bausteinen eines PDF-Dokuments implementiert wird. Kleinster Textbaustein ist der Chunk, ein String mit einem definierten Font. Mehrere Chunks bilden eine Phrase. Dieses Layoutelement hat als zusätzliches Attribut einen definierten Zeilenabstand. Paragraph ist eine Subklasse von Phrase und bietet zusätzlich Einrückung und Ausrichtung. Anchor ist eine spezielle Phrase und die Basis für Hyperlinks.
Neben den Textbausteinen gibt es weitere Elemente. Das »Rectangle« wurde beim obigen Beispiel schon angesprochen. Dort kommen noch weitere vor: »HeaderFooter« für Kopf- und Fußzeilen, »Image« für Bilder und »Table« beziehungsweise »Cell« für Tabellen.
Ein komplexeres Anwendungsbeispiel benutzt diese Grundelemente und demonstriert die Gestaltungsmöglichkeiten des I-Text-API: die Umwandlung einer Liste von Bildern in eine PDF-basierte Bildergalerie. Der Vorteil gegenüber den weit verbreiteten Webgalerien ist, dass alles in einer einzigen, wenn auch recht großen Datei steckt.
Das Galerie-Programm erhält die Liste der Bilder als Argument. Metadaten sowie die Titel der Bilder holt es aus einer Properties-Datei. Im Prinzip ließe sich alles (Farben, Fonts und so weiter) aus dieser Properties-Datei konfigurieren, der Übersichtlichkeit halber verzichtet das Beispiel darauf.
Die Struktur der Datei »Gallery.java« ist einfach, siehe Listing 3, die komplette Datei findet sich unter[7]. Zuerst erzeugt es ein »Document«, einen »Writer«, setzt die Metainformationen und öffnet schließlich das Dokument (Zeilen 80 bis 83). Danach bearbeitet es die Liste der Bilder mit einer Seite pro Bild (ab Zeile 86). Am Schluss erzeugt das Programm einen Index und schließt das Dokument (ab Zeile 90).
Der Acrobat Reader soll die Bilder im Vollbildmodus anzeigen, mit einem automatischen Übergang zwischen den Bildern nach drei Sekunden. Die entsprechenden Attribute setzt man überraschenderweise im »PdfWriter« (Zeilen 122 bis 130) und nicht im »Document« - das I-Text-API ist nicht immer konsistent. Unter Linux mit dem veralteten Acrobat Reader 5 funktioniert der Bilder-show-Effekt aber leider nicht.
Die Zeilen 154 bis 159 zeigen, wie aus einem »Rectangle« und dem »HeaderFooter«-Objekt eine Kopfzeile entsteht. Jeder Rand sowie seine Farbe und Dicke sind einzeln konfigurierbar. Bilder in die Galerie aufnehmen ist ein Kinderspiel. Die Methode »addPage()« erzeugt ein »Image«-Objekt, skaliert es und fügt es ins Dokument ein (ab Zeile 174).
I-Text bettet die komplette Bilddatei in das Bild ein, beim Skalieren ändert sich also die Pixeldichte. Abbildung 3 demonstriert das Ergebnis. Der Titel des Bildes ist ein Anchor-Objekt (ab Zeile 180). Er dient als Hypertext-Sprungmarke vom Index aus.
Die »addIndex()«-Methode (Zeilen 198 bis 226) ist eigentlich unnötig, da jedes PDF-Dokument auch eine Thumbnail-Sicht bietet - über eine entsprechende Writer-Konfiguration kann man sie sogar automatisch einblenden. Der selbst gemachte Index dient aber zur Demonstration von Tabellen und Hyperlinks.
Tabellen lassen sich entweder mit Reihen- und Spaltenzahl oder nur durch die Spaltenzahl definieren. Im letzteren Fall fügt I-Text bei Bedarf neue Zeilen an die Tabellen an. Zellen können ebenfalls fehlen, sie werden dann aber auch nicht angezeigt.
Der Index des Galerie-Beispiels braucht zwei Spalten. Die erste nimmt die Thumbnails (verkleinerte Vorschaubilder) auf, die zweite Spalte den Bildtitel (oder den Dateinamen, wenn es keinen Titel gibt). Das Programm liest die Bildgröße (Zeilen 206 und 207) und verkleinert das Thumbnail so, dass die längere Bildkante 80 Pixel groß ist. Das entstandene »Image«-Objekt verpackt man anschließend in ein »Cell«-Objekt und fügt es in die Tabelle ein.
Leider hat I-Text die Semantik der Hyperlinks von HTML übernommen. Hier wie dort ist es verwirrend, dass ein Anchor-Objekt als Sprungziel, aber auch als Absprungquelle dient. Andererseits vereinfacht dies aber auch die Verwendung von Links. Das Ziel ist wie in HTML mit vorgestelltem Gartenzaun anzugeben (Zeilen 219 bis 223). Neben diesen internen Links kann PDF natürlich auch externe Dokumente referenzieren, was auch I-Text unterstützt.
Weitere Möglichkeiten von I-Text seien hier nur kurz erwähnt. Natürlich beherrscht es auch die Gliederung in Kapitel und Abschnitte, was die Navigation in großen Dokumenten deutlich erleichtert. Nützlich sind außerdem die Möglichkeiten der so genannten Page-Events. Mit ihnen lässt sich auf vergleichsweise einfache Weise zum Beispiel ein Index bauen. Die erforderlichen Schritte beschreibt das Tutorial.
Für die Feinsteuerung des Layouts steht das »Graphics2D«-Objekt von I-Text zur Verfügung. Es bietet alle Möglichkeiten des Java-2D-API, was zum Beispiel für die Ausgabe von Diagrammen und Funktionskurven nützlich ist. Die in früheren Coffee-Shops vorgestellten Projekte JFreechart[4] und JFreereport[5] nutzen diese Technik von I-Text.
Noch nicht Teil der Kerndistribution von I-Text, aber in Entwicklung ist die Unterstützung von XML. Neben der XML-Ausgabe über einen »XMLWriter« (analog zum »PdfWriter« in den Beispielen) bringt das Paket noch eine interessantere Möglichkeit mit: die Erzeugung von PDF aus XML. Dafür gibt es eine Klasse, die XML-Dokumente gemäß der I-Text-DTD verarbeitet. Sie unterstützt zwar noch nicht alle Konstrukte, ist aber fast komplett. Mit grundlegenden XSLT-Kenntnissen ist der Weg vom XML-Input zum PDF-Ergebnis nicht weit.
Wer mit der recht komplexen XSLT-Syntax auf Kriegsfuß steht, dem hilft I-Text mit einer einfacheren Technologie. Eine simple Tag-Map setzt eigene Tags in I-Text-Tags um. Folgendes Beispiel erzeugt aus dem ursprünglichen Markup »Definition« einen fett gedruckten I-Text-»paragraph«:
<tag name="paragraph" alias="Definition"> <attribute name="style" value="bold" /> </tag>
I-Text ist ein mächtiges Toolkit für die Textausgabe im PDF-Format. Dank der guten Dokumentation bleibt die Lernkurve flach. Ein einziger Schönheitsfehler ist anzumerken: Weil die I-Text-Klassen genauso heißen wie die Java-Standardklassen (zum Beispiel »Rectangle« und »Font«), ist es erforderlich, im eigenen Programm vollqualifizierte Klassennamen zu verwenden.
Das Lesen von PDFs beherrscht I-Text nur rudimentär. Die Bibliothek bringt dazu ein paar Utilities mit (Concat, Split, Handout und Encrypt), die Dokumentation ist allerdings nicht auf dem aktuellen Stand. Wer bestehende PDFs lesen und weiterverarbeiten will, sollte eher einen Blick auf die Seite des PDF-Box-Projekts[6] werfen. (ofr)
Infos |
[1] I-Text-Homepage: [http://www.lowagie.com/iText/] [2] Dynamically Creating PDFs in a Web Application: [http://www.onjava.com/pub/a/onjava/2003/06/18/dynamic_files.html] [3] "Druck gemacht - mit iText dynamisch PDF-Dokumente generieren": "iX" 08/04, S. 116 [4] Bernhard Bablok, "Malen nach Zahlen - Mit JFreechart Diagramme zeichnen": Linux-Magazin 05/04, S. 116 [5] Bernhard Bablok, "Zahlen zeigen - Daten aufbereiten mit JFreereport": Linux-Magazin 06/04, S. 118 [6] PDF-Box-Projekt: [http://www.pdfbox.org] [7] Listings dieses Coffee-Shops: [http://www.linux-magazin.de/Service/Listings/2004/11/Coffeeshop] |
Der Autor |
Bernhard Bablok arbeitet bei der AGIS mbH als Anwendungsentwickler. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen. |
Listing 3: »Gallery.java« |
037 public class Gallery { 038 ... 067 private static final Font iFont = FontFactory 068 .getFont(FontFactory.HELVETICA,20,Font.BOLD,iColor); 069 070 /////////////////////////////////////////////////////// 071 072 public static void main(String[] args) { 073 try { 074 // setup environment 075 Gallery gallery = new Gallery(); 076 String galleryName = gallery.parseArguments(args); 077 gallery.loadProperties(); 078 079 // prepare document and writer 080 Document document = gallery.getDocument(); 081 gallery.getWriter(document,galleryName); 082 gallery.addMetaInfo(document); 083 document.open(); 084 085 // process individual images 086 for (int i=0;i<iImageName.length;++i) 087 gallery.addPage(document,iImageName[i]); 088 089 // finish processing 090 gallery.addIndex(document); 091 document.close(); 092 } catch (Exception e) { 093 e.printStackTrace(); 094 } 095 } 122 private PdfWriter getWriter(Document doc, String file) throws Exception { 123 PdfWriter writer = 124 PdfWriter.getInstance(doc,new FileOutputStream(file)); 126 writer.setViewerPreferences(PdfWriter.PageModeFullScreen); 127 PdfTransition transition = new PdfTransition(PdfTransition.OUTBOX,3); 128 writer.setTransition(transition); 129 return writer; 130 } 131 140 private void addMetaInfo(Document doc) { 141 String prop; 142 if ((prop = iProps.getProperty("gallery.title")) != null) 143 doc.addTitle(prop); 144 if ((prop = iProps.getProperty("gallery.subject")) != null) 145 doc.addSubject(prop); 146 if ((prop = iProps.getProperty("gallery.keywords")) != null) 147 doc.addKeywords(prop); 148 if ((prop = iProps.getProperty("gallery.author")) != null) 149 doc.addAuthor(prop); 150 if ((prop = iProps.getProperty("gallery.creator")) != null) 151 doc.addCreator(prop); 152 // the header is in fact no metainfo, but it fits here 153 if ((prop = iProps.getProperty("gallery.header")) != null) { 154 HeaderFooter header = 155 new HeaderFooter(new Phrase(prop,iFont),false); 156 header.setBorder(Rectangle.BOTTOM); 157 header.setBorderColor(iColor); 158 header.setAlignment(Element.ALIGN_CENTER); 159 doc.setHeader(header); 160 } 161 } 162 172 private void addPage(Document doc,String filename) throws Exception { 173 // create image 174 Image img = Image.getInstance(filename); 175 img.setAlignment(Image.MIDDLE); 176 img.scaleToFit(600,400); 177 doc.add(img); 178 180 File file = new File(filename); 181 String imageTitle = iProps.getProperty(file.getName(),file.getName()); 182 Anchor anchor = new Anchor(imageTitle,iFont); 183 anchor.setName(file.getName()); 184 Paragraph title = new Paragraph(anchor); 185 title.setAlignment(Element.ALIGN_CENTER); 186 doc.add(title); 187 doc.newPage(); 188 } 189 198 private void addIndex(Document doc) throws Exception { 199 Table indexTable = new Table(2); 200 indexTable.setCellsFitPage(true); 201 202 // iterate over images 203 for (int i=0; i<iImageName.length; ++i) { 204 Image img = Image.getInstance(iImageName[i]); 205 // scale to thumbnail-size 206 float width = img.plainWidth(); 207 float height = img.plainHeight(); 208 float ratio; 209 if (width > height) 210 ratio = 80/width; 211 else 212 ratio = 80/height; 213 img.setAlignment(Image.MIDDLE); 214 img.scalePercent(ratio*100); 215 indexTable.addCell(new Cell(img)); 216 217 // set image description 218 219 File file = new File(iImageName[i]); 220 String imageTitle = iProps.getProperty(file.getName(),file.getName()); 221 Anchor title = new Anchor(imageTitle,iFont); 222 title.setReference("#"+file.getName()); 223 indexTable.addCell(new Cell(title)); 224 } 225 doc.add(indexTable); 226 } 282 } |