How to Build a Cordova PDF Viewer with PDF.js

24 Jul 2020

author
Dustin Riley

In this article (a six-minute read), you’ll learn how to quickly build a PDF viewer with Cordova and PDF.js, a popular open-source PDF viewer.

Here’s what we’re going to build:

The source code for this project is available in our Git repo.

Prerequisites

Step 1 - Install the Cordova CLI

The Cordova CLI makes it a lot easier to scaffold a new project from the Node.js command line. To install it globally, we will open our Node.js terminal and type the following command:

npm install -g cordova

Step 2 - Create the Project

To create our project we will simply run in Node.js these commands:

cordova create pdfjs-viewer express.pdfjs.viewer pdfjsViewer
cd pdfjs-viewer

This command will create our project structure, but Cordova will need a target platform to build to. To see the list of available platforms, you can run this command:

cordova platform ls

For this tutorial, we will be using the browser platform, but you can add another platform or choose a different one. To add the browser platform to your project run the following command:

cordova platform add browser

To check to see if you meet the requirements to build the project for all the platforms you have added (the browser platform doesn’t have any requirements) you can do so with this command:

cordova requirements

Once you have downloaded all the necessary files to pass the requirements, we can build and run the project via the following commands:

cordova build
cordova run browser

Navigating to http://localhost:8000/index.html should show us the default Cordova project:

Step 4 - Integrating React

Next, we are going to integrate React into our Cordova app. Integrating React will allow us to run React natively on any platform you chose in the previous step, meaning you would be able to run your React web app as a native IOS/Android app.

First, we need to create our React app within the pdfjs-viewer directory. To do so, run the following command in the Node.js terminal within the pdfjs-viewer directory.

npx create-react-app pdfjs-viewer-react

This command will create a new folder pdfjs-viewer/pdfjs-viewer-react within our project, inside of whice is our React app.

Now in the new React folder find the pdfjs-viewer-react/src/index.js file and replace it contents with the following:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
const startApp = () => {
  ReactDOM.render(<App />, document.getElementById("root"));
};

if (!window.cordova) {
  startApp();
} else {
  document.addEventListener("deviceready", startApp, false);
}

In the pdfjs-viewer-react/package.json file, above the dependencies add the following line:

"homepage": "../www",

In the same file within the scripts: {} object replace the line that has build as it's key with:

"build": "react-scripts build && robocopy .\\build ..\\www /MIR",

replacing robocopy with cp-r if you are working with a Mac/Linux enviroment.

To run the project run the following set of commands

cd pdfjs-viewer-react
npm run build
cd ../
cordova build
cordova run browser

This will build the React app copy that build into the pdfjs-viewer/www folder, then we build the Cordova project and run it with the browser platform. If the builds worked correctly, when you navigate to http://localhost:8000/ you should see:

Step 5 - Implementing PDF.js

We will now integrate the open-source PDF.js library into our project to render a PDF inside our app. We will start by downloading the latest stable release from GitHub and then extracting the contents into a new pdfjs-viewer/pdfjs-viewer-react/public/lib folder. Make sure this is the public folder within the pdfjs-viewer-react directory not pdfjs-viewer.

We will also need a PDF file to view, which we will place in the web folder. You can use your own or download one from here.

The new file structure in our project folder will look like the following:

pdfjs-viewer-react
├── public
│    ├── lib
│    │   ├── ui
│    │   │   ├── ...
│    │   ├── core
│    │   │   ├── ...
│    │   └── webviewer.min.js
│    ├── favicon.ico
│    ├── index.html
│    ├── logo192.png
│    ├── logo512.png
│    ├── manifest.json
│    ├── robots.text
│    └── ...
└── ...

Step 6 - Creating the viewer

Next we will create a PDF.js viewer component within our React project pdfjs-viewer-react. To start, we will create a new folder pdfjs-viewer-react/src/components in this new folder we will create a new file called PdfjsViewer.js. Within this file, we will add the following code:

import React from 'react'

const PdfJSViewer = ({ height, width, url }) => {
    const path = 'lib/web/viewer.html'
    let pdfPath = url ? path + '?file=' + url : path;
    return (
        <div style={{ width, height }}>
            {
                <iframe
                    title={pdfPath}
                    width='100%'
                    height='100%'
                    src={pdfPath}
                >
                </iframe>
            }
        </div>
    )
}
export default PdfJSViewer

Also within the existing file pdfjs-viewer-react\src\App.js we will replace all the existing code with:

import React from 'react';
import PdfJsViewer from './components/PdfJsViewer'
import './App.css';

function App() {
  return (
    <div  className="App">
      <PdfJsViewer height='100vh' />
    </div>
  );
}

export default App;

Now if we run our set of build commands again:

cd pdfjs-viewer-react
npm run build
cd ../
cordova build
cordova run browser

We should see the default PDF.js viewer running at http://localhost:8000/index.html.

Step 7 - Customizing the PDF.js Toolbar

As a final step, we will reorganize the toolbar by moving elements around, removing buttons, and changing the icons.

Let’s open public/lib/web/viewer.html and add the following to the <head> section:

<script src="customToolbar.js"></script>

Next, we’ll create customToolbar.js inside the public/lib/web folder and add the following code:

let sheet = (function() {
  let style = document.createElement("style");
  style.appendChild(document.createTextNode(""));
  document.head.appendChild(style);
  return style.sheet;
 })();
 function editToolBar(){
  //when the page is resized, the viewer hides and move some buttons around.
  //this function forcibly show all buttons so none of them disappear or re-appear on page resize
  removeGrowRules();

  /* Reorganizing the UI */
  // the 'addElemFromSecondaryToPrimary' function moves items from the secondary nav into the primary nav
  there are 3 primary nav regions (toolbarViewerLeft, toolbarViewerMiddle, toolbarViewerRight)

  //adding elements to left part of toolbar
  addElemFromSecondaryToPrimary('pageRotateCcw', 'toolbarViewerLeft')
  addElemFromSecondaryToPrimary('pageRotateCw', 'toolbarViewerLeft')
  addElemFromSecondaryToPrimary('zoomIn', 'toolbarViewerLeft')
  addElemFromSecondaryToPrimary('zoomOut', 'toolbarViewerLeft')

  //adding elements to middle part of toolbar
  addElemFromSecondaryToPrimary('previous', 'toolbarViewerMiddle')
  addElemFromSecondaryToPrimary('pageNumber', 'toolbarViewerMiddle')
  addElemFromSecondaryToPrimary('numPages', 'toolbarViewerMiddle')
  addElemFromSecondaryToPrimary('next', 'toolbarViewerMiddle')

  //adding elements to right part of toolbar
  addElemFromSecondaryToPrimary('secondaryOpenFile', 'toolbarViewerRight')

  /* Changing icons */
  changeIcon('previous', 'icons/baseline-navigate_before-24px.svg')
  changeIcon('next', 'icons/baseline-navigate_next-24px.svg')
  changeIcon('pageRotateCcw', 'icons/baseline-rotate_left-24px.svg')
  changeIcon('pageRotateCw', 'icons/baseline-rotate_right-24px.svg')
  changeIcon('viewFind', 'icons/baseline-search-24px.svg');
  changeIcon('zoomOut', 'icons/baseline-zoom_out-24px.svg')
  changeIcon('zoomIn', 'icons/baseline-zoom_in-24px.svg')
  changeIcon('sidebarToggle', 'icons/baseline-toc-24px.svg')
  changeIcon('secondaryOpenFile', './icons/baseline-open_in_browser-24px.svg')

  /* Hiding elements */
  removeElement('secondaryToolbarToggle')
  removeElement('scaleSelectContainer')
  removeElement('presentationMode')
  removeElement('openFile')
  removeElement('print')
  removeElement('download')
  removeElement('viewBookmark')
 }
 function changeIcon(elemID, iconUrl){
  let element = document.getElementById(elemID);
  let classNames = element.className;
  classNames = elemID.includes('Toggle')? 'toolbarButton#'+elemID :
 classNames.split(' ').join('.');
  classNames = elemID.includes('view')? '#'+elemID+'.toolbarButton' : '.'+classNames
  classNames+= "::before";
  addCSSRule(sheet, classNames, `content: url(${iconUrl}) !important`, 0)
 }
 function addElemFromSecondaryToPrimary(elemID, parentID){
  let element = document.getElementById(elemID);
  let parent = document.getElementById(parentID);
  element.style.minWidth = "0px";
  element.innerHTML =''
  parent.append(element);
 }
 function removeElement(elemID){
  let element = document.getElementById(elemID);
  element.parentNode.removeChild(element);
 }
 function removeGrowRules(){
  addCSSRule(sheet, '.hiddenSmallView *', 'display:block !important');
  addCSSRule(sheet, '.hiddenMediumView', 'display:block !important');
  addCSSRule(sheet, '.hiddenLargeView', 'display:block !important');
  addCSSRule(sheet, '.visibleSmallView', 'display:block !important');
  addCSSRule(sheet, '.visibleMediumView', 'display:block !important');
  addCSSRule(sheet, '.visibleLargeView', 'display:block !important');
 }
 function addCSSRule(sheet, selector, rules, index) {
  if("insertRule" in sheet) {
  sheet.insertRule(selector + "{" + rules + "}", index);
  }
  else if("addRule" in sheet) {
  sheet.addRule(selector, rules, index);
  }
 }
 window.onload = editToolBar

The PDF.js primary toolbar is broken down into 3 regions:

Screenshot: PDF.js Viewer Primary Toolbar Regions

The secondary toolbar is accessed via the chevron icon in the right region:

Screenshot: PDF.js Viewer Secondary Toolbar

We can move elements from the secondary toolbar into the left, middle, or right regions of the primary toolbar with the addElemFromSecondaryToPrimary function in customToolbar.js. For example, uncommenting this line will move the counter-clockwise rotation tool to the left region of the primary toolbar:

  addElemFromSecondaryToPrimary('pageRotateCcw', 'toolbarViewerLeft')

If you wanted to move pageRotateCcw to the middle region instead, you’d replace toolbarViewerLeft with toolbarViewerMiddle, or toolbarViewerRight for the right region. To move a different tool, replace the pageRotateCcw ID with the element ID you want to move. (See below for a full list of element IDs.)

We can also hide elements like this:

  removeElement('print')
  removeElement('download')

To hide different elements, replace print or download with the element ID.

NOTE: Hiding the download and print buttons is not a bulletproof way to protect our PDF, because it’s still possible to look at the source code to find the file. It just makes it a bit harder.

We can also customize the icons for various tools by swapping out the SVG file, like this:

changeIcon('previous', 'icons/baseline-navigate_before-24px.svg')

In the above example, previous is the element ID, while icons/baseline-navigate_before-24px.svg is the path to the tool icon.

And that’s it!

Element ID Reference for PDF.js User Interface Customization

Here’s handy reference with the IDs of the various toolbar icons:

Toolbar Icon ID
PDF Viewer UI Icon: sidebarToggle sidebarToggle
PDF Viewer UI Icon: viewFind viewFind
PDF Viewer UI Icon: pageNumber pageNumber
PDF Viewer UI Icon: numPages numPages
PDF Viewer UI Icon: zoomOut zoomOut
PDF Viewer UI Icon: zoomIn zoomIn
PDF Viewer UI Icon: next page next
PDF Viewer UI Icon: previous page previous
PDF Viewer UI Icon: presentationMode presentationMode
PDF Viewer UI Icon: openFile openFile
PDF Viewer UI Icon: print print
PDF Viewer UI Icon: download download
PDF Viewer UI Icon: viewBookmark viewBookmark
PDF Viewer UI Icon: secondaryToolbarToggle secondaryToolbarToggle
PDF Viewer UI Icon: scaleSelectContainer scaleSelectContainer
PDF Viewer UI Icon: secondaryPresentationMode secondaryPresentationMode
PDF Viewer UI Icon: secondaryOpenFile secondaryOpenFile
PDF Viewer UI Icon: secondaryPrint secondaryPrint
PDF Viewer UI Icon: secondaryDownload secondaryDownload
PDF Viewer UI Icon: secondaryViewBookmark secondaryViewBookmark
PDF Viewer UI Icon: firstPage firstPage
PDF Viewer UI Icon: lastPage lastPage
PDF Viewer UI Icon: pageRotateCw pageRotateCw
PDF Viewer UI Icon: pageRotateCcw pageRotateCcw
PDF Viewer UI Icon: cursorSelectTool cursorSelectTool
PDF Viewer UI Icon: cursorHandTool cursorHandTool
PDF Viewer UI Icon: scrollVertical scrollVertical
PDF Viewer UI Icon: scrollHorizontal scrollHorizontal
PDF Viewer UI Icon: scrollWrapped scrollWrapped
PDF Viewer UI Icon: spreadNone spreadNone
PDF Viewer UI Icon: spreadOdd spreadOdd
PDF Viewer UI Icon: spreadEven spreadEven
PDF Viewer UI Icon: documentProperties documentProperties

Conclusion

As you can see, rendering a PDF with Cordova isn't difficult using open-source libraries.

Building a PDF Viewer with Cordova is relatively straightforward, but once you want to start annotating, signing, or filling forms, you would have to implement these things yourself. See our PDF.js Build vs Buy and Guide to Evaluating PDF.js to learn more.

That’s where PDF.js Express comes in. It’s a commercial PDF.js viewer that wraps a React-based UI around the open-source PDF.js rendering engine and offers out-of-the-box features like annotations, form filling and e-signatures. It’s fully compatible with the Cordova framework -- check out the demo, and let us know what you think!

If you need high-fidelity rendering, increased reliability, and faster performance, you could consider PDFTron WebViewer. It’s a JavaScript PDF library that integrates with the Cordova framework and offers hundreds of features, like redaction, editing, page manipulation, real-time document collaboration, digital signatures, and much more. Check out the WebViewer demo.

If you have any questions about implementing PDF.js Express in your project, please contact us and, we will be happy to help!