The Portable Document Format (PDF) is the de facto standard for sharing documents, but for a long time, creating and manipulating them on the web has been a server-side affair. That's no longer the case. Thanks to powerful libraries like pdf-lib.js, you can now build sophisticated PDF tools that run entirely in the user's browser. This guide will walk you through creating a complete, browser-based PDF editor from scratch.
While other libraries like jsPDF are excellent for generating PDFs from scratch, `pdf-lib.js` excels at both creating new documents and, more importantly, modifying existing ones. Its API is intuitive and robust, allowing you to:
This makes it the perfect choice for building an editor where users can interact with and modify existing PDF files.
First, we need an HTML file to serve as the foundation of our application. This file will contain the user interface (UI) elements for our editor and the necessary script tags to include `pdf-lib` and Prism.js for code highlighting.
We'll include the `pdf-lib.js` library from a CDN. We'll also include Prism.js for syntax highlighting, along with its line-number and copy-to-clipboard plugins to make the code easy to read and use.
The UI for our editor will be simple but effective. We need:
This is where the magic happens. We'll write JavaScript code to handle the following tasks:
The heart of our editor is the `modifyPdf` function. Let's break down its key parts.
First, we load the raw PDF data into a `PDFDocument` object. This object is our gateway to interacting with the PDF's content.
const pdfDoc = await PDFDocument.load(existingPdfBytes);
To draw text, we need a font. `pdf-lib` allows you to embed custom fonts, but for simplicity, we'll use one of the standard fonts that all PDF viewers support.
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
We get an array of all the pages in the document and then access the specific page the user wants to edit. Remember that pages are zero-indexed, so we subtract 1 from the user's input.
const pages = pdfDoc.getPages();
const page = pages[pageNumber - 1]; // Page numbers are 1-based in UI
This is where we perform the actual modification. We use the `page.drawText` method, providing the text, coordinates, font, and size. A crucial detail is the coordinate system: `pdf-lib`'s origin `(0, 0)` is at the bottom-left corner of the page, not the top-left like in most browser contexts.
page.drawText(textToAdd, {
x: x,
y: y,
font: helveticaFont,
size: 24,
color: rgb(0.95, 0.1, 0.1),
});
After modifying the document, we save it. This converts the `PDFDocument` object into a `Uint8Array`, which is a binary representation of the PDF file.
const pdfBytes = await pdfDoc.save();
This `Uint8Array` can then be used to create a `Blob` and generate a URL for downloading.
Here is the complete code for our browser-based PDF editor. You can save this as a single HTML file and open it in your web browser to run the application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Editor with pdf-lib.js</title>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<style>
body { font-family: sans-serif; padding: 2em; }
.container { max-width: 800px; margin: auto; }
.form-group { margin-bottom: 1em; }
label { display: block; margin-bottom: .5em; }
input[type="text"], input[type="number"], input[type="file"] { width: 100%; padding: .5em; }
button { padding: .7em 1.5em; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Browser-Based PDF Editor</h1>
<div class="form-group">
<label for="pdfFile">Upload PDF:</label>
<input type="file" id="pdfFile" accept=".pdf">
</div>
<div class="form-group">
<label for="textToAdd">Text to Add:</label>
<input type="text" id="textToAdd" placeholder="Enter text...">
</div>
<div class="form-group">
<label for="pageNumber">Page Number:</label>
<input type="number" id="pageNumber" value="1" min="1">
</div>
<div class="form-group">
<label for="xCoord">X Coordinate:</label>
<input type="number" id="xCoord" value="50">
</div>
<div class="form-group">
<label for="yCoord">Y Coordinate:</label>
<input type="number" id="yCoord" value="50">
</div>
<button id="modifyBtn">Add Text to PDF</button>
<div class="form-group" style="margin-top: 2em;">
<a id="downloadLink" style="display:none;">Download Modified PDF</a>
</div>
</div>
<script>
const { PDFDocument, rgb, StandardFonts } = PDFLib;
const fileInput = document.getElementById('pdfFile');
const modifyBtn = document.getElementById('modifyBtn');
const downloadLink = document.getElementById('downloadLink');
let pdfBytes = null;
fileInput.addEventListener('change', async (e) => {
if (e.target.files.length > 0) {
const file = e.target.files[0];
pdfBytes = await file.arrayBuffer();
console.log("PDF loaded.");
}
});
modifyBtn.addEventListener('click', async () => {
if (!pdfBytes) {
alert("Please upload a PDF file first.");
return;
}
const textToAdd = document.getElementById('textToAdd').value;
const pageNumber = parseInt(document.getElementById('pageNumber').value);
const x = parseInt(document.getElementById('xCoord').value);
const y = parseInt(document.getElementById('yCoord').value);
if (!textToAdd) {
alert("Please enter text to add.");
return;
}
try {
const modifiedPdfBytes = await modifyPdf(pdfBytes, textToAdd, pageNumber, x, y);
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.download = 'modified.pdf';
downloadLink.style.display = 'block';
alert("PDF modified successfully! Click the download link.");
} catch (error) {
console.error(error);
alert("Failed to modify PDF. Check console for details.");
}
});
async function modifyPdf(existingPdfBytes, textToAdd, pageNumber, x, y) {
// Load a PDFDocument from the existing PDF bytes
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Embed the Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Get the page to modify
const pages = pdfDoc.getPages();
if (pageNumber > pages.length || pageNumber < 1) {
throw new Error(`Invalid page number: ${pageNumber}. PDF has ${pages.length} pages.`);
}
const page = pages[pageNumber - 1]; // pdf-lib pages are 0-indexed
// Draw a string of text diagonally across the first page
page.drawText(textToAdd, {
x: x,
y: y,
font: helveticaFont,
size: 24,
color: rgb(0.95, 0.1, 0.1),
});
// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();
return pdfBytes;
}
</script>
</body>
</html>
You've just built a fully functional, client-side PDF editor! This is a powerful starting point. From here, you could expand the functionality in numerous ways:
By leveraging `pdf-lib.js`, you've unlocked the ability to create rich, interactive PDF experiences directly in the browser, opening up a world of possibilities for your web applications.