Building a PDF Viewer From Scratch
How to build a simple, custom PDF viewer using the PDF.js library. PDF.js is an open-source PDF viewer library by Mozilla, built with HTML5, that renders and parses PDFs.
Join the DZone community and get the full member experience.
Join For FreePortable Document Format (PDF) is widely used to share documents across various platforms. PDF is popular because it keeps the document's format and layout in such a way that no operating system or PDF viewer shows any signs of modification. PDFs are displayed using software tools called PDF viewers, which have many functionalities for interacting with documents, such as navigation, zooming in and out, and jumping to specific pages.
Each PDF viewer has specific capabilities, which limits the possibilities for new features. Hence, a developer might want to create their own PDF viewer software to cater to their particular needs and preferences, such as document analysis or data extraction. There are many advantages to creating a custom PDF viewer, some of which are given below:
- Customization: Developing your own PDF viewer allows you to tailor its interface, features, and functionalities to suit your specific requirements.
- Integration: Building a custom PDF viewer gives you seamless integration with your platform or application without sacrificing any features.
- Security and privacy: A custom PDF viewer gives you control over security measures like encryption and access controls to ensure the confidentiality of sensitive content in a PDF file.
In this article, you'll be building a simple, custom PDF viewer using the PDF.js library. PDF.js is an open-source PDF viewer library by Mozilla, built with HTML5, that renders and parses PDFs.
Developing a PDF Viewer Using the PDF.js Library
Before getting started, you'll need a code editor of your choice and the PDF.js library, which can be connected through a CDN (used in this tutorial) or downloaded from GitHub.
Setting Up the Environment
You'll first set up a Vanilla JS project that consists of a simple web page with HTML, CSS, and JavaScript functionality.
Set up a project folder named PDF Viewer and create three files in the folder as listed below:
- index.html
- styles.css
- viewer.js
The project folder structure should look like this:
Project structure
Creating the User Interface for the PDF Viewer
The next step is to create an interface to use the PDF viewer.
Open the index.html
file, then enter the following code into the file and save it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple PDF Viewer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Simple PDF Viewer</h1>
<div id="pdf-container"></div>
<div id="page-controls">
<button id="prev-page">Previous</button>
<span id="page-num">Page 1</span>
<button id="next-page">Next</button>
<input type="number" id="go-to-page-input" placeholder="Go to Page">
<button id="go-to-page-btn">Go</button>
</div>
<div>
<button id="zoom-out">Zoom Out</button>
<button id="zoom-in">Zoom In</button>
</div>
<input type="file" id="file-input" accept=".pdf">
</body>
</html>
The above code creates a simple interface with the following elements:
- A container to display the PDF file
- "Previous" and "Next" buttons to navigate the pages of the PDF file
- An input field to jump to the specified page number of the PDF file
- "Zoom In" and "Zoom Out" buttons
- A button for choosing a PDF file from the user's local file explorer
Other than creating the user interface, there are two script tags for external JavaScript files that add functionality to the web page.
The first script tag, https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js
, is the link to the PDF.js library for rendering PDF documents. This integrates the PDF.js library into your project.
The second script tag, viewer.js
, is the link to the custom JavaScript file that implements the PDF viewer functionality.
This concludes the code in the HTML file. The resulting web page should look like this:
Styling the Interface With CSS
While this tutorial is not focused on styling the UI, you can make it a little more visually appealing. Save the code below in styles.css
:
/* styles.css */
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
#pdf-container {
border: 1px solid #ccc;
overflow: auto;
width: 80%;
max-width: 800px;
height: 70vh;
}
.button-group, #zoom-controls, #file-controls {
display: flex;
align-items: center;
margin-top: 10px;
}
.button-group button, #zoom-controls button, #file-controls button, #file-controls input[type="file"] {
margin: 0 5px;
padding: 5px 10px;
border: none;
background-color: #007bff;
color: #fff;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: background-color 0.3s ease;
}
.button-group button:hover, #zoom-controls button:hover, #file-controls button:hover, #file-controls input[type="file"]:hover {
background-color: #0056b3;
}
#go-to-page-input, #go-to-page-btn {
margin-left: 5px;
}
The above code centers the elements, organizes a space for displaying the PDF, and aligns all the buttons and input fields.
The resulting web page should now look like this:
PDF viewer with CSS styling
Adding Functionality With JavaScript Using PDF.js
You've now organized the interface of the PDF viewer with CSS. The final step is to add functionality with JavaScript and the PDF.js library.
First, to add functionality to the PDF viewer, you'll need to place all the relevant code into an event listener called DOMContentLoaded
. This ensures that the JavaScript code is executed after the HTML document is fully loaded:
document.addEventListener('DOMContentLoaded', function() {
// All the code here
});
You'll also need to enable interaction with the HTML (DOM) elements, such as buttons and input fields, through JavaScript. The document.getElementById()
function allows access to the Document Object Model (DOM) elements, which allows you to create variables for every element to be used later:
const pdfContainer = document.getElementById('pdf-container');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageNumSpan = document.getElementById('page-num');
const goToPageInput = document.getElementById('go-to-page-input');
const goToPageBtn = document.getElementById('go-to-page-btn');
const zoomOutBtn = document.getElementById('zoom-out');
const zoomInBtn = document.getElementById('zoom-in');
const fileInput = document.getElementById('file-input');
You'll then initialize the following three variables: pdfDoc
, pageNum
, and scale
. The pdfDoc
variable stores the PDF document instance that is loaded, the pageNum
variable keeps track of the current page number displayed and the scale
variable is used to keep track of the zoom scale for the zoom-in and out functionality:
let pdfDoc = null;
let pageNum = 1;
let scale = 1.0;
Next, the renderPage()
function renders a specific page of the loaded PDF file. It fetches a PDF page, adjusts canvas dimensions to match the page, clears the container, and renders the page's content onto the canvas, effectively displaying the page within the container:
async function renderPage(num) {
const page = await pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
const canvasContext = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
pdfContainer.innerHTML = '';
pdfContainer.appendChild(canvas);
const renderContext = {
canvasContext,
viewport,
};
await page.render(renderContext);
}
The loadPDF()
function loads a PDF document from a given URL using the pdfjsLib.getDocument()
method. It then calls renderPage()
to display the initial page and update the page count:
async function loadPDF(url) {
const loadingTask = pdfjsLib.getDocument(url);
pdfDoc = await loadingTask.promise;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
Next, you'll add event listeners to multiple buttons for click functionality, such as:
- The
prevPageBtn
andnextPageBtn
buttons for page navigation, which also update the page number to be shown accordingly - The
goToPageBtn button
, which targets the value (page number) entered in thegoToPageInput
field to jump to a specific page of the PDF file - The
zoomOutBtn
andzoomInBtn
buttons for zooming in and out of the displayed page
prevPageBtn.addEventListener('click', () => {
if (pageNum > 1) {
pageNum--;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
nextPageBtn.addEventListener('click', () => {
if (pageNum < pdfDoc.numPages) {
pageNum++;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
goToPageBtn.addEventListener('click', () => { // Event listener for the "Go to Page" button
const targetPage = parseInt(goToPageInput.value);
if (targetPage >= 1 && targetPage <= pdfDoc.numPages) {
pageNum = targetPage;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
zoomOutBtn.addEventListener('click', () => {
if (scale > 0.25) {
scale -= 0.25;
renderPage(pageNum);
}
});
zoomInBtn.addEventListener('click', () => {
if (scale < 3) {
scale += 0.25;
renderPage(pageNum);
}
});
The final bit to add is the fileInput
listener that checks for change
to load and render any chosen file from the user's local file explorer:
fileInput.addEventListener('change', async (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
const fileURL = URL.createObjectURL(selectedFile);
loadPDF(fileURL);
}
});
The complete code for adding the functions is provided below. Open the already-created viewer.js
file, then enter the following code and save it:
document.addEventListener('DOMContentLoaded', function() {
const pdfContainer = document.getElementById('pdf-container');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageNumSpan = document.getElementById('page-num');
const goToPageInput = document.getElementById('go-to-page-input'); // Added input element
const goToPageBtn = document.getElementById('go-to-page-btn'); // Added button element
const zoomOutBtn = document.getElementById('zoom-out');
const zoomInBtn = document.getElementById('zoom-in');
const fileInput = document.getElementById('file-input');
let pdfDoc = null;
let pageNum = 1;
let scale = 1.0;
async function renderPage(num) {
const page = await pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
const canvasContext = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
pdfContainer.innerHTML = '';
pdfContainer.appendChild(canvas);
const renderContext = {
canvasContext,
viewport,
};
await page.render(renderContext);
}
async function loadPDF(url) {
const loadingTask = pdfjsLib.getDocument(url);
pdfDoc = await loadingTask.promise;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
prevPageBtn.addEventListener('click', () => {
if (pageNum > 1) {
pageNum--;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
nextPageBtn.addEventListener('click', () => {
if (pageNum < pdfDoc.numPages) {
pageNum++;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
goToPageBtn.addEventListener('click', () => { // Event listener for the "Go to Page" button
const targetPage = parseInt(goToPageInput.value);
if (targetPage >= 1 && targetPage <= pdfDoc.numPages) {
pageNum = targetPage;
renderPage(pageNum);
pageNumSpan.textContent = `Page ${pageNum} of ${pdfDoc.numPages}`;
}
});
zoomOutBtn.addEventListener('click', () => {
if (scale > 0.25) {
scale -= 0.25;
renderPage(pageNum);
}
});
zoomInBtn.addEventListener('click', () => {
if (scale < 3) {
scale += 0.25;
renderPage(pageNum);
}
});
fileInput.addEventListener('change', async (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
const fileURL = URL.createObjectURL(selectedFile);
loadPDF(fileURL);
}
});
});
This concludes the development of a custom PDF viewer using PDF.js.
Testing the PDF Viewer
With the PDF viewer project completed using PDF.js, let's now try rendering some PDFs and see how it works.
If you render a PDF file, it will look like this:
PDF viewer testing
Zoom in:
PDF viewer: zooming in
PDF viewer: zooming out
Next page:
PDF viewer: next page
PDF viewer: previous page
Conclusion
In this article, you explored how to develop a custom PDF viewer from scratch using the PDF.js library. You can build on the steps here to create many variations of a PDF viewer, adding any functions you need.
You can find all the code used in this article in this GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments