How to Remove PDF Annotations in the Browser with Vue 3 and pdf-lib

sunshey

sunshey

2026-08-03T12:54:13Z

4 min read

After a week of collaborative review, your 50-page PDF is covered in highlights, sticky notes, strikethroughs, and stamps. The content is final — but the markup makes it look like a work-in-progress. Before sending it to a client or publishing it, you need a clean copy.

Manually deleting each annotation is impractical. A dedicated tool can strip all non-widget annotations in one operation while preserving form fields. Here's how to build it with Vue 3 and pdf-lib.

Why preserve form widgets?

Not all annotations are markup. Form fields — text inputs, checkboxes, dropdowns, signature fields — are technically a subtype of annotation called widget annotations (Subtype: /Widget). If you blindly delete all annotations, you destroy every fillable form field in the document.

The smart approach: filter by annotation subtype. Remove everything that's not a widget.

The stack

  • Vue 3 with Composition API
  • pdf-lib for low-level PDF manipulation
  • Vite for bundling

Understanding annotation types in PDF

Every page has an /Annots array containing all interactive elements. Each annotation has a Subtype:

Subtype Category Action
/Text Comment ❌ Remove
/Highlight Markup ❌ Remove
/StrikeOut Markup ❌ Remove
/Underline Markup ❌ Remove
/Ink Drawing ❌ Remove
/Stamp Approval ❌ Remove
/Link Navigation ❌ Remove
/Widget Form field Keep

The core implementation

<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, PDFName, PDFArray, PDFDict } from 'pdf-lib'

const file = ref<File | null>(null)
const annotationCount = ref(0)
const removing = ref(false)
const result = ref<Uint8Array | null>(null)

async function removeAnnotations() {
  if (!file.value) return
  removing.value = true

  const arrayBuffer = await file.value.arrayBuffer()
  const pdf = await PDFDocument.load(arrayBuffer)

  const pages = pdf.getPages()
  let totalRemoved = 0

  for (const page of pages) {
    const annots = page.node.lookup(PDFName.of('Annots'))
    if (!annots || !(annots instanceof PDFArray)) continue

    const kept = PDFArray.withContext(page.doc.context)
    const existing = annots.asArray()

    for (const annotRef of existing) {
      const annot = page.doc.context.lookup(annotRef)
      if (!(annot instanceof PDFDict)) {
        kept.push(annotRef) // Can't identify — keep it
        continue
      }

      const subtype = annot.get(PDFName.of('Subtype'))

      // Widget annotations are form fields — keep them
      if (subtype === PDFName.of('Widget')) {
        kept.push(annotRef)
        continue
      }

      // Everything else: Text, Highlight, StrikeOut, Underline,
      // Ink, Stamp, Link, etc. — remove
      totalRemoved++
    }

    // Write back filtered annotations
    if (kept.size() === 0) {
      page.node.delete(PDFName.of('Annots'))
    } else {
      page.node.set(PDFName.of('Annots'), kept)
    }
  }

  annotationCount.value = totalRemoved

  const cleaned = await pdf.save()
  result.value = cleaned
  removing.value = false
}
</script>
Enter fullscreen mode Exit fullscreen mode

Key implementation details

1. The widget whitelist approach

Instead of listing every annotation type to remove (and risking missing one), we use a whitelist strategy: keep widgets, remove everything else:

// ✅ Whitelist: only Widget survives
if (subtype === PDFName.of('Widget')) {
  kept.push(annotRef) // Keep form fields
} else {
  totalRemoved++ // Remove everything else
}

// ❌ Blacklist: easy to miss annotation types
const REMOVE = ['Text', 'Highlight', 'StrikeOut', 'Underline', 'Ink', 'Stamp', 'Link', ...]
if (REMOVE.includes(subtype)) { ... }
Enter fullscreen mode Exit fullscreen mode

The whitelist approach is more robust because new or uncommon annotation subtypes (like /Caret, /FileAttachment, /Sound, /FreeText, /Polygon, /PolyLine, /Redact, /Watermark) are automatically caught and removed without needing to be individually enumerated.

2. Preserving form field functionality

Widget annotations are the visual representation of AcroForm fields. Their appearance is controlled by the field's value and the field's appearance stream — not by the annotation itself. By preserving widgets, form fields remain fully functional after annotation removal:

// Before removal: page has 3 highlights, 2 text annotations, 5 form widgets
// After removal: page has only 5 form widgets — all still interactive
Enter fullscreen mode Exit fullscreen mode

3. Indirect references and lookup

As with other PDF object manipulation, references in the /Annots array are often indirect. We must resolve each reference before inspecting its dictionary:

const annotRef = existing[i] // This is a reference, not the actual object
const annot = page.doc.context.lookup(annotRef) // Resolve it
Enter fullscreen mode Exit fullscreen mode

If lookup fails or returns a non-dictionary, we keep the reference — it's safer to preserve an unknown element than to accidentally delete something important.

4. What about annotation appearance streams?

Some annotations have appearance streams that are drawn into the page content. Simply removing the annotation from /Annots also removes its appearance — because the appearance stream is referenced from the annotation object, not embedded in the page content stream.

After removal, the visual effect is immediate: highlights disappear, sticky note icons vanish, and stamps are gone. The underlying text and page content remain untouched.

5. Edge case: locked or read-only annotations

Some PDFs have annotations marked as read-only (/F flag bit). These are still annotations — they're still removed by our filter. Read-only is a permission flag, not a content type distinction.


Summary

Building a browser-based PDF annotation removal tool involves:

  1. Loading the PDF with PDFDocument.load()
  2. Iterating pages and accessing /Annots arrays
  3. Filtering with a widget whitelist — keep /Widget, remove everything else
  4. Resolving indirect references with context.lookup()
  5. Saving with pdf.save()

The result: a clean PDF with all markup stripped, but form fields still functional. Try it at en.sotool.top/remove-annotations.