Es gibt viele Situationen, in denen man in einer Ruby- oder Rails-Anwendung dynamisch erzeugte PDF-Dokumente haben möchte, z.B. zur drucker- und downloadfreundlichen Ausgabe von Inhalten oder als Anhang automatisch verschickter E-Mails.
Mit dem Gem PDFKit lassen sich PDFs einfach aus HTML- und CSS-Code generieren.
Standard-Verwendung in Rails
Grundsätzlich ist der Einsatz von PDFKit unkompliziert, das Gem lässt sich mit dem folgendem Eintrag im Gemfile der Rails-App integrieren:
# Gemfile
gem 'pdfkit'
Weiterhin ist wkhtmltopdf als Backend nötig.
wkhtmltopdf rendert HTML auf Basis von WebKit und erzeugt daraus dann die PDF-Daten. Für alle gängigen Betriebssysteme (Mac OS X, Linux, Windows) steht eine Version zum Download bereit.
Hat man PDFKit sowie wkhtmltopdf installiert, lässt es sich folgendermaßen im Rails-Code verwenden:
kit = PDFKit.new(html, options)
Hier wird ein neues PDFKit
-Objekt erzeugt; als Parameter html
kann entweder
HTML-Code direkt als String übergeben werden oder alternativ eine URL auf ein
HTML-Dokument. options
ist ein Hash, dessen Elemente als Optionen an
wkhtmltopdf weitergereicht werden.
CSS-Stylesheets lassen sich wie folgt einbinden:
kit.stylesheets << '/path/to/css/file'
Natürlich kann man Stylesheets auch im HTML-<head>
referenzieren. Hierbei
muss, wie bei allen Ressourcen-Referenzen, beachtet werden, dass
absolute URIs (inklusive Domainname) verwendet werden!
Nun lassen sich die PDF-Daten generieren:
pdf = kit.to_pdf
Verwendung bei Nix-wie-weg®
Leider ist wkhtmltopdf in keiner seiner Release-Versionen frei von Bugs. Dazu kommt, dass die PDF-Ergebnisse auf unterschiedlichen Betriebssystemen selbst unter Verwendung der gleichen Version von wkhtmltopdf unterschiedlich ausfallen können!
wkhtmltopdf in der Version 0.11.0 unter Mac OS X (10.7) z.B. rendert Zeilenumbrüche innerhalb von Paragraphen im HTML-Code nicht als whitespaces - wkhtmltopdf in derselben Version auf einem Linuxrechner macht dies anstandslos.
Wir bei Nix-wie-weg® entwickeln unter Mac OS X, unsere Produktivserver laufen jedoch mit Linux-Distributionen. Daher verfolgen wir folgenden Lösungsansatz: wkhtmltopdf wird ausschließlich auf den Produktivservern installiert, die Entwicklungsrechner greifen dann bei Bedarf über das Netzwerk darauf zu.
Konfiguration der Produktivserver
Wir müssten also die aktuellste wkhtmltopdf-binary herunterladen und auf dem Produktivserver platzieren - um die Installation von wkhtmltopdf aber so eng wie möglich mit der Rails-App zu verzahnen, packen wir die binary lieber mit ins Rails-Projekt, um sie dann anschließend auf dem Server zu deployen. Für die genauen Schritte hierzu siehe Konfiguration der Rails-App.
Da unsere Server über keine grafische Benutzeroberfläche verfügen, wkhtmltopdf aber auf einen X-Server angewiesen ist, um in vollem Umfang zu funktionieren, lässt sich mit Xvfb nachhelfen: es handelt sich hierbei um einen “virtuellen X-Server”. Diesen können wir aufgrund seiner vielen dependencies nicht mit in die Rails-App packen, sondern müssen ihn händisch auf dem Produktivserver installieren. Das geht (unter Debian/Ubuntu) standardmäßig über die Paketverwaltung:
$ apt-get install xvfb
Des Weiteren müssen wir dafür sorgen, dass sich alle Fonts, die von den zu generierenden PDFs benötigt werden, ebenfalls auf dem Server befinden. Unter Debian/Ubuntu ist das Verzeichnis hierfür /usr/share/fonts/truetype. Es sollte darauf geachtet werden, dass die Font-Dateien im .ttf-Format vorliegen, da andere Formate nicht immer fehlerfrei verarbeitet werden.
Konfiguration der Rails-App
Für die wkhtmltopdf-binary legen wir im Rails-Projekt im Verzeichnis bin ein neues Verzeichnis namens wkhtmltopdf an. In das Verzeichnis entpacken wir dieses Archiv (da wir 64bit-Ubuntu auf unseren Servern verwenden). Idealerweise benennen wir die entpackte binary noch schlicht in wkhtmltopdf um.
Nun benötigen wir noch ein Wrapper-Shellskript (im selben Verzeichnis wie die binary), welches dafür sorgt, dass der virtuelle X-Server gestartet und wkhtmltopdf als dessen Client ausgeführt wird:
# bin/wkhtmltopdf/xvfb-run-wkhtmltopdf-wrapper.sh
xvfb-run -a /path/to/wkhtmltopdf --use-xserver $*
/path/to/wkhtmltopdf
muss durch den Pfad ersetzt werden, den die
wkhtmltopdf-binary in der Rails-App nach dem Deployment auf dem
Produktivserver haben wird!
Die beiden neu hinzugefügten Dateien sollten nun direkt deployt werden, damit sie für die nachfolgende Entwicklungsarbeit verwendet werden können.
Um während der Entwicklung also auf das Wrapper-Skript auf dem Produktivserver zugreifen zu können, benötigen wir ein weiteres Skript (im Verzeichnis script):
# script/remote-wkhtmltopdf-wrapper.sh
ssh host /path/to/xvfb-run-wkhtmltopdf-wrapper.sh $*
Als host
muss natürlich der Hostname des Produktivservers angegeben werden,
/path/to/xvfb-run-wkhtmltopdf-wrapper.sh
sollte selbsterklärend sein.
Nun gilt es, das PDFKit-Gem in die App zu integrieren. Wir erweitern das Gemfile um folgenden Eintrag:
Gemfile
gem 'pdfkit', '~> 0.5.3'
Weiterhin legen wir folgenden initializer an:
# config/initializers/pdfkit.rb
PDFKit.configure do |config|
if Rails.env.production?
config.wkhtmltopdf = '/path/to/xvfb-run-wkhtmltopdf-wrapper.sh'
config.root_url = 'production_url'
else
config.wkhtmltopdf =
Rails.root.join('script/remote-wkhtmltopdf-wrapper.sh').to_s
config.root_url = 'development_url'
end
end
/path/to/xvfb-run-wkhtmltopdf-wrapper.sh
ist wieder selbsterklärend,
production_url
und development_url
sind jeweils die Adressen, über die der
Produktiv- respektive Entwicklungsserver erreichbar sind, also z.B.
“http://www.nix-wie-weg.de” als production_url
und
“http://developer.x.nix-wie-weg.de:3000” als development_url
.
Diese URLs sind wichtig, da sie verwendet werden müssen, um Ressourcen wie Bilder und CSS in den zu konvertierenden HTML-Dateien (Views) zu referenzieren. Ergo müssen diese Ressourcen über jene URLs erreichbar sein, d.h. sich im Ordner public der Rails-App befinden! Idealerweise lässt sich hier noch ein Unterordner pdf anlegen, in den dann alle PDF-spezifischen Ressourcen abgelegt werden.
Für die beschriebene Konfiguration bietet sich ein Rails-Helper an, um die Views etwas vereinfachen zu können:
# app/helpers/pdf_helper.rb
module PdfHelper
def pdf_stylesheet_path(stylesheet)
"#{PDFKit.configuration.root_url}/pdf/stylesheets/#{stylesheet}"
end
def pdf_image_path(image)
"#{PDFKit.configuration.root_url}/pdf/images/#{image}"
end
end
Schließlich kann PDFKit ganz einfach verwendet werden:
kit = PDFKit.new(html, options)
pdf = kit.to_pdf
Einschränkungen und best practices
Durch unsere spezielle PDFKit-Konfiguration ergeben sich ein paar
Einschränkungen. So kann die zu PDFKit gehörige Methode to_file(path)
(um das PDF direkt in eine Datei zu rendern) nicht mehr verwendet werden,
da hier dann gleichermaßen auf dem Produktiv- (wkhtmltopdf-seitig) wie auch
auf dem Entwicklungsrechner (Ruby-seitig) versucht würde, den übergebenen Pfad
aufzulösen. Stattdessen lassen sich PDF-Dateien einfach wie folgt anlegen:
File.open('/path/to/file', 'w') { |f| f.write(kit.to_pdf) }
Weiterhin gibt es einen
bekannten Bug
in wkhtmltopdf, der ein Einbinden von Fonts via CSS (@font-face
) nicht
korrekt ermöglicht. Aus diesem Grund müssen die Fonts im Betriebssystem
installiert sein, wie auch schon unter Konfiguration der Produktivserver
beschrieben.
Will man schick formatierte PDF-Seiten z.B. im DIN-A4-Format (oder beliebigen anderen Formaten) erzeugen, gibt es einige best practices, um dies zu bewerkstelligen:
Zum Einen sollten folgende Optionen beim Aufruf von PDFKit.new
mitgeben
werden:
PDFKit.new(
html,
:margin_top => '0', :margin_left => '0', # standardmäßiges Seitenränder-
:margin_bottom => '0', :margin_right => '0', # margin von wkhtmltopdf
# deaktivieren
:page_size => 'A4', # Seitenformat (evtl. anpassen)
:disable_smart_shrinking => true # "automatisches Verkleinern
# von Inhalten" deaktivieren
)
Im Anschluss sollte dann im CSS die Seitengröße (wenigstens die Breite) explizit angegeben werden (für andere Formate als DIN-A4 natürlich entsprechend angepasst):
body {
width: 210mm
}
Fazit
Wer funktionierende PDF-Dateien dynamisch erzeugen lassen und dabei auf altbewährte Techniken wie HTML und CSS zurückgreifen will, für den ist die Kombo PDFKit + wkhtmltopdf sehr zu empfehlen. Verwendet man nur ein Betriebssystem (idealerweise Linux), so ist die Installation und Verwendung noch dazu sehr einfach!