Converting DOM elements to PDF using JSPDF and HTML2Canvas

The Challenge: Generate a PDF from an array of image links returned from an API endpoint

An unforseen challenge(and how I found my way through it): CORS(Cross-Origin Resource Sharing), used a proxy server

I was given a challenge to work on which involved generating a PDF from an array of images returned from an API endpoint. This seemed like an easy enough task once I found the package JSPDF. I linked to the package using the CDN link provided from the docs and ran a quick test using the example function provided.

var doc = new jsPDF()
doc.text('Hello world!', 10, 10)
doc.save('a4.pdf')`

Sure enough, this works, but I don’t want to export text to PDF, I want to export an image to PDF, so how do I do that? I could not find any official documentation on this function, but through a combination of Google and Stack Overflow I discovered that I could simply add an image to the variable doc which represents my PDF like so:

doc.addImage(imgData, 'png', 10, 10)

Where do I get this imgData from? What does imgData represent? imgData is a screenshot of a DOM element generated by a package called HTML2Canvas. HTML2Canvas takes a screenshot of the element I want to convert to PDF and returns a data URI which represents the image in the format specified (here we have specified png). Below is a function I created called savePDF that uses both HTML2Canvas and JSPDF to save a PDF of a specified DOM element.

function savePDF(){
  document.getElementById('generatePDF').innerText = 'Saving PDF...'
  html2canvas(document.getElementById("newCanvas"), {
    onrendered: function(canvas) {
      var imgData = canvas.toDataURL(
        'image/png');
      var doc = new jsPDF('p', 'mm');
      doc.addImage(imgData, 'png', 10, 10);
      doc.save(`${mapName}.pdf`);
      //Change text of button back after PDF has been saved
      document.getElementById('generatePDF').innerText = 'Generate PDF';
    }
  });
}

This function works if the element is already on the page, but we are working with images that are returned from an API endpoint, so how do we create a DOM element with these images in it that can be manipulated by HTML2Canvas and JSPDF? The answer I found was to draw these images onto a canvas. The reasoning behind this decision was that I had to encode the images into base 64 in order to bypass a CORS restriction from the HTML2Canvas package. HTMl2Canvas will not export “tainted canvases” which means those loaded from a different domain, so the way I found to bypass this was to use a proxy that encodes the images in base64 and then returns the data URI for these images to draw onto the canvas. I also had to use a proxy to bypass a CORS restriction on the server where I was retrieving my images from, which would not allow me to make a request to their API from my client-side code. The creator of HTML2Canvas has created a npm package for using a proxy with HTML2Canvas that I used as boilerplate for my proxy server. While this was a great starting point for what I needed, I had to make some changes in order to get the proxy to do what I needed. The original proxy was just passing the query string that was given from the request body like so:

request({url: req.query.url, encoding: 'binary'}

After some investigation on why this was not working with the images I was passing to it from my API, I discovered that for some reason the authentication credentials required to access these images from the server were getting cut off from the URL, so I had to concatenate them back onto the URL like this:

let url = '';
   url += req.query.url;
   url += `&Signature=${req.query.Signature}`;
   url += `&Key-Pair-Id=${req.query["Key-Pair-Id"]}`;
   request({url: url, encoding: 'binary'};

I pushed this code up to a GitHub repo I had forked from the original and then created a Heroku app to host this proxy server. Now that I have the ability to use this proxy to draw cross origin images onto a canvas, it’s time to write the function that can actually do this for me.

//Draws an item on the canvas
function drawCanvas(data, x, y){
  let newCanvas = document.getElementById("newCanvas");
  let ctx = newCanvas.getContext("2d");
  let img = new Image();
  //Base 64 data from call to proxy server
  img.src=data;
  img.onload = function(){
    ctx.drawImage(img, x, y, 320, 320);
    //once last image has been drawn, call generate PDF function
    if (y >= 300) savePDF();
  }
}

Here is my function drawCanvas that takes in three parameters, a data parameter that represents the data URI returned from the proxy, and X and Y coordinates to represent where on the canvas we want to draw this image. This function gets a reference to a canvas that has already been drawn on the page in our index.html file, and then gets the context of that canvas so we can draw on it. A new image is created and we set the source of that image to be the data argument passed into drawCanvas. Once this image is loaded, we draw it onto the canvas with our X and Y coordinates and the height and width we want. Finally we have an arbitrary conditional that when our final Y coordinate is reached, we can save the PDF. This conditional prevents the PDF from being saved before all the images have been drawn. In this instance I only have two images to draw, but with more images the conditional statement should be adjusted accordingly.

The function that calls drawCanvas is called generatePDF

//Loops through images array, gets base 64 string of image from proxy server, and draws to canvas
function generatePDF() {
  //Change text of button to show user PDF is being generated
  document.getElementById('generatePDF').innerText = 'Generating PDF...'
  let x = 0;
  let y = 0;
  // Loop through array of images returned from API request, for each image send AJAX get request to my proxy server
  images.forEach(function(element, index){
    $.get( `https://pure-gorge-83413.herokuapp.com/?url=${element}`, function( data ) {
      drawCanvas(data, x, y)
      y += 300;
    });
  });
};

This function defines our X and Y coordinates as starting at 0 and then iterates through the array of image links returned from our API endpoint and makes an AJAX get request for each image link to the proxy server with the link of our image as the URL. We then call the drawCanvas function and pass it the data returned from this AJAX request and the current values of our X and Y coordinates. After that, we increase the Y coordinate by 300, so that there is room for the next image. These two particular images were meant to stack on top of one another, however this example is arbitrary and with different image layouts you would need to adjust the amount you increase Y (and X) by accordingly.

Written on January 10, 2018