Blog Post

How to Build a JavaScript PDF Editor with pdf-lib

Illustration: How to Build a JavaScript PDF Editor with pdf-lib

In the first part of this tutorial, we’ll show how to build a simple free JavaScript PDF editor using pdf-lib. In the second part, we’ll walk through how you can deploy PDF editing in PSPDFKit’s JavaScript PDF Viewer.

Choosing the Right JavaScript PDF Editor

If you’re looking for an open source PDF-processing library, pdf-lib is a great option. According to its GitHub page, pdf-lib’s main features are that it:

  • Supports modification (editing) of existing documents.

  • Works in all JavaScript environments.

If you’re looking for a commercial JavaScript PDF editor, our Web SDK offers some additional benefits:

  • A customizable out-of-the-box UI to deploy faster and save developer resources.

  • The ability to persist your PDF edits and changes across devices with a syncing framework like PSPDFKit Instant.

  • More features — like annotating and collaborating on PDFs, redaction, and digital signatures.

  • Access to dedicated customer support to help speed up deployment.

So now, let’s move on to building a simple PDF editor that adds and removes pages and draws text on a document.

Building a PDF Editor with pdf-lib

This section will cover how you can use pdf-lib to create and integrate a PDF editor into your project.

Initial Setup

Before diving into the editor implementation, we’ll lay out a simple HTML skeleton to initialize our project:

<html>
	<head>
		<meta charset="utf-8" />
		<script src="https://unpkg.com/pdf-lib@1.4.0"></script>
		<script src="https://unpkg.com/downloadjs@1.4.7"></script>
	</head>
	<body>
		<iframe id="pdf" style="width: 90%; height: 90%;"></iframe>
	</body>
</html>

As you can see, we’re using the hosted version from unpkg, but you could also download the pdf-lib library via a package manager like npm or Yarn.

Loading and Rendering the Document

With pdf-lib, you can either create a new document or modify an existing one. For our example, we’ll load an existing document from our project folder. Please note that the PDF needs to be embedded in an iframe tag.

Here’s how we load our document:

const { PDFDocument } = PDFLib;

let pdfDoc;

async function loadPdf() {
	// Fetch an existing PDF document.
	const url = './demo.pdf';
	const existingPdfBytes = await fetch(url).then((res) => res.arrayBuffer());

	// Load a `PDFDocument` from the existing PDF bytes.
	return PDFDocument.load(existingPdfBytes);
}

And here’s how we render it:

async function saveAndRender(doc) {
	// Serialize the `PDFDocument` to bytes (a `Uint8Array`).
	const pdfBytes = await doc.save();

	const pdfDataUri = await doc.saveAsBase64({ dataUri: true });
	document.getElementById('pdf').src = pdfDataUri;
}

Adding and Removing Pages

To add or remove the pages, we create two buttons:

<body>
	<div>
		<button onclick="addPage()">Add page</button>
		<button onclick="removePage()">Remove page</button>
	</div>
	<iframe id="pdf" style="width: 90%; height: 90%;"></iframe>
</body>

Every time they’re clicked, they each call one of these two functions:

async function addPageToDoc(doc) {
	doc.addPage();
	return doc;
}

async function removePageToDoc(doc) {
	const totalPages = doc.getPageCount();
	doc.removePage(totalPages - 1);
	return doc;
}

async function addPage() {
	pdfDoc = await addPageToDoc(pdfDoc);
	await saveAndRender(pdfDoc);
}

async function removePage() {
	pdfDoc = await removePageToDoc(pdfDoc);
	await saveAndRender(pdfDoc);
}

Our buttons add and remove pages at the end of the document, but if, for example, you’d like to add a page at a specific page index, you can use the .insertPage(index, page?) method instead of addPage().

Drawing Text

Now let’s say we’d like to draw some text on the added pages. pdf-lib exposes a drawText() API, so we can achieve this by adding few lines of code to our addPageToDoc function:

async function addPageToDoc(doc) {
	const page = doc.addPage();
	const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);
	const { width, height } = page.getSize();
	const fontSize = 30;
	page.drawText('Adding a page in JavaScript is awesome!', {
		x: 50,
		y: height - 4 * fontSize,
		size: fontSize,
		font: timesRomanFont,
		color: rgb(0, 0.53, 0.71),
	});

	return doc;
}

This is how our code will look in the end:

<html>
 <head>
   <meta charset="utf-8" />
   <script src="https://unpkg.com/pdf-lib@1.4.0"></script>
   <script src="https://unpkg.com/downloadjs@1.4.7"></script>
 </head>

 <body>
   <div>
   <button onclick="addPage()">Add page</button>
   <button onclick="removePage()">Remove page</button>
 </div>
   <iframe id="pdf" style="width: 90%; height: 90%;"></iframe>
 </body>

 <script>
   const { degrees, PDFDocument, rgb, StandardFonts } = PDFLib
   let pdfDoc;

   async function loadPdf() {
     // Fetch an existing PDF document.
     const url = './demo.pdf'
     const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())

     // Load a `PDFDocument` from the existing PDF bytes.
     return await PDFDocument.load(existingPdfBytes)
   }

   async function addPageToDoc(doc) {
     const page = doc.addPage()
     const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
     const { width, height } = page.getSize()
     const fontSize = 30
     page.drawText('Adding a page in JavaScript is awesome!', {
       x: 50,
       y: height - 4 * fontSize,
       size: fontSize,
       font: timesRomanFont,
       color: rgb(0, 0.53, 0.71),
     })

     return doc;
   }

   async function removePageToDoc(doc) {
     const totalPages = doc.getPageCount()
     doc.removePage(totalPages - 1)
     return doc;
   }

   async function saveAndRender(doc) {
     // Serialize the `PDFDocument` to bytes (a `Uint8Array`).
     const pdfBytes = await doc.save()

     const pdfDataUri = await doc.saveAsBase64({ dataUri: true });
     document.getElementById('pdf').src = pdfDataUri;
   }

   async function addPage() {
     pdfDoc = await addPageToDoc(pdfDoc);
     await saveAndRender(pdfDoc);
   }

   async function removePage() {
     pdfDoc = await removePageToDoc(pdfDoc);
     await saveAndRender(pdfDoc)
   }

   loadPdf().then((doc) => {
     pdfDoc = doc;
     return saveAndRender(pdfDoc);
   })

 </script>
</html>

Integrating PSPDFKit’s JavaScript PDF Editor

This next section will cover how to integrate PSPDFKit’s JavaScript PDF editor into your project.

Adding PSPDFKit to Your Project

First, you need to have:

PSPDFKit for Web library files are distributed as an archive that can be installed as an npm module by running:

yarn add pspdfkit

Copy the PSPDFKit for Web distribution to the assets directory in your project’s folder:

cp -R ./node_modules/pspdfkit/dist/ ./assets/

Make sure your assets directory contains the pspdfkit.js file and a pspdfkit-lib directory with the library assets.

Integrating into Your Project

Add the PDF document you want to display to your project’s directory. For a quick test, you can use our the License.pdf document, which you’ll find in the PSPDFKit for Web directory.

Now, let’s lay out another simple HTML skeleton to initialize our project:

<!DOCTYPE html>
<html>
	<head>
		<title>My App</title>
		<!-- Provide proper viewport information so that the layout works on mobile devices. -->
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
		/>
	</head>
	<body></body>
</html>

Add an empty <div> element with a defined width and height to where PSPDFKit will be mounted:

<div id="pspdfkit" style="width: 100%; height: 100vh;"></div>

Import pspdfkit into your application and initialize PSPDFKit for Web in JavaScript by calling PSPDFKit.load():

import './assets/pspdfkit.js';

// We need to inform PSPDFKit where to look for its library assets, i.e. the location of the `pspdfkit-lib` directory.
const baseUrl = `${window.location.protocol}//${window.location.host}/assets/`;

PSPDFKit.load({
	baseUrl,
	container: '#pspdfkit',
	document: 'path/to/document.pdf',
})
	.then((instance) => {
		console.log('PSPDFKit loaded', instance);
	})
	.catch((error) => {
		console.error(error.message);
	});

Import index.js into your HTML page:

<script type="module" src="index.js"></script>

Here’s how the final HTML file will look:

<!DOCTYPE html>
<html>
	<head>
		<title>My App</title>
		<!-- Provide proper viewport information so that the layout works on mobile devices. -->
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
		/>
	</head>
	<body>
		<!-- Element where PSPDFKit will be mounted. -->
		<div id="pspdfkit" style="width: 100%; height: 100vh;"></div>

		<script type="module" src="index.js"></script>
	</body>
</html>

The last thing left to do is serve your website.

We’ll use the npm serve package as a simple HTTP server.

  • Install the serve package:

yarn global add serve
  • Serve the contents of the current directory:

serve -l 8080 .

Open http://localhost:8080 in your browser to view the website.

And that’s it! All the features we built using pdf-lib are already present out of the box in our SDK, so we don’t need to do anything else.

Conclusion

pdf-lib is a good free JavaScript PDF editor option for modifying a PDF document, and it’s a great choice in many cases. However, implementing a feature-rich editor isn’t a trivial task. And sometimes businesses require more complex features, such as:

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. 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
DEVELOPMENT  |  Web • TypeScript • Insights

Migrating Our Web Codebase from Flow to TypeScript

DEVELOPMENT  |  Web • JavaScript • Tips

Practical Uses of Object URLs

PRODUCTS  |  Web • Releases • Components

PSPDFKit for Web 2021.6 Adds Document Comparison and Date/Time Picker Feature