Using SQLite3 to Enable Collaboration

This is a WebViewer sample to show how you can construct a real time collaboration server for WebViewer using WebSocket, SQLite3, and Node.js server.

WebViewer provides a slick out-of-the-box responsive UI that enables you to view, annotate and manipulate PDFs and other document types inside any web project.

Click the button below to view the full project in GitHub.

1const viewerElement = document.getElementById('viewer');
2
3let annotationManager = null;
4const DOCUMENT_ID = 'webviewer-demo-1';
5const hostName = window.location.hostname;
6const url = `ws://${hostName}:8181`;
7const connection = new WebSocket(url);
8const nameList = ['Andy', 'Andrew', 'Logan', 'Justin', 'Matt', 'Sardor', 'Zhijie', 'James', 'Kristian', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'David', 'Joseph', 'Thomas', 'Naman', 'Nancy', 'Sandra'];
9const serializer = new XMLSerializer();
10
11const sendAnnotationChanges = (annotations, action) => {
12 if (!annotations) {
13 return;
14 }
15
16 annotations.childNodes.forEach((child) => {
17 sendAnnotationChange(child, action);
18 });
19};
20
21connection.onerror = error => {
22 console.warn(`Error from WebSocket: ${error}`);
23}
24
25WebViewer.Iframe({
26 path: 'lib', // path to the PDFTron 'lib' folder
27 initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf',
28 documentXFDFRetriever: async () => {
29 const rows = await loadXfdfStrings(DOCUMENT_ID);
30 return JSON.parse(rows).map(row => row.xfdfString);
31 },
32}, viewerElement).then( instance => {
33
34 // Instance is ready here
35 instance.UI.openElements(['leftPanel']);
36 annotationManager = instance.Core.documentViewer.getAnnotationManager();
37 // Assign a random name to client
38 const randomValue = new Uint32Array(1);
39 crypto.getRandomValues(randomValue);
40 annotationManager.setCurrentUser(nameList[Math.floor((randomValue[0] / 2 ** 32) * nameList.length)]);
41 annotationManager.addEventListener('annotationChanged', async (_annotations, _action, info = {}) => {
42 // If annotation change is from import, return
43 if (info.imported) {
44 return;
45 }
46
47 const xfdfString = await annotationManager.exportAnnotationCommand();
48 // Parse xfdfString to separate multiple annotation changes to individual annotation change
49 const parser = new DOMParser();
50 const commandData = parser.parseFromString(xfdfString, 'text/xml');
51 const addedAnnots = commandData.getElementsByTagName('add')[0];
52 const modifiedAnnots = commandData.getElementsByTagName('modify')[0];
53 const deletedAnnots = commandData.getElementsByTagName('delete')[0];
54
55 // List of added annotations
56 sendAnnotationChanges(addedAnnots, 'add');
57 // List of modified annotations
58 sendAnnotationChanges(modifiedAnnots, 'modify');
59 // List of deleted annotations
60 sendAnnotationChanges(deletedAnnots, 'delete');
61 });
62
63 connection.onmessage = async (message) => {
64 const data = typeof message.data === 'string' ? message.data : await message.data.text();
65 const annotation = JSON.parse(data);
66 const annotations = await annotationManager.importAnnotationCommand(annotation.xfdfString);
67 await annotationManager.drawAnnotationsFromList(annotations);
68 }
69});
70
71const loadXfdfStrings = (documentId) => {
72 return new Promise((resolve, reject) => {
73 fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
74 method: 'GET',
75 }).then((res) => {
76 if (res.status < 400) {
77 res.text().then(xfdfStrings => {
78 resolve(xfdfStrings);
79 });
80 } else {
81 reject(new Error(`Failed to load XFDF strings for document ${documentId}: ${res.status} ${res.statusText}`));
82 }
83 }).catch((error) => {
84 reject(new Error(`Failed to fetch XFDF strings for document ${documentId}: ${error.message}`));
85 });
86 });
87};
88
89
90// wrapper function to convert xfdf fragments to full xfdf strings
91const convertToXfdf = (changedAnnotation, action) => {
92 let xfdfString = `<?xml version="1.0" encoding="UTF-8" ?><xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve"><fields />`;
93 if (action === 'add') {
94 xfdfString += `<add>${changedAnnotation}</add><modify /><delete />`;
95 } else if (action === 'modify') {
96 xfdfString += `<add /><modify>${changedAnnotation}</modify><delete />`;
97 } else if (action === 'delete') {
98 xfdfString += `<add /><modify /><delete>${changedAnnotation}</delete>`;
99 }
100 xfdfString += `</xfdf>`;
101 return xfdfString;
102}
103
104// helper function to send annotation changes to WebSocket server
105const sendAnnotationChange = (annotation, action) => {
106 if (annotation.nodeType !== annotation.TEXT_NODE) {
107 const annotationString = serializer.serializeToString(annotation);
108 connection.send(JSON.stringify({
109 documentId: DOCUMENT_ID,
110 annotationId: annotation.getAttribute('name'),
111 xfdfString: convertToXfdf(annotationString, action)
112 }));
113 }
114}

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales