Learn More About Document Collaboration in WebViewer

WebViewer's JavaScript document collaboration library contains APIs that allow you to export/import annotations from/to a document. Using those APIs and a server, you can set up realtime collaboration easily.

Apryse does not ship any server side collaboration tools - you must take the basic concepts outlined below and use them to implement your own flow within your application.

Technologies

Enabling collaboration in WebViewer requires you to build a server that can sync annotations back and forth between users. This typically involves storing annotation data (XFDF) in a database and enabling some kind of real-time connection between your clients and your server. This is typically accomplished through the use of Web Sockets.

You can use any backend technologies you wish. We provide samples using SQLite and Firebase, but collaboration is not restricted to these technologies.

Collaboration basics

Syncing annotations (client side)

Setting up real time collaboration typically involves binding to the WebViewer annotationChanged event, and syncing the clients annotation state with your servers annotation state.

This usually will look something like this

JavaScript

1WebViewer(...)
2 .then(instance => {
3 const { annotationManager } = instance.Core;
4 annotationManager.addEventListener('annotationChanged', (annotations, action) => {
5
6 // It is recommended to assign a unique ID to each document
7 const documentId = getDocumentId()
8
9 // Sync annotations with server depending on the action
10 if (action === 'add') {
11 await addAnnotations(annotations, documentId)
12 } else if (action === 'modify') {
13 await modifyAnnotations(annotations, documentId)
14 } else if (action === 'delete') {
15 await deleteAnnotations(annotations, documentId)
16 }
17 });
18 }))

The addAnnotations, modifyAnnotations, and deleteAnnotations functions will typically contain your own logic to extract the XFDF from the changed annotations and push the updates to your server.

Here's an example of what an addAnnotation function might look like.

Example "addAnnotation" function

1async function addAnnotations(annotations, annotationManager, documentId) {
2 const payload = [];
3 for(const annotation of annotations) {
4 const xfdf = await annotationManager.exportAnnotations({ annotList: [annotation] })
5 payload.push({
6 xfdf,
7 id: annotation.Id,
8 documentId
9 })
10 }
11 // Sync the new annotations to your server / database
12 await fetch("/api/annotations", {
13 body: JSON.stringify(payload),
14 method: "POST"
15 })
16}

It is recommended to assign a unique ID to each document in your system. This will allow you to easily fetch and sync annotations on a per-document basis.

You should generate these ID's with your own system instead of using the ID generated by WebViewer.

Syncing annotations (server side)

Once you have your client sending XFDF data to your server, you need to update your server to handle that data. This will typically involve setting up endpoints for each annotation operation (add, modify, delete).

Each of these endpoints should have 3 primary functions:

  1. Authenticate the user
  2. Sync the annotation to the database
  3. Send real-time updates to other connected users

User authentication will depend on your server technology, so that will not be explained in this guide.

Syncing the annotation to your database will usually mean writing the XFDF, the userId, the documentId, and an annotationId.

If you are using an ORM like Prisma, this could look like this:

Writing an annotation to your database

1app.post('/annotations', (req, res) => {
2 const annotations = req.body;
3
4 for(const annotation of annotations) {
5 await prisma.Annotation.create({
6 data: {
7 id: annotation.id,
8 xfdf: annotation.xfdf,
9 userId: req.user.id,
10 documentId: annotation.documentId
11 }
12 })
13 }
14 // Send real-time events here
15 // syncClients()
16
17 res.send(200)
18}))

The same logic applies for modifying data, except instead of writing a new row to your database, you will be editing or deleting an existing row.

Real-time syncing between users

Once you have your basic annotation sync in place, you can start implementing the real-time updates between users.

There are many strategies you can use to accomplish this, but the most common are

If possible, we recommend using WebSockets as they provide a true real-time user experience.

The general strategy to implementing real-time updates is as follows

  1. Each client that is viewing a document subscribes to a "topic", usually identified by the document ID
  2. Any time your server receives a request to add/edit/delete annotations, it pushes an event to that topic. The payload for the event should contain the updated XFDF. These events should then fan out to your clients.
  3. When your client receives an event from the server, it should take the payload and use WebViewer APIs to sync the new annotation with the viewer

Here is a code sample using socket.io to show what this might look like:

Subscribing to events on the client

1const socket = io();
2WebViewer({
3...
4}, document.getElementById('viewer'))
5 .then(instance => {
6 const { documentViewer, annotationManager } = instance.Core;
7 documentViewer.addEventListener('documentLoaded', () => {
8 // Join the room for this document
9 socket.join(documentId)
10 socket.on("annotationAdded", (annotation) => {
11 const { xfdf } = annotation;
12 annotationManager.importAnnotations(xfdf)
13 })
14 })
15 });
16

Broadcasting events on the server

1const io = require("socket.io")(httpServer, {
2 // ...
3});
4app.post('/annotations', (req, res) => {
5 const annotations = req.body;
6
7 for(const annotation of annotations) {
8 await prisma.Annotation.create({
9 data: {
10 id: annotation.id,
11 xfdf: annotation.xfdf,
12 userId: req.user.id,
13 documentId: annotation.documentId
14 }
15 })
16
17 // Send event to anyone subscribed to this documents topic
18 io.to(annotation.documentId).emit("annotationAdded", annotation);
19 }
20 res.send(200)
21})
22

Loading annotations on document load

The final piece of functionality for a basic real-time collaboration flow is displaying a document's annotations on load.

This is done by querying all the annotations belonging to the document from your database, and importing the XFDF for those annotations into WebViewer.

Here is an example of what this might look like:

Query annotations on the client

1WebViewer(...)
2 .then(instance => {
3 const { documentViewer, annotationManager } = instance.Core;
4 documentViewer.addEventListener('documentLoaded', async () => {
5 const documentId = getDocumentId() // get your doc ID here
6 const resp = await fetch(`/api/annotations?docId=${documentId}`)
7 const data = await resp.json()
8 for(const annotation of data) {
9 const { xfdf } = annotation;
10 annotationManager.importAnnotations(xfdf)
11 }
12 });
13 })

Fetch annotations on the server

1app.get("/annotations", (req, res) => {
2 const { docId } = req.query;
3 const annots = await prisma.Annotations.findMany({
4 where: {
5 documentId: docId
6 }
7 })
8 return res.json(annots)
9})

Next steps

The rest of the guides in this section walk you through how to implement a real-time collaboration flow using Firebase using the same concepts we discussed above.

Get started by setting up your viewer.

If you are not using firebase, we still recommend reading the remaining guides as the same concepts with almost any technology stack.

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales