Skip to content

XFDF Controller viewer>=4.3.0 & annotation>=1.6.0

AnnotationXfdfControl

An instance object for exporting and importing PDF annotations in XFDF format. XFDF (XML Forms Data Format) is an Adobe standard for exchanging annotation data between PDF viewers and external systems.

annotationXfdfControl is always available on the viewer instance. exportToXfdf() and importFromXfdf() require the @vue-pdf-viewer/annotation plugin to be loaded. A console warning is shown and an empty result is returned if they are called without it.

Plugin Required

exportToXfdf() and importFromXfdf() require the annotation plugin. Pass it via the plugins prop: <VPdfViewer :plugins="[annotationPlugin]" />.

Methods

NameDescriptionType
exportToXfdfCollect all current editable annotations and serializes them to an XFDF XML string.
Return an XFDF XML string, or trigger a browser file download. Return an empty string if the plugin is not loaded.
(options?: XfdfExportOptions) => string
importFromXfdfParse an XFDF XML string and loads the recognized annotations into the viewer. Return an XfdfImportResult with the imported annotations, skipped entries, and any parse errors.
Return an empty result if the plugin is not loaded.
(xfdfString: string) => XfdfImportResult

Export Flow

exportToXfdf() internally collects all editable annotations (created by the user or loaded via importFromXfdf()) and serializes them. You do not need to gather annotations manually. The optional pdfFilename overrides the PDF name embedded in the XFDF <f> element. If omitted, the current PDF filename is used automatically.

Image / stamp annotations — Adobe Acrobat compatibility

Image and stamp annotations created in VPV are exported with an <imagedata> child element containing the image as a base64-encoded PNG. This is a well-known extension used by Apryse WebViewer for image interoperability, but it is not part of the Adobe XFDF standard. Adobe Acrobat does not recognize <imagedata> and will not display image annotations when importing an XFDF produced by VPV.

VPV round-trips (exportToXfdfimportFromXfdf) preserve image annotations correctly. Cross-application exchange of image annotations via XFDF with Adobe Acrobat is not supported.

Example

vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { VPdfViewer, type VPVInstance } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

const viewerRef = ref<VPVInstance>()
// Computed so the controller is always resolved after the viewer mounts
const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

const downloadXfdf = () => {
  // pdfFilename is embedded in the XFDF <f> element; download: true triggers a browser save dialog
  xfdfControl.value?.exportToXfdf({ pdfFilename: 'my-document.pdf', download: true })
}
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <button @click="downloadXfdf">Download XFDF</button>
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script setup>
import { ref, computed } from 'vue'
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

const viewerRef = ref()
// Computed so the controller is always resolved after the viewer mounts
const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

const downloadXfdf = () => {
  // pdfFilename is embedded in the XFDF <f> element; download: true triggers a browser save dialog
  xfdfControl.value?.exportToXfdf({ pdfFilename: 'my-document.pdf', download: true })
}
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <button @click="downloadXfdf">Download XFDF</button>
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script lang="ts">
import { ref, computed, defineComponent } from 'vue'
import { VPdfViewer, type VPVInstance } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

export default defineComponent({
  components: { VPdfViewer },
  setup() {
    const viewerRef = ref<VPVInstance>()
    // Computed so the controller is always resolved after the viewer mounts
    const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

    const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

    const downloadXfdf = () => {
      // pdfFilename is embedded in the XFDF <f> element; download: true triggers a browser save dialog
      xfdfControl.value?.exportToXfdf({ pdfFilename: 'my-document.pdf', download: true })
    }

    return { viewerRef, annotationPlugin, downloadXfdf }
  }
})
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <button @click="downloadXfdf">Download XFDF</button>
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script>
import { ref, computed, defineComponent } from 'vue'
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

export default defineComponent({
  components: { VPdfViewer },
  setup() {
    const viewerRef = ref()
    // Computed so the controller is always resolved after the viewer mounts
    const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

    const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

    const downloadXfdf = () => {
      // pdfFilename is embedded in the XFDF <f> element; download: true triggers a browser save dialog
      xfdfControl.value?.exportToXfdf({ pdfFilename: 'my-document.pdf', download: true })
    }

    return { viewerRef, annotationPlugin, downloadXfdf }
  }
})
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <button @click="downloadXfdf">Download XFDF</button>
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>

Import Flow

Pass the full XFDF XML string. The viewer uses the loaded PDF's page count internally to validate that annotation page indices are in range.

Example

vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { VPdfViewer, type VPVInstance } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

const viewerRef = ref<VPVInstance>()
// Computed so the controller is always resolved after the viewer mounts
const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

const importXfdfFromFile = (event: Event) => {
  const file = (event.target as HTMLInputElement).files?.[0]
  if (!file) return
  const reader = new FileReader()
  // FileReader is async; the XFDF string is only available inside onload
  reader.onload = (e) => {
    const xfdf = e.target?.result as string
    const result = xfdfControl.value?.importFromXfdf(xfdf)
    console.log(`Imported ${result?.annotations.length} annotation(s)`)
    // Log any entries the parser skipped or could not process
    if (result?.skipped.length) console.warn('Skipped:', result.skipped)
    if (result?.errors.length) console.error('Errors:', result.errors)
  }
  reader.readAsText(file)
}
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <input type="file" accept=".xfdf" @change="importXfdfFromFile" />
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script setup>
import { ref, computed } from 'vue'
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

const viewerRef = ref()
// Computed so the controller is always resolved after the viewer mounts
const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

const importXfdfFromFile = (event) => {
  const file = event.target.files?.[0]
  if (!file) return
  const reader = new FileReader()
  // FileReader is async; the XFDF string is only available inside onload
  reader.onload = (e) => {
    const xfdf = e.target.result
    const result = xfdfControl.value?.importFromXfdf(xfdf)
    console.log(`Imported ${result?.annotations.length} annotation(s)`)
    // Log any entries the parser skipped or could not process
    if (result?.skipped.length) console.warn('Skipped:', result.skipped)
    if (result?.errors.length) console.error('Errors:', result.errors)
  }
  reader.readAsText(file)
}
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <input type="file" accept=".xfdf" @change="importXfdfFromFile" />
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script lang="ts">
import { ref, computed, defineComponent } from 'vue'
import { VPdfViewer, type VPVInstance } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

export default defineComponent({
  components: { VPdfViewer },
  setup() {
    const viewerRef = ref<VPVInstance>()
    // Computed so the controller is always resolved after the viewer mounts
    const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

    const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

    const importXfdfFromFile = (event: Event) => {
      const file = (event.target as HTMLInputElement).files?.[0]
      if (!file) return
      const reader = new FileReader()
      // FileReader is async; the XFDF string is only available inside onload
      reader.onload = (e) => {
        const xfdf = e.target?.result as string
        const result = xfdfControl.value?.importFromXfdf(xfdf)
        console.log(`Imported ${result?.annotations.length} annotation(s)`)
        // Log any entries the parser skipped or could not process
        if (result?.skipped.length) console.warn('Skipped:', result.skipped)
        if (result?.errors.length) console.error('Errors:', result.errors)
      }
      reader.readAsText(file)
    }

    return { viewerRef, annotationPlugin, importXfdfFromFile }
  }
})
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <input type="file" accept=".xfdf" @change="importXfdfFromFile" />
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>
vue
<script>
import { ref, computed, defineComponent } from 'vue'
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'

export default defineComponent({
  components: { VPdfViewer },
  setup() {
    const viewerRef = ref()
    // Computed so the controller is always resolved after the viewer mounts
    const xfdfControl = computed(() => viewerRef.value?.annotationXfdfControl)

    const annotationPlugin = VPdfAnnotationPlugin({ textSelection: true, freeText: true })

    const importXfdfFromFile = (event) => {
      const file = event.target.files?.[0]
      if (!file) return
      const reader = new FileReader()
      // FileReader is async; the XFDF string is only available inside onload
      reader.onload = (e) => {
        const xfdf = e.target.result
        const result = xfdfControl.value?.importFromXfdf(xfdf)
        console.log(`Imported ${result?.annotations.length} annotation(s)`)
        // Log any entries the parser skipped or could not process
        if (result?.skipped.length) console.warn('Skipped:', result.skipped)
        if (result?.errors.length) console.error('Errors:', result.errors)
      }
      reader.readAsText(file)
    }

    return { viewerRef, annotationPlugin, importXfdfFromFile }
  }
})
</script>

<template>
  <div :style="{ width: '1028px', height: '700px' }">
    <div class="toolbar">
      <input type="file" accept=".xfdf" @change="importXfdfFromFile" />
    </div>
    <VPdfViewer
      ref="viewerRef"
      src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
      :plugins="[annotationPlugin]"
    />
  </div>
</template>