Blog Post

Implement a Simple PDF Viewer with PDF.js

Illustration: Implement a Simple PDF Viewer with PDF.js

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. The source code is available on codesandbox.io, and what’s shown below is the final result.

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

Initial Setup

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

<!DOCTYPE html>
<link rel="stylesheet" href="./styles.css" />

<div id="app">
	<div 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" role="main"></div>
</div>

<script src="https://unpkg.com/pdfjs-dist@latest/build/pdf.min.js"></script>
<script src="./index.js"></script>
<script>
	window.onload = () => {
		initPDFViewer('assets/example.pdf');
	};
</script>

We’re using the CDN-hosted version of PDF.js from unpkg, but this file could also be downloaded and hosted locally.

Loading Documents

The initPDFViewer function above accepts the URL of a PDF file. Since PDF.js will fetch the document making an AJAX request, our viewer needs to run on a web server. Otherwise, the browser will block the request for security reasons.

Inside initPDFViewer, we get the document, save a reference to its instance and the count of the pages, and render the first page:

let currentPageIndex = 0;
let pdfInstance = null;
let totalPagesCount = 0;

window.initPDFViewer = function (pdfURL) {
	pdfjsLib.getDocument(pdfURL).then((pdf) => {
		pdfInstance = pdf;
		totalPagesCount = pdf.numPages;
		initPager();
		render();
	});
};
var currentPageIndex = 0;
var pdfInstance = null;
var totalPagesCount = 0;

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

Rendering

Every time we call render, our viewer renders the page at currentPageIndex as follows:

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

function render() {
	pdfInstance.getPage(currentPageIndex + 1).then((page) => {
		viewport.innerHTML = `<div><canvas></canvas></div>`;
		renderPage(page);
	});
}
var viewport = document.querySelector('#viewport')

function render() {
  pdfInstance.getPage(currentPageIndex + 1)
    .then(function(page) => {
      viewport.innerHTML = `<div><canvas></canvas></div>`
      renderPage(page)
    })
}

getPage retrieves the page proxy object that allows us to work with a document page. We increment currentPageIndex, since getPage expects the page number instead of the page index.

Finally, we can render the given page:

function renderPage(page) {
	let pdfViewport = page.getViewport(1);

	const container = viewport.children[0];

	// Render at the page size scale.
	pdfViewport = page.getViewport(container.offsetWidth / pdfViewport.width);
	const canvas = container.children[0];
	const context = canvas.getContext('2d');
	canvas.height = pdfViewport.height;
	canvas.width = pdfViewport.width;

	page.render({
		canvasContext: context,
		viewport: pdfViewport,
	});
}
function renderPage(page) {
	var pdfViewport = page.getViewport(1);

	var container = viewport.children[0];

	// Render at the page size scale.
	pdfViewport = page.getViewport(container.offsetWidth / pdfViewport.width);
	var canvas = container.children[0];
	var context = canvas.getContext('2d');
	canvas.height = pdfViewport.height;
	canvas.width = pdfViewport.width;

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

page.render returns a RenderTask, which can be used to either determine when a page has finished rendering or abort the rendering process. In a real-world application, RenderTasks can be useful for avoiding unnecessary work when quickly changing pages or zooming.

Changing Pages

To change pages, we create two buttons to increment or decrement the currentPageIndex on click. We then call into render again to render the page at the new currentPageIndex:

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 onPagerButtonsClick(event) {
	var 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();
	}
}

In the snippet above, we assume that the increment/decrement pageMode is 1. In the codesandbox.io example you can see how, with the help of a cursor (cursorIndex), we handle navigation when pageMode is greater than 1.

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. Please check out the final result on codesandbox!

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

Related Products
Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
PRODUCTS  |  Web • Releases • Components

PSPDFKit for Web 2024.3 Features New Stamps and Signing UI, Export to Office Formats, and More

PRODUCTS  |  Web • Releases • Components

PSPDFKit for Web 2024.2 Features New Unified UI Icons, Shadow DOM, and Tab Ordering

PRODUCTS  |  Web

Now Available for Public Preview: New Document Authoring Experience Provides Glimpse into the Future of Editing