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
- Inspect first to see what's hidden
- Remove Metadata — strip document properties
- Remove Annotations — clean review markup
- Remove Links — strip hyperlinks
- 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
}
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('')
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
}
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
}
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
}
Summary
Building a comprehensive PDF privacy cleanup pipeline involves:
- Inspecting the document first (Privacy Inspector)
- Removing metadata (simple field clearing)
- Removing annotations while preserving form widgets (whitelist approach)
- Removing links (filter by subtype)
- Detecting and removing JavaScript (highest priority)
- 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)