Blog Post

How to Build an Electron PDF Viewer with PDF.js

Illustration: How to Build an Electron PDF Viewer with PDF.js

In this post, you’ll learn how to build an Electron PDF viewer with PDF.js. Electron is an open source framework maintained by GitHub and written in many languages, including JavaScript. This makes building a cross-platform desktop application with Electron as easy as writing some JavaScript. Meanwhile, PDF.js is an open source JavaScript library created and maintained by Mozilla, and it allows you to view PDF documents in your browser.

In the first part, you’ll create a PDF viewer with PDF.js In the second part, you’ll create the PDF viewer using our Electron PDF library. Our viewer library provides some benefits beyond what PDF.js provides, including:

  • A prebuilt UI — Save time with a well-documented list of APIs when customizing the UI to meet your exact requirements.
  • Annotation tools — Draw, circle, highlight, comment, and add notes to documents with 15+ prebuilt annotation tools.
  • Multiple file types — Support client-side viewing of PDFs, MS Office documents, and image files.
  • 30+ features — Easily add features like PDF editing, digital signatures, form filling, real-time document collaboration, and more.
  • Dedicated support — Deploy faster by working 1-on-1 with our developers.

Building an Electron PDF Viewer with PDF.js

To get started, you’ll first get everything ready.

Setting Up

  1. Open your terminal, create a folder, and change your directory into the new folder:

mkdir electron-pdf-viewer && cd electron-pdf-viewer
  1. Now, open your preferred code editor and initialize a Node.js project with the following command:

npm init --yes
  1. Go to the package.json file and add a start script to run the application:

{
	"name": "electron-pdfjs-viewer",
	"version": "1.0.0",
	"description": "A desktop PDF viewer",
	"main": "src/main.js",
	"scripts": {
		"start": "electron ."
	}
}

Also, make sure main points to the src/main.js file.

  1. Then, install Electron in your devDependencies section:

npm install --save-dev electron

Adding PDF.js

  1. Install PDF.js via npm:

npm install pdfjs-dist

Displaying a PDF Document

  1. You can now create a src folder, which is where you’ll add the main.js file with the content shown below:

// src/main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
	// Create the browser window.
	const mainWindow = new BrowserWindow({
		width: 1200,
		height: 800,
	});

	mainWindow.loadFile('./src/index.html');
}

// After initialization, you can create browser windows.
app.whenReady().then(createWindow);
  1. That’s it for the src/main.js file. Now, add the index.html file, which will be loaded by the mainWindow:

<!-- src/ìndex.html -->
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<meta
			http-equiv="Content-Security-Policy"
			content="default-src 'self'; script-src 'self'"
		/>
		<title>Electron PDF.js Desktop PDF Viewer Example</title>
	</head>
	<body>
		<h1>Electron PDF.js Desktop PDF Viewer Example</h1>
		<canvas id="pdfContainer" />
		<script src="./pdf.min.js"></script>
		<script src="./renderer.js"></script>
	</body>
</html>

You’ll notice you added two <script> tags to the index.html file: The first one will load the PDF.js library to your window, and the second one will load your renderer.js file, which is where you’ll include your client code to operate with the library.

  1. However, since the PDF.js library is installed but not yet available in the ./src folder, you need to copy it there:

cp ./node_modules/pdfjs-dist/build/pdf.min.js ./src
cp ./node_modules/pdfjs-dist/build/pdf.worker.min.js ./src
  1. Now, edit the renderer.js file, which may look like this:

// src/renderer.js
async function renderPdf(pdfDocument) {
	try {
		const pdf = await pdfjsLib.getDocument(pdfDocument).promise;
		const pageNumber = 1;
		const page = await pdf.getPage(pageNumber);
		const scale = 1.5;
		const viewport = page.getViewport({ scale });
		const canvas = document.getElementById('pdfContainer');
		const context = canvas.getContext('2d');
		canvas.height = viewport.height;
		canvas.width = viewport.width;
		const renderContext = {
			canvasContext: context,
			viewport,
		};
		page.render(renderContext);
	} catch (error) {
		console.error(error);
	}
}
renderPdf('../assets/example.pdf'); // This is the path to the PDF file.

You can use any PDF file for testing — including our sample PDF — as long as it’s located in the ./assets folder and referenced by its file name.

Your file directory now should look like this:

electron-pdf-viewer
├── assets
│   └── example.pdf
├── src
│   ├── main.js
│   └── renderer.js
│   └── pdf.min.js
│   └── pdf.worker.min.js
│   └── index.html
├── package.json
└── package-lock.json
  1. And that’s it! Now you can open your application and see the PDF viewer:

npx electron .
Electron PDF.js Desktop PDF Viewer Example

Building an Electron PDF Viewer with PSPDFKit

If you’ve come this far and played a bit with the example application you just built, you may miss some handy features like PDF annotations, thumbnails, and so on.

PSPDFKit for Web, a powerful cross-platform PDF viewer and annotating tool, is compatible with Electron, and it’s easy to install and use. So now, you’ll integrate PSPDFKit into your new or existing Electron projects.

Creating a New Electron Project

  1. Start by creating a new folder and changing the directory into the newly created folder:

mkdir pspdfkit-electron-pspdfkit-viewer && cd pspdfkit-electron-pspdfkit-viewer
  1. Open your code editor and navigate to your project. Initialize a new Node.js project with the following command:

npm init --yes

This will create a package.json file in the root of your project.

  1. Now, install the Electron and electron-packager packages as devDependencies:

npm install --save-dev electron electron-packager
  1. Add a start script in the package.json file to run the application:

{
	"name": "PSPDFKit Electron Example",
	"version": "1.0.0",
	"main": "index.js",
	"license": "MIT",
	"devDependencies": {
		"electron": "^16.0.0",
		"electron-packager": "^15.4.0"
	},
	"scripts": {
		"start": "electron ."
	}
}

Adding PSPDFKit

You can add PSPDFKit to your project as an npm package by running the following command:

npm install pspdfkit

Displaying a PDF

  1. Add the PDF document you want to display to the assets directory. You can use our demo document as an example.

  2. Create an index.html file to set up a mount target in the HTML file of your renderer process:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
		/>
		<title>PSPDFKit for Electron Example App</title>

		<style>
			html,
			body {
				margin: 0;
				padding: 0;
				background: #f6f7fa;
			}

			header {
				display: none;
			}

			#root {
				width: 100vw;
				height: 100vh;
			}

			/**
       * Offset the frameless window alternative on macOS.
       * https://electronjs.org/docs/api/frameless-window#alternatives-on-macos
       */

			body.platform-darwin header {
				-webkit-app-region: drag;
				display: block;
				height: 22px;
				background-color: rgb(252, 253, 254);
			}

			body.platform-darwin #root {
				height: calc(100vh - 22px);
			}
		</style>
	</head>

	<body>
		<header></header>
		<div id="root"></div>

		<script src="./pspdfkit.js"></script>

		<script type="module">
			import { dragAndDrop } from './lib/drag-and-drop.js';
			import { makeToolbarItems } from './lib/toolbar.js';

			document.body.classList.add(
				`platform-${window.electron.processPlatform()}`,
			);
			let instance = null;

			const {
				documentExport,
				documentImport,
				askUserToDiscardChanges,
			} = window.electron;

			let hasUnsavedAnnotations = false;

			function createOnAnnotationsChange() {
				let initialized = false;

				return () => {
					if (initialized) {
						hasUnsavedAnnotations = true;
					} else {
						initialized = true;
					}
				};
			}

			async function load(document) {
				if (instance) {
					PSPDFKit.unload(instance);
					hasUnsavedAnnotations = false;
					instance = null;
				}

				// Create our custom toolbar.
				const toolbarItems = makeToolbarItems(
					PSPDFKit.defaultToolbarItems,
					function exportFile() {
						documentExport(
							instance,
							() => (hasUnsavedAnnotations = false),
						);
					},
					function importFile() {
						if (hasUnsavedAnnotations) {
							askUserToDiscardChanges(() =>
								documentImport(load),
							);
						} else {
							documentImport(load);
						}
					},
				);

				// Set up the configuration object. A custom style sheet is used to customize
				// the look and feel of PSPDFKit.
				const configuration = {
					document,
					container: '#root',
					styleSheet: ['./pspdfkit.css'],
					// `appName` must match the license's bundle ID.
					appName: 'pspdfkit-electron-example',
					// Add when using a license key
					// `licenseKey`: "LICENSE KEY GOES HERE",
				};

				instance = await PSPDFKit.load(configuration);

				instance.setToolbarItems(toolbarItems);
				instance.addEventListener(
					'annotations.change',
					createOnAnnotationsChange(),
				);

				dragAndDrop(instance, (file) => {
					if (hasUnsavedAnnotations) {
						askUserToDiscardChanges(() => load(file));
					} else {
						load(file);
					}
				});
			}

			// Open a default document when the app is started.
			window.onload = () => load('./assets/example.pdf');
		</script>
	</body>
</html>
  1. Create a preload script to run before the main renderer process:

const { contextBridge } = require("electron");

const {
  documentExport,
  documentImport,
  askUserToDiscardChanges,
} = require("./lib/modals");

// Electron helpers exposed in the browser context as `window.electron`.
contextBridge.exposeInMainWorld("electron", {
  processPlatform: () => process.platform,
  documentExport,
  documentImport,
  askUserToDiscardChanges,
});

Preload scripts have access to Node.js APIs and can be used to expose specific functionalities of the Node.js API to the renderer process window object through the contextBridge module. This overcomes some serious security implications present when using node integration.

  1. Now, create the main JavaScript file to get access to the preload script inside the BrowserWindow constructor’s webPreferences option:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const pspdfkitMain = require('./lib/pspdfkit-main');

const path = require('path');
const url = require('url');

let mainWindow = null;
let windowInCreation = false;

function createWindow() {
	windowInCreation = true;
	pspdfkitMain.initialize();

	// Create the browser window.
	mainWindow = new BrowserWindow({
		width: 1200,
		height: 800,
		titleBarStyle: 'hidden',
		webPreferences: {
			contextIsolation: true,
			nodeIntegration: false,
			preload: path.join(__dirname, 'preload.js'),
		},
	});

	windowInCreation = false;

	// And load the `index.html` of the app.
	mainWindow.loadURL(
		url.format({
			pathname: path.join(__dirname, 'index.html'),
			protocol: 'file:',
			slashes: true,
		}),
	);

	// Open the DevTools.
	// mainWindow.webContents.openDevTools();

	// Emitted when the window is closed.
	mainWindow.on('closed', function () {
		mainWindow = null;
		pspdfkitMain.cleanup();
	});
}

app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
	if (process.platform !== 'darwin') {
		app.quit();
	}
});

app.on('activate', function () {
	if (mainWindow === null && windowInCreation === false) {
		createWindow();
	}
});

By setting contextIsolation to true in webPreferences, you won’t expose the Node.js runtime, which makes your application more secure. Read more about context isolation, migrating from older versions, and why you shouldn’t use remote modules in your Electron applications.

Information

From Electron 12 and later, context isolation defaults to `true`. This means if you’re using a lower version of Electron, you’ll need to set the `BrowserWindow` constructor’s `webPreferences` option to `true` to enable context isolation.

mainWindow = new BrowserWindow({
	webPreferences: {
		contextIsolation: true,
		nodeIntegration: false,
		preload: path.join(__dirname, 'preload.js'),
	},
});
  1. Finally, you can start your application:

npm start

Conclusion

In this post, you saw how to build an Electron PDF viewer with both the PDF.js open source library and our Electron PDF SDK that enables you to display and render PDF files in your web application.

PDF.js offers a great low-cost solution and is optimal for simple use cases where the primary objective is viewing PDF documents. However, for more complex use cases that involve annotating, signing, forms, etc., a commercial PDF viewer can provide some additional benefits — including speeding up development time and reducing costs from not having to build out these functions in-house.

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 advanced use cases. Try it for free, or visit our demo to see it in action.

We created similar how-to blog posts using different web frameworks and libraries:

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