Blog Post

PDF.js viewer example: Implement a simple PDF viewer with PDF.js

Illustration: PDF.js viewer example: Implement a simple PDF viewer with PDF.js
Information

This article was first published in January 2019 and was updated in November 2024.

When looking for free and open source PDF processing libraries for the web, PDF.js is usually a good option if you’re willing to implement a user interface on your own or use its demo one.

In an earlier post, we showed how to render a PDF page in the browser with PDF.js and how to integrate its sample UI. In this blog post, we’ll see how to build a simple custom PDF viewer to display PDF documents on a page.

Our simple viewer can load PDF documents from a URL and has buttons to go to the next page or the previous page.

The example also shows a Page Mode feature that allows us to display multiple pages at once. Although we won’t discuss it in this article, its implementation is available in the codesandbox.io example.

At Nutrient, we build a rich, advanced web viewer. Feel free to check out our demo if you’re looking for a professional viewer with a solid API.

Introduction to PDF.js

PDF.js is a JavaScript library developed by Mozilla that enables the rendering of PDF documents in web browsers. It provides a straightforward and efficient way to display PDFs in web applications, making it a popular choice among developers. With PDF.js, users can view, navigate, and interact with PDF documents directly in their web browser, without the need for external plugins or software.

This powerful library leverages the capabilities of modern web technologies to deliver a seamless PDF viewing experience. Its open-source nature and robust community support make it an excellent choice for developers looking to integrate PDF functionality into their web applications.

Understanding PDF.js Basics

To get started with PDF.js, it’s essential to understand the basics of how it works. PDF.js uses the HTML5 canvas element to render PDF pages, allowing for fast and efficient rendering of complex PDF documents. The library provides a range of APIs and tools for customizing the viewer, including options for zooming, panning, and navigating through pages.

One of the key benefits of PDF.js is its ability to handle complex PDF files, including those with multiple pages, images, and fonts.

By understanding these basics, developers can leverage PDF.js to create a simple yet powerful PDF viewer that meets the needs of their web application. Whether you’re building a document management system or any other application that requires PDF functionality, PDF.js provides the foundational tools to get started.

1. Initial setup

Before we dive into the viewer implementation, we’re going to lay down a minimal HTML skeleton for our application:

<!DOCTYPE html>
<html>
	<head>
		<title>PDF Viewer PDF.js</title>
		<meta charset="UTF-8" />
	</head>

	<body>
		<div id="app">
			<div role="toolbar" id="toolbar">
				<div id="pager">
					<button data-pager="prev">prev</button>
					<button data-pager="next">next</button>
				</div>
				<div id="page-mode">
					<label>
						Page Mode
						<input type="number" value="1" min="1" />
					</label>
				</div>
			</div>
			<div id="viewport-container">
				<div role="main" id="viewport"></div>
			</div>
		</div>

		<!-- Load PDF.js library -->
		<script
			src="https://unpkg.com/pdfjs-dist@latest/build/pdf.min.mjs"
			type="module"
		></script>
		<script src="index.js" type="module"></script>
	</body>
</html>

This HTML file includes the basic toolbar for navigating the pages, an input for controlling page mode, and a viewport where the PDF will be rendered. We use a CDN version of pdf.min.mjs to include PDF.js.

2. Importing the PDF.js library

First, we import the pdfjsLib from a CDN and set the worker script to handle rendering PDF files in the background.

import * as pdfjsLib from 'https://unpkg.com/pdfjs-dist@latest/build/pdf.min.mjs';

// Setting worker src for PDF.js.
pdfjsLib.GlobalWorkerOptions.workerSrc =
	'https://unpkg.com/pdfjs-dist@latest/build/pdf.worker.min.mjs';

This setup ensures the PDF.js library functions correctly by offloading tasks — like rendering and processing PDF files — to a web worker, improving performance.

3. Declaring global variables

Here, we define some variables to manage the PDF file and the viewer’s state:

let currentPageIndex = 0;
let pageMode = 1; // Determines the number of pages shown at a time.
let cursorIndex = Math.floor(currentPageIndex / pageMode);
let pdfInstance = null; // Holds the PDF document instance.
let totalPagesCount = 0; // Total number of pages in the PDF.

const viewport = document.querySelector('#viewport');
  • currentPageIndex — Tracks the current page being displayed.

  • pageMode — Defines how many pages are shown at once (one for single-page mode, two for two-page view, etc.).

  • cursorIndex — Calculates the page index based on the pageMode.

  • pdfInstance — Holds the reference to the loaded PDF document.

  • totalPagesCount — Stores the total number of pages in the PDF file.

4. Initializing the PDF viewer

The initPDFViewer function is used to load and initialize the PDF. It takes the URL of the PDF file as an argument and uses pdfjsLib.getDocument to load the document:

window.initPDFViewer = function (pdfURL) {
	pdfjsLib.getDocument(pdfURL).promise.then((pdf) => {
		pdfInstance = pdf;
		totalPagesCount = pdf.numPages;
		initPager(); // Initializes the page navigation.
		initPageMode(); // Initializes the page mode (single or multiple pages).
		render(); // Renders the initial page.
	});
};

This function:

  • Loads the PDF document.

  • Initializes the pager and page mode.

  • Calls render() to display the initial page(s).

5. Setting up pagination controls

We add event listeners for the previous and next page buttons to allow navigation through the pages:

function onPagerButtonsClick(event) {
	const action = event.target.getAttribute('data-pager');
	if (action === 'prev') {
		if (currentPageIndex === 0) return;
		currentPageIndex -= pageMode;
		if (currentPageIndex < 0) currentPageIndex = 0;
		render();
	}
	if (action === 'next') {
		if (currentPageIndex === totalPagesCount - 1) return;
		currentPageIndex += pageMode;
		if (currentPageIndex > totalPagesCount - 1)
			currentPageIndex = totalPagesCount - 1;
		render();
	}
}
  • prev and next actions decrement and increment currentPageIndex based on the pageMode and call render() to update the display.

6. Handling page mode

To enable the user to switch between single-page and multipage views, we add a page mode input and handle the changes:

function onPageModeChange(event) {
	pageMode = Number(event.target.value);
	render();
}

function initPageMode() {
	const input = document.querySelector('#page-mode input');
	input.setAttribute('max', totalPagesCount);
	input.addEventListener('change', onPageModeChange);
}
  • When the user changes the page mode, the onPageModeChange function updates pageMode and rerenders the pages.

7. Rendering the PDF pages

The render function is responsible for rendering the pages based on the current pageMode and currentPageIndex:

function render() {
	cursorIndex = Math.floor(currentPageIndex / pageMode);
	const startPageIndex = cursorIndex * pageMode;
	const endPageIndex =
		startPageIndex + pageMode < totalPagesCount
			? startPageIndex + pageMode - 1
			: totalPagesCount - 1;

	const renderPagesPromises = [];
	for (let i = startPageIndex; i <= endPageIndex; i++) {
		renderPagesPromises.push(pdfInstance.getPage(i + 1));
	}

	Promise.all(renderPagesPromises).then((pages) => {
		const pagesHTML = pages
			.map(
				() =>
					`<div style="width: ${
						pageMode > 1 ? '50%' : '100%'
					}"><canvas></canvas></div>`,
			)
			.join('');
		viewport.innerHTML = pagesHTML;
		pages.forEach((page, index) => renderPage(page, index));
	});
}
  • startPageIndex and endPageIndex are calculated based on the pageMode and currentPageIndex.

  • Pages are rendered using pdfInstance.getPage(), and the results are displayed within div containers, with a canvas element for each page.

8. Rendering each page

The renderPage function is responsible for actually rendering each individual page to its corresponding canvas:

function renderPage(page, pageIndex) {
	let pdfViewport = page.getViewport({ scale: 1 });

	const container = viewport.children[pageIndex]; // Correctly map the page to its container.
	if (!container) {
		console.error('Container not found for page ' + pageIndex);
		return;
	}

	const canvas = container.querySelector('canvas');
	const context = canvas.getContext('2d');

	pdfViewport = page.getViewport({
		scale: container.offsetWidth / pdfViewport.width,
	});

	canvas.height = pdfViewport.height;
	canvas.width = pdfViewport.width;

	page.render({
		canvasContext: context,
		viewport: pdfViewport,
	});
}
  • renderPage ensures that each page is drawn to its respective canvas, adjusting the scale to fit the container width.

Here’s the full code:

import * as pdfjsLib from 'https://unpkg.com/pdfjs-dist@latest/build/pdf.min.mjs';

// Setting worker src for PDF.js.
pdfjsLib.GlobalWorkerOptions.workerSrc =
	'https://unpkg.com/pdfjs-dist@latest/build/pdf.worker.min.mjs';

(function () {
	let currentPageIndex = 0;
	let pageMode = 1;
	let cursorIndex = Math.floor(currentPageIndex / pageMode);
	let pdfInstance = null;
	let totalPagesCount = 0;

	const viewport = document.querySelector('#viewport');

	window.initPDFViewer = function (pdfURL) {
		pdfjsLib.getDocument(pdfURL).promise.then((pdf) => {
			pdfInstance = pdf;
			totalPagesCount = pdf.numPages;
			initPager();
			initPageMode();
			render();
		});
	};

	function onPagerButtonsClick(event) {
		const action = event.target.getAttribute('data-pager');
		if (action === 'prev') {
			if (currentPageIndex === 0) {
				return;
			}
			currentPageIndex -= pageMode;
			if (currentPageIndex < 0) {
				currentPageIndex = 0;
			}
			render();
		}
		if (action === 'next') {
			if (currentPageIndex === totalPagesCount - 1) {
				return;
			}
			currentPageIndex += pageMode;
			if (currentPageIndex > totalPagesCount - 1) {
				currentPageIndex = totalPagesCount - 1;
			}
			render();
		}
	}

	function initPager() {
		const pager = document.querySelector('#pager');
		pager.addEventListener('click', onPagerButtonsClick);
	}

	function onPageModeChange(event) {
		pageMode = Number(event.target.value);
		render();
	}

	function initPageMode() {
		const input = document.querySelector('#page-mode input');
		input.setAttribute('max', totalPagesCount);
		input.addEventListener('change', onPageModeChange);
	}

	function render() {
		cursorIndex = Math.floor(currentPageIndex / pageMode);
		const startPageIndex = cursorIndex * pageMode;
		const endPageIndex =
			startPageIndex + pageMode < totalPagesCount
				? startPageIndex + pageMode - 1
				: totalPagesCount - 1;

		const renderPagesPromises = [];
		for (let i = startPageIndex; i <= endPageIndex; i++) {
			renderPagesPromises.push(pdfInstance.getPage(i + 1));
		}

		Promise.all(renderPagesPromises).then((pages) => {
			// Create containers for each page with appropriate width.
			const pagesHTML = pages
				.map(
					() =>
						`<div style="width: ${
							pageMode > 1 ? '50%' : '100%'
						}"><canvas></canvas></div>`,
				)
				.join('');
			viewport.innerHTML = pagesHTML;
			pages.forEach((page, index) => renderPage(page, index));
		});
	}

	function renderPage(page, pageIndex) {
		let pdfViewport = page.getViewport({ scale: 1 });

		const container = viewport.children[pageIndex]; // Correctly map the page to its container.
		if (!container) {
			console.error('Container not found for page ' + pageIndex);
			return;
		}

		const canvas = container.querySelector('canvas');
		const context = canvas.getContext('2d');

		// Scale canvas to fit container width.
		pdfViewport = page.getViewport({
			scale: container.offsetWidth / pdfViewport.width,
		});

		canvas.height = pdfViewport.height;
		canvas.width = pdfViewport.width;

		page.render({
			canvasContext: context,
			viewport: pdfViewport,
		});
	}
})();

// Initialize the PDF viewer with the example file.
initPDFViewer('assets/example.pdf');

Advanced Features and Customization with PDF.js

PDF.js provides various customization options for developers aiming to build a tailored PDF viewing experience. Using the library’s API, developers can modify the viewer’s appearance, create custom tools, and embed the viewer seamlessly within other web applications.

A key strength of PDF.js is its flexibility with JavaScript, allowing developers to extend the viewer’s capabilities. While PDF.js focuses on rendering, developers can add custom JavaScript functionality, such as creating tools for highlighting or interfacing with form fields, though this may require additional libraries for advanced interactivity.

PDF.js also offers limited support for working with interactive elements like form fields within PDFs, allowing users to view and interact with form data in the browser. However, advanced features such as annotations or e-signatures are not supported out of the box and would require custom development or integration with specialized libraries.

Overall, PDF.js is a robust choice for developers looking to build a customized and responsive PDF viewer. Its rendering capabilities and JavaScript-based customization allow developers to integrate PDF viewing smoothly into web applications and tailor the experience to their specific needs.

Conclusion

And there you have it: With relatively little work, we can not only build an application that can render a PDF document with one or multiple pages at the same time, but we can also add controls to change the page.

PDF.js is a good free option if you’re willing to invest time into implementing a UI for it. The project comes with some examples and API docs. However, implementing a feature-rich PDF viewer isn’t a trivial task, and it can quickly become difficult to maintain and a drain on a business’s resources. Opting for a commercial solution can let you focus on other areas and move up the value chain.

At Nutrient, we offer a commercial, feature-rich, and completely customizable JavaScript PDF library that’s easy to integrate and comes with well-documented APIs to handle the advanced use cases. Check out our demo to see it in action.

FAQ

Here are a few frequently asked questions about PDF.js.

How do I integrate PDF.js into my project?

You can integrate PDF.js by including it via a CDN or by downloading and hosting the library locally.

Can I customize the PDF viewer built with PDF.js?

Yes, you can customize the viewer’s user interface by modifying the provided HTML, CSS, and JavaScript code.

Does PDF.js support multiple pages?

Yes, PDF.js can render multiple pages, and you can add navigation controls for switching between pages.

Do I need a web server to run PDF.js?

Yes, PDF.js fetches PDF documents using AJAX, which requires the viewer to be served over a web server.

Is PDF.js a free library?

Yes, PDF.js is an open source library developed by Mozilla and is available for free.

Author
Hulya Masharipov Technical Writer

Hulya is a frontend web developer and technical writer at Nutrient who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

Related products
Share post
Free trial Ready to get started?
Free trial