Dynamische PDF-Generierung mit PDFKit

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!