MeldUI

Use Case: Save-as-Copy Burn-in

Generate a baked PDF with annotations burned into the page contents — interoperable with any PDF reader.

When you need to deliver a PDF that anyone can open and see the annotations (not just users of DocumentViewer), use saveAsCopy(). It returns a fresh PDF binary with annotations baked into the page contents.

Pattern

const buffer = await viewer.value?.saveAsCopy()
const blob = new Blob([buffer], { type: 'application/pdf' })

// Option A: trigger a download
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'reviewed-doc.pdf'
a.click()
URL.revokeObjectURL(url)

// Option B: POST to your backend for archival
await fetch('/api/documents/reviewed', {
  method: 'POST',
  body: blob,
  headers: { 'Content-Type': 'application/pdf' },
})

When to use this vs. exportAnnotations

NeedUse
Show the annotated doc to someone who doesn’t have DocumentViewersaveAsCopy()
Backup the annotations so the user can later edit them in DocumentViewerexportAnnotations() (preserves editability)
Both (e.g. archive + future re-edit)Do both — store the JSON for editing, ship the PDF for sharing

saveAsCopy() burns annotations into the PDF contents in a way that’s spec-compliant for PDF 1.7 — most professional PDF readers (Adobe, Preview, Foxit) will show them. But the original “editable annotation” metadata may be lost, so subsequent rounds of editing should start from the original source + the exportAnnotations() JSON.

Compliance burn-in (Phase 2: redactions)

When features.redaction ships in Phase 2, saveAsCopy({ applyRedactions: true }) permanently removes the redacted content from the PDF (not just covers it with a black box). This is the correct compliance workflow:

// Phase 2 — once redaction lands
const buffer = await viewer.value?.saveAsCopy({ applyRedactions: true })
// the buffer has the redacted text completely removed from the page contents,
// not just hidden behind a rectangle.

Threads are not embedded

Threads (replies + resolved state) live in the parallel CommentThread[] overlay, not in the PDF binary. If you need to ship both the baked PDF and the thread data:

const [buffer, threads] = await Promise.all([
  viewer.value!.saveAsCopy(),
  fetchAllThreadsFromAppState(),
])

// Bundle them in a multipart upload, or a zip, or two separate endpoints

Performance

saveAsCopy() re-encodes the PDF — for very large documents (1000+ pages with many annotations), this can take several seconds. Show a loading state during the call.

See also