DEV Community

sunshey
sunshey

Posted on

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

Every PDF file carries a hidden document info dictionary — title, author, subject, keywords, creator, producer, and timestamps. This metadata is invisible to most readers, but it's trivially extractable by anyone with a file properties dialog or command-line tool. If you're building a privacy tool, "remove metadata" should be a one-click action.

In this post, I'll walk through implementing a browser-based PDF metadata removal tool using Vue 3 and pdf-lib, processing everything locally so no file ever leaves the user's device.

Why client-side stripping?

Cloud-based "metadata removal" has an obvious problem: you upload a file with sensitive metadata to a server you don't control. Even if they claim to delete it, you have no way to verify that. Browser-based processing:

  • Keeps files entirely on the user's device
  • Doesn't require network at all (after initial page load)
  • Eliminates server-side storage and bandwidth costs
  • Gives users verifiable privacy — they can check network tab

The stack

  • Vue 3 with Composition API
  • pdf-lib for PDF manipulation
  • PDF.js (pdfjs-dist) for displaying current metadata to the user before removal
  • Vite for bundling

The core implementation

pdf-lib represents the document info dictionary via PDFDocument methods. To strip metadata, we need to set each field to undefined (or an empty string), which effectively removes the /Info entry from the PDF trailer.

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

const file = ref<File | null>(null)
const currentMetadata = ref<Record<string, string>>({})
const removing = ref(false)
const result = ref<Uint8Array | null>(null)

async function loadAndInspect() {
  if (!file.value) return
  const arrayBuffer = await file.value.arrayBuffer()
  const pdf = await PDFDocument.load(arrayBuffer, {
    updateMetadata: false, // keep original metadata for inspection
  })

  // Extract current metadata for display
  currentMetadata.value = {
    title: pdf.getTitle() || '(none)',
    author: pdf.getAuthor() || '(none)',
    subject: pdf.getSubject() || '(none)',
    keywords: pdf.getKeywords() || '(none)',
    creator: pdf.getCreator() || '(none)',
    producer: pdf.getProducer() || '(none)',
  }
}

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

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

  // Strip all document-level metadata
  pdf.setTitle('')
  pdf.setAuthor('')
  pdf.setSubject('')
  pdf.setKeywords([])
  pdf.setCreator('')
  pdf.setProducer('')

  const cleanedPdf = await pdf.save()
  result.value = cleanedPdf
  removing.value = false
}

function downloadResult() {
  if (!result.value) return
  const blob = new Blob([result.value], { type: 'application/pdf' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'cleaned.pdf'
  a.click()
  URL.revokeObjectURL(url)
}
</script>
Enter fullscreen mode Exit fullscreen mode

Key implementation details

1. What actually happens when you remove metadata

pdf-lib's setter methods (setTitle(''), setAuthor(''), etc.) work at two levels:

  1. Document-level: They clear the corresponding entries in the document info dictionary (/Info dict in the PDF trailer)
  2. XMP metadata: If the PDF contains an XMP metadata stream (newer PDFs often embed both classic /Info and XMP), pdf-lib also updates or removes it

The PDF spec allows a document to have no /Info dictionary at all — a completely valid, metadata-free PDF. Our implementation moves the document to this clean state.

2. Preserving non-metadata content

Crucially, metadata operations must not touch the page-level content. The pages, fonts, images, vector graphics, annotations, links, and bookmarks must survive unchanged. pdf-lib ensures this because metadata setters only affect the document catalog and trailer — they never iterate pages or rewrite content streams.

We can verify this by checking the page count before and after:

const pagesBefore = pdf.getPageCount()
pdf.setTitle('')
// ... strip all fields ...
const pagesAfter = pdf.getPageCount()
console.assert(pagesBefore === pagesAfter, 'Page count should be unchanged')
Enter fullscreen mode Exit fullscreen mode

3. The difference between empty string and undefined

pdf.setTitle('')       // removes the /Title entry entirely
pdf.setTitle(undefined) // also removes it (pdf-lib treats both as removal)
Enter fullscreen mode Exit fullscreen mode

Both approaches work in pdf-lib — setting a field to an empty value removes it from the document info dictionary. If you need to selectively clear only some fields, just skip the setters for the fields you want to keep:

async function selectiveRemoval(pdf: PDFDocument, keep: Set<string>) {
  if (!keep.has('author')) pdf.setAuthor('')
  if (!keep.has('title')) pdf.setTitle('')
  if (!keep.has('subject')) pdf.setSubject('')
  // ... etc
}
Enter fullscreen mode Exit fullscreen mode

4. Previewing metadata before removal

Good UX means showing users what they're about to strip. Using PDF.js for this preview isn't necessary — pdf-lib's metadata getters work on the loaded document directly. We don't need to render any pages; we're just reading string properties:

interface MetadataSnapshot {
  title: string
  author: string
  subject: string
  keywords: string
  creator: string
  producer: string
}

function snapshot(pdf: PDFDocument): MetadataSnapshot {
  return {
    title: pdf.getTitle() || '(not set)',
    author: pdf.getAuthor() || '(not set)',
    subject: pdf.getSubject() || '(not set)',
    keywords: pdf.getKeywords() || '(not set)',
    creator: pdf.getCreator() || '(not set)',
    producer: pdf.getProducer() || '(not set)',
  }
}
Enter fullscreen mode Exit fullscreen mode

Display this snapshot in a simple table before and after removal so users can verify the result.

Edge cases to handle

  • PDFs with no metadata at all: Some PDF generators produce documents without a /Info dict. Our tool should handle this gracefully — show "(not set)" for all fields and still allow "removal" (which is a no-op but lets the user download anyway).
  • XMP-only metadata: Newer PDFs using ISO 32000-2 may store metadata only in XMP format, with no classic /Info dict. pdf-lib handles both, but it's worth noting in your UI that XMP metadata will also be cleared.
  • Encrypted PDFs: If the PDF is password-protected, metadata may be part of the encrypted object. You'll need to unlock the PDF first before stripping metadata.
  • Very large PDFs: The file size difference after metadata removal is negligible (metadata is typically < 1KB), so we don't need to compute or display a "space saved" metric — it's misleading.

Summary

Building a browser-based PDF metadata removal tool involves:

  1. Loading the PDF with PDFDocument.load(arrayBuffer)
  2. Inspecting current metadata via pdf.getTitle(), pdf.getAuthor(), etc.
  3. Stripping fields with pdf.setTitle(''), pdf.setAuthor(''), etc.
  4. Saving with pdf.save()
  5. Downloading via Blob + temporary URL

The resulting file is visually identical to the original — same pages, same content — but carries no hidden document properties. Try it at en.sotool.top/remove-pdf-metadata.

Top comments (0)