Appearance
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
| Name | Description | Type |
|---|---|---|
exportToXfdf | Collect 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 |
importFromXfdf | Parse 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 (exportToXfdf → importFromXfdf) 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>