Search code examples
jspdfweb-workerhtml2canvasoffscreen-canvas

Convert HTML to PDF Using OffScreenCanvas in React


OK so the question is self-explanatory.

Currently, I am doing the following

  1. Using html2canvas npm package to convert my html to canvas.
  2. Converting Canvas to image
         canvas.toDataURL("image/png");
  1. Using jsPDF to create a pdf using this image after some manipulations.

So basically all of this process is CPU intensive and leads to unresponsive page.

I'm trying to offload the process to a web worker and I'm unable to postMessage either HTML Node or Canvas Element to my worker thread.

Hence, trying to use OffScreenCanvas but I'm stuck about how to go on with this.


Solution

  • The first step can not be done in a Web-Worker. You need access to the DOM to be able to draw it, and Workers don't have access to the DOM.

    The second step could be done on an OffscreenCanvas, html2canvas does accept a { canvas } parameter that you can also set to an OffscreenCanvas.
    But once you get a context from an OffscreenCanvas you can't transfer it anymore, so you won't be able to pass that OffscreenCanvas to the Worker and you won't win anything from it since everything will still be done on the UI thread.
    So the best we can do is to let html2canvas initialize an HTMLCanvasElement, draw on it and then convert it to an image in a Blob. Blobs can traverse realms without any cost of copying and the toBlob() method can have its compression part be done asynchronously.

    The third step can be done in a Worker since this PR.

    I don't know react so you will have to rewrite it, but here is a bare JS implementation:

    script.js

    const dpr = window.devicePixelRatio;
    const worker = new Worker( "worker.js" );
    worker.onmessage = makeDownloadLink;
    worker.onerror = console.error;
    
    html2canvas( target ).then( canvas => {
      canvas.toBlob( (blob) => worker.postMessage( {
        source: blob,
        width: canvas.width / dpr, // retina?
        height: canvas.height / dpr // retina?
      } ) );
    } );
    

    worker.js

    importScripts( "https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js" );
    onmessage = ({ data }) => {
      const { source, width, height } = data;
      const reader = new FileReaderSync();
      const data_url = reader.readAsDataURL( source );
      const doc = new jspdf.jsPDF( { unit: "px", format: [ width, height ], orientation: width > height ? "l" : "p" });
      doc.addImage( data_url, "PNG", 0, 0, width, height, { compression: "NONE" } );
      postMessage( doc.output( "blob" ) );
    };
    

    And since StackSnippet's over-protected iframes do break html2canvas, here is an outsourced live example.