DEV Community

sunshey
sunshey

Posted on

PDF Privacy Cleanup with Vue 3 and pdf-lib: Metadata, Annotations, Links & Inspection

After building four separate privacy tools — Remove Metadata, Remove Annotations, Remove Links, and Privacy Inspector — I realized users need a unified workflow. Instead of running each tool separately, a comprehensive cleanup guide helps users understand the full privacy landscape and take action in the right order.

This post walks through the combined implementation and the recommended cleanup pipeline.

The cleanup pipeline

The correct order matters:

Inspect → Remove Metadata → Remove Annotations → Remove Links → Re-inspect
Enter fullscreen mode Exit fullscreen mode
  1. Inspect first to see what's hidden
  2. Remove Metadata — strip document properties
  3. Remove Annotations — clean review markup
  4. Remove Links — strip hyperlinks
  5. Re-inspect — verify nothing was missed

The stack

  • Vue 3 with Composition API
  • pdf-lib for PDF manipulation
  • PDF.js for text extraction and hidden content detection
  • Vite for bundling

Implementation overview

Each cleanup operation is a separate function that can be chained:

interface CleanupResult {
  metadata: DocumentMetadata
  annotationsRemoved: number
  linksRemoved: number
  javascriptRemoved: boolean
  hiddenTextRemoved: number
  riskScore: number
}

async function cleanupPdf(
  arrayBuffer: ArrayBuffer,
  options: CleanupOptions
): Promise<CleanupResult> {
  const pdf = await PDFDocument.load(arrayBuffer, {
    ignoreEncryption: true,
    updateMetadata: false,
  })

  const result: CleanupResult = {
    metadata: extractMetadata(pdf),
    annotationsRemoved: 0,
    linksRemoved: 0,
    javascriptRemoved: false,
    hiddenTextRemoved: 0,
    riskScore: 0,
  }

  // Step 1: Remove metadata
  if (options.removeMetadata) {
    pdf.setTitle('')
    pdf.setAuthor('')
    pdf.setSubject('')
    pdf.setKeywords([])
    pdf.setCreator('')
    pdf.setProducer('')
    result.metadata = extractMetadata(pdf)
  }

  // Step 2: Remove annotations
  if (options.removeAnnotations) {
    result.annotationsRemoved = await removeAnnotations(pdf)
  }

  // Step 3: Remove links
  if (options.removeLinks) {
    result.linksRemoved = await removeLinks(pdf)
  }

  // Step 4: Remove JavaScript
  if (options.removeJavaScript) {
    result.javascriptRemoved = await removeJavaScript(pdf)
  }

  // Save and return
  const cleaned = await pdf.save()
  result.riskScore = calculateRiskScore(result)
  return result
}
Enter fullscreen mode Exit fullscreen mode

Key implementation details

1. Metadata removal

Simple — set each field to empty string:

pdf.setTitle('')
pdf.setAuthor('')
pdf.setSubject('')
pdf.setKeywords([])
pdf.setCreator('')
pdf.setProducer('')
Enter fullscreen mode Exit fullscreen mode

2. Annotation removal with widget preservation

The key challenge is distinguishing widget annotations (form fields) from markup annotations:

async function removeAnnotations(pdf: PDFDocument): Promise<number> {
  let count = 0
  const pages = pdf.getPages()

  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)
        continue
      }

      const subtype = annot.get(PDFName.of('Subtype'))
      if (subtype === PDFName.of('Widget')) {
        kept.push(annotRef) // Keep form fields
      } else {
        count++ // Remove everything else
      }
    }

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

  return count
}
Enter fullscreen mode Exit fullscreen mode

3. Link removal

Links are also annotations with Subtype: /Link:

async function removeLinks(pdf: PDFDocument): Promise<number> {
  let count = 0
  const pages = pdf.getPages()

  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)
        continue
      }

      const subtype = annot.get(PDFName.of('Subtype'))
      if (subtype === PDFName.of('Link')) {
        count++
        continue // Skip — don't add to kept
      }

      kept.push(annotRef)
    }

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

  return count
}
Enter fullscreen mode Exit fullscreen mode

4. JavaScript detection and removal

JavaScript in PDFs is the highest privacy risk. It can execute on open and exfiltrate data:

async function removeJavaScript(pdf: PDFDocument): Promise<boolean> {
  // Check for JS in the PDF structure
  const pages = pdf.getPages()
  for (const page of pages) {
    const annots = page.node.lookup(PDFName.of('Annots'))
    if (!annots || !(annots instanceof PDFArray)) continue

    const annots_array = annots.asArray()
    for (const annotRef of annots_array) {
      const annot = page.doc.context.lookup(annotRef)
      if (!(annot instanceof PDFDict)) continue

      const action = annot.get(PDFName.of('A'))
      if (action instanceof PDFDict) {
        const s = action.get(PDFName.of('S'))
        if (s === PDFName.of('JavaScript')) {
          // Remove the annotation
          const kept = PDFArray.withContext(page.doc.context)
          for (const ref of annots_array) {
            if (ref !== annotRef) kept.push(ref)
          }
          page.node.set(PDFName.of('Annots'), kept)
          return true
        }
      }
    }
  }
  return false
}
Enter fullscreen mode Exit fullscreen mode

Summary

Building a comprehensive PDF privacy cleanup pipeline involves:

  1. Inspecting the document first (Privacy Inspector)
  2. Removing metadata (simple field clearing)
  3. Removing annotations while preserving form widgets (whitelist approach)
  4. Removing links (filter by subtype)
  5. Detecting and removing JavaScript (highest priority)
  6. Re-inspecting to verify

Each operation is independent and can be combined. The full pipeline runs in the browser — your documents never leave your device. Try the cleanup tools at en.sotool.top.

Top comments (0)