Blog Post

Implement a simple PDF viewer with PDF.js

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

This article was first published in March 2019 and was updated in October 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.

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');

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 PSPDFKit 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