Making a scrolling display with PDFNetJS

With some additional tools, PDFNetJS can create interactive visual displays of PDFs on the browser. This guide teaches you how to combine PDFNetJS with deck.js to produce a sliding display of a PDF document in your browser. You can see this in action using the WebViewer API here.

For an introduction to some of the concepts that PDFNetJS uses, refer to the getting started guide.

Starting from scratch

  1. Download PDFNetJS full and unzip the package to a designated folder.
  2. Create an empty HTML file and place it in the designated folder (file will be referred to as SampleTest.html).
  3. Create an empty JavaScript file and place it in the designated folder (file will be referred to as SampleTest.js).
  4. Place the PDF document that you would like to display into the designated folder.
This guide uses ES6 JavaScript promises and generators. For more information on promises and generators, refer to these online explanations.

Setting up your HTML document

The following code is used to set up our HTML file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=1024, user-scalable=no">

  <title>Your PDF Presentation</title>

  <!-- Required JS files. -->
  <script src="WebViewer/jquery-3.2.1.min.js"></script>
  <script src="WebViewer/samples/deckjs/core/deck.core.js"></script>
  <script src="WebViewer/lib/html5/ControlUtils.js"></script>
  <script src="WebViewer/lib/html5/CoreControls.js"></script>
  <script src="WebViewer/lib/html5/pdf/PDFNet.js"></script>
  <script src="WebViewer/samples/PDFNet/Setup.js"></script>
  <script src="SampleTest.js"></script>
  <script src="WebViewer/samples/deckjs/viewerPDFNet.js"></script>

  <!-- Required stylesheet -->
  <link rel="stylesheet" href="WebViewer/samples/deckjs/core/deck.core.css">

  <!-- Extension CSS files go here. Remove or add as needed. -->
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/hash/deck.hash.css">
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/navigation/deck.navigation.css">
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/status/deck.status.css">
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/scale/deck.scale.css">
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/goto/deck.goto.css">
  <link rel="stylesheet" href="WebViewer/samples/deckjs/extensions/menu/deck.menu.css">

  <!-- Style theme. More available in /themes/style/ or create your own. -->
  <link rel="stylesheet" href="WebViewer/samples/deckjs/themes/style/web-2.0.css">

  <!-- Transition theme. More available in /themes/transition/ or create your own. -->
  <link rel="stylesheet" href="WebViewer/samples/deckjs/themes/transition/horizontal-slide.css">
  <!--<link rel="stylesheet" href="WebViewer/samples/deckjs/custom.css">-->

  <script src="WebViewer/samples/deckjs/modernizr.custom.js"></script>

</head>
<body class="deck-container">
  <!-- Begin extension snippets. Add or remove as needed. -->
  <!-- deck.navigation snippet, adds previous and next browser buttons -->
  <a href="#" class="deck-prev-link" title="Previous"></a>
  <a href="#" class="deck-next-link" title="Next"></a>

  <!-- deck.status snippet / loading-->
  <p class="deck-status">
    <span class="deck-status-current"></span>
    /
    <span class="deck-status-total"></span>
  </p>

  <!-- Creates Goto bar snippet that can be opened with "G" -->
  <form action="." method="get" class="goto-form">
    <label for="goto-slide">Go to slide:</label>
    <input type="text" name="slidenum" id="goto-slide" list="goto-datalist">
    <datalist id="goto-datalist"></datalist>
    <input type="submit" value="Go">
  </form>

  <!-- deck.hash snippet -->
  <a href="." title="Permalink to this slide" class="deck-permalink">#</a>

  <!-- Extension JS files. Add or remove as needed. -->
  <!-- enables internal links to slides and updates address bar with hash -->
  <script src="WebViewer/samples/deckjs/extensions/hash/deck.hash.js"></script>
  <!-- allows page navigation with the in-browser buttons -->
  <script src="WebViewer/samples/deckjs/extensions/navigation/deck.navigation.js"></script>
   <!-- outputs a (current)/(total) page number status -->
  <script src="WebViewer/samples/deckjs/extensions/status/deck.status.js"></script>
  <!-- scales pdf document to fit the browser window -->
  <script src="WebViewer/samples/deckjs/extensions/scale/deck.scale.js"></script>
  <!-- Adds a form for jumping to any slide number, press "G" to use -->
  <script src="WebViewer/samples/deckjs/extensions/goto/deck.goto.js"></script>
  <!-- creates url #indicator of your current page, press "M" to use -->
  <script src="WebViewer/samples/deckjs/extensions/menu/deck.menu.js"></script>

</body>
</html>

Important scripts:

  • WebViewer/samples/deckjs/core/deck.core.js - adds the deck.js library.
  • WebViewer/samples/deckjs/viewerPDFNet.js - adds a custom script used to integrate PDFNetJS with deck.js.

Setting up your JavaScript document

The following code is used to set up our SampleTest.js JavaScript file:

(function(exports) {
  "use strict";

  // the path to where the PDF worker files are
  exports.CoreControls.setWorkerPath('WebViewer/lib/html5');

  function* initAll(docurl) {
    try {
      // yields promise
      yield exports.PDFNet.initialize();
      PDFNet.beginOperation();
      var doc = yield exports.PDFNet.PDFDoc.createFromURL(docurl);
      doc.initSecurityHandler();
      doc.lock();
      var pagecount = yield doc.getPageCount();
      var pdfdraw = yield exports.PDFNet.PDFDraw.create(100);
      return {
        doc: doc,
        pdfdraw: pdfdraw,
        pagecount: pagecount
      };
    } catch (err) {
      console.log(err.stack);
    }
  }

  exports.loadDocument = function(docurl) {
    var capability = createPromiseCapability();

    // asyncVal is result of yielded promise, sometimes undefined
    function continueIteration(asyncVal) {
      // goes back to the yield, passes it back in case we store, goes to next yield
      var result = iterator.next(asyncVal)
      if (!result.done) { // return
        var promise = result.value;
        // repeat
        promise.then(continueIteration);
      } else {
        // once it's finished...
        if (result.value) {
          capability.resolve(result.value);
        } else {
          capability.reject();
        }
      }
    }
    // create generator obj
    var iterator = initAll(docurl);
    iterator.next().value.then(continueIteration);
    return capability.promise;
  };

  function* renderPage(renderData, pageIndex) {
    try {
      var doc = renderData.doc;
      var pdfdraw = renderData.pdfdraw;

      var currentPage = yield doc.getPage(pageIndex);
      var bitmapInfo = yield pdfdraw.getBitmap(currentPage, exports.PDFNet.PDFDraw.PixelFormat.e_rgba, false);
      var bitmapWidth = bitmapInfo.width;
      var bitmapHeight = bitmapInfo.height;
      var bitmapArray = new Uint8ClampedArray(bitmapInfo.buf);

      var drawingCanvas = document.createElement('canvas');
      drawingCanvas.width = bitmapWidth;
      drawingCanvas.height = bitmapHeight;

      var ctx = drawingCanvas.getContext('2d');
      var imgData = ctx.createImageData(bitmapWidth, bitmapHeight);
      imgData.data.set(bitmapArray);

      ctx.putImageData(imgData, 0, 0);
      return drawingCanvas;

    } catch(err) {
      console.log(err.stack);

    } finally {

    }
  }

  exports.loadCanvasAsync = function(renderData, pageIndex) {
    var capability = createPromiseCapability();
    function continueIteration(asyncVal) {
      var result = iterator.next(asyncVal);
      if (!result.done) {
        var promise = result.value;
        promise.then(continueIteration);
      } else {
        // once it's finished...
        if(result.value) {
          capability.resolve(result.value);
        } else {
          capability.reject();
        }
      }
    }
    var iterator = renderPage(renderData, pageIndex);
    iterator.next().value.then(continueIteration);
    return capability.promise;
  };
})(window);

SampleTest.js contains four functions:

  • initAll(docurl)
  • exports.loadDocument(docurl)
  • renderPage(renderData, pageIndex)
  • exports.loadCanvasAsync(renderData, pageIndex)

Initializing PDFNetJS

The initAll() function carries out a number of tasks required to initialize PDFNetJS. Here we carry out all our basic tasks of initializing a worker, creating a PDF document object and locking the document (for both PNaCl and Emscripten backends).

yield exports.PDFNet.initialize();
PDFNet.beginOperation();
var doc = yield exports.PDFNet.PDFDoc.createFromURL(docurl);
doc.initSecurityHandler();
doc.lock();

In addition to these basic steps required for all scripts using PDFNetJS, for this guide we also need to get the total number of pages in the document and a PDFDraw object. The PDFDraw object will allow the user to manipulate a PDF page as if it were an image and for this guide we have set the PDFDraw object's DPI to 100.

var pagecount = yield doc.getPageCount();
var pdfdraw = yield exports.PDFNet.PDFDraw.create(100);

We return the newly created document, the PDFDraw object, and the total page count as a single object that will be used by other functions such as renderPage().

return { doc: doc, pdfdraw: pdfdraw, pagecount: pagecount };

Handling initAll()'s asynchronicity

The exports.loadDocument() function contains boilerplate to run the initAll() generator and is what allows initAll() to have a synchronous appearance despite being asynchronous. exports.loadDocument() iterates through every asynchronous call in initAll() and either resolves or rejects the return value of initAll() once it has finished running.

Rendering PDF pages

The renderPage(renderData, pageIndex) function creates and returns a canvas containing information for one PDF page. It takes in two parameters, a renderData object which is the same object that initAll() returns, and a pageIndex variable representing the page number of the current page we want to render.

We first retrieve our document and PDFDraw object from renderData. renderData also contains the total number of pages, but this piece of information will not be used in this function.

var doc = renderData.doc;
var pdfdraw = renderData.pdfdraw;

From the document object we extract the page we want to render, and using the PDFDraw object we draw that page. This will create a buffer containing the rendered image data.

We then extract the buffer, width and height of the image. A Uint8ClampedArray view into the buffer is created since the data will soon be copied into an ImageData object (which uses another Uint8ClampedArray object).

var currentPage = yield doc.getPage(pageIndex);
var bitmapInfo = yield pdfdraw.getBitmap(currentPage, exports.PDFNet.PDFDraw.PixelFormat.e_rgba, false);
var bitmapWidth = bitmapInfo.width;
var bitmapHeight = bitmapInfo.height;
var bitmapArray = new Uint8ClampedArray(bitmapInfo.buf);

Now that we have the bitmap dimensions of our page, we create a Canvas object of the same size.

var drawingCanvas = document.createElement('canvas');
drawingCanvas.width = bitmapWidth;
drawingCanvas.height = bitmapHeight;

To insert our bitmap buffer into the canvas, we need to get the canvas' context and use it to create a blank ImageData object with the same dimensions as our bitmap buffer. We then fill in the ImageData object with the Uint8ClampedArray bitmap buffer.

var ctx = drawingCanvas.getContext('2d');
var imgData = ctx.createImageData(bitmapWidth, bitmapHeight);
imgData.data.set(bitmapArray);

Finally, we insert our ImageData object into our canvas' context and we return our canvas which now contains the PDF page.

// replace pixel data of canvas
ctx.putImageData(imgData, 0, 0);
return drawingCanvas;

Handling renderPage()'s asynchronicity

Similar to the exports.loadDocument() function, exports.loadCanvasAsync() contains boilerplate to handle the asynchronous function calls in renderPage() allowing renderPage() to have a synchronous appearance.

Understanding viewerPDFNetJS

viewerPDFNet.js is a JavaScript file located in the "WebViewer/samples/deckjs" folder that contains custom code used to integrate deck.js and PDFNetJS to run this particular guide. For ease of accessibilty, a copy of the code has been added here.

// Code inside viewerPDFNet.js
$(function() {
  var $document = $(document);
  // The total number of pages in the document
  var pageCount = 0;
  // The loading status of every page
  var status = {
    NOT_STARTED: 0,
    QUEUED: 1,
    STARTED: 2,
    FINISHED: 3
  };
  // used to keep track of whether we have loaded the page or not
  var pageStatus = [];
  // used to ensure that all commands will be run in order without conflicts
  var renderQueue = [];

  var queryParams = window.ControlUtils.getQueryStringMap();
  // get the document location from the query string (for example ?d=/files/myfile.xod)
  var docLocation = queryParams.getString('d');

  if (docLocation === null) {
    return;
  }

  var renderData;
  window.loadDocument(docLocation).then(function(renderInfo){
    renderData = renderInfo;
    $document.trigger('documentLoaded');
  });

  $document.on('documentLoaded', function() {
    pageCount = renderData.pagecount;
    for (var i = 0; i < pageCount; i++) {
      addSlide(i);
      pageStatus.push(status.NOT_STARTED);
    }

    // initially load the first five pages
    for (var i = 0; i < Math.min(pageCount, 5); i++) {
      loadCanvas(i);
    }

    // initialize the deck
    $.deck('.slide');
  });

  $document.on('deck.change', function(event, from, to) {
    // load the previous, current and next pages on a page change
    // note that if they are already loaded they won't be loaded again
    loadCanvas(to - 1);
    loadCanvas(to);
    loadCanvas(to + 1);
  });

  function addSlide(pageIndex) {
    var slide = $('<section>').attr('id', 'page' + pageIndex).addClass('slide');
    slide.append($('<div class="loading">'));
    $('body').append(slide);
  }

  function loadCanvas(pageIndex) {
    if (pageIndex < 0 || pageIndex >= pageCount) {
      return;
    }

    var renderNextPage = function() {
      var pageIndex = renderQueue[0];
      pageStatus[pageIndex] = status.STARTED;

      window.loadCanvasAsync(renderData, pageIndex+1).then(function(canvas){
        pageStatus[pageIndex] = status.FINISHED;
        var $canvas = $(canvas);
        $canvas.addClass('canvasPage');

        var pageContainer = $('#page' + pageIndex);
        pageContainer.append($canvas);
        pageContainer.find('.loading').remove();

        // trigger page rescale
        $.deck('enableScale');

        // make sure page is centered for very large page sizes by using a negative margin
        var widthDiff = parseFloat($canvas.css('width')) - pageContainer.find('.deck-slide-scaler').width();
        $canvas.css('margin-left', (-widthDiff / 2) + 'px');
        if(renderQueue.length > 1) {
          setTimeout(function(){
            renderQueue.shift();
            renderNextPage();
          }, 0);
        } else {
          renderQueue.shift();
        }
      });
    }

    if (pageStatus[pageIndex] === status.NOT_STARTED) {
      renderQueue.push(pageIndex);
      pageStatus[pageIndex] = status.QUEUED;
      if(renderQueue.length === 1) {
        renderNextPage();
      }
    }
  }
});

Initializing the sample

To start up our sample, we first call our loadDocument() function described earlier in SampleTest.js

This returns to us an object containing the document, the PDFDraw object, and the number of pages that the document has and we then store this object into renderData.

window.loadDocument is equivalent to exports.loadDocument
var renderData;
window.loadDocument(docLocation).then(function(renderInfo){
  renderData = renderInfo;
  $document.trigger('documentLoaded');
});

Once everything is initialized and the return value stored, we set the document status to "documentLoaded", which triggers our sample to begin setting up the deck.

For every page in the PDF document, we add a new empty slide to our browser and give it the status "NOT_STARTED". To start off, we also begin rendering the first 5 pages using loadCanvas().

$document.on('documentLoaded', function() {
  pageCount = renderData.pagecount;
  for (var i = 0; i < pageCount; i++) {
    addSlide(i);
    pageStatus.push(status.NOT_STARTED);
  }
  for (var i = 0; i < Math.min(pageCount, 5); i++) {
    loadCanvas(i);
  }
  // initializes the deck
  $.deck('.slide');
});

Running loadCanvas()

loadCanvas() checks and queues up all commands to load a new canvas before rendering each page in order.

function loadCanvas(pageIndex) {
  // Check if valid page
  if (pageIndex < 0 || pageIndex >= pageCount) {
    return;
  }

  var renderNextPage = function(){
    var pageIndex = renderQueue[0];
    pageStatus[pageIndex] = status.STARTED;

    window.loadCanvasAsync(renderData, pageIndex+1).then(function(canvas){
      pageStatus[pageIndex] = status.FINISHED;
      var $canvas = $(canvas);
      $canvas.addClass('canvasPage');

      var pageContainer = $('#page' + pageIndex);
      pageContainer.append($canvas);
      pageContainer.find('.loading').remove();

      // trigger page rescale
      $.deck('enableScale');

      // make sure page is centered for very large page sizes by using a negative margin
      var widthDiff = parseFloat($canvas.css('width')) - pageContainer.find('.deck-slide-scaler').width();
      $canvas.css('margin-left', (-widthDiff / 2) + 'px');
      if(renderQueue.length > 1) {
        setTimeout(function(){
          renderQueue.shift();
          renderNextPage();
        }, 0);
      } else {
        renderQueue.shift();
      }
    });
  }

  if (pageStatus[pageIndex] === status.NOT_STARTED) {
    renderQueue.push(pageIndex);
    pageStatus[pageIndex] = status.QUEUED;
    if(renderQueue.length === 1) {
      renderNextPage();
    }
  }
}

In loadCanvas(), we use a queue named renderQueue to keep track of the order that each page should be rendered in. Since createFromBuffer calls are asynchronous and we are only using a single PDFDraw object, there is the possibility that the PDFDraw object used for one command may be affected halfway through the process by a different command.

To avoid potential conflicts between commands, we store page numbers representing not-yet-rendered pages in the RenderQueue array and execute each one in order by calling renderNextPage().

if (pageStatus[pageIndex] === status.NOT_STARTED) {
  renderQueue.push(pageIndex);
  pageStatus[pageIndex] = status.QUEUED;
  if (renderQueue.length === 1) {
    renderNextPage();
  }
}

renderNextPage() is where we process a given page by getting its number from renderQueue, setting its status to "STARTED", and finally running loadCanvasAsync() on the page.

var renderNextPage = function() {
  var pageIndex = renderQueue[0];
  pageStatus[pageIndex] = status.STARTED;
  window.loadCanvasAsync(renderData, pageIndex+1).then(function(canvas) {
    ...
  }
}

pageContainer represents the current page slide where we append our canvas so that it can be displayed. All slides were initially given a "loading" div that is removed once the canvas is appended.

var pageContainer = $('#page' + pageIndex);
pageContainer.append($canvas);
pageContainer.find('.loading').remove();

Finally, we continue processing the remaining pages in renderQueue.

if(renderQueue.length > 1) {
  setTimeout(function() {
    renderQueue.shift();
    renderNextPage();
  }, 0);
} else {
  renderQueue.shift();
}

To see the browser-based sliding display of your PDF document, open up SampleTest.html in your web server and at the end of the address add "#d=YOURPDFNAME.pdf". (for example abc/123/SampleTest.html#d=newsletter.pdf)