How to Flatten PDF Annotations in JavaScript Using Dynamsoft Document Viewer

Burning (flattening) annotations means including them as vector graphics in the content of a PDF page, leaving them uneditable. Dynamsoft Document Viewer is a JavaScript SDK for document scanning and viewing, which can add annotations and export PDF files. In this article, we are going to explore how to use it.

PDF annotation flattening result — before and after

What you’ll build: A browser-based JavaScript app that loads a PDF, lets users add annotations via an edit viewer, and exports a new PDF with all annotations flattened (burned) into the page content using Dynamsoft Document Viewer.

Key Takeaways

  • Dynamsoft Document Viewer provides a saveAnnotation: "flatten" option that merges annotations as vector graphics into the PDF page content, making them permanent and uneditable.
  • You can flatten all annotations at once or selectively flatten individual annotations by setting annotation.flattened = true before saving with saveAnnotation: "annotation".
  • The flattening process removes the /Annots array from the page dictionary and appends the rendered annotation graphics directly to the page content stream.
  • This approach works entirely in the browser with no server-side processing required.

Common Developer Questions

  • How do I flatten PDF annotations in JavaScript without a server?
  • How do I burn annotations into a PDF page so they cannot be edited?
  • What is the difference between flatten, annotation, image, and none options when saving a PDF with Dynamsoft Document Viewer?

Prerequisites

Step 1: Set Up the Document Viewer with Annotation Support

  1. Create a new HTML file with the following template.

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
      <title>Burn PDF Annotation</title>
      <style>
      </style>
    </head>
    <body>
    </body>
    <script>
    </script>
    </html>
    
  2. Include Dynamsoft Document Viewer’s files.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.1.0/dist/ddv.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.1.0/dist/ddv.css">
    
  3. Initialize Dynamsoft Document Viewer with a license. You can apply for one here.

    Dynamsoft.DDV.Core.license = "LICENSE-KEY"; 
    Dynamsoft.DDV.Core.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.1.0/dist/engine";// Lead to a folder containing the distributed WASM files
    await Dynamsoft.DDV.Core.init();
    
  4. Create a new document instance.

    const docManager = Dynamsoft.DDV.documentManager;
    const doc = docManager.createDocument();
    
  5. Create an instance of edit viewer, bind it to a container and use it to view the document we just created. The buttons on it can be configured using a UIConfig object. Here, we add the annotation button to enable adding annotations.

    HTML:

    <div id="viewer"></div>
    

    JavaScript:

    Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
    let uiConfig = {
      type: "Layout",
      flexDirection: "column",
      className: "ddv-edit-viewer-desktop",
      children: [
        {
          type: "Layout",
          className: "ddv-edit-viewer-header-desktop",
          children: [
            {
              type: "Layout",
              children: [
                "ThumbnailSwitch",
                "FitMode",
                "DisplayMode",
                "RotateLeft",
                "Crop",
                "Filter",
                "Undo",
                "Redo",
                "DeleteCurrent",
                "DeleteAll",
                "Pan",
                "SeparatorLine",
                "AnnotationSet"
              ],
              enableScroll: true
            },
            {
              type: "Layout",
              children: [
                {
                  "type": "Pagination",
                  "className": "ddv-edit-viewer-pagination-desktop"
                },
                {
                  type: Dynamsoft.DDV.Elements.Button,
                  className: "ddv-button-download",
                  events: {
                    click: "exportPDFWithOptions",
                  },
                },
              ]
            }
          ]
        },
        "MainView"
      ]
    }
    editViewer = new Dynamsoft.DDV.EditViewer({
      uiConfig: uiConfig,
      container: document.getElementById("viewer")
    });
    

    CSS:

    #viewer {
      width: 320px;
      height: 480px;
    }
    
  6. Use input to select an image or PDF file and load it into the document instance.

    HTML:

    <label>
      Select a file to load:
      <br/>
      <input type="file" id="files" name="files" onchange="filesSelected()"/>
    </label>
    

    JavaScript:

    async function filesSelected(){
      let filesInput = document.getElementById("files");
      let files = filesInput.files;
      if (files.length>0) {
        const file = files[0];
        const blob = await readFileAsBlob(file);
        await doc.loadSource(blob); // load the file
      }
    }
    
    function readFileAsBlob(file){
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = async function(e){
          const response = await fetch(e.target.result);
          const blob = await response.blob();
          resolve(blob);
        };
        fileReader.onerror = function () {
          reject('oops, something went wrong.');
        };
        fileReader.readAsDataURL(file); 
      })
    }
    

We’ll be able to see a viewer like the following:

editviewer

Step 2: Flatten (Burn) the Annotations and Export PDF

Dynamsoft Document Viewer supports four ways to handle the PDF annotations:

  • none: Discard all the annotations
  • image: Merge all the content into a raster image
  • flatten: Flatten all the annotations
  • annotation: Save the annotations in an editable form. Individual annotations marked as flattened will still be flattened

We can save the PDF file and burn the annotations using the flatten option.

let blob = await doc.saveToPdf({
  saveAnnotation: "flatten"
})

If we want to keep some annotations editable while making some annotations flattened, we can use the flatten attribute of annotations and save the PDF using the annotation option.

let annotations = Dynamsoft.DDV.annotationManager.getAnnotationsByDoc(doc.uid);
let annotation = annotations[0];
annotation.flattened = true;
let blob = await doc.saveToPdf({
  saveAnnotation: "annotation"
})

How PDF Annotation Flattening Works Internally

PDF files are described using PostScript. We are going to use some examples to show how the flattening works internally.

A PDF file contains a list of dictionaries and here is an example of a page dictionary:

4 0 obj
<<
  /Type/Page                 % Specifies that this dictionary defines a page.
  /Annots[ 8 0 R ]           % A list of references to annotation objects on this page.
  /Contents 7 0 R            % Reference to page content stream.
  /MediaBox[ 0 0 147 143.25] % Page dimensions.
  % Other page properties
>>
endobj
7 0 obj
<</Filter/FlateDecode/Length 44>>stream
x??41W0 BCc=#S039椝 ?J缫w媹0Tp蒞? 卵	
endstream
endobj

The above page has a reference to the following annotation dictionary:

8 0 obj
<<
  /Type/Annot
  /AP<<
  /Contents(annotation)
    /CreationDate(D:20241227135119+08'00')
    /DA(0.9411764705882353 0.07450980392156863 0.0784313725490196 rg /Helvetica 16 Tf)
    /DS(font:  'Helvetica' 16pt; text-align:left; color:#F01314)
    /F 4
    /IT/FreeTextTypeWriter/M(D:20241227135125+08'00')/NM(m56c4eb9uq)/RC(<?xml version="1.0"?><body xmlns="http://www.w3.org/1999/xhtml" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/" xfa:APIVersion="Acrobat:18.11.0" xfa:spec="2.0.2" style="font-size:16pt;text-align:left;color:#f01314;font-weight:normal;font-style:normal;font-family:'Helvetica';font-stretch:normal;"><p dir="ltr"><span>annotation</span></p></body>)
    /Rect[ 22.1854 112.467 98.423 129.217]
    /Subj()
    /Subtype
    /FreeText
    /T()
>>
endobj

After flattening, the page dictionary will become the following. It no longer has the annotation node and a node of the converted annotation graphics is appended to its content.

4 0 obj
<<
  /Type/Page
  /Contents 13 0 R 
  /MediaBox[ 0 0 147 143.25]
>>
endobj
7 0 obj
<</Filter/FlateDecode/Length 48>>stream
x???41W0 BCc=#S039椝 ?J缫w媹0Tp蒞溻
 靔	?
endstream
endobj
13 0 obj
[ 7 0 R  14 0 R ]
endobj
14 0 obj
<</Filter/FlateDecode/Length 29>>stream
x?T0T0 B櫆珷镦b犩挴 M+?
endstream
endobj

Common Issues & Edge Cases

  • Annotations not visible after flattening: If you saved with saveAnnotation: "none", all annotations are discarded rather than flattened. Use "flatten" to burn them into the page, or "annotation" combined with flattened = true on individual annotations.
  • Selectively flattened annotations appearing editable after reload: When using saveAnnotation: "annotation", only annotations with flattened = true are burned in. Re-opening the PDF in a viewer that supports PDF annotations will still show the remaining editable annotations — this is expected behavior.
  • Large PDFs causing slow export: The saveToPdf call is asynchronous; always await it and consider showing a loading indicator for multi-page documents with many annotations.

Source Code

Get the source code of the demo to have a try:

https://github.com/tony-xlh/Burn-PDF-Annotation/