This PDF.js Express sample demonstrates how you can apply permissions to users viewing your PDF document (no other external dependencies required). In this specific example you define how a user interacts with annotations in the PDF viewer. One common use case is removing a user’s ability to add annotation by providing them read-only access. Another example would be defining the types of annotations a user sees on a document – they can see highlighted text from another user but not the sticky notes they added. For other users you can give them the ability to respond to comments or give administrative rights to delete annotations. View demo
Note: This sample uses a custom Server
class created in this guide.
const server = new Server();
WebViewer({
path: '/static/WebViewer/lib/', // make sure that this relative path to the lib folder is correct
pdftronServer: 'https://demo.pdftron.com/', // comment this out to do client-side only
initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
}, document.getElementById('viewer'))
.then((instance) => {
const { docViewer, annotManager } = instance;
const urlInput = document.getElementById('url');
const copyButton = document.getElementById('copy');
instance.openElement('notesPanel');
if (window.location.origin === 'http://localhost:3000') {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4 && xhttp.status === 200) {
urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
}
};
xhttp.open('GET', '/ip', true);
xhttp.send();
} else {
urlInput.value = 'https://pdftron.com/samples/web/samples/annotation/realtime-collaboration/';
}
copyButton.onclick = () => {
urlInput.select();
document.execCommand('copy');
document.getSelection().empty();
};
docViewer.on('documentLoaded', () => {
let authorId = null;
async function onAnnotationCreated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotManager.redrawAnnotation(annotation);
}
async function onAnnotationUpdated(data) {
// Import the annotation based on xfdf command
const [annotation] = annotManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotManager.redrawAnnotation(annotation);
}
function onAnnotationDeleted(data) {
// data.key would return annotationId since our server method is designed as
// annotationsRef.child(annotationId).set(annotationData)
const command = `<delete><id>${data.key}</id></delete>`;
annotManager.importAnnotCommand(command).then(importedAnnotations => { });
}
function openReturningAuthorPopup(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotManager.setCurrentUser(authorName);
// Open popup for the returning author
window.alert(`Welcome back ${authorName}`);
}
function updateAuthor(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotManager.setCurrentUser(authorName);
// Create/update author information in the server
server.updateAuthor(authorId, { authorName });
}
function openNewAuthorPopup() {
// Open prompt for a new author
const name = window.prompt('Welcome! Tell us your name :)');
if (name) {
updateAuthor(name);
}
}
// Bind server-side authorization state change to a callback function
// The event is triggered in the beginning as well to check if author has already signed in
server.bind('onAuthStateChanged', (user) => {
// Author is logged in
if (user) {
// Using uid property from Firebase Database as an author id
// It is also used as a reference for server-side permission
authorId = user.uid;
// Check if author exists, and call appropriate callback functions
server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
// Bind server-side data events to callback functions
// When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
server.bind('onAnnotationCreated', onAnnotationCreated);
server.bind('onAnnotationUpdated', onAnnotationUpdated);
server.bind('onAnnotationDeleted', onAnnotationDeleted);
} else {
// Author is not logged in
server.signInAnonymously();
}
});
// Bind annotation change events to a callback function
annotManager.on('annotationChanged', (annotations, type, e) => {
// e.imported is true by default for annotations from pdf and annotations added by importAnnotCommand
if (e.imported) {
return;
}
// Iterate through all annotations and call appropriate server methods
annotations.forEach((annotation) => {
annotManager.exportAnnotCommand().then(xfdf =>{
let parentAuthorId = null;
if (type === 'add') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.createAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'modify') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.updateAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'delete') {
server.deleteAnnotation(annotation.Id);
}
});
});
});
// Overwrite client-side permission check method on the annotation manager
// The default was set to compare the authorName
// Instead of the authorName, we will compare authorId created from the server
annotManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
});
});
const server = new Server();
WebViewer({
path: '/static/WebViewer/lib/', // make sure that this relative path to the lib folder is correct
pdftronServer: 'https://demo.pdftron.com/', // comment this out to do client-side only
initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
}, document.getElementById('viewer'))
.then((instance) => {
const { documentViewer, annotationManager } = instance.Core;
const urlInput = document.getElementById('url');
const copyButton = document.getElementById('copy');
instance.UI.openElement('notesPanel');
if (window.location.origin === 'http://localhost:3000') {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4 && xhttp.status === 200) {
urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
}
};
xhttp.open('GET', '/ip', true);
xhttp.send();
} else {
urlInput.value = 'https://pdftron.com/samples/web/samples/annotation/realtime-collaboration/';
}
copyButton.onclick = () => {
urlInput.select();
document.execCommand('copy');
document.getSelection().empty();
};
documentViewer.addEventListener('documentLoaded', () => {
let authorId = null;
async function onAnnotationCreated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotationManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotationManager.redrawAnnotation(annotation);
}
async function onAnnotationUpdated(data) {
// Import the annotation based on xfdf command
const [annotation] = annotationManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotationManager.redrawAnnotation(annotation);
}
function onAnnotationDeleted(data) {
// data.key would return annotationId since our server method is designed as
// annotationsRef.child(annotationId).set(annotationData)
const command = `<delete><id>${data.key}</id></delete>`;
annotationManager.importAnnotCommand(command).then(importedAnnotations => { });
}
function openReturningAuthorPopup(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotationManager.setCurrentUser(authorName);
// Open popup for the returning author
window.alert(`Welcome back ${authorName}`);
}
function updateAuthor(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotationManager.setCurrentUser(authorName);
// Create/update author information in the server
server.updateAuthor(authorId, { authorName });
}
function openNewAuthorPopup() {
// Open prompt for a new author
const name = window.prompt('Welcome! Tell us your name :)');
if (name) {
updateAuthor(name);
}
}
// Bind server-side authorization state change to a callback function
// The event is triggered in the beginning as well to check if author has already signed in
server.bind('onAuthStateChanged', (user) => {
// Author is logged in
if (user) {
// Using uid property from Firebase Database as an author id
// It is also used as a reference for server-side permission
authorId = user.uid;
// Check if author exists, and call appropriate callback functions
server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
// Bind server-side data events to callback functions
// When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
server.bind('onAnnotationCreated', onAnnotationCreated);
server.bind('onAnnotationUpdated', onAnnotationUpdated);
server.bind('onAnnotationDeleted', onAnnotationDeleted);
} else {
// Author is not logged in
server.signInAnonymously();
}
});
// Bind annotation change events to a callback function
annotationManager.addEventListener('annotationChanged', (annotations, type, e) => {
// e.imported is true by default for annotations from pdf and annotations added by importAnnotCommand
if (e.imported) {
return;
}
// Iterate through all annotations and call appropriate server methods
annotations.forEach((annotation) => {
annotationManager.exportAnnotCommand().then(xfdf =>{
let parentAuthorId = null;
if (type === 'add') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.createAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'modify') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.updateAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'delete') {
server.deleteAnnotation(annotation.Id);
}
});
});
});
// Overwrite client-side permission check method on the annotation manager
// The default was set to compare the authorName
// Instead of the authorName, we will compare authorId created from the server
annotationManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
});
});
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="../../style.css">
<script src="/static/WebViewer/lib/webviewer.min.js"></script>
<script src='../../old-browser-checker.js'></script>
<script src="https://www.gstatic.com/firebasejs/3.5.3/firebase.js"></script>
<script src="server.js"></script>
</head>
<body>
<header>
<div className="title sample">Realtime collaboration sample</div>
</header>
<aside>
<h1>Controls</h1>
<h2>Share this url:</h2>
<input id="url" type="text" style={{"width":"calc(100% - 8px)","padding":"2px"}} readonly>
<br />
<button id="copy">Copy</button>
<hr />
<h1>Instructions</h1>
<p>Type your name when the prompt comes up. It will set your username and log you in to the realtime collaboration backend. Please note that the backend is being shared globally, so you might see annotations created by strangers.</p>
<p>Share the link with others in your local network to start collaborating!</p>
</aside>
<div id="viewer"></div>
<script src="../../menu-button.js"></script>
<script src="realtime-collaboration.js"></script>
</body>
</html>