PDF.js text highlighting: Display annotations in JavaScript

Table of contents

    Display existing PDF highlights with PDF.js and HTML overlays, or create new highlights with Nutrient Web SDK and export an annotated PDF.
    PDF.js text highlighting: Display annotations in JavaScript
    TL;DR
    • PDF.js — Read existing highlight annotations and display their bounding rectangles as HTML overlays. The overlays don’t modify the PDF.
    • Nutrient Web SDK — Create highlight annotations programmatically and export an annotated PDF for download or storage.

    PDF.js powers Firefox’s full PDF viewer, which supports creating highlights and saving annotated PDFs(opens in a new tab). That editor is separate from the minimal canvas example in this tutorial: Calling getAnnotations() reads annotations that already exist.

    Downloading the PDF.js library

    Download(opens in a new tab) the PDF.js library as a ZIP file, clone the repository using Git, or install it via npm:

    Terminal window
    npm install pdfjs-dist@6.3.289

    This example was tested with PDF.js 6.3.289 in Chromium.

    If you downloaded the ZIP file, extract it and copy the pdf.mjs and pdf.worker.mjs files from the build/ folder to your project directory. If you installed via npm, copy them from node_modules/pdfjs-dist/build/:

    Terminal window
    cp node_modules/pdfjs-dist/build/pdf.mjs node_modules/pdfjs-dist/build/pdf.worker.mjs .

    Loading and rendering PDF documents

    Serve these files over HTTP using a local server, such as npx serve ., and open the address it prints. Browser module imports and PDF loading won’t work reliably from a file:// URL.

    Save this example as index.html alongside the PDF.js files. Download the PDF.js demo document as annotation.pdf, or replace that file name with your PDF document path. Before opening the page, add the renderHighlightAnnotations() function from the next section to the same script block:

    <!doctype html>
    <html>
    <head>
    <title>PDF.js Highlight Annotations</title>
    <style>
    body {
    margin: 0;
    padding: 20px;
    position: relative;
    }
    #canvas {
    border: 1px solid #ccc;
    }
    </style>
    </head>
    <body>
    <!-- Canvas to place the PDF. -->
    <canvas id="canvas"></canvas>
    <script type="module">
    import * as pdfjsLib from './pdf.mjs';
    // Get the canvas element.
    const canvas = document.getElementById('canvas');
    // Get the PDF file URL.
    const pdfUrl = 'annotation.pdf'; // Replace with the URL of your PDF document.
    // Configure the PDF.js worker source.
    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.mjs';
    // Load the PDF file using PDF.js.
    pdfjsLib.getDocument({ url: pdfUrl }).promise.then(function (pdfDoc) {
    // Get the first page of the PDF file.
    return pdfDoc.getPage(1).then(function (page) {
    const viewport = page.getViewport({ scale: 1 });
    // Set the canvas dimensions to match the PDF page size.
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    // Set the canvas rendering context.
    const ctx = canvas.getContext('2d');
    const renderContext = {
    canvasContext: ctx,
    viewport: viewport,
    // Draw annotations separately in the overlay below.
    annotationMode: pdfjsLib.AnnotationMode.DISABLE,
    };
    // Render the PDF page to the canvas.
    return page.render(renderContext).promise.then(function () {
    console.log('Rendering complete');
    // Call the function to render highlight annotations after the PDF page is rendered.
    return renderHighlightAnnotations(page, viewport);
    });
    });
    }).catch(function (error) {
    console.error(
    'Unable to load or render the PDF. Check the PDF and worker URLs.',
    error,
    );
    });
    </script>
    </body>
    </html>

    Displaying existing highlights with PDF.js

    Use the getAnnotations() method to retrieve existing annotations from the PDF page. Add the following function inside your <script type="module"> block:

    function renderHighlightAnnotations(page, viewport) {
    // Position the overlay over the canvas at its current size and location.
    const parent = canvas.parentNode;
    if (getComputedStyle(parent).position === 'static') {
    parent.style.position = 'relative';
    }
    const overlay = document.createElement('div');
    overlay.style.position = 'absolute';
    overlay.style.left = canvas.offsetLeft + canvas.clientLeft + 'px';
    overlay.style.top = canvas.offsetTop + canvas.clientTop + 'px';
    overlay.style.width = viewport.width + 'px';
    overlay.style.height = viewport.height + 'px';
    overlay.style.pointerEvents = 'none';
    parent.appendChild(overlay);
    return page.getAnnotations().then(function (annotations) {
    annotations.forEach(function (annotation) {
    if (annotation.subtype === 'Highlight') {
    // Convert the PDF user-space rectangle to viewport (canvas)
    // coordinates. This handles the bottom-up Y axis, the current
    // scale, and any page rotation in one step.
    const [left, bottom, right, top] = annotation.rect;
    const [x1, y1] = viewport.convertToViewportPoint(left, bottom);
    const [x2, y2] = viewport.convertToViewportPoint(right, top);
    const highlight = document.createElement('div');
    highlight.style.position = 'absolute';
    highlight.style.left = Math.min(x1, x2) + 'px';
    highlight.style.top = Math.min(y1, y2) + 'px';
    highlight.style.width = Math.abs(x2 - x1) + 'px';
    highlight.style.height = Math.abs(y2 - y1) + 'px';
    highlight.style.backgroundColor = 'yellow';
    highlight.style.opacity = '0.5';
    highlight.style.pointerEvents = 'none';
    overlay.appendChild(highlight);
    }
    });
    });
    }

    The function calls getAnnotations() on the first page and creates a yellow overlay for each annotation whose subtype is 'Highlight'. viewport.convertToViewportPoint() converts two opposite corners of the PDF rectangle to canvas coordinates using the viewport’s current scale and rotation. Setting annotationMode to DISABLE in the canvas render prevents PDF.js from also painting the annotations underneath the overlays.

    This is a simplified display example. It uses each annotation’s bounding rectangle, so a highlight spanning several lines can cover the gaps between them. It also replaces the original color with yellow and hides other annotation appearances. For the document’s original appearance, remove annotationMode: pdfjsLib.AnnotationMode.DISABLE and the call to renderHighlightAnnotations().

    If you change the scale, rotation, or layout, remove the old overlay and rebuild it after rendering the page again. The example doesn’t provide text selection, create new PDF annotations, or save changes.

    Nutrient Web SDK

    Nutrient provides a commercial JavaScript PDF viewer library for web applications. Nutrient Web SDK supports viewing, annotating, editing, and signing PDFs directly in the browser. Nutrient Web SDK is a separate implementation from PDF.js. Use a new project folder for the Nutrient example, or replace the earlier PDF.js HTML with the Nutrient HTML shown here.

    Requirements

    Adding Nutrient to your project

    Install the @nutrient-sdk/viewer package via npm, or download the SDK manually:

    Terminal window
    npm i @nutrient-sdk/viewer

    To run Nutrient Web SDK in the browser, copy the required library files (artifacts) to your assets folder:

    Terminal window
    cp -R ./node_modules/@nutrient-sdk/viewer/dist/ ./assets/

    Make sure your assets/ folder contains:

    • nutrient-viewer.js (entry point)
    • A nutrient-viewer-lib/ directory with the required runtime assets

    Integrating into your project

    1. Add the PDF document you want to display (e.g. document.pdf) to the root of your project. You can use the Nutrient demo document as an example.

    2. Add an empty <div> element to your HTML where the viewer will mount:

      <div id="nutrient" style="height: 100vh;"></div>
    3. Save the following complete example as index.html in your project root. It imports the SDK and initializes it using NutrientViewer.load():

    <!doctype html>
    <html lang="en">
    <head>
    <title>Nutrient PDF Viewer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body>
    <div id="nutrient" style="height: 100vh;"></div>
    <script type="module">
    import './assets/nutrient-viewer.js';
    const baseUrl = `${window.location.protocol}//${window.location.host}/assets/`;
    NutrientViewer.load({
    container: '#nutrient',
    document: 'document.pdf',
    baseUrl,
    })
    .then((instance) => {
    console.log('Nutrient Viewer loaded', instance);
    })
    .catch((error) => {
    console.error(error.message);
    });
    </script>
    </body>
    </html>

    Nutrient PDF viewer displaying a document

    Creating highlight annotations with Nutrient

    The NutrientViewer.Annotations.HighlightAnnotation constructor creates a highlight annotation object with a page index and rectangles to highlight. Replace the earlier NutrientViewer.load() block with the following code so the viewer is initialized only once:

    NutrientViewer.load({
    container: '#nutrient',
    document: 'document.pdf',
    baseUrl,
    })
    .then(async function (instance) {
    try {
    console.log('Nutrient loaded', instance);
    const rects = NutrientViewer.Immutable.List([
    new NutrientViewer.Geometry.Rect({
    left: 10,
    top: 120,
    width: 200,
    height: 10,
    }),
    new NutrientViewer.Geometry.Rect({
    left: 10,
    top: 150,
    width: 200,
    height: 10,
    }),
    ]);
    const annotation = new NutrientViewer.Annotations.HighlightAnnotation(
    {
    pageIndex: 0,
    rects: rects,
    boundingBox: NutrientViewer.Geometry.Rect.union(rects),
    },
    );
    await instance.create(annotation);
    console.log('Highlight annotation added successfully.');
    } catch (error) {
    console.error('Error adding highlight annotation:', error.message);
    }
    })
    .catch(function (error) {
    console.error('NutrientViewer failed to load:', error.message);
    });

    The rects list defines two areas on the first page (pageIndex: 0). Adjust these coordinates to match the text in your document. Calling instance.create(annotation) adds the annotation to the open viewer instance.

    To export the new highlight, call await instance.save() followed by await instance.exportPDF() inside the same async callback, after await instance.create(annotation). Handle the returned ArrayBuffer to download or upload the PDF. Exporting doesn’t overwrite the PDF at its original URL. See saving annotations into a PDF and exporting a PDF to an ArrayBuffer for the complete persistence workflow.

    Serving your website

    1. Install the serve package:

      Terminal window
      npm install --global serve
    2. Serve the contents of the current directory:

      Terminal window
      serve -l 8080 .
    3. Navigate to http://localhost:8080 to view the website.

    resulting image showing highlight annotation added to PDF

    Add highlighting to your application

    Use Nutrient Web SDK to add highlights at your document’s text coordinates and export the annotated PDF for download or storage.

    Try highlighting a PDF in the Nutrient demo, or contact Sales to discuss adding annotation and export features to your application.

    FAQ

    Do HTML highlight overlays appear in a downloaded PDF?

    No. HTML overlays belong to the webpage and don’t modify the PDF. To include highlights in the downloaded document, create PDF annotations and export the updated file.

    How do I integrate Nutrient Web SDK into my existing project?

    Install the @nutrient-sdk/viewer package via npm, copy the library assets to your project, and initialize the viewer with NutrientViewer.load().

    Does Nutrient Web SDK support creating highlight annotations programmatically?

    Yes. Construct a NutrientViewer.Annotations.HighlightAnnotation and pass it to instance.create(). Save and export the document when you need a PDF file containing the annotation.

    Can PDF.js create and save highlight annotations?

    Yes. Its full viewer supports creating and saving highlights. In a custom canvas integration, getAnnotations() reads existing annotations; drawing an HTML overlay doesn’t create or save a PDF annotation.

    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?