Von jqplot Diagrammen zu einer PDF Datei

ich sags gleich, ein steiniger Weg.

Die Anforderung

Zum Zweck der analogen Archivierung musste ich für ein aktuelles Projekt (CHES) eine moderne HTML-Seite (mit mehreren Canvas-Elementen) in eine druckbare Form bringen. Mit CSS-Anweisungen ist es mir nicht gelungen, nur in zwei modernen Browsern ein halbwegs vergleichbares Layout zu Papier zu bringen. Mit viel Javascript ging es eher in diese Richtung, aber im dritten Browser war dann auch da wieder alles anders.

Eine PDF-Datei war die schlüssige Entscheidung. Diese kann dann auch digital archiviert werden, was bei CHES, einem Projekt im Medizinumfeld, durchaus sinnvoll scheint. Jetzt wäre der einfachste Weg folgender: Verwenden Sie Google-Chrome, drücken Sie auf diesen Button und wählen Sie im anschließenden Dialog „Speichern als PDF“. Aber das wäre ja zu einfach …

Das Ausgangsmaterial

CHES Longitudinal Report demo

Mit serverseitig-erzeugten PDFs habe ich schon mehrfach Erfahrung: mit PHP, wkhtmltopdf, und batik gab es immer brauchbare Ergebnisse. Bei CHES verwenden wir die jqplot-Bibliothek von Chris Leonello, die (mehr oder weniger) aufwändige Diagramme mit Hilfe von HTML und canvas im Browser anzeigt. Die Darstellung kann clientseitig angepasst werden, soweit so gut. Serverseitig kann ich diese Bibliothek aber nicht verwenden und um die veränderte Seite durch wkhtmltopdf zu jagen, müsste ich alle Veränderungen mitgeben und … ach, zu kompliziert. Also versuchen wir das PDF am Client zu erzeugen: jspdf.com.

jspdf.com

jspdf ist noch ein recht junges Projekt und vom Funktionsumfang überschaubar. Für die Ansprüche im aktuellen Projekt schien es möglich, einen Versuch damit zu starten. Mit canvas kann jspdf (noch) nicht umgehen, darum werden die Diagramme zuerst in Grafiken konvertiert. Der Qualitätsverlust ist schmerzlich, wo jspdf obendrein nur mit JPEGs und nicht mit PNG arbeiten will.

  var doc = new jsPDF();
  $(jquery_selector_fuer_jqplots_hier).each(function() {
    // hier werden xoff, yoff, w, h berechnet
    // jqplot muss ge-patcht werden, damit nicht image/png sondern image/jpeg kommt
    var img = $(this).jqplotToImageStr();
    doc.addImage(img, 'JPEG', xoff, yoff, w, h);
  });
  doc.save("ches_report_"+what+"_"+CHES.patientWithoutPrefix+".pdf");

Und hier das Resultat, not too bad (Firefox, Linux):

CHES Report PDF mit jspdf

Wunderbar, und sehr übersichtlicher Code. Funktioniert auch in Firefox, Chrome, Safari (sogar am iPad), you name it. Ach ja, der IE, da geht das aller leider nicht. Mehrere Problem:

  • IE8: Kennt das canvas Element nicht. Damit man im IE8 überhaupt jqplot Diagramme sehen kann lädt man die excanvas Bibliothek. Alles wunderbar, sehr langsam, aber immerhin, 100% Funktionalität. Fast, die toDataURL()-Methode ist nicht unterstützt und nach meinen ausführlichen google-Recherchen wird das im IE8 auch nie möglich sein. Die Folge: Der IE8 kann diese Seiten weder drucken noch in PDFs konvertieren.
  • IE9: Hmm, pdfjs funktioniert wunderbar, allein das PDF kann weder angezeigt noch gespeichert werden. Klingt absurd, ist aber so. Der workaround ist noch aberwitziger: Man lade eine kleine Flash-Bibliothek, die einen Button anzeigt, der den PDF-String herunterladen lässt. Unglaublich aber wahr, hier der Code (vollständiges Beispiel bei github.com):
        if (badIE === true) {
          var docString = doc.output('datauristring');
          $("#ieWarning").show();
            Downloadify.create('downloadify',{
              filename: "ches_report_"+what+"_"+CHES.patientWithoutPrefix+".pdf",
              data: docString,
              onComplete: function(){ $("#ieWarning").hide(); },
              onCancel: function(){ $("#ieWarning").hide();  },
              onError: function(){ $("#ieWarning").hide(); },
              dataType: 'base64',
              transparent: false,
              swf: 'js/jspdf/libs/Downloadify/media/downloadify.swf',
              downloadImage: 'js/jspdf/libs/Downloadify/images/download.png',
              width: 100,
              height: 30,
              append: false
            });
        }
    
  • IE10: soll das alles ohne schmutzige Tricks können, konnte ich aber noch nicht testen.

tl;dr — Fazit

Das sehr einfache PDF-Dokument kann mit jspdf gut erstellt werden. Der Ausschluss des IE8 ist mit der mangelnden Unterstützung des canvas-Elements begründet und nicht mit jspdf an sich. Der IE9 workaround ist aberwitzig, aber kann vermutlich bald entfernt werden (der IE9-Marktanteil ist eh rückläufig). Alle anderen, aktuellen Browser können gut bedient werden und erzeugen eine druckbare PDF Datei, die plattformübergreifend gleich aussieht.

Leaflet Vektor Layer mit PostGIS

In der Karte im Viel-Falter Projekt sollte zu den gekachelten Rasterlayern ein weiterer Layer mit der Biotop-Kartierung von Tirol dazukommen. Da die gesamte Vektor-Geometrie mit über 200 MB im GeoJSON Format zu Buche schlägt, muss der Datensatz auf die Ausmaße aktuellen Ansicht (BoundingBox) eingeschränkt werden. Einigermaßen überrascht stellte ich fest, dass es hierzu (fast) keine fertige Lösung gibt. Hier mein Weg:

PostGIS

Da der Vektordatensatz aus vielen kleinen Polygonen besteht eignet er sich wunderbar für Einschränkungen anhand der aktuellen BoundingBox. Postgresql in Kombination mit PostGIS macht diese Berechnungen sehr einfach und mit einem räumlichen Index auch sehr schnell. Also war der erste Schritt den Vektordatensatz im Shapefile Format in die Postgresql Datenbank zu importieren.

shp2pgsql -I -W LATIN1 -s 31254 BIK_PL.shp > bik.sql

Das Datenfile im Shapefile lag in der Kodierung LATIN1 (ISO-8859-1) und in der Projektion EPSG:31254 (MGI / Austria GK West) vor. Der Parameter -I erzeugt automatisch einen räumlichen Index am Ende des Imports.

Leaflet Vector Layer

Um mit der leafletjs-Bibliothek auf die PostGIS Daten zugreifen zu können braucht man noch zwei zusätzliche Software Komponenten: Leaflet-Vector-Layer von Jason Sanford und den das PostGIS RESTful Web Service Framework. Um die Javascript Bibliothek Leaflet-Vector-Layer zu installieren reicht es die Datei lvector.js in das Projektverzeichnis zu kopieren und entsprechend zu referenzieren. Die Layerdefinition in leaflet ist wie folgt:

    pglayer = new lvector.PRWSF({
        url: "http://meinserver.at/pgrest",
        geotable: "bik_pl",
        fields: "gid,name",
        singlePopup: true,
        uniqueField: "gid",
        srid: 31254,
        scaleRange: [15, 19],
        popupTemplate: '<div class="biotop"><h3>{name}</h3>'
          +'<footer class="popup">Datenquelle: Land Tirol - data.tirol.gv.at</footer></div>',
        symbology: {
            type: "single",
            vectorOptions: {
                fillColor: "#2f4a00",
                fillOpacity: 0.4,
                weight: 1.8,
                color: "#2f4a00",
                opacity: 1,
                clickable: true
            }
        }
    });

PostGIS RESTful Web Service Framework

Was jetzt noch fehlt ist die Schnittstelle zur Datenbank. Mit dem PostGIS RESTful Web Service Framework ist dieser Zugang sehr einfach, denn im besten Fall reicht es die PHP Skripte an die entsprechende Position zu kopieren. Anschließend muss noch die Datenbank-Verbindung eingetragen werden und, vorausgesetzt die aufwändig Rechte Konfiguration in Postgresql ist korrekt, der Layer scheint in der Karte auf.

viel-falter Karte mit Biotopkartierung