JavaScript PDF viewer tutorial with PDF.js
This article was first published in September 2021 and was updated in November 2024.
In this tutorial, you’ll learn how to build a JavaScript PDF viewer using PDF.js, a popular open-source library for rendering PDF files in the browser. With PDF.js, you can create a PDF viewer that allows users to view and interact with PDF documents directly in a web browser without needing an external PDF reader.
This tutorial is divided into two parts:
-
Part 1: Render a PDF and embed a basic PDF viewer in the browser with PDF.js.
-
Part 2: Build a fully featured PDF viewer using the Nutrient JavaScript PDF library for enhanced functionality.
Introduction to PDF.js
PDF.js is a powerful JavaScript library developed by Mozilla that enables rendering of Portable Document Format (PDF) files within web browsers. By leveraging HTML5 and modern web technologies, PDF.js provides a seamless, plugin-free PDF viewing experience. Its open-source nature and community-driven development make it an ideal choice for web developers looking to add PDF functionality to their applications.
With PDF.js, you can:
-
Render PDF files accurately across different browsers.
-
Integrate PDF viewing capabilities without external plugins or software.
-
Build both simple and advanced PDF viewers tailored to your application’s needs.
Requirements
Before getting started, make sure you have the following installed:
Understanding PDF.js Architecture
PDF.js operates through three main layers:
-
Core: Interprets the binary format of PDFs and performs heavy parsing operations, often running in a web worker to keep the main thread responsive.
-
Display: Builds on the core layer and provides an API for rendering PDF pages into
<canvas>
elements using JavaScript. -
Viewer: A ready-to-use interface with features like search, rotation, and a thumbnail sidebar.
To use PDF.js, simply download a recent copy and integrate it into your project.
Getting Started with PDF.js
There are two main ways to set up PDF.js in your web project:
1. Build from Source
Building from source provides the most customization options. Here’s how to do it:
-
Clone the PDF.js repository from GitHub.
-
Install required dependencies (Node.js and gulp).
-
Run the build command to generate the library files.
This method is ideal for developers comfortable with build tools who want complete control over the PDF.js code, allowing you to modify the library and add custom features.
2. Use a Pre-built Distribution
For a simpler setup, use the pre-built distribution available on the PDF.js GitHub page:
-
Download the pre-built library files.
-
Include them directly in your project.
This approach provides all essential PDF.js features with minimal setup, perfect for developers needing quick and easy integration without diving into the build process.
Building a Basic PDF Viewer
PDF.js enables rendering of PDFs via AJAX and displaying them in a <canvas>
element. Here’s a step-by-step guide to setting up a basic viewer:
Step 1 - Download PDF.js Files
To render a specific page of a PDF into a <canvas>
element, you can use the display
layer.
To get started:
-
Extract all the files in the downloaded copy of PDF.js.
-
Move
pdf.mjs
andpdf.worker.mjs
from thebuild/
folder of the download to a new empty folder. -
Create an
index.html
file and anindex.js
file.
The HTML file needs to point to the pdf.mjs
source code and to the custom application code (index.js
).
Creating the HTML file
-
Create the
canvas
element, which you want to render the PDF inside of:
<!-- Canvas to place the PDF --> <canvas id="canvas" class="canvas__container"></canvas>
-
Create a toolbar inside the
<header>
element to add navigation and zoom controls to the website.
Next, add some icons for the previous (<
), next (>
), and zoom buttons. For that, you’ll use the Font Awesome library; all you need to do is include the CDN link for it.
Add the following code to the index.html
file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>PDF Viewer in JavaScript</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="style.css" /> </head> <body> <header> <ul class="navigation"> <li class="navigation__item"> <!-- Navigate to the Previous and Next pages --> <a href="#" class="previous round" id="prev_page"> <i class="fas fa-arrow-left"></i> </a> <!-- Navigate to a specific page --> <input type="number" value="1" id="current_page" /> <a href="#" class="next round" id="next_page"> <i class="fas fa-arrow-right"></i> </a> <!-- Page Info --> Page <span id="page_num"></span> of <span id="page_count"></span> </li> <!-- Zoom In and Out --> <li class="navigation__item"> <button class="zoom" id="zoom_in"> <i class="fas fa-search-plus"></i> </button> <button class="zoom" id="zoom_out"> <i class="fas fa-search-minus"></i> </button> </li> </ul> </header> <!-- Canvas to place the PDF --> <canvas id="canvas" class="canvas__container"></canvas> <!-- Load PDF.mjs --> <script src="./pdf.mjs" type="module"></script> <script src="index.js" type="module"></script> </body> </html>
Loading the PDF
-
Import the PDF.js Worker Module: In your
index.js
file, add the following line to import theGlobalWorkerOptions
class from the PDF.js library:
import { GlobalWorkerOptions } from 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs';
-
Configure PDF.js to Use the Correct Worker Script: Set the workerSrc property on
GlobalWorkerOptions
to point to the worker script. This configuration is essential for PDF.js to operate correctly:
GlobalWorkerOptions.workerSrc =
'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.mjs';
-
Initialize PDF Document and Application State: In your
index.js
file, create a variable to hold the PDF document and usedocument.querySelector
to access DOM elements. Additionally, set up an object to maintain the initial state of the application.
const pdf = 'document.pdf'; const pageNum = document.querySelector('#page_num'); const pageCount = document.querySelector('#page_count'); const currentPage = document.querySelector('#current_page'); const previousPage = document.querySelector('#prev_page'); const nextPage = document.querySelector('#next_page'); const zoomIn = document.querySelector('#zoom_in'); const zoomOut = document.querySelector('#zoom_out'); const initialState = { pdfDoc: null, currentPage: 1, pageCount: 0, zoom: 1, };
Pass the name of your PDF document to the pdf
variable.
-
Use the
pdfjsLib.getDocument(pdf)
method to load the PDF file, with thepdf
parameter being the path to the PDF file. Then, use the.promise.then()
method to handle the promise.
The .then()
method takes a callback function that will be called when the promise is fulfilled. Here, you can access the PDF file and set the initialState.pdfDoc
to the PDF:
// Load the document. pdfjsLib .getDocument(pdf) .promise.then((data) => { initialState.pdfDoc = data; console.log('pdfDocument', initialState.pdfDoc); pageCount.textContent = initialState.pdfDoc.numPages; renderPage(); }) .catch((err) => { alert(err.message); });
This API makes heavy use of Promise
, a JavaScript feature for handling future values. If you’re not familiar with this pattern, check out the MDN web docs.
After loading the document, set the text content of the pageCount
to the number of pages in the document.
-
As you can see, there’s a function called
renderPage()
. Use this function to render the current page of your document:
// Render the page. const renderPage = () => { // Load the first page. console.log(initialState.pdfDoc, 'pdfDoc'); initialState.pdfDoc .getPage(initialState.currentPage) .then((page) => { console.log('page', page); const canvas = document.querySelector('#canvas'); const ctx = canvas.getContext('2d'); const viewport = page.getViewport({ scale: initialState.zoom, }); canvas.height = viewport.height; canvas.width = viewport.width; // Render the PDF page into the canvas context. const renderCtx = { canvasContext: ctx, viewport: viewport, }; page.render(renderCtx); pageNum.textContent = initialState.currentPage; }); };
-
Now that the PDF has been initialized, invoke
getPage()
on the document instance. The returnedPromise
resolves with apage
object you can use to render the first page of your document. -
To draw something on the canvas, use the
canvas.getContext()
method. This method returns a context object you can use to draw on the canvas. -
The
getViewport(scale)
method returns a viewport object that represents the page at the given scale. -
Use the viewport information to set the dimensions of the
<canvas>
element, and then start the page renderer with therender(options)
API. -
At the end, set the text content of the page number to the current page. This will dynamically update the page number.
Navigating through the PDF pages
Until now, you’ve only rendered the first page of your PDF document. Now, you’ll work on making the previous and next icons functional when a user clicks on them:
const showPrevPage = () => { if (initialState.pdfDoc === null || initialState.currentPage <= 1) return; initialState.currentPage--; // Render the current page. currentPage.value = initialState.currentPage; renderPage(); }; const showNextPage = () => { if ( initialState.pdfDoc === null || initialState.currentPage >= initialState.pdfDoc._pdfInfo.numPages ) return; initialState.currentPage++; currentPage.value = initialState.currentPage; renderPage(); }; // Button events. previousPage.addEventListener('click', showPrevPage); nextPage.addEventListener('click', showNextPage);
As you can see, the showPrevPage
and showNextPage
functions are similar. They both check if the current page is the first or last page, and if it is, they do nothing. Otherwise, they decrement or increment the current page and then render the page.
Displaying a specific page
Here, you can specify which page to render based on a user input. You’re using the keypress
event to listen for the enter key
. When the user presses the enter key, you’ll get the page number from the currentPage
input and check if it’s between the ranges of PDF pages. If it is, you’ll set the currentPage
to the desired page and render the page:
// Keypress event. currentPage.addEventListener('keypress', (event) => { if (initialState.pdfDoc === null) return; // Get the key code. const keycode = event.keyCode ? event.keyCode : event.which; if (keycode === 13) { // Get the new page number and render it. let desiredPage = currentPage.valueAsNumber; initialState.currentPage = Math.min( Math.max(desiredPage, 1), initialState.pdfDoc._pdfInfo.numPages, ); currentPage.value = initialState.currentPage; renderPage(); } });
For cases when a user types a number that’s either negative or greater than the number of pages, set the currentPage
to the first or last page, respectively, and display the page.
Adding a zoom feature to PDF.js
You’re done navigating through the PDF pages. Now, you’ll add the zoom feature:
// Zoom events. zoomIn.addEventListener('click', () => { if (initialState.pdfDoc === null) return; initialState.zoom *= 4 / 3; renderPage(); }); zoomOut.addEventListener('click', () => { if (initialState.pdfDoc === null) return; initialState.zoom *= 2 / 3; renderPage(); });
Similar to how it is with navigation, when the user clicks on the zoomIn
or zoomOut
buttons, you’ll increment or decrement the zoom value and then render the page.
Serving your website
-
Make sure to copy a PDF file into the folder and rename it to
document.pdf
. -
Install the
serve
package:
npm install --global serve
-
Serve the contents of the current directory:
serve -l 8080 .
-
Navigate to http://localhost:8080 to view the website.
Embedding the PDF.js viewer in an HTML window
While the display
layer provides fine-grained control over which parts of a PDF document are rendered, there are times when you might prefer a ready-to-use viewer. Luckily, PDF.js has you covered. In this part, you’ll integrate the PDF.js default viewer into the website.
-
Go back to the
pdfjs-dist
folder you downloaded earlier. -
Copy the entire
web/
folder into a new directory. -
Create a
build/
folder inside the new directory and copy thepdf.mjs
andpdf.worker.mjs
files there. -
Create an
index.html
file that will include the viewer via an<iframe>
. Locate this file in the root of your project.
This allows you to easily embed the viewer into an existing webpage. The viewer is configured via URL parameters, a list of which can be found here. For this example, you’ll only configure the source PDF file, and you no longer need the index.js
file:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>PDF.js Example</title> </head> <body> <iframe src="/web/viewer.html?file=document.pdf" width="1000px" height="1000px" style="border: none" /> </body> </html>
For more advanced features (like saving the PDF document to your web server again), you can start modifying the viewer.html
file provided by PDF.js. You can find the viewer.html
file inside the web/
folder.
-
Now, place your PDF file inside the
web
directory. -
Run
serve -l 8080 .
from the root directory to serve the contents of theindex.html
file, and navigate to thehttp://localhost:8080/web/viewer?file=document.pdf
URL to view the PDF. According to the name of your local PDF file, you can change thedocument.pdf
to your PDF file name. -
Your file directory now should look like this:
pdfjs-viewer-example ├── build | └── pdf.mjs | └── pdf.worker.mjs ├── web | └── viewer.html | └── document.pdf | └── ...other files └── index.html
You can access the demo applications on GitHub.
Final words on PDF.js
All in all, PDF.js is a great solution for many use cases. However, sometimes your business requires more complex features, such as the following, for handling PDFs in the browser:
- PDF annotation support — PDF.js will only render annotations that were already in the source file, and you can use the core API to access raw annotation data. It doesn’t have annotation editing support, so your users won’t be able to create, update, or delete annotations to review a PDF document.
- PDF form filling — While PDF.js has started working with interactive forms, our testing found there are still a lot of issues left open. For example, form buttons and actions aren’t supported, making it impossible to submit forms to your web service.
- Mobile support — PDF.js comes with a clean mobile interface, but it misses features that provide a great user experience and are expected nowadays, like pinch-to-zoom. Additionally, downloading an entire PDF document for mobile devices might result in a big performance penalty.
- Persistent management — With PDF.js, there’s no option to easily share, edit, and annotate PDF documents across a broad variety of devices (whether it be other web browsers, native apps, or more). If your business relies on this service, consider looking into a dedicated annotation syncing framework like Nutrient Instant.
- Digital signatures — PDF.js currently has no support for digital signatures, which are used to verify the authenticity of a filled-out PDF.
- Advanced view options — The PDF.js viewer only supports a continuous page view mode, wherein all pages are laid out in a list and the user can scroll through them vertically. Single- or double-page modes — where only one (or two) pages are shown at once (a common option to make it easier to read books or magazines) — aren’t possible.
- Render fidelity — There are many known problems with the render fidelity of PDF.js, where PDFs look different, have different colors or shapes, or even miss parts of the document altogether.
If your business relies on any of the above features, consider looking into alternatives.
Building a JavaScript PDF viewer with Nutrient
We at Nutrient work on the next generation of PDF viewers for the web. Our commercial JavaScript Document Viewer library can easily be integrated into your web application. It comes with 30+ features that let you view, annotate, edit, and sign documents directly in your browser. Out of the box, it has a polished and flexible UI that you can extend or simplify based on your unique use case.
Nutrient offers a range of advanced features that set it apart from PDF.js. Here’s a comparison of key features:
Feature | PDF.js | Nutrient |
---|---|---|
Basic PDF rendering | Yes | Yes |
Annotation tools | No | Yes |
Form filling | No | Yes |
Document collaboration | No | Yes |
PDF editing | No | Yes |
Search functionality | Basic | Advanced |
Customizable UI | Limited | Highly customizable |
Security features | Basic | Advanced, with encryption options |
Overview
Nutrient Web SDK operates in a standalone mode, leveraging WebAssembly technology to render and edit documents directly in the browser. This approach eliminates the need for a server, plugins, or internet access, offering a serverless solution with the following benefits:
-
Faster setup — No servers to deploy or maintain.
-
Lower infrastructure costs — Rendering and processing are handled by the client.
-
Enhanced security — Documents remain on the client side, avoiding network transfer.
You can evaluate Nutrient Web SDK without a trial license key, but with some limitations, such as a red watermark on documents. For an unrestricted evaluation, obtain a trial key.
Nutrient does not collect data during your evaluation. For a comprehensive introduction, you can watch our Getting started with Nutrient video guide.
Adding Nutrient to your project
-
Install the
pspdfkit
package fromnpm
. If you prefer, you can also download Nutrient Web SDK manually:
npm install pspdfkit
-
For Nutrient Web SDK to work, it’s necessary to copy the directory containing all the required library files (artifacts) to the
assets
folder. Use the following command to do this:
cp -R ./node_modules/pspdfkit/dist/ ./assets/
Make sure your assets
directory contains the pspdfkit.js
file and a pspdfkit-lib
directory with the library assets.
Integrating into your project
-
Add the PDF document you want to display to your project’s directory. You can use our demo document as an example.
-
Add an empty
<div>
element with a defined height to where Nutrient will be mounted:
<div id="pspdfkit" style="height: 100vh;"></div>
-
Include
pspdfkit.js
in your HTML page:
<script src="assets/pspdfkit.js"></script>
-
Initialize Nutrient Web SDK in JavaScript by calling
PSPDFKit.load()
:
<script> PSPDFKit.load({ container: "#pspdfkit", document: "document.pdf" // Add the path to your document here. }) .then(function(instance) { console.log("PSPDFKit loaded", instance); }) .catch(function(error) { console.error(error.message); }); </script>
You can see the full index.html
file below:
<!DOCTYPE html> <html> <head> <title>My App</title> <!-- Provide proper viewport information so that the layout works on mobile devices. --> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> </head> <body> <!-- Element where PSPDFKit will be mounted. --> <div id="pspdfkit" style="height: 100vh;"></div> <script src="assets/pspdfkit.js"></script> <script> PSPDFKit.load({ container: '#pspdfkit', document: 'document.pdf', }) .then(function (instance) { console.log('PSPDFKit loaded', instance); }) .catch(function (error) { console.error(error.message); }); </script> </body> </html>
You can serve the application with the serve
package like you did for PDF.js.
You should now have our JavaScript PDF viewer up and running in your web application. If you hit any snags, don’t hesitate to reach out to our support team for help.
If you want to download Nutrient manually or integrate it as a module, you can check out our JavaScript getting started guide.
Adding even more capabilities
Once you’ve deployed your viewer, you can start customizing it to meet your specific requirements or easily add more capabilities. To help you get started, here are some of our most popular JavaScript guides:
- Adding annotations
- Editing documents
- Filling PDF forms
- Adding signatures to documents
- Real-time collaboration
- Redaction
- UI customization
Conclusion
This tutorial covered how to build a basic [JavaScript PDF viewer][js] using PDF.js and enhance it with Nutrient’s advanced features. Both libraries offer powerful capabilities for handling PDFs in the browser. Depending on your project needs, you might opt for the flexibility of PDF.js or the comprehensive features of Nutrient.
We encourage you to experiment with both libraries and explore their documentation to fully leverage their potential. For more advanced use cases or support, feel free to reach out to our team or check out the Nutrient documentation for additional guidance.
We created similar how-to blog posts using different web frameworks and libraries:
- How to build an Angular PDF viewer with PDF.js
- How to build a Vue.js PDF viewer with PDF.js
- How to build a React PDF viewer with PDF.js
- How to build a Bootstrap 5 PDF viewer with PDF.js
- How to build an Electron PDF viewer with PDF.js
- How to build a jQuery PDF viewer with PDF.js
To see a list of all web frameworks, start your free trial. Or, launch our demo to see our viewer in action.
FAQ
Here are a few frequently asked questions about building a JavaScript PDF viewer with PDF.js
What is PDF.js, and how does it work?
PDF.js is an open source JavaScript library that renders PDF files directly within a web browser without the need for plugins. It leverages HTML5, <canvas>
, and other web technologies to seamlessly display PDF documents.
How do I set up PDF.js in my JavaScript project?
To set up PDF.js, download the library from its official repository, include the necessary scripts in your HTML file, and initialize the viewer by loading a PDF document and rendering it on a canvas element.
What are the key features of a PDF viewer built with PDF.js?
A PDF viewer built with PDF.js includes features such as rendering PDF documents, navigating through pages, zooming in and out, searching text, and adding annotations.
What are some common challenges when building a PDF viewer with PDF.js?
Some common challenges include handling large or complex PDF documents, ensuring cross-browser compatibility, and optimizing performance for smooth rendering and navigation.