JavaScript PDF editor tutorial using pdf‑lib

Table of contents

    This tutorial will guide you through building a simple JavaScript PDF editor with pdf-lib. Then, it’ll show how to deploy your editor in Nutrient’s JavaScript PDF viewer.
    JavaScript PDF editor tutorial using pdf‑lib
    TL;DR

    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:

    YouTube video player

    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 collaborationInstant 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 supportView 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

    Featurepdf-libNutrient
    LicenseMIT (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 supportCommunityDedicated

    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:

    index.html
    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

    1. First, you need to have:

    2. Install the @nutrient-sdk/viewer package via npm:

      Terminal window
      npm i @nutrient-sdk/viewer
    3. Copy the required viewer artifacts to your assets directory:

      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

    1. 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.

    2. 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>
    3. Import the viewer in your JavaScript entry file:

      import "./assets/nutrient-viewer.js";
    4. 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

    1. Install the serve package globally:

      Terminal window
      npm install --global serve
    2. Start a local server from the current directory:

      Terminal window
      serve -l 8080 .
    3. Open your browser and go to http://localhost:8080 to 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:

    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

    How can I build a PDF editor using JavaScript?

    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.

    What are the basic steps to create a PDF editor in JavaScript?

    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.

    Can I add annotations to PDFs using a JavaScript PDF editor?

    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.

    What are the benefits of using a JavaScript-based PDF editor?

    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.

    What are some common challenges when building a PDF editor with JavaScript?

    Common challenges include handling large PDF files, ensuring cross-browser compatibility, maintaining performance, and providing a user-friendly interface for editing complex documents.

    Veronica Marini

    Veronica Marini

    Web Senior Software Engineer

    Veronica’s passion for puzzles got her into programming. She likes everything frontend, bringing design to life, and measuring herself with coding. She also collects hobbies: from yoga to surfing to playing Brazilian drums.

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

    Hulya is a frontend web developer and technical writer who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    Try for free Ready to get started?