Quickly import and export PDF annotations using XFDF — an XML-based format that captures details like position, color, and content.
This demo allows you to:
Implementation steps
To add XFDF import and export capability on PDFs with WebViewer:
Step 1: Get started with WebViewer in your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude 3.5 Sonnet, July 28, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// XFDF Annotations section
8//
9// Code to customize the WebViewer to export and import XFDF strings
10// and annotations.
11//
12
13// Default document with annotations
14const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/demo-annotated.pdf';
15
16// XFDF string to be loaded as an annotation into the viewer
17let xfdf =
18 `<?xml version="1.0" encoding="UTF-8" ?>
19 <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
20 <pdf-info xmlns="http://www.pdftron.com/pdfinfo" version="2" import-version="4" />
21 <fields />
22 <annots />
23 <pages>
24 <defmtx matrix="1,0,0,-1,0,792" />
25 </pages>
26 </xfdf>`
27
28const customizeUI = async (instance) => {
29 // Load the default file
30 await instance.Core.documentViewer.loadDocument(defaultDoc);
31};
32
33// Export annotations from the document as XFDF string
34const extractXFDFFromDocument = async (instance) => {
35 const xfdfString = await instance.Core.annotationManager.exportAnnotations();
36 if (xfdfString) {
37 xfdf = xfdfString;
38 }
39};
40
41// Get the XFDF code from the code block element to insert into the viewer
42const getCodeFromCodeBlock = async (instance) => {
43 const code = document.querySelector('#xfdf-code')?.textContent;
44 if (code) {
45 await instance.Core.annotationManager.importAnnotations(code);
46 instance.Core.documentViewer.refreshAll();
47 }
48};
49
50// Format the XFDF string for better readability in the code block
51const getFormattedXFDF = () => {
52 let formatted = '';
53 let indent = '';
54 const tab = ' ';
55
56 // Clean up the XFDF string by removing unnecessary whitespace
57 const xml = xfdf.replace(/>\s+</g, '><').replace(/></g, '>\n<');
58
59 // Format the cleaned XFDF string
60 xml.split(/>\s*</).forEach((node) => {
61 if (node.match(/^\/\w/)) {
62 indent = indent.substring(tab.length);
63 }
64 formatted += indent + '<' + node + '>\n';
65 if (node.match(/^<?\w[^>]*[^\/]$/)) {
66 indent += tab;
67 }
68 });
69
70 return formatted.substring(1, formatted.length - 2);
71};
72
73
74// WebViewer section
75//
76// This code initializes the WebViewer with the basic settings
77// that are found in the default showcase WebViewer
78//
79
80const searchParams = new URLSearchParams(window.location.search);
81const history = window.history || window.parent.history || window.top.history;
82const licenseKey = 'YOUR_LICENSE_KEY';
83const element = document.getElementById('viewer');
84
85// Initialize WebViewer with the specified settings
86WebViewer({
87 path: '/lib',
88 licenseKey: licenseKey,
89}, element).then((instance) => {
90 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
91 const cloudyTools = [
92 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
93 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
94 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
95 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
96 ];
97 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
98 instance.UI.disableTools(cloudyTools);
99
100 // Set default toolbar group to Annotate
101 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
102
103 // Set default tool on mobile devices to Pan.
104 // https://apryse.atlassian.net/browse/WVR-3134
105 if (isMobileDevice()) {
106 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
107 }
108
109 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
110 if (searchParams.has('file')) {
111 searchParams.delete('file');
112 history.replaceState(null, '', '?' + searchParams.toString());
113 }
114 });
115
116 instance.Core.annotationManager.enableAnnotationNumbering();
117
118 instance.UI.NotesPanel.enableAttachmentPreview();
119
120 // Add the demo-specific functionality
121 customizeUI(instance).then(() => {
122 // Create UI controls after demo is initialized
123 createUIControls(instance);
124 });
125});
126
127// Function to check if the user is on a mobile device
128const isMobileDevice = () => {
129 return (
130 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
131 window.navigator.userAgent
132 ) ||
133 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
134 window.navigator.userAgent.substring(0, 4)
135 )
136 );
137}
138
139// Cleanup function for when the demo is closed or page is unloaded
140const cleanup = (instance) => {
141 if (typeof instance !== 'undefined' && instance.UI) {
142 // Clean up any resources if needed
143 console.log('Cleaning up xfdf-annotations demo');
144 }
145};
146
147// Register cleanup for page unload
148window.addEventListener('beforeunload', () => cleanup());
149window.addEventListener('unload', () => cleanup());
150
151// UI section
152//
153// Helper code to add controls to the viewer holding the buttons
154// This code creates a container for the buttons, styles them, and adds them to the viewer
155//
156
157// Choose File button
158const filePicker = (instance) => {
159 const button = document.createElement('button');
160 button.className = 'btn-filepicker';
161 button.textContent = 'Choose File';
162 button.onclick = () => {
163 const input = document.createElement('input');
164 input.type = 'file';
165 input.accept = '.pdf';
166 input.onchange = async (event) => {
167 const file = event.target.files[0];
168 if (file) {
169 const doc = await instance.Core.createDocument(file);
170 instance.Core.documentViewer.loadDocument(doc);
171 }
172 };
173 input.click();
174 };
175
176 return button;
177}
178
179// XFDF Modal for displaying and editing XFDF code
180const xfdfModal = (instance) => {
181 const modal = document.createElement('div');
182 modal.className = 'xfdf-modal';
183
184 // Close the modal when clicking outside of it
185 modal.onclick = (event) => {
186 if (event.target === modal) {
187 modal.style.display = 'none';
188 }
189 };
190
191 // Modal content (non-shaded part)
192 const content = document.createElement('div');
193 content.className = 'xfdf-modal-content';
194
195 // header
196 const header = document.createElement('div');
197 header.className = 'xfdf-modal-header';
198
199 // Title
200 const title = document.createElement('h2');
201 title.textContent = 'Form Data';
202
203 // Close button
204 const close = document.createElement('button');
205 close.className = 'btn-xfdf-modal-close';
206 close.textContent = '×';
207 close.onclick = () => {
208 modal.style.display = 'none';
209 };
210
211 // Assemble the modal header
212 header.appendChild(title);
213 header.appendChild(close);
214
215 // Code block for XFDF
216 const codePre = document.createElement('pre');
217 codePre.className = 'xfdf-pre';
218 const codeBlock = document.createElement('code');
219 codeBlock.id = 'modal-xfdf-code';
220 codeBlock.contentEditable = true;
221 codeBlock.textContent = getFormattedXFDF();
222 codeBlock.onchange = () => {
223 xfdf = codeBlock.textContent;
224 };
225 codePre.appendChild(codeBlock);
226
227 // Modal footer
228 const footer = document.createElement('div');
229 footer.className = 'xfdf-modal-footer';
230
231 // Zoom buttons
232 const zoom = document.createElement('div');
233 zoom.className = 'xfdf-modal-zoom';
234
235 // Zoom in button
236 const zoomIn = document.createElement('button');
237 zoomIn.className = 'btn-zoom-in';
238 zoomIn.textContent = '+';
239 zoomIn.onclick = () => {
240 const codeBlock = document.querySelector('#modal-xfdf-code');
241 if (codeBlock) {
242 const currentFontSize = parseFloat(window.getComputedStyle(codeBlock).fontSize);
243 codeBlock.style.fontSize = (currentFontSize + 2) + 'px';
244 }
245 };
246
247 // Zoom label
248 const zoomLabel = document.createElement('span');
249 zoomLabel.textContent = 'Zoom';
250
251 // Zoom out button
252 const zoomOut = document.createElement('button');
253 zoomOut.className = 'btn-zoom-out';
254 zoomOut.textContent = '-';
255 zoomOut.onclick = () => {
256 const codeBlock = document.querySelector('#modal-xfdf-code');
257 if (codeBlock) {
258 const currentFontSize = parseFloat(window.getComputedStyle(codeBlock).fontSize);
259 codeBlock.style.fontSize = (currentFontSize - 2) + 'px';
260 }
261 };
262
263 // Assemble the zoom controls
264 zoom.appendChild(zoomIn);
265 zoom.appendChild(zoomLabel);
266 zoom.appendChild(zoomOut);
267
268 // Ok button to accept changes and close the modal
269 const ok = document.createElement('button');
270 ok.className = 'btn-xfdf-modal-ok';
271 ok.textContent = 'OK';
272 ok.onclick = async () => {
273 xfdf = codeBlock.textContent;
274 modal.style.display = 'none';
275 };
276
277 // Assemble the modal footer
278 footer.appendChild(zoom);
279 footer.appendChild(ok);
280
281 // Append all parts to the modal content
282 content.appendChild(header);
283 content.appendChild(codePre);
284 content.appendChild(footer);
285 modal.appendChild(content);
286
287 return modal;
288};
289
290// XFDF Code Block Element
291const xfdfElement = () => {
292 const wrapper = document.createElement('div');
293 wrapper.className = 'xfdf-wrapper';
294
295 // Open modal for XFDF code block
296 const button = document.createElement('button');
297 button.className = 'btn-modal';
298 button.textContent = 'Open in Dialog';
299 button.onclick = () => {
300 const modal = document.querySelector('.xfdf-modal');
301 if (!modal) {
302 console.error('XFDF modal not found');
303 return;
304 }
305 // Set the XFDF code block content
306 const modalCodeBlock = modal.querySelector('#modal-xfdf-code');
307 if (modalCodeBlock) {
308 modalCodeBlock.textContent = getFormattedXFDF();
309 }
310 modal.style.display = 'block';
311 };
312
313 // Container for the XFDF code block
314 const container = document.createElement('div');
315 container.className = 'xfdf-container';
316
317 // Code block for XFDF
318 const codePre = document.createElement('pre');
319 codePre.className = 'xfdf-pre';
320
321 const codeBlock = document.createElement('code');
322 codeBlock.id = 'xfdf-code';
323 codeBlock.contentEditable = true;
324 codeBlock.textContent = getFormattedXFDF();
325
326 // Assemle the XFDF code block
327 codePre.appendChild(codeBlock);
328 container.appendChild(codePre);
329 wrapper.appendChild(container);
330 wrapper.appendChild(button);
331 return wrapper;
332};
333
334// Export XFDF button
335const exportButton = (instance) => {
336 const button = document.createElement('button');
337 button.className = 'btn-export';
338 button.textContent = 'View Document XFDF';
339 button.onclick = async () => {
340 await extractXFDFFromDocument(instance);
341 const codeBlock = document.querySelector('#xfdf-code');
342 if (codeBlock) {
343 codeBlock.textContent = getFormattedXFDF();
344 }
345 };
346 return button;
347};
348
349// Import XFDF button
350const importButton = (instance) => {
351 const button = document.createElement('button');
352 button.className = 'btn-import';
353 button.textContent = 'Update Document XFDF';
354 button.onclick = async () => {
355 await getCodeFromCodeBlock(instance);
356 };
357 return button;
358};
359
360const createUIControls = (instance) => {
361 // Create a container for all controls
362 const controlsContainer = document.createElement('div');
363 controlsContainer.className = 'button-container';
364
365 // Insert the XFDF modal in the right panel (Notes Panel)
366 const notesPanel = instance.UI.NotesPanel;
367 if (notesPanel) {
368 // Create the modal and append it to the notes panel container
369 const modal = xfdfModal(instance);
370 // Try to find the notes panel element in the DOM
371 setTimeout(() => {
372 const notesPanelElement = document.querySelector('[data-element="notesPanel"]') ||
373 document.querySelector('.NotesPanel') ||
374 document.querySelector('[class*="notes-panel"]');
375
376 if (notesPanelElement) {
377 notesPanelElement.appendChild(modal);
378 } else {
379 // Fallback: append to document body
380 document.body.appendChild(modal);
381 }
382 }, 1000); // Wait for UI to be ready
383 } else {
384 // Fallback: add to main element
385 element.insertBefore(xfdfModal(instance), element.firstChild);
386 }
387
388 // Add the file picker and Import/Export buttons to the controls container
389 controlsContainer.appendChild(filePicker(instance));
390 controlsContainer.appendChild(exportButton(instance));
391 controlsContainer.appendChild(importButton(instance));
392
393 // Add the XFDF code block element and controls container to the viewer
394 element.insertBefore(xfdfElement(), element.firstChild);
395 element.insertBefore(controlsContainer, element.firstChild);
396};
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales