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
11connection.onerror = error => {
12 console.warn(`Error from WebSocket: ${error}`);
13}
14
15WebViewer.Iframe({
16 path: 'lib', // path to the PDFTron 'lib' folder
17 initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf',
18 documentXFDFRetriever: async () => {
19 const rows = await loadXfdfStrings(DOCUMENT_ID);
20 return JSON.parse(rows).map(row => row.xfdfString);
21 },
22}, viewerElement).then( instance => {
23
24 // Instance is ready here
25 instance.UI.openElements(['leftPanel']);
26 annotationManager = instance.Core.documentViewer.getAnnotationManager();
27 // Assign a random name to client
28 annotationManager.setCurrentUser(nameList[Math.floor(Math.random()*nameList.length)]);
29 annotationManager.addEventListener('annotationChanged', async e => {
30 // If annotation change is from import, return
31 if (e.imported) {
32 return;
33 }
34
35 const xfdfString = await annotationManager.exportAnnotationCommand();
36 // Parse xfdfString to separate multiple annotation changes to individual annotation change
37 const parser = new DOMParser();
38 const commandData = parser.parseFromString(xfdfString, 'text/xml');
39 const addedAnnots = commandData.getElementsByTagName('add')[0];
40 const modifiedAnnots = commandData.getElementsByTagName('modify')[0];
41 const deletedAnnots = commandData.getElementsByTagName('delete')[0];
42
43 // List of added annotations
44 addedAnnots.childNodes.forEach((child) => {
45 sendAnnotationChange(child, 'add');
46 });
47
48 // List of modified annotations
49 modifiedAnnots.childNodes.forEach((child) => {
50 sendAnnotationChange(child, 'modify');
51 });
52
53 // List of deleted annotations
54 deletedAnnots.childNodes.forEach((child) => {
55 sendAnnotationChange(child, 'delete');
56 });
57 });
58
59 connection.onmessage = async (message) => {
60 const annotation = JSON.parse(message.data);
61 const annotations = await annotationManager.importAnnotationCommand(annotation.xfdfString);
62 await annotationManager.drawAnnotationsFromList(annotations);
63 }
64});
65
66const loadXfdfStrings = (documentId) => {
67 return new Promise((resolve, reject) => {
68 fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
69 method: 'GET',
70 }).then((res) => {
71 if (res.status < 400) {
72 res.text().then(xfdfStrings => {
73 resolve(xfdfStrings);
74 });
75 } else {
76 reject(res);
77 }
78 });
79 });
80};
81
82
83// wrapper function to convert xfdf fragments to full xfdf strings
84const convertToXfdf = (changedAnnotation, action) => {
85 let xfdfString = `<?xml version="1.0" encoding="UTF-8" ?><xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve"><fields />`;
86 if (action === 'add') {
87 xfdfString += `<add>${changedAnnotation}</add><modify /><delete />`;
88 } else if (action === 'modify') {
89 xfdfString += `<add /><modify>${changedAnnotation}</modify><delete />`;
90 } else if (action === 'delete') {
91 xfdfString += `<add /><modify /><delete>${changedAnnotation}</delete>`;
92 }
93 xfdfString += `</xfdf>`;
94 return xfdfString;
95}
96
97// helper function to send annotation changes to WebSocket server
98const sendAnnotationChange = (annotation, action) => {
99 if (annotation.nodeType !== annotation.TEXT_NODE) {
100 const annotationString = serializer.serializeToString(annotation);
101 connection.send(JSON.stringify({
102 documentId: DOCUMENT_ID,
103 annotationId: annotation.getAttribute('name'),
104 xfdfString: convertToXfdf(annotationString, action)
105 }));
106 }
107}
1const fs = require('fs');
2const sqlite3 = require('sqlite3').verbose();
3const TABLE = 'annotations';
4const WebSocket = require('ws');
5const wss = new WebSocket.Server({ port: 8181});
6
7module.exports = (app) => {
8
9 // Create and initialize database
10 if (!fs.existsSync('server/xfdf.db')) {
11 fs.writeFileSync('server/xfdf.db', '');
12 }
13 const db = new sqlite3.Database('./xfdf.db');
14 db.serialize(() => {
15 db.run(`CREATE TABLE IF NOT EXISTS ${TABLE} (documentId TEXT, annotationId TEXT PRIMARY KEY, xfdfString TEXT)`);
16 });
17
18 // Connect to WebSocket client
19 wss.on('connection', ws => {
20 // When message is received from client
21 ws.on('message', message => {
22 const documentId = JSON.parse(message).documentId;
23 const annotationId = JSON.parse(message).annotationId;
24 const xfdfString = JSON.parse(message).xfdfString.replace(/\'/g, `''`);
25 // Prepare statement to sanitize input
26 let statement = db.prepare(`INSERT OR REPLACE INTO annotations VALUES (?, ?, ?)`);
27 db.serialize(() => {
28 statement.run(documentId, annotationId, xfdfString);
29 });
30 wss.clients.forEach((client) => {
31 // Broadcast to every client except for the client where the message came from
32 if (client.readyState === WebSocket.OPEN && ws !== client) {
33 client.send(message);
34 }
35 });
36 })
37 });
38
39 app.get('/server/annotationHandler.js', (req,res) => {
40 const documentId = req.query.documentId;
41 db.all(`SELECT annotationId, xfdfString FROM ${TABLE} WHERE documentId = '${documentId}'`, (err, rows) => {
42 if(err) {
43 res.status(204);
44 } else {
45 res.setHeader('Content-Type', 'application/json');
46 res.status(200).send(rows);
47 }
48 res.end();
49 });
50 });
51}
52
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales