How to Build a PDF.js Viewer in React + Typescript

26 Feb 2020

author
Logan Bittner

PDF is a well- known, universal document format used by many industries for reports, contracts, blueprints, and much more. If you have ever dealt with any documents, you've probably dealt with PDFs. That is why if you are developing any sort of software that deals with documents (even just displaying them), it is crucial to support PDF.

Luckily, PDF.js provides a great library for rendering PDFs inside your web application. PDF.js is an open-source JavaScript library that lets users view PDFs directly in a browser with no server dependencies or user plugins required. Since launching in 2011, PDF.js has been deployed in a variety of software applications (ex. Dropbox & Slack) and has an active community with responsive contributors.

To get you started integrating a PDF.js viewer in your application, we’ve put together this blog post. We’ll first walk you through setting up a simple project and then help you add a PDF Viewer using PDF.js, React, and Typescript.

This will be part 1 of a series of posts that will cover how to add features to our viewer, including zooming, rotating, and more.

If you want to get the source code first and then follow along, download it here.

Project Setup

We'll initialize our project by running npm init inside an empty directory. This command will create a package.json for you (after anwsering a few questions).

Once you’ve initialized your package.json file, we'll need to install our dependencies.

We'll be using Parcel and Typscript for development dependencies. We'll also need to install the Typescript types for React. Install those by running:

npm i typescript parcel@next @types/react -D

Our front end framework will be React. Install that by running npm i react react-dom --save

Now we'll add a couple scripts to our package.json to help us get up and running. Add the following code to your package.json:

{
  "scripts": {
    "start": "parcel src/index.html"
  }
}

We also need to configure Typescript to compile react. Lets add tsconfig.json and add the following:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "jsx": "react",
    "target": "es5",
    "lib": ["ES6", "DOM"]
  }
}

App Structure

All of our source code will live in a src folder, and the entry point to our app will be src/index.html (hence the script above). Add src/index.html and add the following code to it:

<!-- src/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf.js'></script>
  </head>
  <body>
    <div id='app'></div>
    <script src='./app.tsx'></script>
  </body>
</html>

The #app div will hold our entire app. You'll also notice we are importing app.tsx. This will be our Javascript entry point.

We're also importing PDF.js from a CDN.

Since PDF.js does not have Typescript support, we need to tell the Typescript compiler that we want to use the any type for all PDF.js functions. We can do this by adding a file called ./src/global.ts and adding the following:

declare global {

  interface Window {
    pdfjsLib: any;
  }

}

export { };

Make a file called src/app.tsx and add the following code:

// src/app.tsx
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <div>
      Hello world!
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('app'));

This will be our React entry point and will also use react-dom to render our app to the DOM. For now we will just add some placeholder text here.

After all these setup steps, our project should look something like this:

If you run npm start and navigate to http://localhost:1234/, you should see Hello world! printed on the page.

Adding the viewer

Now we're ready to actually start building the viewer! Let's start by adding a folder at ./src/components/Viewer. Next, inside that directory lets create two files; one called Viewer.tsx and another caled Viewer.scss. The tsx file will be the React component and the scss file will contain the styles for the component.

We're also going to add some folders called ./src/api and ./src/hooks. The api folder will contain all of our PDF.js code, and the hooks folder will contain our React hooks, which we will use to connect our UI to the PDF.js APIs.

The first thing we need to accomplish is getting the pages to render in our viewer. Create the file ./src/api/getDocumentPages.ts and add the following code:

// src/api/getDocumentPages.ts
interface GetDocumentPagesOptions {
  scale?: number;
  url: string
}

export default async ({
  scale = 1,
  url
}: GetDocumentPagesOptions): Promise<Array<string>> => {
  const PDFJS = window.pdfjsLib;

  // First, we need to load the document using the getDocument utility
  const loadingTask = PDFJS.getDocument(url);
  const pdf = await loadingTask.promise;

  const { numPages } = pdf;

  const canvasURLs = [];

  // Now for every page in the document, we're going to add a page to the array
  for (let i = 0; i < numPages; i++) {
    const page = await pdf.getPage(i + 1);

    const viewport = page.getViewport(scale);
    const { width, height } = viewport;
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    canvas.className = 'page'
    await page.render({
      canvasContext: canvas.getContext('2d'),
      viewport
    })

    canvasURLs.push(canvas.toDataURL());
  }

  return canvasURLs;
}

This code uses the getDocument API to retrieve a PDF.js document object, and then loops over each page in the document, creates a canvas for it, and renders the PDF to the canvas. It then converts each canvas to an image using the native toDataURL() function, and adds the result of that to the returned array. These data urls are what we are going to use to display the pages to the user.

Now we are going to build a React hook that uses this API. If you are familiar with hooks, I strongly recommend reading up on them!

Create the file src/hooks/useDocument.ts and add the following code:

// src/hooks/useDocument.ts
import { useState, useEffect } from 'react';
import getDocumentPages from '../api/getDocumentPages';

export default ({
  url
}) => {
  const [pages, setPages] = useState<string[]>([]);
  useEffect(() => {
    const getPages = async () => {
      const canvases = await getDocumentPages({
        url
      });

      setPages(canvases);
    }
    getPages();
  }, [url])
  return {
    pages
  }
}

This code accepts a url option and returns the result of the api we created earlier. The reason we are doing this in a hook is so that if the url parameter ever changes, our React component will "react" and update automatically!

Now it's time to bring it all together. In src/components/Viewer.tsx, add the following code:

// src/components/Viewer.tsx
import React from 'react';
import useDocument from '../../hooks/useDocument';
import './Viewer.scss';

export default () => {

  const { pages } = useDocument({
    url: "https://pdfjs-express.s3-us-west-2.amazonaws.com/docs/choosing-a-pdf-viewer.pdf"
  });

  return (
    <div className='viewer'>
      {
        pages.map((canvasURL, idx) => {
          return <img src={canvasURL} key={idx} />
        })
      }
    </div>
  )
}

All this code does is loop over our pages array and adds the canvas to the DOM.

Now we just need to tell our app to render the viewer. In src/app.tsx, import our Viewer component and render it!

// src/app.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import Viewer from './components/Viewer/Viewer';

const App = () => {
  return (
    <div>
      <Viewer />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('app'));

If you go back to your app, you should see a bunch of images rendered, similar to this:

Let's add some small tweaks to this to make it look a bit better. In Viewer.scss, add the following code:

.viewer {
  width: 100%;
  height: 100%;
  background-color: aliceblue;
  img {
    display: block;
    margin: 5px auto;
  }
}

Now when you look at your app, the images will be stacked and spaced out nicely!

Conclusion

PDF.js is great for simply rendering a PDF to the screen, but it also has other features that we have not seen yet. In future blogs we will go over implementing features like zooming and rotating pages, as well as adding user controls in a nav bar at the top of the app.

The source code for this blog can be viewed and downloaded here.

At PDF.js Express, we offer a modern and customizable commercial viewer that wraps around the PDF.js rendering engine. It lets you easily add additional functionality to your viewer, including 26 out-of-the-box annotations, form filling, e-signatures and more. Check out our demo or see the difference between PDF.js vs PDF.js Express.

If you have any questions, please feel free to contact us!.