xhtmlrenderer: DOCTYPE

Wenn man mit dem xhtmlrenderer ein PDF erzeugen möchte sollte man in der Quell-XHTML-Datei die DOCTYPE Definition weglassen. Bleibt diese nämlich drin wird die parse() Methode des DocumentBuilder versuchen die DTD zu laden. Und wenn das nicht möglich ist (zum Beispiel, weil keine Netzwerkverbindung aufgebaut werden kann/darf) bleibt das Programm an dieser Stelle einfach (und zunächst ohne Fehler) stehen.

File inputFile   = new File( "input.xhtml" );
String baseUrl = inputFile.toURI().toURL().toString().replace( inputFileName, "");

DocumentBuilder dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = dBuilder.parse( inputFile );

ITextRenderer renderer = new ITextRenderer();
renderer.setDocument( doc, baseUrl );
renderer.layout();

FileOutputStream pdfOutputStream = new FileOutputStream(outputFileName);

renderer.createPDF( pdfOutputStream );

pdfOutputStream.close();

xhtmlrenderer: PDFs mit Bookmarks / Inhaltsverzeichnis erstellen

Dass man mit dem xhtmlrenderer (The Flying Saucer Project) aus xhtml Dateien PDFs erzeugen kann ist weitläufig bekannt. Dass man über spezielle XML-Elemente zudem Lesezeichen (Bookmarks) im PDF erzeugen kann, weniger. Man kann sogar die <bookmark />-Tags verschachteln um so eine Hierarchie (wie zum Beispiel ein Inhaltsverzeichnis) abzubilden. Das Hauptelement <bookmarks> muss dabei im <head> platziert werden.

<html>
    <head>
        <bookmarks>
            <bookmark name="A bookmark" href="#bm" />
            <bookmark name="A bookmark 2" href="#bm2">
                <bookmark name="A bookmark 3" href="#bm3" />
            </bookmark>
            <bookmark name="A bookmark invalid" href="#bm99" />
        </bookmarks>
    </head>
    <body>
        <a name="bm1">Jumpmark 1</a>
        <a name="bm2">Jumpmark 2</a>
        <a name="bm3">Jumpmark 3</a>
    </body>
</html>

Im PDF sieht es dann so aus:

PDFs mit Attachments und xhtmlrenderer (FlyingSaucer) erstellen

Nun, genau wie mein Arbeitsfeld ändern sich auch die Themen dieses Blogs ständig. Daher schreibe ich heute mal etwas aus der bunten Welt von Java. Um etwas genauer zu sein: Es geht um die serverseitige Erstellung von PDF-Dateien mithilfe von xhtmlrenderer, aka The Flying Saucer Project. Und zwar natürlich nicht nur normale PDFs, nein, das wäre ja geradezu trivial. Ich möchte an die PDFs noch Dateianhänge, sogenannte Attachments, fügen.
Die offizielle iText-Seite – iText ist die Komponente zur Erstellung von PDFs in xhtmlrenderer. Der xhtmlrenderer selbst ist eigentlich mehr ein Browser und dient dazu (x)html als Quelle für die iText-Konvertierung zu verwenden. – beschreibt auf dem einzigen verfügbaren Tutorial die Erstellung von PDFs mit Attachments etwa so:

Document document = new Document( PageSize.A4);
PdfWriter writer
  = PdfWriter.getInstance( document, new FileOutputStream( new File( "myDocument.pdf" ) ) );
writer.setViewerPreferences( PdfWriter.PageModeUseAttachments );
document.open();
PdfFileSpecification fs = 
   PdfFileSpecification.fileEmbedded( writer, "somePhysicalFilename.ext", "someDisplayFilename.ext", null );
writer.addFileAttachment( fs );
document.add( ... );
document.close();

Das Problem bei der Verwendung von xhtmlrenderer ist, dass man kein Document und damit auch kein PdfWriter Objekt zur verfügung hat. Dort erstellt man ein PDF nämlich etwa so:

ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
DocumentBuilder dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = dBuilder.parse( new InputSource( new StringReader( "<html> ... </html>" ) ) );
File basePath = new File( "/somewhere/" );

ITextRenderer renderer = new ITextRenderer();
renderer.setDocument( doc, basePath.toURI().toURL().toString() );
renderer.layout();
renderer.createPDF( pdfStream );

byte[] pdfByte = pdfStream.toByteArray();
pdfStream.close();

File uploadDirectory = new File( "/someUploadDirectory" );
uploadDirectory.mkdirs();

File pdfFile = new File( uploadDirectory, "myDocument.pdf" );
FileOutputStream fos = new FileOutputStream( pdfFile );
fos.write( pdfByte );
fos.close();

Zwar gibt es die Möglichkeit einen PdfWriter zu erlangen, indem man renderer.getWriter() ausführt, jedoch hat dieses Vorgehen bei mir immer nur beschädigte Dateien erzeugt.
Die Lösung habe ich in zwei Dingen gefunden:

  1. Im Zwischenspeichern der Anhanglosen Datei und …
  2. … in der PdfStamper Klasse.

Nehmen wir das vorangegangene Listing als Basis sieht das Hinzufügen von Attachments folgendermaßen aus:

File pdfFileWithAttachments = new File( "myDocumentWithAttachments.pdf" );
FileOutputStream fosFinal = new FileOutputStream( pdfFileWithAttachments );
PdfReader pr = new PdfReader( pdfFile.toURI().toURL() );
PdfStamper ps = new PdfStamper( pr, fosFinal );

ps.setViewerPreferences( PdfWriter.PageModeUseAttachments );
File attachment = new File( "somePhysicalFilename.ext" );
PdfFileSpecification fa = PdfFileSpecification.fileEmbedded(
						ps.getWriter(),
						attachment.getAbsolutePath(),
						attachment.getName(),
						null );
ps.addFileAttachment( attachment.getName(), fa );
ps.close();
fosFinal.close();

Ja und das war’s auch eigentlich schon.