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)

Today SaveAsCopyOptions exposes includeAnnotations and flatten:

const buffer = await viewer.value?.saveAsCopy({
  includeAnnotations: true, // default — bake annotations into the copy
  flatten: true, // flatten editable annotations to fixed content
})

When features.redaction ships in Phase 2, saveAsCopy will gain an option to permanently remove redacted content from the PDF (not just cover it with a black box) — the correct compliance workflow. The exact option name is not finalized yet, so don’t rely on a specific flag until redaction lands.

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