QVINTVS · SCRIBET

Prawn mit Querverweisen

Es gibt ein undokumentiertes Feature in der PDF-Lib Prawn, mit dem man Querverweise innerhalb des generierten PDF-Dokuments erzeugen kann.

Hintergrund

Um mit Querverweisen in PDFs richtig umgehen zu können, braucht es ein wenig Hintergrundwissen. In PDF können laut Spezifikation einzelne Seiten mit sogenannten Destinations versehen werden. Diese Sprungziele können dann an späterer (oder auch an früherer — man denke nur an ein Inhaltsverzeichnis) Stelle referenziert werden, indem eine sogenannte Annotation des Typs Link für einen bestimmten Bereich auf irgendeiner Seite definiert wird. Was sich kompliziert anhört, ist in Prawn jedoch leicht umzusetzen, weil die notwendigen Strukturen (Destinations und Annotations) von Prawn bereits auf einem höheren Level unterstützt werden, wobei sie leider nicht dokumentiert sind. Oder besser gesagt, die Dokumentation unterschlagen wird, wie ich bei einem Blick in den Quellcode säuerlich feststellen musste:

 module Destinations #:nodoc:

Liebe Leute, nodoc dient dazu, Interna zu verstecken. Ich sehe es ja bis zu einem gewissen Grad ein, dass Destinations Lowlevel-Objekte sind, mit denen man als normaler Nutzer nicht zu tun hat, aber auf der anderen Seite dann das Internals-Modul öffentlich dokumentieren, ist sinnlos. Dazu kommt, dass es sich bei Destinations keinesfalls um privates Zeug handelt. Prawn-Plugins können das nach Belieben benutzen. Na klasse.

Umsetzung

Insgesamt ist die Anwendung recht leicht. Zunächst erstellen wir einfach mal das Grundgerüst für ein PDF-Dokument:

Prawn::Document.generate("foo.pdf") do |pdf|
  pdf.text "Das ist ein Testtext"
end

Nichts besonderes so weit. Später im Dokument soll nun ein Querverweis auf diese Seite folgen — dazu müssen wir eine Destination erstellen und dem PDF-Dokument hinzufügen. Prawn bietet uns dazu die #dest_*- und #add_dest-Methoden, wobei man sich bei ersteren üblicherweise einfach #dest_fit-s bedient, weil es den geringsten Aufwand erzeugt. Falls ihr gerne noch die exakte Position der Seite angeben wollt (um etwa exakt die gewünschte Zeile zu treffen), könnt ihr eine der anderen #dest_*-Methoden verwenden (siehe Quellcode).

Die aktuelle Version von Prawn (0.12.0) hat allerdings einen Bug, der in der momentanen Entwicklerversion allerdings schon behoben ist. Er erzeugt bei Verwendung der dest_*-Methoden ohne explizite Angabe der Seitennummer einen NameError, da diese Methoden versuchen, die nicht existente Methode #current_page aufzurufen. Der Workaround bis zum nächsten Release von Prawn ist, die Seitenzahl explizit anzugeben.

pdf.add_dest("foodest", pdf.dest_fit(pdf.page_count - 1)) # 0-based index

Der obige Code erstellt zunächst für die aktuelle Seite (pdf.page_count - 1) eine Destination. Das - 1 ist erforderlich, weil PDF die Seiten intern ab Null numeriert und nicht ab eins. Danach füge ich mithilfe von #add_dest diese neu erstellte Destination dem PDF-Dokument hinzu und gebe ihr den Namen »foodest«. Um das Ganze ein wenig interessanter zu machen, noch ein wenig Füllmaterial:

pdf.start_new_page
pdf.text "Zweite Seite mit Testtext: #{pdf.page_count}"
pdf.start_new_page

So. Wir befinden uns nun auf der dritten Seite und wollen einen Querverweis auf die Erste anlegen. Ein Blick in die üppige Dokumentation der #text-Methode offenbart das halb geheime Feature:

pdf.text 'Dritte Seite mit <link anchor="foodest">Testtext</link>', :inline_format => true

Der Trick ist, mit :inline_format Prawns Inline-Formatter zu aktivieren, der es euch erlaubt, einfache HTML-ähnliche Textformatierungen forzunehmen, unter anderem auch mithilfe von link wie oben gezeigt Links einzupflegen. Das anchor-Attribut gibt dabei den Namen der Destination an, die ihr bei Klick anspringen wollt, ich habe hier den Namen der von mir zuvor definierten Destination für Seite 1 gewählt. Wenn ihr statt anchor href verwendet, könnt ihr übrigens auch Links ins WWW machen.

Insgesamt sieht der Code so aus:

require "prawn"

Prawn::Document.generate "foo.pdf" do |pdf|
  pdf.text "Das ist ein Testtext"
  pdf.add_dest("foodest", pdf.dest_fit(pdf.page_count - 1)) # 0-based index
  pdf.start_new_page
  pdf.text "Zweite Seite mit Testtext: #{pdf.page_count}"
  pdf.start_new_page
  pdf.text 'Dritte Seite mit <link anchor="foodest">Testtext</link>', :inline_format => true
end

Ich möchte darauf hinweisen, dass Prawns Standardfont keine Informationen für Unterschneidung enthält und daher einfach nur typographisch monströs ist. Abgesehen davon aber sollte das von obigem Code erzeugte PDF-Dokument den Erwartungen gerecht werden; lediglich ist noch zu erwähnen, dass ein Link nicht automatisch visuell gekennzeichnet wird (standardmäßig würde der Link im laufenden Text gar nicht auffallen), d.h. ihr müsst schon Prawns Zeichenmethoden bemühen, um ihn rot anzupinseln oder zu unterstreichen.

Valete.