Guide

Export & Import

Export PDFs and save annotation drafts

KViewer supports exporting PDFs with annotations baked in, and saving/restoring annotation state as JSON for draft workflows.

Export a PDF

Use the template ref to export the current document with annotations:

<template>
  <div class="h-screen">
    <KViewer ref="viewer" :source="pdfUrl" />
    <button @click="exportPdf">Download PDF</button>
  </div>
</template>

<script setup lang="ts">
const viewer = ref()
const pdfUrl = '/sample.pdf'

async function exportPdf() {
  const bytes = await viewer.value?.exportPdf({
    flatten: true,
    download: true,
    fileName: 'annotated-document.pdf',
  })
}
</script>

Export Options

OptionTypeDefaultDescription
flattenbooleanfalseBurn annotations into page content (non-editable)
downloadbooleanfalseTrigger a browser file download
fileNamestringauto-generatedCustom filename for download
preserveOriginalAnnotationsbooleanfalseKeep unmodified native PDF annotations

Get Bytes for Upload

Export without downloading to get raw bytes for server upload:

const bytes = await viewer.value?.exportPdf({ flatten: true })

// Upload to your server
await fetch('/api/documents/save', {
  method: 'POST',
  body: bytes,
  headers: { 'Content-Type': 'application/pdf' },
})

Flatten Annotations

When flatten: true, annotations are burned into the PDF page content and become part of the rendered page. They can no longer be edited or removed. This includes annotations that were already in the source PDF: they are replaced by their burned-in version rather than kept as live annotations on top of it.

When flatten: false (default), annotations are added as standard PDF annotation objects that can be edited in other PDF viewers.

flatten only affects annotations. Form fields are always written as live AcroForm widgets — to bake individual fields in, use the per-field option below.

Flatten Individual Fields

Set flatten: true on a form field to burn just that field into the page content on export. The field stays fully interactive inside the viewer; only the exported PDF has it baked in, so it can no longer be edited, moved, or deleted in other readers, while every other field remains a live widget.

The option is a regular field property, so it works everywhere field properties do:

// Every text field the user places from now on is flattened on export
const fieldDefaults: PlacedFieldDefaults = {
  text: { flatten: true },
}

// Programmatic placement
viewer.value?.addFormField({
  pageNumber: 1,
  fieldType: 'text',
  rect: [50, 700, 250, 720],
  flatten: true,
})

// Existing fields — including ones parsed from the source PDF
viewer.value?.updateFormField(def.id, { flatten: true })

In form-edit mode the property sidebar exposes the same switch as Flatten on export.

Flattening happens per PDF field name: widgets sharing a fieldName (a radio group, mirrored text widgets) are flattened together as soon as one of them opts in. The baked appearance is the same one a reader would show for the live widget, using the final value at export time; an unsigned signature field flattens to nothing.

A flattened field does not survive a re-import — after exportPdf() it is plain page graphics, not a field. Keep the field interactive (the default) if a later session still needs to fill it.

Handle Native PDF Annotations

When a PDF has existing annotations that were auto-imported by KViewer:

// Recommended: don't preserve originals to avoid duplicates
const bytes = await viewer.value?.exportPdf({
  flatten: false,
  preserveOriginalAnnotations: false,
})
Setting preserveOriginalAnnotations: true when native annotations were auto-imported may create duplicate annotations in the exported PDF. Use false in this workflow.

Author attribution

Each non-flattened annotation can carry an author name, written to the PDF annotation's /T field. KViewer takes the author from the userName prop:

<!-- Annotations are attributed to "Jane Doe" -->
<KViewer :source="pdfUrl" user-name="Jane Doe" />

<!-- No userName → annotations carry no author -->
<KViewer :source="pdfUrl" />

When userName is omitted, no /T is written at all — KViewer does not fall back to a placeholder name. This matches how Firefox's built-in PDF viewer (pdf.js) behaves, and it matters for downstream systems: archiving/DMS tools often render an author + timestamp caption over each markup annotation. A placeholder author (e.g. a generic "User") would show up burned into that caption, overlapping the page. Leaving userName unset keeps the rendered preview clean.

The date is written as /CreationDate for newly authored annotations and /M (modification date) for edits to annotations that already existed in the source PDF — again mirroring pdf.js.

Pass a real author (the signed-in user's name) via userName when you want attribution preserved in the exported PDF; leave it unset when you want anonymous, caption-free annotations.
This applies to non-flattened exports (flatten: false). With flatten: true, annotations are rasterized into the page, so there are no annotation objects and no /T/date metadata at all.

Save Annotation Drafts

Save the current annotation state as JSON for later restoration:

// Save draft
const annotations = viewer.value?.getAnnotations() ?? []
const draft = JSON.stringify(annotations)
localStorage.setItem('document-draft', draft)

// Or save to your API
await fetch('/api/drafts', {
  method: 'POST',
  body: draft,
  headers: { 'Content-Type': 'application/json' },
})

Restore Annotation Drafts

Import previously saved annotations:

// Load draft
const draft = localStorage.getItem('document-draft')
if (draft) {
  const annotations = JSON.parse(draft)
  const result = await viewer.value?.importAnnotations(annotations, {
    mode: 'replace',
  })
  console.log(`Loaded ${result.loaded}, skipped ${result.skipped}`)
}

Import Modes

ModeDescription
replaceClear all existing annotations, then add the imported ones
mergeAdd imported annotations alongside existing ones, skipping collisions

Full Round-Trip Example

<template>
  <div class="h-screen">
    <KViewer ref="viewer" :source="pdfUrl" text-layer />
    <div class="flex gap-2 p-4">
      <button @click="saveDraft">Save Draft</button>
      <button @click="loadDraft">Load Draft</button>
      <button @click="exportFlattened">Export PDF</button>
    </div>
  </div>
</template>

<script setup lang="ts">
const viewer = ref()
const pdfUrl = '/sample.pdf'

function saveDraft() {
  const annotations = viewer.value?.getAnnotations() ?? []
  localStorage.setItem('draft', JSON.stringify(annotations))
}

async function loadDraft() {
  const saved = localStorage.getItem('draft')
  if (saved) {
    await viewer.value?.importAnnotations(JSON.parse(saved), { mode: 'replace' })
  }
}

async function exportFlattened() {
  await viewer.value?.exportPdf({
    flatten: true,
    download: true,
    fileName: 'final-document.pdf',
  })
}
</script>
Copyright © 2026