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 .
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?
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;
}
};
})();