Wer schon einmal einem Kunden eine Webanwendung erstellen sollte, die auch auf veralteten Browsern wie dem Internet Explorer 8 (IE8) laufen soll, weiß, dass dies häufig ein nicht ganz einfaches Unterfangen ist. Vor allem dann nicht, wenn der Kunde nicht auf Möglichkeiten verzichten möchte, die aktuelle Browser mit Leichtigkeit leisten, aber in alten Versionen schlicht und einfach nicht vorhanden sind.
In einer solchen Situation fand ich mich wieder, als ich für einen Kunden eine einfache Verwaltungsapp für seinen SharePoint 2010 entwickeln sollte, die auch im IE8 noch tadellos läuft. Zudem äußerte er den Wunsch, dass ich der Anwendung auch einen client-seitigen PDF-Export hinzufügen solle.
Das Problem an der Sache ist, dass der IE8, im Gegensatz zu moderneren Versionen, kein standardmäßig integriertes Feature für die Erstellung eines PDF besitzt, das aus dem JavaScript heraus angesprochen und für einen solchen Zweck verwendet werden könnte. Es gibt zwar Add-On’s, die eine solche Funktionalität bieten könnten, jedoch ziehen diese vielerlei Probleme nach sich, die bis zum Einfrieren des IE8 reichen und sind somit keine geeignete Wahl für eine client-seitige PDF-Erstellung. Dazu kommt, dass dies voraussetzen würde, dass der Client das Add-On ebenfalls installiert und aktiviert hat und das war mit den Rahmenbedingungen unseres Kunden nicht vereinbar.
Dennoch nahm ich die Herausforderung an und begann, ein wenig nachzuforschen, ob es nicht vielleicht doch ein paar annehmbare Lösungen für dieses Problem gibt.
Nach einiger Recherche bin ich schließlich auf die Open-Source JS-Bibliothek jsPDF gestoßen. Diese bietet zwar eingeschränkte Möglichkeiten zur PDF-Erstellung, jedoch ist es mit einigen Anpassungen möglich, ein PDF zu erzeugen, das ganz ansehnlich aussieht.
JsPDF nutzt Shims, um IE8-kompatibel zu sein. Ein Shim fängt API-Aufrufe ab und passt Parameter an, leitet Aufrufe um oder führt entsprechend definierte Operationen aus und versucht so, die Kompatibilität mit alten Funktionalitäten zu erreichen. Vom Benutzer wird die Arbeit des Shims nicht wahrgenommen.
Damit war es eine gute Wahl für unseren Bedarf.
Dennoch, ganz ohne Anpassungen ging es nicht. In unserem Fall war es nötig, die ‚output‘-Funktion von jsPDF anzupassen, da wir Probleme bekamen beim Versuch, Blobs zu verwenden.
Der ursprüngliche Codeschnipsel:
{…} // Need to add the file to BlobBuilder as a Uint8Array length = data.length; array = new Uint8Array(new ArrayBuffer(length)); for (i = 0; i < length; i++) { array[i] = data.charCodeAt(i); } blob = new Blob([array], {type: "application/pdf"}); saveAs(blob, options); break; {…}
Die angepasste Version:
{…} // Need to add the file to BlobBuilder as a Uint8Array length = data.length; array = new Uint8Array(new ArrayBuffer(length)); for (i = 0; i < length; i++) { array[i] = data.charCodeAt(i); } //blob = new Blob([array], {type: "application/pdf"}); callback(array, options); break; {…}
Hier habe ich einen Blob erstellt und die Ausführung der ’save‘-Funktion herausgenommen und statt letzterer eine Callback-Funktion frei als Argument mitgegeben, um weitere Anpassungen auszulagern. Was in dieser Callback-Funktion geschieht, erläutere ich später.
Des Weiteren habe ich dem Plugin, für Versionen unterhalb von IE9, ebenfalls eine Callback-Funktion hinzugefügt, um diese als Argument weiterzugeben:
{…} API.output = function(type, options, callback) { // If not IE then return early return this.internal.output(type, options, callback); {…}
Mithilfe einer neuen ‚output‘-Funktion, die ich dem Dokumenten-Objekt mitgegeben habe, konnte ich die Anbindung an unseren SharePoint erreichen, um das generierte PDF dort abzulegen:
doc.output('save', 'Export.pdf', function (bitArray, filename) { var array64 = new SP.Base64EncodedByteArray(); for (var i = 0; i < bitArray.length; i++) { array64.append(bitArray[i]); } var filename = self.getExportAttachmentUrl(); /* Fuer 2010 Kompatibilitaet darf hier kein JSOM genutzt werden(FileCreationInformation) */ $().SPServices({ operation: "DeleteAttachment", listName: Lib.Constant.ItemListName, listItemID: self.Id(), url: filename, async: true }).always(function () { $().SPServices({ operation: "AddAttachment", listName: Lib.Constant.ItemListName, listItemID: self.Id(), fileName: "Export.pdf", attachment: array64.toBase64String(), async: true }).done(function () { deferred.resolve(); }).fail(function () { Lib.Util.showErrorDialog("Fehler beim generieren der PDF-Datei."); deferred.reject(); }); }); });
Diese Funktion wird vom jsPDF später aufgerufen.
Soviel zu den nötigen Anpassungen.
Um nun ein PDF zu erstellen, erzeugt man zunächst ein jsPDF-Objekt und gibt diesem die Ausrichtung des PDFs, sowie die Größeneinheit in der gerechnet werden soll und das Format mit:
var doc = new jsPDF('portrait', 'pt', 'a4');
Die Schriftart wird festgelegt, indem man einen Array mit dem Namen, der Schriftart und deren Ausprägung erstellt:
var font = ['Helvetica', ''];
Um die momentane Position auf dem PDF zu bestimmen, wird noch eine Variable benötigt, die die vertikale Verschiebung im Blick behält. Vernachlässigt man dies, dann kann es schnell passieren, dass Text übereinander geschrieben wird.
Zusätzlich habe ich noch ein paar Variablen erstellt: die Zeilenumbruch-Verschiebung, linken und rechten Einzug, Seitenhöhe und -breite und einen Zähler, der bestimmt, ab wie vielen Zeichen ein Zeilenumbruch gemacht werden soll.
Darüber hinaus haben wir die Möglichkeit genutzt, mit jsPDF Bilder auf das PDF zu bringen. Diese sollten als Header immer am Anfang einer neuen Seite erstellt werden. Dazu haben wir das Bild als Base64-String ebenfalls als Variable angelegt:
var verticalStartOffset = 20; var verticalOffset = verticalStartOffset; var standardHorizontalOffset = 20; var standardBrOffset = 11; var brOffset14 = 14; var seperationOffset = 20; var standardValAlignWidth = 150; var imageOffset = 60; var pageHeight = 840; var pageWidth = 595; var contentBoxWidth = pageWidth - 20; var contentBoxHeight = pageHeight - 20; var charCountToBreak = 90; //Einrückungsplatz für Listen var listSpacing = " "; //Image var image = "data:image/jpeg;base64,/…";
Um das Bild darzustellen, nutzt man die ‚addImage‘-Funktion und gibt dieser den Base64-String, die horizontale Koordinate des Startpunktes, die vertikale Koordinate des Startpunktes sowie die Breite und die Höhe des Bildes an:
doc.addImage(image, 'JPEG', 500, verticalOffset, 60, 29);
Wichtig zu beachten ist auch im Folgenden, dass immer geprüft werden muss, ob eine neue Seite zum Dokument hinzugefügt werden muss. Ansonsten wird der Text, der über die Grenzen des PDFs hinausgeht, nicht mit auf das Papier gebracht. Dafür habe ich eine Prüffunktion geschrieben, die diese Aufgabe übernimmt:
checkForPageAdd: function (doc, verticalOffset, contentBoxHeight, standardHeight, image, imageOffset) { if (verticalOffset > contentBoxHeight) { doc.addPage(); verticalOffset = standardHeight; if (image) { doc.addImage(image, 'JPEG', 500, verticalOffset, 60, 29) if (imageOffset) { verticalOffset += imageOffset; } } return verticalOffset; } else { return verticalOffset; } },
Diese schreibt das Bild als Header wieder mit auf das Dokument, wenn eine neue Seite hinzugefügt wird. Um die vertikale Verschiebung im Blick zu behalten, gibt diese Funktion einen angepassten Wert zurück.
Um die Schrift zu ändern, nutzt man die ’setFont‘-, die ’setFontType‘- und die ’setFontSize‘-Methoden:
doc.setFont(font[0], font[1]); doc.setFontType("bold"); doc.setFontSize(fontSizes[0]);
Die ’setFont‘-Methode setzt die Schriftart, die ’setFontType‘-Methode setzt den Typ der Schrift (Fettgedruckt, Kursiv, etc.) und die ’setFontSize‘-Methode setzt die Schriftgröße fest.
Darüber hinaus ist es noch wichtig, auf Zeilenumbrüche zu prüfen, die durch zu lange Zeilen entstehen. Dafür habe eine Methode namens ‚checkForRowBreak‘ geschrieben:
checkForRowBreak: function (doc, text, charCountToBreak, horizontalOffset, verticalOffset, standardBrOffset, contentBoxHeight, verticalStartOffset, image, imageOffset, isFromTextEditor) { var self = this; if (!isFromTextEditor){ isFromTextEditor = false; } if (text === null){ text = ""; } var array = text.split("\n"); for (var i = 0; i < array.length; ++i) { if (array[i].length > charCountToBreak) { var insertPoint = array[i].substring(0, charCountToBreak).lastIndexOf(" "); if (insertPoint <= 0) { insertPoint = charCountToBreak; } else { insertPoint = insertPoint + 1; } array[i] = [array[i].slice(0, insertPoint), "\n", array[i].slice(insertPoint)].join(''); for (var j = 0; j < array.length; ++j) { if (j < array.length - 1) { array[j] += "\n"; } } text = array.join(''); array = text.split("\n"); i = i - 1; } } for (var i = 0; i < array.length; ++i){ if (isFromTextEditor) { //workaround for IE8 and Sharepoint 2010 array[i] = array[i].replace(new RegExp("\n", "g"), ";;n;;"); array[i] = jQuery("<div></div>").html(array[i]).text(); array[i] = array[i].replace(new RegExp(";;n;;", "g"), "\n"); doc.text(array[i], horizontalOffset, verticalOffset); } else { doc.text(array[i], horizontalOffset, verticalOffset); } verticalOffset += standardBrOffset; verticalOffset = self.checkForPageAdd(doc, verticalOffset, contentBoxHeight, verticalStartOffset, image, imageOffset); } return verticalOffset; },
Diese prüft auf die gesetzte maximale Zeichennummer und bricht um, falls diese überschritten wird. Sie gibt auch die vertikale, aktuelle Position auf dem Dokument, gegebenenfalls angepasst, zurück.
Letztendlich kommt noch das Wichtigste: den Text auf das Dokument bringen. Dies geht mit der ‚text‘-Funktion des jsPDF-Objekts:
doc.text("Text:", standardHorizontalOffset, verticalOffset);
Diese rechnet jedoch die vertikale Verschiebung nicht hoch. Dementsprechend muss noch darauf geachtet werden, diese zu erhöhen, um zu vermeiden, dass zwei Zeilen Text in eine Zeile geschrieben werden und sich so überlagern.
JsPDF ermöglicht es auch, Formen auf das PDF zu zeichnen. Ein Beispiel ist die ‚line‘-Funktion:
doc.line(standardHorizontalOffset, verticalOffset, 580, verticalOffset);
Diese zeichnet eine Linie von den angegebenen Start- bis zu den Endkoordinaten auf das Dokument.
Darüber hinaus gibt es auch noch Möglichkeiten, Rechtecke, Kreisformen , Dreiecke usw. auf das PDF zu zeichnen.
Mithilfe dieser Funktionen kann man sich dann ein eigens angepasstes PDF-Dokument erstellen. Doch es erfordert eine Menge Anpassungsarbeit, sobald man die Generierung eines PDFs dynamisch gestalten möchte.
Für unsere Zwecke hat es jedoch genügt und der Kunde ist nun in der Lage, PDFs zu generieren, die zwar nicht ganz so ausgefallen sind wie heutzutage oft üblich, für seine Zwecke aber vollkommen ausreichend sind.
Fazit
Moderne Funktionen in alten Browsern und Systemen darzustellen, wirkt auf den ersten Blick immer sehr schwierig – aber nicht unmöglich. In vielen Firmen findet man auch jetzt immer noch den mittlerweile nicht mehr supporteten IE8 und ältere SharePoint-Versionen. Allerdings gibt es sehr viele Funktionalitäten und freie Bibliotheken, die einen bei der Arbeit unterstützen, moderne Webstandards in älteren Systemen zu ermöglichen. Es ist möglich, mit einigen Anpassungen und Erweiterungen die älteren Systeme um die neuen Funkionen zu erweitern.
Kategorien: SharePoint, Technical
Schlagwörter: Dokumente, HowTo, JavaScript, PDF, SharePoint
Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?
Nehmen Sie mit uns Kontakt auf!
Wir unterstützen Sie gerne bei Ihren SharePoint-Vorhaben!