JavaScript PDF editor tutorial using pdf‑lib
Table of contents
Learn how to build a JavaScript PDF editor with pdf-lib (open source) for basic operations, or use Nutrient for advanced features like annotations, signatures, and real-time collaboration.
If you prefer a video walkthrough, check out our step-by-step guide:
Introduction to pdf-lib
pdf-lib(opens in a new tab) is a JavaScript library for creating and modifying PDF documents in any JavaScript environment, including browsers, Node.js, Deno, and React Native. With pdf-lib, you can create PDFs from scratch, modify existing PDFs, and perform various operations such as adding text, images, and vector graphics.
Prerequisites
Before starting this tutorial, ensure you have:
- Basic knowledge of HTML and JavaScript
- A modern web browser (Chrome, Firefox, Safari, or Edge)
- A text editor or IDE
- Node.js installed (optional, for serving the project locally)
Choosing the right JavaScript PDF editor
For open source options, pdf-lib(opens in a new tab) is a solid choice. Its primary features include:
- Support for modifying existing documents
- Compatibility across all JavaScript environments
For commercial needs, Nutrient’s JavaScript PDF editor offers enhanced functionality:
- Ready-to-use user interface (UI) — A fully customizable viewer and editor interface that works out of the box, saving months of development time.
- Annotations — Support for 18+ annotation types, including highlights, comments, stamps, ink drawings, shapes, and measurement tools.
- Digital signatures — Create, validate, and manage PDF digital signatures with full certificate support.
- Form handling — Advanced form creation, filling, and validation with support for all PDF form field types.
- Real-time collaboration — Instant synchronization lets multiple users edit the same document simultaneously.
- Redaction — Permanently remove sensitive content from documents with certified redaction tools.
- OCR — Extract searchable text from scanned documents and images.
- Document editing — Merge, split, rotate, and reorder pages with a drag-and-drop interface.
- Office document support — View and edit Word, Excel, and PowerPoint documents directly in the browser.
- Cross-platform SDKs — Native support for iOS, Android, React Native, Flutter, and more.
- Enterprise support — Dedicated technical support, SLAs, and professional services.
pdf-lib vs. Nutrient comparison
| Feature | pdf-lib | Nutrient |
|---|---|---|
| License | MIT (free) | Commercial |
| Create and modify PDFs | ✅ | ✅ |
| Add and remove pages | ✅ | ✅ |
| Embed text and images | ✅ | ✅ |
| Fill form fields | ✅ | ✅ |
| Prebuilt UI | ❌ | ✅ |
| Annotations (18+ types) | ❌ | ✅ |
| Digital signatures | ❌ | ✅ |
| Real-time collaboration | ❌ | ✅ |
| Redaction | ❌ | ✅ |
| OCR | ❌ | ✅ |
| Office document viewing | ❌ | ✅ |
| Mobile SDKs | ❌ | ✅ |
| Technical support | Community | Dedicated |
When to choose each option
Choose pdf-lib when
- You need basic PDF manipulation (create, modify, and fill forms).
- Your project has a limited budget.
- You’re building a simple internal tool.
- You don’t need a prebuilt UI.
Choose Nutrient when
- You need advanced features like annotations, signatures, or collaboration.
- You want a production-ready UI without building one from scratch.
- Your application requires enterprise-grade security and compliance.
- You need to support multiple platforms (web, iOS, and Android).
- You want dedicated technical support and documentation.
Building a simple PDF editor with pdf-lib
This tutorial will cover setting up a basic PDF editor that can add, remove, and draw text on pages.
Initial setup
Create a basic HTML structure to initialize the 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>Loading and rendering the document
With pdf-lib, you can load an existing PDF or create a new one. For this tutorial, you’ll load an existing PDF from the project folder:
const { PDFDocument } = PDFLib;
let pdfDoc;
async function loadPdf() { const url = "./demo.pdf"; const existingPdfBytes = await fetch(url).then((res) => res.arrayBuffer()); return PDFDocument.load(existingPdfBytes);}
async function saveAndRender(doc) { const pdfBytes = await doc.save(); const pdfDataUri = await doc.saveAsBase64({ dataUri: true }); document.getElementById("pdf").src = pdfDataUri;}Adding and removing pages
Create buttons to add or remove pages:
<div> <button onclick="addPage()">Add page</button> <button onclick="removePage()">Remove page</button></div>Then add these 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);}Drawing text on pages
To add text to a page, use drawText() from pdf-lib:
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("New Page Content", { x: 50, y: height - 4 * fontSize, size: fontSize, font: timesRomanFont, color: rgb(0, 0.53, 0.71), }); return doc;}Embedding images and fonts
To embed images, use embedJpg or embedPng with the image data:
async function embedImage() { const imageBytes = await fetch("image.png").then((res) => res.arrayBuffer()); const image = await pdfDoc.embedPng(imageBytes); const page = pdfDoc.addPage(); page.drawImage(image, { x: 100, y: 100, width: 200, height: 150 }); await saveAndRender();}You can also embed fonts with embedFont:
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);Filling form fields
To fill form fields in existing PDFs, retrieve the form fields and set values:
async function fillForm() { const formPdfBytes = await fetch("form.pdf").then((res) => res.arrayBuffer()); pdfDoc = await PDFDocument.load(formPdfBytes);
const form = pdfDoc.getForm(); const nameField = form.getTextField("name"); nameField.setText("Jane Doe");
await saveAndRender(pdfDoc);}Setting document metadata
You can set metadata like title, author, and subject:
async function setMetadata() { pdfDoc.setTitle("My PDF Document"); pdfDoc.setAuthor("John Doe"); pdfDoc.setSubject("This is a sample PDF document"); await saveAndRender(pdfDoc);}This is how your final HTML and JavaScript structure should look:
110 collapsed lines
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>PDF Editor</title> <script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script> </head> <body> <h1>PDF Editor</h1> <button onclick="addPage()">Add Page</button> <button onclick="removePage()">Remove Page</button> <button onclick="embedImage()">Embed Image</button> <button onclick="fillForm()">Fill Form</button> <button onclick="setMetadata()">Set Metadata</button> <iframe id="pdfViewer" width="100%" height="600"></iframe>
<script> let pdfDoc; const pdfViewer = document.getElementById("pdfViewer");
// Load an existing PDF document on initialization. async function loadPdf() { const existingPdfBytes = await fetch("demo.pdf").then((res) => res.arrayBuffer(), ); pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes); await saveAndRender(); }
// Save and display the PDF in the iframe. async function saveAndRender() { const pdfBytes = await pdfDoc.saveAsBase64({ dataUri: true, }); pdfViewer.src = pdfBytes; }
// Add a new page with text. async function addPage() { const page = pdfDoc.addPage(); const font = await pdfDoc.embedFont(PDFLib.StandardFonts.Helvetica); page.drawText("New Page", { x: 50, y: 700, font, size: 24, }); await saveAndRender(); }
// Remove the last page. async function removePage() { if (pdfDoc.getPageCount() > 1) { // Ensure at least one page remains. pdfDoc.removePage(pdfDoc.getPageCount() - 1); await saveAndRender(); } else { alert("Cannot remove the last page!"); } }
// Embed an image into the PDF. async function embedImage() { const imageBytes = await fetch("image.png").then((res) => res.arrayBuffer(), ); const image = await pdfDoc.embedPng(imageBytes); const page = pdfDoc.addPage(); page.drawImage(image, { x: 100, y: 100, width: 200, height: 150, }); await saveAndRender(); }
// Fill out form fields (assuming the PDF has form fields). async function fillForm() { if (!pdfDoc) { alert("Load a PDF with form fields to use this feature."); return; } try { const form = pdfDoc.getForm(); const nameField = form.getTextField("name"); // Change 'name' to your form field name. nameField.setText("Jane Doe"); await saveAndRender(); } catch (e) { console.error("Form filling error:", e); alert( "Error filling form. Make sure the PDF has the correct form fields.", ); } }
// Set metadata for the PDF document. async function setMetadata() { pdfDoc.setTitle("My PDF Document"); pdfDoc.setAuthor("John Doe"); pdfDoc.setSubject("This is a sample PDF document"); await saveAndRender(); }
// Initial load. loadPdf(); </script> </body></html>Integrating Nutrient’s JavaScript PDF editor
This section walks you through installing the Nutrient SDK, copying the required assets, and initializing the viewer in your project.
Adding Nutrient to your project
First, you need to have:
- The latest stable version of Node.js(opens in a new tab).
- A package manager compatible with npm(opens in a new tab), such as Yarn(opens in a new tab). The examples in this tutorial use
yarn.
Install the
@nutrient-sdk/viewerpackage via npm:Copy the required viewer artifacts to your
assetsdirectory:Terminal window cp -R ./node_modules/@nutrient-sdk/viewer/dist/ ./assets/
Make sure the assets directory contains:
nutrient-viewer.js(or an equivalent main script)- A
nutrient-viewer-lib/directory with supporting library files
Integrating into your project
Include a sample document (such as
document.pdf) in the public or root folder of your project. You can use our demo image as an example.Add a mounting
<div>and a script reference to your HTML:<div id="nutrient" style="width: 100%; height: 100vh;"></div><script type="module" src="index.js"></script>Import the viewer in your JavaScript entry file:
import "./assets/nutrient-viewer.js";Initialize the viewer using
NutrientViewer.load():const baseUrl = `${window.location.protocol}//${window.location.host}/assets/`;NutrientViewer.load({baseUrl,container: "#nutrient",document: "document.pdf",}).then((instance) => {console.log("Nutrient loaded", instance);}).catch((error) => {console.error(error.message);});
Serving your website
Install the
servepackage globally:Terminal window npm install --global serveStart a local server from the current directory:
Terminal window serve -l 8080 .Open your browser and go to
http://localhost:8080to view your website.
All the features you built using pdf-lib are already present out of the box in our SDK, so you don’t need to do anything else.
Enabling annotations
Annotations are enabled by default. You can customize the annotation toolbar by filtering or rearranging tools:
NutrientViewer.load({ baseUrl, container: "#nutrient", document: "document.pdf", // Customize the annotation toolbar for ink annotations. annotationToolbarItems: (annotation, { defaultAnnotationToolbarItems }) => { if (annotation instanceof NutrientViewer.Annotations.InkAnnotation) { // Remove the delete button for ink annotations. return defaultAnnotationToolbarItems.filter( (item) => item.type !== "delete" ); } return defaultAnnotationToolbarItems; },}) .then((instance) => { console.log("Annotations enabled"); }) .catch(console.error);Adding electronic signatures
Enable electronic signature capabilities with custom creation modes:
NutrientViewer.load({ baseUrl, container: "#nutrient", document: "document.pdf", electronicSignatures: { // Configure which signature creation modes are available. creationModes: [ NutrientViewer.ElectronicSignatureCreationMode.TYPE, NutrientViewer.ElectronicSignatureCreationMode.IMAGE, ], },}) .then((instance) => { console.log("Electronic signatures enabled"); }) .catch(console.error);Customizing the toolbar
Tailor the UI to your application’s needs by filtering or extending the default toolbar:
NutrientViewer.load({ baseUrl, container: "#nutrient", document: "document.pdf", // Remove the print button from the toolbar. toolbarItems: NutrientViewer.defaultToolbarItems.filter( (item) => item.type !== "print" ),}) .then((instance) => { console.log("Custom toolbar loaded"); }) .catch(console.error);You can also add custom toolbar items:
NutrientViewer.load({ baseUrl, container: "#nutrient", document: "document.pdf", toolbarItems: [ ...NutrientViewer.defaultToolbarItems, { type: "content-editor", dropdownGroup: "editor" }, ],}) .then((instance) => { console.log("Extended toolbar loaded"); }) .catch(console.error);Extracting text from PDFs
Use the built-in text extraction API:
NutrientViewer.load({ baseUrl, container: "#nutrient", document: "document.pdf",}) .then(async (instance) => { // Get text content from page 0. const textLines = await instance.textLinesForPageIndex(0); const fullText = textLines.map((line) => line.contents).join("\n"); console.log("Extracted text:", fullText); }) .catch(console.error);Use cases
- Document review workflows — Real-time collaboration for teams to annotate and approve contracts with full audit trails.
- eSignature applications — Electronic and digital signatures with eIDAS and ESIGN compliance.
- Customer portals — Self-service document viewing, filling, and signing with a customizable UI.
- Healthcare and insurance — Form processing, OCR, and HIPAA-compliant redaction.
- Education platforms — Course material distribution with annotation tools for grading and feedback.
Conclusion
pdf-lib is a good free JavaScript PDF editor option for modifying a PDF document. However, implementing a feature-rich editor requires significant effort. And sometimes businesses require more complex features, such as:
- An annotation syncing framework to persist changes to a PDF across devices
- The ability to add and read digital signatures with specialized APIs
- An easy-to-integrate UI that enables you to customize every aspect of the viewer
At Nutrient, 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.
FAQ
You can build a PDF editor using JavaScript by leveraging libraries like pdf-lib, Nutrient, or PDF.js. These libraries provide APIs to create, modify, and interact with PDF documents.
Install the chosen library using npm or a CDN, initialize the library in your JavaScript code, and use its methods to load, edit, and save PDF documents. You can add features like text editing, annotations, and form filling.
Yes. Most PDF libraries support adding annotations, such as highlights, comments, and shapes. You can use the library’s API to create and manipulate these annotations.
A JavaScript-based PDF editor allows for client-side processing, reducing the need for server resources. It also provides a seamless user experience by enabling PDF editing directly within the browser.
Common challenges include handling large PDF files, ensuring cross-browser compatibility, maintaining performance, and providing a user-friendly interface for editing complex documents.