Wednesday, July 6, 2016

Creating a PDF on the fly with Ionic/Cordova

So you've got an Ionic app and you need to create PDFs from within the app. You don't want to offload to a server for PDF generation/download because the PDF is user specific and unique each time it is generated, so why not just generated it client side? For one, that was near impossible a number of years ago. Two, you've tried before and it just never worked right. Well, I managed to get PDF generation in an Ionic app and wanted to share my experience so you can do the same.

What we are going to do is capture a portion of the HTML, turn it into a PDF, and trigger the download for the user.

The first step is to add the required libraries:
Cordova Plugins:
"cordova-plugin-file-transfer"
"cordova-plugin-file-opener2"

index.html (install via bower or point to CDN):
<script src="lib/jspdf/dist/jspdf.min.js"></script>
<script src="lib/html2canvas/dist/html2canvas.js"></script>

Next, we create the PDF generation function:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$scope.generatePDF = function (method) {

       //set the margins for the PDF
 var margins = {
  top: 40,
  left: 40
 };

        // function to create a hidden clone of the target element,
        // allowing manipulation specific for the PDF output without changing the actual target element.
 var hiddenClone = function (element) {
  document.documentElement.style.overflow = 'visible';
  document.body.style.overflow = 'visible';

  // Create clone of element
  var clone = element.cloneNode(true);

  // Position element relatively within the
  // body but still out of the viewport
  var style = clone.style;
  style.position = 'relative';
  style.top = window.innerHeight + 'px';
  style.left = 0;
  style.width = '535px';
  style.zIndex = '9999';

  // Append clone to body and return the clone
  document.body.appendChild(clone);
  return clone;
 }

        // now create the clone of your target element and send it to html2canvas
 var html2Pdf = hiddenClone(document.getElementById('pdfTarget'));
 html2canvas(html2Pdf, {
  onrendered: function (canvas) {
                        // once the canvas is rendered, create new jsPDF object and working variables
   var doc = new jsPDF('p', 'pt', 'letter');
   var dataUrl = canvas.toDataURL('image/jpeg');

   doc.addImage(dataUrl, margins.top, margins.left, canvas.width, canvas.height);
   var dataUrlPdf = doc.output();
   var dataUrlPdfBase64 = 'data:application/pdf;base64,' + btoa(dataUrlPdf);

                        // once we've got the canvas, we can remove the clone element and reset styles
   document.body.removeChild(html2Pdf);
   document.documentElement.style.overflow = 'hidden';
   document.body.style.overflow = 'hidden';

   var filename = 'PDF-' + $filter('date')(new Date(), 'yyyy-M-d') + '.pdf';

   // Save location
   var targetPath = cordova.file.externalRootDirectory + filename;

                        // use the cordova file transfer plugin to send the file to the cordova file opener plugin,
                        // allowing user to pick download method (only seems to work on Android)
   $cordovaFileTransfer.download(dataUrlPdfBase64, targetPath, {}, true).then(function (fileEntry) {
    console.log('Success');
    console.log(fileEntry);
    fileEntry.file(function (file) {
     cordova.plugins.fileOpener2.open(
      file.localURL,
      file.type, {
       error: function (e) {
        console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
       },
       success: function () {
        console.log('file opened successfully');
       }
      }
     );
    });
   }, function (error) {
    console.log('Error');
    console.log(error);
   }, function (progress) {
    // PROGRESS HANDLING GOES HERE
   });
  }
 });
};

Now you can run the function and try it out...
Hopefully you'll have successful client side PDF generation from HTML. I will say these plugins/libraries are touchy, especially when used together but this formula worked for me. Best of luck and feel free to share your implementations and feedback!