Unit-testen von generierten PDFs mit PHPUnit und PDFBox

Zu den Features, die sich nicht leicht mit Unit Tests prüfen lassen gehört das generieren von PDF-Dateien.

Hilfreich dazu ist das Kommandozeilen-Tool PDFBox mit der Option ExtractText:

PDF

This application will extract all text from the given PDF document.

Das erlaubt uns schon mal, den Text-Inhalt des Dokuments zu testen oder nach bestimmten Strings darin zu suchen.

Interessant wird es mit der Option -html, die das PDF zu HTML konvertiert. Damit sind auch Struktur und Formatierungen ansatzweise testbar.

Leider arbeitet das Tool nicht mit Streams, wir müssen also temporäre Dateien nutzen. Ein einfaches Beispiel für eine Funktion, die ein PDF Dokument als String entgegennimmt, mit PdfBox in HTML konvertiert und das HTML als String zurückgibt:

/**
 * @var string $streamIn binary string with generated PDF
 * @return string HTML string
 */
function htmlFromPdf($streamIn)
{
  $pdf = tempnam();
  file_put_contents($pdf, $streamIn);
  $txt = tempnam();
  exec('java -jar pdfbox-app-x.y.z.jar ExtractText -encoding UTF-8 -html ' . $pdf . ' ' . $txt);
  $streamOut = file_get_contents($txt);
  unlink($pdf);
  unlink($txt);
  return $streamOut;
}

Für Regressionstests oder beim Refaktorisieren kann es mitunter ausreichen, zu testen, dass das generierte PDF sich gegenüber dem Referenz-Dokument nicht geändert hat. Hier bietet sich ein Hash an, die Datei selbst ist aber – vermutlich aufgrund von Timestamps – nicht bei jedem Durchlauf exakt gleich. Ein Hash des nach HTML konvertierten Dokuments dagegen genügt:

        // In PHPUnit test case:
        $converter = new PdfBox();
        $html = $converter->htmlFromPdfStream($pdf);
        $this->assertEquals('336edd9ee49b57e6dba5dc04602765056ce05b91', sha1($html), 'Hash of PDF content');

In diesem Beispiel nutze ich eine kleine selbstgeschriebene Klasse PdfBox, die den Aufruf von PDFBox kapselt. Den Code gibt es unter BSD Lizenz auf GitHub: PHP PdfBox

PHP PdfBox

Voraussetzungen

  1. Java Runtime Environment, mit “java” im Systempfad. Um das zu testen, einfach mal java -version auf der Kommandozeile eingeben. Wenn Informationen über die Java-Version angezeigt werden, ist alles OK.
  2. Apache PdfBox als ausführbare JAR Datei. Diese lässt sich hier herunterladen: http://pdfbox.apache.org/downloads.html
  3. Die PHP-Funktion exec() zum Ausführen von System-Kommandos darf nicht deaktiviert sein. Auf Shared Hosts ist das in der Regel aus Sicherheitsgründen der Fall; für das lokale Ausführen von Unit Tests sollte es aber kein Problem sein, exec() zu erlauben. PHP-CLI, also PHP auf der Kommandozeile nutzt üblicherweise eine andere php.ini Konfigurationsdatei als PHP-CGI fürs Web. Der Aufruf php --ini zeigt, welche INI-Datei(en) geladen werden, diese können ggf. editiert werden, um exec von der disable_functions Liste zu entfernen.
  4. Ein PSR-0 kompatibler Autoloader, wie ihn die meisten Frameworks mitbringen. Andernfalls müssen die einzelnen PHP-Dateien noch inkludiert werden.

Nutzung

Zunächst muss der vollständige Pfad zum PdfBox JAR bekannt gemacht werden. Anschließend lassen sich die Konvertierungsmethoden aufrufen, z.B:

use SGH\PdfBox

//$pdf = GENERATED_PDF;
$converter = new PdfBox;
$converter->setPathToPdfBox('/usr/bin/pdfbox-app-1.7.0.jar');
$text = $converter->textFromPdfStream($pdf);
$html = $converter->htmlFromPdfStream($pdf);
$dom  = $converter->domFromPdfStream($pdf);

Es gibt die folgenden Konvertierungs-Methoden:

  • string textFromPdfStream($content, $saveToFile = null)
  • string htmlFromPdfStream($content, $saveToFile = null)
  • DomDocument domFromPdfStream($content, $saveToFile = null)
  • string textFromPdfFile($fileName, $saveToFile = null)
  • string htmlFromPdfFile($fileName, $saveToFile = null)
  • DomDocument domFromPdfFile($fileName, $saveToFile = null)

Der erste Parameter ist jeweils entweder das PDF als binärer String ($content) oder der Dateiname eines PDF ($fileName). Der zweite Parameter, falls angegeben, ist ein Dateiname für die Ausgabe. In dieser Datei wird also der Text bzw. das HTML gespeichert.

Einige zusätzliche PdfBox-Optionen können auch nützlich sein:

// Nur Seite 2-5 extrahieren:
$converter->getOptions()
    ->setStartPage(2)
    ->setEndPage(5);

// Korrupte PDF-Objekte ignorieren:
$converter->getOptions()
    ->setForce(true);

Alles weitere sollte sich aus den PhpDoc Kommentaren ergeben. Happy Testing!