4

I successfully created a PDF using a JavaScript plug-in (pdfmake) and it was great. But when I try to render an ~8,000-row inventory/ledger printout, it freeze for over a minute.

This is how I usually declare my docDefinition

var docDefinition = { 
pageOrientation: orientation, 
footer: function(currentPage, pageCount) { return {text: currentPage.toString() + ' / ' + pageCount, fontSize:8, alignment:'center'}; }, 
content:[ 
   printHeader, 
  { fontSize: 8, alignment: 'right', style: 'tableExample', 
   table: { 
       widths: width, 
       headerRows: 1, body: arr }, 
   layout: 'lightHorizontalLines' }] }

where

var printHeader =   [ { text: 'COMPANY NAME',alignment:'center' },
{ text: 'Address 1',alignment:'center' },
{ text: 'Address 2',alignment:'center' },
{ text: 'Additional Details,alignment:'center' },
{ text: 'document title',alignment:'center' }];

and

 var arr = [[{"text":"","alignment":"left"},"text":"Date","alignment":"left"},
{"text":"Trans #","alignment":"left"},{"text":"Description","alignment":"left"},
{"text":"Ref #","alignment":"left"},{"text":"Debit","alignment":"left"},
{"text":"Credit","alignment":"left"},{"text":"Amount","alignment":"left"},
{"text":"Balance","alignment":"left"}],[{"text":"ACCOUNT : Merchandise Inventory","alignment":"left","colSpan":8},"","","","","","","",
{"text":"1,646,101.06"}],["","10/13/2015","ST#0094",{"text":"","alignment":"left"},{"text":"","alignment":"left"},"546.94","0.00","546.94","1,646,648.00"],[{"text":"Total","alignment":"left","bold":true},"","","","",
{"text":"546.94","alignment":"right","bold":true},
{"text":"0.00","alignment":"right","bold":true},
{"text":"","alignment":"right","bold":true},
{"text":"546.94","alignment":"right","bold":true}],[{"text":"ACCOUNT : Accounts Payable-Main","alignment":"left","colSpan":8},"","","","","","","",
{"text":"-1,741,953.62"}],["","10/13/2015","ST#0094",
{"text":"","alignment":"left"},
{"text":"","alignment":"left"},"0.00","546.94","-546.94","-1,742,500.56"],
[{"text":"Total","alignment":"left","bold":true},"","","","",
{"text":"0.00","alignment":"right","bold":true},
{"text":"546.94","alignment":"right","bold":true},
{"text":"","alignment":"right","bold":true},
{"text":"-546.94","alignment":"right","bold":true}]

generated file.

I searched about web workers and see that it can solve this UI freezing problem. So I tried to create a web worker for it:

$('#makepdf').click(function(){
    var worker = new Worker("<?php echo URL::to('/'); ?>/js/worker.js");
  worker.addEventListener('message',function(e){
  console.log('Worker said: ',e.data);
},false);

worker.postMessage(docDefinition);

//worker.js

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);

Output from console.log():

Worker said: Object {pageOrientation: "portrait", content: Array[7]} its logging correctly the json structure.

So far so good. But after I added pdfmake.min.js and vfs_font.js to the worker, I get the error Uncaught TypeError: Cannot read property 'createElementNS' of undefined.

I get the error before I even started using the worker.

Is it possible to implement web workers with the pdfmake plug-in?

melvnberd
  • 3,093
  • 6
  • 32
  • 69
  • looks like that tool needs the DOM/document, which workers don't have. you MIGHT be able to use a virtual dom lib for node that you browserify, that could be cool... – dandavis Oct 17 '15 at 03:26
  • Thank you very much for the attention sir.. is there any chance that you can hint me some links on where to start.. it would really help me alot.. thanks again – melvnberd Oct 18 '15 at 10:07

1 Answers1

9

Simple answer

Just provide a dummy constructor:

var document = { 'createElementNS': function(){ return {} } };
var window = this;
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );

Alternatively, if you think it is too dirty, import XML-JS (only 60k) and create a virtual document for pdfmake.

importScripts( 'tinyxmlw3cdom.js' );
var window = this;
var document = new DOMDocument( new DOMImplementation() );
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );

Explanation

pdfmake is known to be incompatible with worker.

By itself, pdfmake does not use createElementNS. However its minified script pdfmake.min.js do, apparently to create a download link.

We don't need download link anyway, so just give it a dummy to keep it happy (for now).

If in the future it needs a real DOM, bad news is document is unavailable in web worker. Good news is we have a pure javascript implementation. Download XML-JS, extract and find tinyxmlw3cdom.js, import it and you can create a functional document.

In addition to the document, since vfs_fonts.js get pdfmake through the window variable, we need to introduce window as an alias for global.

For me, these steps make pdfmake works in web worker.

To save the file, you need to convert the base64 data provided by pdfmake into a binary download. A number of scripts are available, such as download.js. In the code below I am using FileSaver.


Code

My worker is a Builder that can be cached. If you compose the worker on server side, you can get rid of the stack and build functions and directly call pdfmake, making both js code much simpler.

Main HTML:

<script src='FileSaver.min.js'></script>
<script>
function base64ToBlob( base64, type ) {
    var bytes = atob( base64 ), len = bytes.length;
    var buffer = new ArrayBuffer( len ), view = new Uint8Array( buffer );
    for ( var i=0 ; i < len ; i++ )
      view[i] = bytes.charCodeAt(i) & 0xff;
    return new Blob( [ buffer ], { type: type } );
}
//////////////////////////////////////////////////////////

var pdfworker = new Worker( 'worker.js' );

pdfworker.onmessage = function( evt ) {
   // open( 'data:application/pdf;base64,' + evt.data.base64 ); // Popup PDF
   saveAs( base64ToBlob( evt.data.base64, 'application/pdf' ), 'General Ledger.pdf' );
};

function pdf( action, data ) {
   pdfworker.postMessage( { action: action, data: data } );
}

pdf( 'add', 'Hello WebWorker' );
pdf( 'add_table', { headerRows: 1 } );
pdf( 'add', [ 'First', 'Second', 'Third', 'The last one' ] );
pdf( 'add', [ { text: 'Bold value', bold: true }, 'Val 2', 'Val 3', 'Val 4' ] );
pdf( 'close_table' );
pdf( 'add', { text: 'This paragraph will have a bigger font', fontSize: 15 } );
pdf( 'gen_pdf' ); // Triggers onmessage when it is done

// Alternative, one-size-fit-all usage
pdf( 'set', { pageOrientation: 'landscape', footer: { text: 'copyright 2015', fontSize: 8, alignment:'center'}, content:[ "header", { fontSize: 8, alignment: 'right', table: { headerRows: 1, body: [[1,2,3],[4,5,6]] } }] } );
pdf( 'gen_pdf' );

</script>

Worker:

//importScripts( 'tinyxmlw3cdom.js' );
//var document = new DOMDocument( new DOMImplementation() );
var document = { 'createElementNS': function(){ return {} } };
var window = this;
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );

(function() { 'use strict';

var doc, current, context_stack;

function set ( data ) {
   doc = data;
   if ( ! doc.content ) doc.content = [];
   current = doc.content;
   context_stack = [ current ];
}
set( {} );

function add ( data ) {
   current.push( data );
}

function add_table ( template ) {
   if ( ! template ) template = {};
   if ( ! template.table ) template = { table: template };
   if ( ! template.table.body ) template.table.body = [];
   current.push( template ); // Append table
   push( template.table.body ); // Switch context to table body
}

function push ( data ) {
   context_stack.push( current );
   return current = data;
}

function pop () {
   if ( context_stack.length <= 1 ) return console.warn( "Cannot close pdf root" );
   context_stack.length -= 1;
   return current = context_stack[ context_stack.length-1 ];
}

function gen_pdf() {
   pdfMake.createPdf( doc ).getBase64( function( base64 ) {
      postMessage( { action: 'gen_pdf', base64: base64 } );
   } );
}

onmessage = function( evt ) {
   var action = evt.data.action, data = evt.data.data;
   switch ( action ) {
      case 'set': set( data ); break;
      case 'add': add( data ); break;
      case 'add_table'  : add_table( data ); break;
      case 'close_table': pop(); break;
      case 'gen_pdf': gen_pdf(); break;
   }
};

})();
Community
  • 1
  • 1
Sheepy
  • 17,324
  • 4
  • 45
  • 69
  • Thank you very much for your help sir @Sheepy..! your code works really.. now I am just adjusting it to fit on my layout.. hmm is there away to send the docDefinition right away instead of the manual array? and also the open function (gen_pdf) doesnt work when dealing with large table, I think its the browser's problem so It would be much better if its a download functionality instead. Thank you very much for the help sir! this would solve future problems on my project.\ – melvnberd Oct 19 '15 at 12:58
  • I tried replacing `open( 'data:application/pdf;base64,' + evt.data.base64 );` to `download('General Ledger From : to .pdf');` but it gives me an 'download' not defined error. – melvnberd Oct 19 '15 at 13:24
  • 1
    @melvnberd Currently browsers do not have a consistent download API, so I'd recommend 3rd party libraries. There are a few, and I updated the answer's explanation and code with one that is relatively cross platform. – Sheepy Oct 19 '15 at 15:49
  • Thank you very much sir @sheepy for really assisting me with this one.. I dont have any experience using any webworkers really.. but in this case I really need it specially I am generating 10mb> files.. your answer can also help alot of people :) because documentation on web workers is verylimited for makePDF – melvnberd Oct 19 '15 at 16:03
  • Hello again sir @sheepy.. now I can already print the data into my pdf. im doing it by `pdf( 'add_table', { headerRows: 1, widths: width, layout: 'lightHorizontalLines' } ); arr.map(function(obj){ pdf( 'add', obj ); }); pdf( 'close_table' );` but there is one thing i cant do. its adding property dynamically. I tried `pdf( 'add',fontSize = 8);` but it has no effect, I also tried pdf('add',{fontSize:8});` but it gives me `Uncaught Unrecognized document structure: {"fontSize":8,"_margin":null}` error – melvnberd Oct 20 '15 at 02:27
  • @melvnberd You always specify style alongside the content do you not? ex. `pdf( 'add', { text: 'Bigger font', fontSize: 15 } );` You may defer adding content until you know its properties. – Sheepy Oct 20 '15 at 03:13
  • I usually add a general property in a the table so that I wouldnt have to add a property everytime.. like this one `var docDefinition = { pageOrientation: orientation, footer: function(currentPage, pageCount) { return {text: currentPage.toString() + ' / ' + pageCount, fontSize:8, alignment:'center'}; }, content:[ printHeader, { fontSize: 8, alignment: 'right', style: 'tableExample', table: { widths: width, headerRows: 1, body: arr }, layout: 'lightHorizontalLines' }] }; ` – melvnberd Oct 20 '15 at 04:54
  • so this basically lets you set a general property on the content then lets you add a specific property to over-ride it if necessary.. the above code was my original code before I am trying to convert it into a web worker. – melvnberd Oct 20 '15 at 04:58
  • 1
    @melvnberd Ah. You see, functions cannot be passed to webworker. Workarounds are too advanced and is likely out of scope because of its complexity. I have done my best by adding a set command - it allows you to define the whole pdf, as long as they are all clonable objects. (object, array, number, string etc. search "structured clone algorithm" for details.) – Sheepy Oct 20 '15 at 07:09
  • ahh i see.. thank you very much sir @sheepy.. you already helped me way more than necessary .. its time for me to do my part by researching more deeply in this subject .. :) your help is really appreciated..Have a nice day sir..! – melvnberd Oct 20 '15 at 07:26
  • How can I apply this solution with Angular, using require js? – Gabriel Jun 07 '16 at 20:11