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}
1const fs = require('node:fs');
2const sqlite3 = require('sqlite3').verbose();
3const TABLE = 'annotations';
4const WebSocket = require('ws');
5const wss = new WebSocket.Server({ port: 8181 });
6const DB_PATH = 'server/xfdf.db';
7
8const annotationHandler = (app) => {
9
10 // Create and initialize database
11 if (!fs.existsSync(DB_PATH)) {
12 fs.writeFileSync(DB_PATH, '');
13 }
14 const db = new sqlite3.Database(DB_PATH);
15 db.serialize(() => {
16 db.run(`CREATE TABLE IF NOT EXISTS ${TABLE} (documentId TEXT, annotationId TEXT PRIMARY KEY, xfdfString TEXT)`);
17 });
18
19 // Connect to WebSocket client
20 wss.on('connection', ws => {
21 // When message is received from client
22 ws.on('message', rawMessage => {
23 let payload;
24 try {
25 payload = JSON.parse(rawMessage.toString());
26 } catch (error) {
27 console.warn('Skipping invalid WebSocket payload', error);
28 return;
29 }
30
31 const { documentId, annotationId, xfdfString } = payload;
32 if (!documentId || !annotationId || !xfdfString) {
33 return;
34 }
35
36 // Persist annotation payload
37 db.run(
38 `INSERT OR REPLACE INTO ${TABLE} (documentId, annotationId, xfdfString) VALUES (?, ?, ?)`,
39 [documentId, annotationId, xfdfString],
40 (err) => {
41 if (err) {
42 console.warn('Failed to persist annotation payload', err);
43 }
44 },
45 );
46
47 const message = JSON.stringify(payload);
48 wss.clients.forEach((client) => {
49 // Broadcast to every client except for the client where the message came from
50 if (client.readyState === WebSocket.OPEN && ws !== client) {
51 client.send(message);
52 }
53 });
54 });
55 });
56
57 app.get('/server/annotationHandler.js', (req, res) => {
58 const documentId = req.query.documentId;
59 db.all(`SELECT annotationId, xfdfString FROM ${TABLE} WHERE documentId = ?`, [documentId], (err, rows) => {
60 if (err) {
61 res.status(204);
62 } else {
63 res.setHeader('Content-Type', 'application/json');
64 res.status(200).send(rows);
65 }
66 res.end();
67 });
68 });
69};
70
71module.exports = annotationHandler;
72
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales