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>
Key implementation details
1. What actually happens when you remove metadata
pdf-lib's setter methods (setTitle(''), setAuthor(''), etc.) work at two levels:
-
Document-level: They clear the corresponding entries in the document info dictionary (
/Infodict in the PDF trailer) -
XMP metadata: If the PDF contains an XMP metadata stream (newer PDFs often embed both classic
/Infoand 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')
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)
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
}
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)',
}
}
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
/Infodict. 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
/Infodict. 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:
- Loading the PDF with
PDFDocument.load(arrayBuffer) - Inspecting current metadata via
pdf.getTitle(),pdf.getAuthor(), etc. - Stripping fields with
pdf.setTitle(''),pdf.setAuthor(''), etc. - Saving with
pdf.save() - 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)