Client-side PDF Generation with AngularJS

Creating PDF’s with Client Side JavaScript Sucks!

I’ve learned that creating PDFs with JavaScript is not easy but it can be done and can be done well.  It can be done using HTML/CSS/JavaScript.  In this post, I’ll be using AngularJS but I’ve seen how this can be done with Angular as well.  There can be a lot of gotchas especially if you need it to work on mobile.

So let’s get to it!

Thing’s You’ll Need

  • HTML2Canvas – https://github.com/niklasvh/html2canvas – This library will convert HTML and create a canvas out of it.  We’ll be using this to turn our HTML into an image which can then be inserted into our PDF document since the PDF doesn’t take HTML.
  • PDFMake – https://github.com/bpampuch/pdfmake – this library will take the content we put into and put it in a PDF document on the client side.  I’ve used this in a mobile application using Ionic.

Getting Started

The way I got this solution to work is to have an outside HTML file to use as a template file.  Here is an example:

 

PDF Template Image

Things to point out:

  1. Any styles has to be put inline or in an internal stylesheet.  I put these in the head.
  2. Background images or fonts need to be loaded in the stylesheet in Base64 encoded format.  Links to other resources won’t be available on a PDF so their data needs to be in the template
  3. Any images that are used in the html template file must also be Base64 encoded. Links to other resources won’t be available on a PDF so their data needs to be in the template
  4. The div’s having an id of ‘page-X’, is important.  This is how the PDF service will know how to break up the pages when generating the PDF content.
  5. You can use most styling that would available in email clients.  This will be very touch when it comes to a PDF so the simpler the better.
  6. This html has tabular data so I used a table but you can use what you want.
  7. The “td id=name” is an example of how you could variable data in your PDF. These ideas will tell the service where to put the data you want show that is not hard coded.

AngularJS PDF Generation Service

(function () {
 'use strict';
 angular.module("services").factory("pdfServiceFactory", pdfServiceFactory);
 pdfServiceFactory.$inject = ["$rootScope", "$http", "configuration", "$q"];

function generateDomForPDF($rootScope, $http, configuration, $q) { }
function convertDomToCanvas(numberOfPages) { }
function createPdf(pdfFileName, share) { }
})();

The above is the definition of the AngularJS Factory I use to generate PDF’s.  I broke it down this way so I can talk about each method individually.

Generate DOM for PDF

function generateDomForPDF(filename, pdfFileName, dictionary, share) {
 	// 1. Get Local HTML file
 	var rawFile = new XMLHttpRequest();
 	var contents = "";
 	rawFile.open("GET", filename, false);
 	rawFile.onreadystatechange = function () {
 		if (rawFile.readyState === 4) {
 			if (rawFile.status === 200 || rawFile.status == 0) {
 				contents = rawFile.responseText;
 			}
 		}
 	}

	//2. Get the file add a new element to the calling page and append the HTML Template file to the DOM.
 	rawFile.send(null);
	var element = document.createElement("div");
	element.innerHTML = contents;
 	//Give the new element the ID of pdf-results so we can find it
 	element.id = "pdf-results";
 	document.body.appendChild(element);

	//3. Loop through the DOM and add our variable information to the Template's html searching by element ID
 	for (var i = 0; i < dictionary.length; i++) {
 		document.getElementById(dictionary[i].variable).innerHTML = dictionary[i].value;
 	}

	//4. Find each page selector and generate a canvas of that element.
 	var promises = [];
	var elements = document.querySelectorAll("[id^='page-']");
 	for (var numberOfPages = 1; numberOfPages <= elements.length; numberOfPages++) {
 		promises.push(convertDomToCanvas(numberOfPages));
 	}

	//5. When all of the page elements have been turned to canvas generated the PDF.
 	$q.all(promises).then(function () {
 		generatePageOrPdf(pdfFileName, share)
 	});
 }

Explanation

  1. This step is the call to get the local HTML template file which is the image I posted above this section.  After we get the file, we dump its raw response into a variable that we’ll use later
  2. Next we create a new element on the current page’s DOM and we dump the raw response into the innerHTML of the element we just created.  This results in pretty junky HTML on the current page but we don’t care for the PDF.  We’ll delete it when we are done.
  3. After we attached our template html to the DOM we’ll loop through the DOM looking for element ID’s that match the same name as our variable names.  When found we’ll set the innerHTML to the value of the variable. Now our HTML is ready to be converted to a Canvas.
  4. We’ll now find each page element in the DOM and convert the DOM elements to Canvas.  This is a promise so this could take some time depending on the number of elements in the canvase
  5. Finally, once all the promises are finished will finally create our PDF.

Convert DOM To Canvas

function generateCanvas(numberOfPages) {
 	var deferred = $q.defer();
 	html2canvas(document.getElementById("page-" + numberOfPages), {
 		logging: true, allowTaint: true, useCORS: true, onrendered: function (canvas) {
 			var data = canvas.toDataURL();
 			var pageData = {
 				image: data,
 				fit: [510, 761],
 				pageBreak: "after"
 			};
 			docDefinition.content[numberOfPages - 1] = pageData;
 			deferred.resolve();
 			}
 		});
 	return deferred.promise;
 }
  1. This method is pretty small and self explanatory.  We’ll take the DOM element of the page we are trying turn into canvas.  This will be done by HTML2Canvas
  2. Once HTML2Canvas, finishes we’ll take the method property docDefinition, which contains the image data url of the page we just turned into a canvas, and added to the content array.
  3. We are returning a promise here so we can asynchronously generate the canvas for all the pages.

Generate PDF

function generatePageOrPdf(pdfFileName, share) {
 	if (docDefinition.content[docDefinition.content.length - 1].pageBreak) {
 		delete docDefinition.content[docDefinition.content.length - 1].pageBreak;
 	}

	pdfMake.createPdf(docDefinition).getBase64(function (base64) {
 		var pdf = atob(base64);
 		var arr = new Array(pdf.length);
 		for (var i = 0; i < pdf.length; i++) {
 			arr[i] = pdf.charCodeAt(i);
 		}

		var byteArray = new Uint8Array(arr);
 		var blob = new Blob([byteArray], { type: 'application/pdf' });
 		folderpath += "//folder/Path";

		var filename = pdfFileName + ".pdf";
 		//Write file to disk
		//Open File in Browser from Disk
	});
	document.body.removeChild(document.getElementById("pdf-results"));
 }
  1. This method is also pretty small.  First we take the array in our doc definition and if we are on the last page, we need to delete the extra page break.
  2. Then we use the library pdfMake to actually create the PDF.  Here I am getting the base64 encoded string decode it.  Then we loop through all the data into the array and turn it into a byte array and turn into a BLOB that has the mimetype of applicaton/pdf.
  3. Finally we get the folder we want to save the pdf too, name the file and write the file to disk
  4. Then we open the file in a browser.
  5. Finally we delete the extra DOM elements we created earlier to generate the PDF
  6. We’re done.

Gotchas

  1. HTML2Canvas is buggy – It’s the best tool out there that I found but it hasn’t been updated in awhile.  I had issues with some pages DOM elements not displaying in my PDF.  The only fix I found was to add another page and the missing elements would show.
  2. If your Canvas elements are larger than your PDF pages, they won’t even render.
  3. If you don’t properly promise the application, it will take a lot longer to generated the PDF’s and it could generate your pages out of order.

Conclusion

If you haven’t already figured it out, the above code is mostly generalization to give you the idea of how to do it and not the actual code.  Copying it line for line will give you errors.  I plan on generating a later blog post about how to use this and also use D3 and push them to the PDF’s as well.

Thanks for reading!  If you have questions, comments, tips, please leave a comment!

Advertisements

2 comments

    1. Thanks for commenting! Yes, I had to account for several multi-page documents. If you look at the section titled Generate DOM for PDF there is a loop (section 4) that will look for DIV elements with the “page” id in the template HTML. It then converts each of these into separate images. If your “page” element is larger than one of your PDF pages, you’ll have to break it down even further.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s