pdfdraw/src/pdfdraw.js

1432 lines
40 KiB
JavaScript

/* eslint indent: ["error", 2, { "outerIIFEBody": 0, "SwitchCase": 1 }] */
/**
* @copyright Copyright (C) 2018, struktur AG
*
* @author Joachim Bauch <mail@joachim-bauch.de>
*
* @license AGPL-3.0
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import $ from 'jquery';
import io from 'socket.io-client';
import iro from '@jaames/iro';
import paper from 'paper';
import { pdfjsLib, PDFViewerApplication, PDFViewerApplicationOptions } from 'pdfjs-dist-viewer-min';
(function() {
"use strict";
// TODO(jojo): Should get this from a viewer.js property.
var CSS_UNITS = 96.0 / 72.0;
var INITAL_COLORS = [
"#ff0000",
"#008080",
"#00ffff",
"#00ff00",
"#008000",
"#c0c0c0",
"#f7347a",
"#990000",
"#ccff00",
"#3399ff",
"#f6546a",
"#ffff00",
"#ffa500",
"#0000ff",
"#800080",
];
var PERMISSION_CREATE = 4;
var PERMISSION_READ = 1;
var PERMISSION_UPDATE = 2;
var PERMISSION_DELETE = 8;
var PERMISSION_SHARE = 16;
var PERMISSION_ALL = 31;
// Taken from https://gist.github.com/jed/982883
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function getRandomColor() {
return INITAL_COLORS[Math.floor(Math.random() * INITAL_COLORS.length)];
}
// Required for running in older versions of Qt (see #31).
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
String.prototype.padStart = function padStart(targetLength, padString) {
targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
padString = String(padString !== undefined ? padString : ' ');
if (this.length >= targetLength) {
return String(this);
} else {
targetLength = targetLength - this.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
}
return padString.slice(0, targetLength) + String(this);
}
};
}
var uid = uuidv4();
var object_count = 0;
function getObjectId() {
return uid + ":" + (++object_count);
}
var Storage = function(prefix) {
this.prefix = prefix;
this.storage = window.localStorage;
if (!this.storage) {
console.warn('LocalStorage not available, saving of settings will be disabled.');
}
};
Storage.prototype._getKey = function(key) {
if (!this.prefix) {
return key;
}
return this.prefix + '.' + key;
};
Storage.prototype.get = function(key) {
if (!this.storage) {
return null;
}
return this.storage.getItem(this._getKey(key));
};
Storage.prototype.set = function(key, value) {
if (!this.storage) {
return;
} else if (!value) {
this.storage.removeItem(this._getKey(key));
return;
}
this.storage.setItem(this._getKey(key), value);
};
var BaseDrawer = function(annotator, previous_mode) {
this.annotator = annotator;
this.previous_mode = previous_mode;
};
BaseDrawer.prototype.destroy = function() {};
BaseDrawer.prototype.activate = function(page_annotator) {};
BaseDrawer.prototype.onMouseDown = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseUp = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseDrag = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseEnter = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseLeave = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseMove = function(page_annotator, event) {};
BaseDrawer.prototype.onClick = function(page_annotator, event) {};
BaseDrawer.prototype.onKeyUp = function(page_annotator, event) {};
BaseDrawer.prototype.onItemMoved = function(page_annotator, name, item, event) {};
var NullDrawer = function() {
BaseDrawer.apply(this, arguments);
};
NullDrawer.prototype = Object.create(BaseDrawer.prototype);
var FreehandDrawer = function(annotator) {
BaseDrawer.apply(this, arguments);
this.path = null;
this.intervalId = null;
};
FreehandDrawer.prototype = Object.create(BaseDrawer.prototype);
FreehandDrawer.prototype.sendPath = function() {
if (this.path && this.path.segments.length) {
this.annotator.sendItem(this.page_annotator, this.path);
}
};
FreehandDrawer.prototype.onMouseDown = function(page_annotator, event) {
page_annotator.activate();
this.page_annotator = page_annotator;
this.intervalId = setInterval(this.sendPath.bind(this), 250);
this.path = page_annotator.createPath({
name: getObjectId(),
strokeColor: this.annotator.color,
strokeWidth: 5
});
};
FreehandDrawer.prototype.onMouseUp = function(page_annotator, event) {
if (!this.path) {
return;
}
if (this.path.segments.length) {
this.path.smooth();
this.annotator.sendItem(page_annotator, this.path);
}
this.path = null;
clearInterval(this.intervalId);
};
FreehandDrawer.prototype.onMouseDrag = function(page_annotator, event) {
if (!this.path) {
return;
}
this.path.add(event.point);
};
var LineDrawer = function(annotator) {
FreehandDrawer.apply(this, arguments);
};
LineDrawer.prototype = Object.create(FreehandDrawer.prototype);
LineDrawer.prototype.onMouseDrag = function(page_annotator, event) {
if (!this.path) {
return;
}
this.path.removeSegment(1);
this.path.add(event.point);
};
var RectangleDrawer = function(annotator) {
BaseDrawer.apply(this, arguments);
this.options = {};
this.rect = null;
};
RectangleDrawer.prototype = Object.create(BaseDrawer.prototype);
RectangleDrawer.prototype.onMouseDown = function(page_annotator, event) {
page_annotator.activate();
this.options = {
name: getObjectId(),
strokeColor: this.annotator.color,
strokeWidth: 5,
from: [event.point.x, event.point.y],
to: [event.point.x, event.point.y],
};
this.update(page_annotator);
};
RectangleDrawer.prototype.update = function(page_annotator) {
var create = page_annotator.createRectangle.bind(page_annotator);
if (!this.rect) {
this.rect = create(this.options);
} else {
this.rect = this.rect.replaceWith(create(this.options));
}
};
RectangleDrawer.prototype.onMouseUp = function(page_annotator, event) {
if (!this.rect) {
return;
}
this.options.to = [event.point.x, event.point.y];
this.update(page_annotator);
this.rect.ready = true;
this.annotator.sendItem(page_annotator, this.rect);
this.options = {};
this.rect = null;
};
RectangleDrawer.prototype.onMouseDrag = function(page_annotator, event) {
if (!this.rect) {
return;
}
this.options.to = [event.point.x, event.point.y];
this.update(page_annotator);
};
var EllipseDrawer = function(annotator) {
BaseDrawer.apply(this, arguments);
this.options = {};
this.ellipse = null;
};
EllipseDrawer.prototype = Object.create(BaseDrawer.prototype);
EllipseDrawer.prototype.onMouseDown = function(page_annotator, event) {
page_annotator.activate();
this.options = {
name: getObjectId(),
strokeColor: this.annotator.color,
strokeWidth: 5,
from: [event.point.x, event.point.y],
to: [event.point.x, event.point.y],
};
this.update(page_annotator);
};
EllipseDrawer.prototype.update = function(page_annotator) {
var create = page_annotator.createEllipse.bind(page_annotator);
if (!this.ellipse) {
this.ellipse = create(this.options);
} else {
var temp_ellipse = create(this.options);
this.ellipse = this.ellipse.replaceWith(temp_ellipse);
}
};
EllipseDrawer.prototype.onMouseUp = function(page_annotator, event) {
if (!this.ellipse) {
return;
}
this.options.to = [event.point.x, event.point.y];
this.update(page_annotator);
this.ellipse.ready = true;
this.annotator.sendItem(page_annotator, this.ellipse);
this.options = {};
this.ellipse = null;
};
EllipseDrawer.prototype.onMouseDrag = function(page_annotator, event) {
if (!this.ellipse) {
return;
}
this.options.to = [event.point.x, event.point.y];
this.update(page_annotator);
};
var PointerDrawer = function(annotator) {
BaseDrawer.apply(this, arguments);
};
PointerDrawer.prototype = Object.create(BaseDrawer.prototype);
PointerDrawer.prototype.destroy = function() {
this.annotator.destroyCursors();
};
PointerDrawer.prototype.onMouseMove = function(page_annotator, event) {
this.annotator.renderCursor(page_annotator, event.point.x, event.point.y);
};
PointerDrawer.prototype.onMouseLeave = function(page_annotator, event) {
this.annotator.hideCursor();
};
var SelectDrawer = function(annotator) {
BaseDrawer.apply(this, arguments);
this.selected_item = null;
this.pending_items = {};
this.send_interval = setInterval(this._sendPending.bind(this), 100);
};
SelectDrawer.prototype = Object.create(BaseDrawer.prototype);
SelectDrawer.prototype._select = function(item) {
if (this.selected_item === item) {
return;
}
this._unselect();
this.selected_item = item;
this.selected_item.shadowColor = "black";
this.selected_item.shadowBlur = 10;
};
SelectDrawer.prototype._unselect = function() {
if (!this.selected_item) {
return;
}
this.selected_item.shadowColor = null;
this.selected_item.shadowBlur = 0;
this.selected_item = null;
};
SelectDrawer.prototype._sendItem = function(page_annotator, item) {
// Need to remove "selected" layout while generating item JSON.
var is_selected = (item === this.selected_item);
if (is_selected) {
this._unselect();
}
this.annotator.sendItem(page_annotator, item);
if (is_selected) {
this._select(item);
}
};
SelectDrawer.prototype._sendPending = function() {
var now = Date.now();
for (var name in this.pending_items) {
if (!this.pending_items.hasOwnProperty(name)) {
continue;
}
var info = this.pending_items[name];
var page_annotator = info[0];
var item = info[1];
var last_change = info[2];
if (now - last_change < 250) {
continue;
}
this._sendItem(page_annotator, item);
delete this.pending_items[name];
}
};
SelectDrawer.prototype.destroy = function() {
clearInterval(this.send_interval);
this._sendPending();
this._unselect();
};
SelectDrawer.prototype.onMouseDown = function(page_annotator, event) {
var hit = page_annotator.scope.project.hitTest(event.point);
if (!hit) {
this._unselect();
return;
}
this._select(hit.item);
};
SelectDrawer.prototype.onClick = function(page_annotator, event) {
var hit = page_annotator.scope.project.hitTest(event.point);
if (!hit) {
this._unselect();
return;
}
this._select(hit.item);
};
SelectDrawer.prototype.onKeyUp = function(page_annotator, event) {
if (!this.selected_item) {
return;
}
switch (event.keyCode) {
case 8: // Backspace
// Fallthrough
case 46: // Delete key
this.annotator.deleteItem(page_annotator, this.selected_item);
this.selected_item.remove();
this._unselect();
break;
}
};
SelectDrawer.prototype.onItemMoved = function(page_annotator, name, item, event) {
event.stopPropagation();
item.position.x += event.delta.x;
item.position.y += event.delta.y;
if (this.pending_items.hasOwnProperty(name)) {
this.pending_items[name][0] = page_annotator;
this.pending_items[name][1] = item;
} else {
this.pending_items[name] = [page_annotator, item, Date.now()];
}
};
var ColorPickerDrawer = function(annotator, previous_mode) {
BaseDrawer.apply(this, arguments);
this.annotator.showColorPicker();
};
ColorPickerDrawer.prototype = Object.create(BaseDrawer.prototype);
ColorPickerDrawer.prototype.destroy = function() {
this.annotator.hideColorPicker();
};
ColorPickerDrawer.prototype.onClick = function(page_annotator, event) {
this.annotator.setDrawMode(this.previous_mode);
};
var Cursor = function(annotator, userid, radius) {
this.annotator = annotator;
this.page_annotator = null;
this.name = getObjectId();
this.userid = userid;
this.radius = radius;
this.circle = null;
this.label = $("<div class='cursor'></div>");
this.label.css('position', 'absolute');
};
Cursor.prototype.destroy = function() {
if (this.page_annotator) {
this.page_annotator.unregisterPendingActivate(this);
}
if (this.circle) {
this.circle.remove();
this.circle = null;
}
this.label.remove();
};
Cursor.prototype.draw = function(page_annotator, x, y, color, text) {
this.x = x;
this.y = y;
var center = new paper.Point(x, y);
if (this.page_annotator && page_annotator !== this.page_annotator) {
this.page_annotator.unregisterPendingActivate(this);
if (this.page_annotator.container) {
this.page_annotator.container.remove(this.label);
}
this.page_annotator = null;
this.destroy();
}
if (!this.circle) {
page_annotator.activate();
this.circle = new paper.Path.Circle(center, this.radius);
}
if (!this.page_annotator) {
this.page_annotator = page_annotator;
if (this.page_annotator.container) {
this.page_annotator.container.append(this.label);
} else {
this.page_annotator.registerPendingActivate(this);
}
}
this.circle.set({
fillColor: color,
shadowColor: color,
shadowBlur: 10,
position: center
});
this.label.html(text);
this.update();
};
Cursor.prototype.activate = function(page_annotator) {
if (page_annotator !== this.page_annotator) {
return;
}
this.label.remove();
if (page_annotator.container) {
page_annotator.container.append(this.label);
}
};
Cursor.prototype.update = function() {
var radius = this.circle.bounds.width / 2;
var newRadius = this.radius / this.page_annotator.scale;
if (radius !== newRadius) {
this.circle.scale(newRadius / radius);
}
var x = this.x * this.page_annotator.scale;
var y = this.y * this.page_annotator.scale;
this.label.css('left', x + (this.radius * 2));
this.label.css('top', y - (this.radius));
};
var PageAnnotator = function(annotator, pagenum, container, page) {
this.annotator = annotator;
this.pagenum = pagenum;
this.container = container;
this.page = page;
this.pending_activation = {};
this.canvas = document.createElement("canvas");
this.canvas.style.position = "absolute";
this.canvas.style.left = 0;
this.canvas.style.top = 0;
this.canvas.style.right = 0;
this.canvas.style.bottom = 0;
this.scope = new paper.PaperScope();
this.scope.setup(this.canvas);
if (page) {
this.setPage(page, container);
}
this.view = this.scope.getView();
this.view.on('mousedown', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseDown.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('mouseup', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseUp.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('mousedrag', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseDrag.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('mouseenter', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseEnter.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('mouseleave', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseLeave.apply(this.annotator.drawer, args);
}.bind(this));
// Sometimes the "mouseleave" event of paper.js doesn't fire when
// switching views, also handle "mouseleave" on the canvas itself
// as a workaround.
$(this.canvas).on('mouseleave', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseLeave.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('mousemove', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onMouseMove.apply(this.annotator.drawer, args);
}.bind(this));
this.view.on('click', function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onClick.apply(this.annotator.drawer, args);
}.bind(this));
$(window).keyup(function(event) {
if (PageAnnotator.prototype.__active !== this) {
return;
}
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
this.activate();
this.annotator.drawer.onKeyUp.apply(this.annotator.drawer, args);
}.bind(this));
};
PageAnnotator.prototype.activate = function() {
PageAnnotator.prototype.__active = this;
for (var drawerName in this.pending_activation) {
if (!this.pending_activation.hasOwnProperty(drawerName)) {
continue;
}
this.pending_activation[drawerName].activate(this);
}
this.pending_activation = {};
this.scope.activate();
};
PageAnnotator.prototype.registerPendingActivate = function(drawer) {
this.pending_activation[drawer.name] = drawer;
};
PageAnnotator.prototype.unregisterPendingActivate = function(drawer) {
delete this.pending_activation[drawer.name];
};
PageAnnotator.prototype.setPage = function(page, container) {
this.container = container;
this.page = page;
var pagesize = page.view;
this.pagewidth = (pagesize[2] - pagesize[0]) * CSS_UNITS;
this.pageheight = (pagesize[3] - pagesize[1]) * CSS_UNITS;
if (this.container) {
this.container.append(this.canvas);
}
};
PageAnnotator.prototype.update = function(scale) {
if (this.container && !this.container.has(this.canvas).length) {
this.container.append(this.canvas);
}
this.scale = scale;
var width = this.pagewidth * scale;
var height = this.pageheight * scale;
this.view.viewSize = new paper.Size(width, height);
this.view.center = new paper.Point(
(width + (this.pagewidth * (1 - scale))) / 2,
(height + (this.pageheight * (1 - scale))) / 2);
this.view.zoom = scale;
this.view._needsUpdate = true;
this.view.requestUpdate();
};
PageAnnotator.prototype.createPath = function(options) {
var path = new paper.Path(options || {});
path.onMouseDrag = function(event) {
this.annotator.drawer.onItemMoved(this, path.name, path, event);
}.bind(this);
return path;
};
PageAnnotator.prototype.createRectangle = function(options) {
var rect = new paper.Path.Rectangle(options || {});
rect.onMouseDrag = function(event) {
if (rect.ready) {
this.annotator.drawer.onItemMoved(this, rect.name, rect, event);
}
}.bind(this);
return rect;
};
PageAnnotator.prototype.createEllipse = function(options) {
var from = new paper.Point(options.from);
var to = new paper.Point(options.to);
var rectangle = new paper.Rectangle(
from, to
);
var ellipse = new paper.Path.Ellipse(rectangle);
ellipse.strokeColor = options.strokeColor;
ellipse.strokeWidth = options.strokeWidth;
ellipse.name = options.name;
ellipse.onMouseDrag = function(event) {
if (ellipse.ready) {
this.annotator.drawer.onItemMoved(this, ellipse.name, ellipse, event);
}
}.bind(this);
return ellipse;
};
PageAnnotator.prototype.drawItem = function(name, data) {
this.activate();
var path = this.scope.project.getItem({"name": name});
if (!path) {
path = this.createPath();
}
try {
path.importJSON(data);
} catch (e) {
path.remove();
path.name = name;
this.annotator.deleteItem(this, path);
console.log("Could not import item", data, e);
return;
}
path.name = name;
};
PageAnnotator.prototype.deleteItem = function(name) {
this.activate();
var path = this.scope.project.getItem({"name": name});
if (!path) {
return;
}
path.remove();
};
function Annotator(socketurl, id, userid, displayname, token) {
this.annotators = {};
this.cursors = {};
this.draw_mode = null;
this.drawer = this.nulldrawer = new NullDrawer();
this.id = id;
this.userid = userid;
this.displayname = displayname;
this.token = token;
this.storage = new Storage('pdfdraw');
var color = this.storage.get('color');
if (!color) {
color = getRandomColor();
this.storage.set('color', color);
}
this.color = color;
this.pageCount = -1;
this.currentPage = null;
this.users = {};
this.has_document = false;
this.setDrawMode('pointer');
this.pending_messages = [];
this.socketurl = socketurl;
this.socket = io(socketurl, {
'transports': ['websocket'],
'query': {
'token': token
}
});
// Also allow polling connections when the connection was interrupted once
// (could be caused by proxy / firewall).
this.socket.io.on('reconnect_attempt', function() {
this.socket.io.opts.transports = ['polling', 'websocket'];
}.bind(this));
this.socket.io.on('reconnect', this.onReconnected.bind(this));
this.socket.on('message', this.onMessage.bind(this));
this.socket.on('user.joined', this.onUserJoined.bind(this));
this.socket.on('user.left', this.onUserLeft.bind(this));
this.socket.on('connect', this.onConnected.bind(this));
this.socket.on('disconnect', this.onDisconnect.bind(this));
this.colorPicker = new iro.ColorPicker("#colorPicker", {
width: 320,
height: 320,
color: this.color,
transparency: true,
});
var setColor = function(color) {
this.color = color.hex8String;
this.storage.set('color', this.color);
$(".modeButton.colorMode, .modeButton.colorMode:focus")
.css("background-color", this.color);
}.bind(this);
this.colorPicker.on("color:init", setColor);
this.colorPicker.on("color:change", setColor);
this.userlist = $("<div class='userlist'></div>");
$("#mainContainer").append(this.userlist);
this.connectionError = $("#connectionError");
this.connectingMessage = $("#connectingMessage");
}
Annotator.prototype.showColorPicker = function() {
$("#colorPicker").show();
};
Annotator.prototype.hideColorPicker = function() {
$("#colorPicker").hide();
};
Annotator.prototype.onDisconnect = function() {
this.users = {};
this.updateUsersList();
this.connectionError.show();
};
Annotator.prototype.onConnected = function() {
if (!this.connectingMessage) {
return;
}
this.connectingMessage.hide();
this.connectingMessage = null;
};
Annotator.prototype.onReconnected = function() {
this.connectionError.hide();
};
function compareEntries(a, b) {
// Compare by display name.
a = a[1].toLocaleLowerCase();
b = b[1].toLocaleLowerCase();
return a.localeCompare(b);
}
Annotator.prototype.updateUsersList = function() {
var entries = [];
for (var userid in this.users) {
if (!this.users.hasOwnProperty(userid)) {
continue;
}
entries.push([userid, this.users[userid].displayname || "Anonymous"]);
}
this.userlist.empty();
if (!entries.length) {
this.userlist.hide();
return;
}
entries.sort(compareEntries);
for (var i = 0; i < entries.length; ++i) {
userid = entries[i][0];
var displayname = entries[i][1];
var elem = $("<div></div>").text(displayname);
if (userid === this.socket.id) {
elem.addClass("own");
}
this.userlist.append(elem);
}
this.userlist.show();
};
Annotator.prototype.onUserJoined = function(message) {
for (var i = 0; i < message.length; ++i) {
var userid = message[i].userid;
if (!userid) {
continue;
}
this.users[userid] = message[i];
}
this.updateUsersList();
};
Annotator.prototype.onUserLeft = function(message) {
var userid = message.userid;
if (!userid || !this.users.hasOwnProperty(userid)) {
return;
}
delete this.users[userid];
this.updateUsersList();
};
Annotator.prototype.documentLoaded = function(pdfDocument) {
this.pageCount = pdfDocument.numPages;
this.has_document= true;
while (this.pending_messages.length) {
var message = this.pending_messages.shift();
this.onMessage(message);
}
if (this.connectingMessage) {
this.connectingMessage.show();
}
};
Annotator.prototype.sendMessage = function(message) {
// console.log("Send", message);
this.socket.emit('message', message);
};
Annotator.prototype.sendItem = function(page_annotator, item) {
var data = item.exportJSON();
if (!data) {
console.log("Can't export to JSON", item);
return;
}
this.sendMessage({
'type': 'item',
'item': {
'page': page_annotator.pagenum,
'name': item.name,
'data': data
}
});
};
Annotator.prototype.deleteItem = function(page_annotator, item) {
this.sendMessage({
'type': 'delete',
'delete': {
'page': page_annotator.pagenum,
'name': item.name
}
});
};
Annotator.prototype.onMessage = function(message) {
// console.log("Received", message);
if (!this.has_document) {
this.pending_messages.push(message);
return;
}
this.processMessage(message);
};
Annotator.prototype.renderCursor = function(page_annotator, x, y) {
var cursor = this.getCursor(this.userid, 5);
cursor.draw(page_annotator, x, y, this.color, this.displayname);
var data = {
'type': 'cursor',
'cursor': {
'action': 'show',
'page': page_annotator.pagenum,
'x': x,
'y': y,
'color': this.color
}
};
this.sendMessage(data);
};
Annotator.prototype.getCursor = function(userid, size) {
var cursor;
if (!this.cursors.hasOwnProperty(userid)) {
cursor = this.cursors[userid] = new Cursor(this, userid, size);
} else {
cursor = this.cursors[userid];
}
return cursor;
};
Annotator.prototype.showCursor = function(userid, data) {
if (!this.users.hasOwnProperty(userid)) {
return;
}
var displayname = this.users[userid].displayname || "Anonymous";
var cursor = this.getCursor(userid, 5);
this.getPage(data.page).then(function(page_annotator) {
cursor.draw(page_annotator, data.x, data.y, data.color, displayname);
});
};
Annotator.prototype.hideCursor = function(userid) {
if (!userid) {
var data = {
'type': 'cursor',
'cursor': {
'action': 'hide'
}
};
this.sendMessage(data);
userid = this.userid;
}
if (!this.cursors.hasOwnProperty(userid)) {
return;
}
var cursor = this.cursors[userid];
delete this.cursors[userid];
cursor.destroy();
};
Annotator.prototype.destroyCursors = function() {
for (var i in this.cursors) {
if (!this.cursors.hasOwnProperty(i)) {
continue;
}
this.cursors[i].destroy();
}
this.cursors = {};
};
Annotator.prototype.getExistingPage = function(pagenum) {
if (typeof(pagenum) === "string") {
pagenum = parseInt(pagenum, 10);
}
if (!this.annotators.hasOwnProperty(pagenum)) {
return null;
}
return this.annotators[pagenum];
};
Annotator.prototype.getPage = function(pagenum, page, container) {
if (typeof(pagenum) === "string") {
pagenum = parseInt(pagenum, 10);
}
return new Promise(function(resolve, reject) {
var page_annotator;
if (!this.annotators.hasOwnProperty(pagenum)) {
page_annotator = this.annotators[pagenum] = new PageAnnotator(this, pagenum, container, page);
} else {
page_annotator = this.annotators[pagenum];
}
if (page_annotator.page && page_annotator.container) {
return resolve(page_annotator);
} else if (page && container) {
page_annotator.setPage(page, container);
return resolve(page_annotator);
}
if (page_annotator.page || !PDFViewerApplication.pdfViewer.pdfDocument) {
return resolve(page_annotator);
}
PDFViewerApplication.pdfViewer.pdfDocument.getPage(pagenum).then(function(page) {
page_annotator.setPage(page, page_annotator.container);
page_annotator.update(PDFViewerApplication.pdfViewer.currentScale);
return resolve(page_annotator);
}, function(error) {
return reject(error);
});
}.bind(this));
};
Annotator.prototype.destroyDrawer = function() {
if (this.drawer && this.drawer !== this.nulldrawer) {
var drawer = this.drawer;
this.drawer = this.nulldrawer;
drawer.destroy();
}
};
Annotator.prototype.updateDrawer = function(previous_mode) {
this.destroyDrawer();
switch (this.draw_mode) {
case "freehand":
this.drawer = new FreehandDrawer(this, previous_mode);
break;
case "rectangle":
this.drawer = new RectangleDrawer(this, previous_mode);
break;
case "ellipse":
this.drawer = new EllipseDrawer(this, previous_mode);
break;
case "pointer":
this.drawer = new PointerDrawer(this, previous_mode);
break;
case "select":
this.drawer = new SelectDrawer(this, previous_mode);
break;
case "color":
this.drawer = new ColorPickerDrawer(this, previous_mode);
break;
case "line":
this.drawer = new LineDrawer(this, previous_mode);
break;
case null:
break;
default:
console.log("Unknown draw mode", this.draw_mode);
return;
}
};
Annotator.prototype.setDrawMode = function(mode) {
if (this.draw_mode === mode) {
if (mode === "color") {
// Toggle back from color picker.
this.setDrawMode(this.drawer.previous_mode);
}
return;
}
var previous_mode = this.draw_mode;
$(".toolbarButton.selected").removeClass('selected');
this.draw_mode = mode;
$("#" + (mode || "none") + "Mode").addClass('selected');
switch (mode) {
case "select":
case "color":
case "pointer":
case null:
break;
default:
$("#drawModeToolbar").addClass('selected');
break;
}
this.updateDrawer(previous_mode);
};
Annotator.prototype.switchPage = function(pagenum) {
if (pagenum === this.currentPage) {
return;
}
this.currentPage = pagenum;
this.sendMessage({
'type': 'control',
'control': {
'type': 'page',
'page': pagenum
}
});
};
Annotator.prototype.processMessage = function(message) {
switch (message.type) {
case 'cursor':
this.processCursorMessage(message.userid, message.cursor);
break;
case 'item':
this.processItemMessage(message.userid, message.item);
break;
case 'delete':
this.processDeleteMessage(message.userid, message.delete);
break;
case 'control':
this.processControlMessage(message.userid, message.control);
break;
default:
console.log('Unknown message', message);
break;
}
};
Annotator.prototype.processCursorMessage = function(userid, message) {
switch (message.action) {
case 'hide':
this.hideCursor(userid);
break;
case 'show':
default:
this.showCursor(userid, message);
break;
}
};
Annotator.prototype.processItemMessage = function(userid, message) {
this.getPage(message.page).then(function(page_annotator) {
page_annotator.drawItem(message.name, message.data);
});
};
Annotator.prototype.processDeleteMessage = function(userid, message) {
this.getPage(message.page).then(function(page_annotator) {
page_annotator.deleteItem(message.name);
});
};
Annotator.prototype.processControlMessage = function(userid, message) {
switch (message.type) {
case 'page':
var page = message.page;
if (page >= 1 && page <= PDFViewerApplication.pagesCount && page !== PDFViewerApplication.page) {
this.currentPage = page;
PDFViewerApplication.page = page;
}
break;
default:
console.log('Unsupported control message', message);
return;
}
};
Annotator.prototype.exportSVG = function() {
return new Promise(function(resolve, reject) {
var result = [];
var pages = [];
var continueExport = function() {
// Remove trailing empty pages.
while (result.length && !result[result.length-1]) {
result.pop();
}
// No annotations yet?
if (!result.length) {
return resolve(result);
}
// Need to keep a last empty page to avoid duplicating the last overlay.
if (result.length < this.pageCount) {
var page = pages[result.length + 1];
if (page) {
var svg = page.scope.project.exportSVG({
asString: true
});
result.push(svg);
}
}
return resolve(result);
}.bind(this);
var remaining = this.pageCount;
for (var i = 1; i <= this.pageCount; i++) {
this.getPage(i).then(function(pagenum, page_annotator) {
pages[pagenum] = page_annotator;
var svg = null;
if (page_annotator) {
svg = page_annotator.scope.project.exportSVG({
asString: true
});
}
result[pagenum - 1] = svg;
remaining -= 1;
if (remaining === 0) {
continueExport();
}
}.bind(this, i));
}
}.bind(this));
};
Annotator.prototype.downloadPdf = function() {
this.exportSVG().then(function(svg) {
if (!svg.length) {
// No items drawn yet, download source PDF.
console.log("Download source PDF");
return;
}
var data = {
"svg": svg,
"token": this.token,
};
// Download code from https://stackoverflow.com/a/23797348
var xhr = new XMLHttpRequest();
var base_url = this.socketurl;
if (base_url[base_url.length-1] !== '/') {
base_url += '/';
}
xhr.open('POST', base_url + "download/" + this.id, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
$("#downloadInProgress").hide();
if (this.status !== 200) {
$("#downloadFailed").show();
setTimeout(function() {
$("#downloadFailed").hide();
}, 4000);
return;
}
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
var type = xhr.getResponseHeader('Content-Type');
var blob = typeof File === 'function'
? new File([this.response], filename, { type: type })
: new Blob([this.response], { type: type });
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by
// closing the blob for which they were created. These URLs will no
// longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);
if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location = downloadUrl;
}
setTimeout(function() {
URL.revokeObjectURL(downloadUrl);
}, 100); // cleanup
}
};
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(data));
$("#downloadInProgress").show();
}.bind(this));
};
$(document).ready(function() {
var fileid = document.getElementsByTagName('head')[0].getAttribute('data-fileid');
var userid = document.getElementsByTagName('head')[0].getAttribute('data-userid');
var displayname = document.getElementsByTagName('head')[0].getAttribute('data-displayname');
var socketurl = document.getElementsByTagName('head')[0].getAttribute('data-socketurl');
var token = document.getElementsByTagName('head')[0].getAttribute('data-token');
var permissions = parseInt(document.getElementsByTagName('head')[0].getAttribute('data-permissions'), 10);
if (!fileid || !socketurl || !token) {
window.location.href = '/';
return;
}
var can_update = ((permissions & PERMISSION_UPDATE) === PERMISSION_UPDATE);
var annotator = new Annotator(socketurl, fileid, userid, displayname, token);
PDFViewerApplication.initializedPromise.then(function() {
var eventBus = PDFViewerApplication.eventBus;
eventBus.on('pagesloaded', function(event) {
annotator.documentLoaded(PDFViewerApplication.pdfViewer.pdfDocument);
if (!can_update) {
$('#viewer .page[data-page-number!=1]').addClass('hiddenPage');
}
});
eventBus.on('pagerendered', function(event) {
var pagenum = event.pageNumber;
if (!pagenum) {
console.log("Rendered event without page number", event);
return;
}
var container = $("#viewer .page[data-page-number='" + pagenum + "']");
if (!container.length) {
console.log("Could not get page container", event);
return;
}
PDFViewerApplication.pdfViewer.pdfDocument.getPage(pagenum).then(function(page) {
annotator.getPage(pagenum, page, container).then(function(page_annotator) {
page_annotator.update(PDFViewerApplication.pdfViewer.currentScale);
});
});
});
eventBus.on('pagechanging', function(event) {
var pagenum = event.pageNumber;
if (can_update) {
annotator.switchPage(pagenum);
} else {
$('#viewer .page').addClass('hiddenPage');
var page = $('#viewer .page[data-page-number=' + pagenum +']');
page.removeClass('hiddenPage');
// Need to redraw currently visible page to fix any layout issues.
setTimeout(function() {
PDFViewerApplication.pdfViewer.update();
}, 0);
}
});
});
$(".modeButton").click(function(event) {
var button = $(event.target);
annotator.setDrawMode(button.data("mode") || null);
});
var $btnDrawMode = $(".toolbarButton.drawMode");
$btnDrawMode.click(function(event) {
$('#drawMenuToolbar').toggleClass('hidden');
});
$('#drawMenuToolbar .toolbarButton').each(function(_, elem) {
var $elem = $(elem);
$elem.click(function() {
var mode = $(this).data('mode');
// TODO(leon): This is an ugly hack to determine the previous mode
$btnDrawMode.get(0).classList.forEach(function(c) {
if (c !== 'drawMode' && c.indexOf('Mode') !== -1) {
$btnDrawMode.removeClass(c);
}
});
$btnDrawMode.addClass(mode + 'Mode');
$('#drawMenuToolbar').addClass('hidden');
});
});
$("#downloadPdf").click(function(event) {
annotator.downloadPdf();
});
$("#secondaryToolbarClose").click(function() {
history.back();
});
if (!can_update) {
// User may not modify the file.
$('#outerContainer').addClass('readonly');
annotator.setDrawMode(null);
}
if (history.length <= 1) {
// Annotation was opened in a new window. Closing windows through JS is not
// possible - that's why we simply hide the button to not confuse users.
$("#secondaryToolbarClose").hide();
}
});
function setupPdfJs() {
console.log("Loaded pdf.js", pdfjsLib.version, pdfjsLib.build);
PDFViewerApplicationOptions.set("sidebarViewOnLoad", 0);
PDFViewerApplicationOptions.set("showPreviousViewOnLoad", false);
PDFViewerApplicationOptions.set("disablePageMode", true);
PDFViewerApplicationOptions.set("isEvalSupported", false);
PDFViewerApplicationOptions.set("cMapUrl", document.getElementsByTagName('head')[0].getAttribute('data-cmapurl'));
PDFViewerApplicationOptions.set("workerSrc", document.getElementsByTagName('head')[0].getAttribute('data-workersrc'));
}
if (document.readyState === 'interactive' || document.readyState === 'complete') {
setupPdfJs();
} else {
document.addEventListener('DOMContentLoaded', setupPdfJs, true);
}
})();