API Docs for: 2.0.0

public/src/pdf_viewer_single.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014

"use strict";

/**
 * @module client
 * @submodule pdf_viewer
 */

PDFJS.workerSrc       = 'lib/pdf.worker.js';
PDFJS.disableWorker   = false;
PDFJS.disableWebGL    = true;
PDFJS.verbosity       = PDFJS.VERBOSITY_LEVELS.infos;
PDFJS.maxCanvasPixels = 67108864; // 8k2

/**
 * PDF viewing application, based on pdf.js library
 *
 * @class pdf_viewer
 */
var pdf_viewer = SAGE2_App.extend({

	/**
	* Init method, creates an 'img' tag in the DOM and a few canvas contexts to handle multiple redraws
	*
	* @method init
	* @param data {Object} contains initialization values (id, width, height, ...)
	*/
	init: function(data) {
		this.SAGE2Init("img", data);

		// Clear the background before first rendering
		this.div.style.background = "black";

		this.resizeEvents = "onfinish";

		this.canvas  = [];
		this.ctx     = [];
		this.loaded  = false;
		this.pdfDoc  = null;
		this.ratio   = null;
		this.currCtx = 0;
		this.numCtx  = 5;
		this.src     = null;
		this.title   = data.title;
		this.gotresize      = false;
		this.enableControls = true;
		this.old_doc_url = "";

		// application specific 'init'
		for (var i = 0; i < this.numCtx; i++) {
			var canvas = document.createElement("canvas");
			canvas.width  = this.element.width;
			canvas.height = this.element.height;
			var ctx = canvas.getContext("2d");

			this.canvas.push(canvas);
			this.ctx.push(ctx);
		}

		this.updateAppFromState(data.date);
	},

	/**
	* Load the app from a previous state, parses the PDF and creates the widgets
	*
	* @method load
	* @param state {Object} object to initialize or restore the app
	* @param date {Date} time from the server
	*/
	load: function(date) {
		this.updateAppFromState(date);
	},

	updateAppFromState: function(date) {
		if (this.old_doc_url !== this.state.doc_url) {
			var _this = this;
			this.loaded = false;

			var docURL = cleanURL(this.state.doc_url);

			PDFJS.getDocument({url: docURL}).then(function getDocumentCallback(pdfDocument) {
				console.log("loaded pdf document", _this.gotresize);
				_this.pdfDoc = pdfDocument;
				_this.loaded = true;

				_this.addWidgetControlsToPdfViewer();

				// if already got a resize event, just redraw, otherwise send message
				if (_this.gotresize) {
					_this.refresh(date);
					_this.gotresize = false;
				} else {
					// Getting the size of the page
					_this.pdfDoc.getPage(1).then(function(page) {
						var viewport = page.getViewport(1.0);
						var w = parseInt(viewport.width,  10);
						var h = parseInt(viewport.height, 10);
						_this.ratio = w / h;
						// Depending on the aspect ratio, adjust one dimension
						if (_this.ratio < 1) {
							_this.sendResize(_this.element.width, _this.element.width / _this.ratio);
						} else {
							_this.sendResize(_this.element.height * _this.ratio, _this.element.height);
						}
					});
				}
			});
			this.old_doc_url = this.state.doc_url;
		} else {
			// load new state of same document
			this.refresh(date);
		}
	},

	/**
	* Adds custom widgets to app
	*
	* @method addWidgetControlsToPdfViewer
	*/
	addWidgetControlsToPdfViewer: function() {
		if (this.pdfDoc.numPages > 1) {
			this.controls.addButton({type: "fastforward", position: 6, identifier: "LastPage"});
			this.controls.addButton({type: "rewind",      position: 2, identifier: "FirstPage"});
			this.controls.addButton({type: "prev",        position: 3, identifier: "PreviousPage"});
			this.controls.addButton({type: "next",        position: 5, identifier: "NextPage"});
			this.controls.addSlider({
				minimum: 1,
				maximum: this.pdfDoc.numPages,
				increments: 1,
				property: "this.state.page",
				label: "Page",
				identifier: "Page"
			});
		}
		this.controls.finishedAddingControls();
	},

	/**
	* Draw function, renders the current page into a canvas
	*
	* @method draw
	* @param date {Date} current time from the server
	*/
	draw: function(date) {
		if (this.loaded === false) {
			return;
		}

		var _this = this;
		var renderPage = this.state.page;
		var renderCanvas;

		var gotPdfPage = function(pdfPage) {
			renderCanvas = _this.currCtx;

			// set the scale to match the canvas
			var viewport = pdfPage.getViewport(_this.canvas[renderCanvas].width / pdfPage.getViewport(1.0).width);

			// Render PDF page into canvas context
			var renderContext = {
				canvasContext: _this.ctx[_this.currCtx],
				viewport: viewport,
				continueCallback: function(cont) {
					cont(); // nothing special right now
				}
			};
			_this.currCtx = (_this.currCtx + 1) % _this.numCtx;

			pdfPage.render(renderContext).then(renderPdfPage);
		};

		var renderPdfPage = function() {
			if (renderPage === _this.state.page) {
				var data = _this.canvas[renderCanvas].toDataURL().split(',');
				var bin  = atob(data[1]);
				var mime = data[0].split(':')[1].split(';')[0];

				var buf  = new ArrayBuffer(bin.length);
				var view = new Uint8Array(buf);
				for (var i = 0; i < view.length; i++) {
					view[i] = bin.charCodeAt(i);
				}

				var blob = new Blob([buf], {type: mime});
				var source = window.URL.createObjectURL(blob);

				if (_this.src !== null) {
					window.URL.revokeObjectURL(_this.src);
				}
				_this.src = source;

				_this.element.src = _this.src;

				var newTitle;
				newTitle = _this.title + " - " + _this.state.page + " / " + _this.pdfDoc.numPages;
				_this.updateTitle(newTitle);
			}
		};
		this.pdfDoc.getPage(this.state.page).then(gotPdfPage);
	},

	/**
	* Resize function, resizes all the canvas contexts
	*
	* @method resize
	* @param date {Date} current time from the server
	*/
	resize: function(date) {
		for (var i = 0; i < this.numCtx; i++) {
			this.canvas[i].width  = this.element.width;
			this.canvas[i].height = this.element.height;
		}
		// Force a redraw after resize
		this.gotresize = true;
		this.refresh(date);
	},

	/**
	* To enable right click context menu support this function needs to be present with this format.
	*
	* Must return an array of entries. An entry is an object with three properties:
	*	description: what is to be displayed to the viewer.
	*	callback: String containing the name of the function to activate in the app. It must exist.
	*	parameters: an object with specified datafields to be given to the function.
	*		The following attributes will be automatically added by server.
	*			serverDate, on the return back, server will fill this with time object.
	*			clientId, unique identifier (ip and port) for the client that selected entry.
	*			clientName, the name input for their pointer. Note: users are not required to do so.
	*			clientInput, if entry is marked as input, the value will be in this property. See pdf_viewer.js for example.
	*		Further parameters can be added. See pdf_view.js for example.
	*/
	getContextEntries: function() {
		var entries = [];
		var entry;

		entry = {};
		entry.description = "First Page";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "first";
		entries.push(entry);

		entry = {};
		entry.description = "Previous Page";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "previous";
		entries.push(entry);

		entry = {};
		entry.description = "Next Page";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "next";
		entries.push(entry);

		entry = {};
		entry.description = "Last Page";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "last";
		entries.push(entry);

		entry = {};
		entry.description = "Jump To: ";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.inputField = true;
		entry.inputFieldSize = 3;
		entries.push(entry);

		// Special callback: dowload the file
		entries.push({
			description: "Download",
			callback: "SAGE2_download",
			parameters: {
				url: this.state.doc_url
			}
		});

		return entries;
	},

	/**
	* Support function to allow page changing through right mouse context menu.
	*
	* @method changeThePage
	* @param responseObject {Object} contains response from entry selection
	*/
	changeThePage: function(responseObject) {
		var page = responseObject.page;
		// if the user did the input option
		if (responseObject.clientInput) {
			page = parseInt(responseObject.clientInput);
			if (page > 0 && page <= this.pdfDoc.numPages) {
				this.state.page = page;
			} else {
				return;
			}
		} else {
			// else check for these word options
			if (page === "first") {
				if (this.state.page === 1) {
					return;
				}
				this.state.page = 1;
			} else if (page === "previous") {
				if (this.state.page <= 1) {
					return;
				}
				this.state.page = this.state.page - 1;
			} else if (page === "next") {
				if (this.state.page >= this.pdfDoc.numPages) {
					return;
				}
				this.state.page = this.state.page + 1;
			} else if (page === "last") {
				if (this.state.page === this.pdfDoc.numPages) {
					return;
				}
				this.state.page = this.pdfDoc.numPages;
			}
		}
		// This needs to be a new date for the extra function.
		this.refresh(new Date(responseObject.serverDate));
	},

	/**
	* Handles event processing, arrow keys to navigate, and r to redraw
	*
	* @method event
	* @param eventType {String} the type of event
	* @param position {Object} contains the x and y positions of the event
	* @param user_id {Object} data about the user who triggered the event
	* @param data {Object} object containing extra data about the event,
	* @param date {Date} current time from the server
	*/
	event: function(eventType, position, user, data, date) {
		// Left Click  - go back one page
		// Right Click - go forward one page

		// if (eventType === "pointerPress") {
		// 	if (data.button === "left") {
		// 		if (this.state.page <= 1) {
		// 			return;
		// 		}
		// 		this.state.page = this.state.page - 1;
		// 		this.refresh(date);
		// 	} else if (data.button === "right") {
		// 		if (this.state.page >= this.pdfDoc.numPages) {
		// 			return;
		// 		}
		// 		this.state.page = this.state.page + 1;
		// 		this.refresh(date);
		// 	}
		// }

		// Keyboard:
		//   spacebar - next
		//   1 - first
		//   0 - last
		if (eventType === "keyboard") {
			if (data.character === " ") {
				this.state.page = (this.state.page % this.pdfDoc.numPages) + 1;
				this.refresh(date);
			} else if (data.character === "1") {
				this.state.page = 1;
				this.refresh(date);
			} else if (data.character === "0") {
				this.state.page = this.pdfDoc.numPages;
				this.refresh(date);
			} else if (data.character === "r" || data.character === "R") {
				this.refresh(date);
			}
		}

		// Left Arrow  - go back one page
		// Right Arrow - go forward one page
		if (eventType === "specialKey") {
			if (data.code === 37 && data.state === "up") {
				// Left Arrow
				if (this.state.page <= 1) {
					return;
				}
				this.state.page = this.state.page - 1;
				this.refresh(date);
			} else if (data.code === 39 && data.state === "up") {
				// Right Arrow
				this.state.page = (this.state.page % this.pdfDoc.numPages) + 1;
				this.refresh(date);
			}
		} else if (eventType === "widgetEvent") {
			switch (data.identifier) {
				case "LastPage":
					this.state.page = this.pdfDoc.numPages;
					break;
				case "FirstPage":
					this.state.page = 1;
					break;
				case "PreviousPage":
					if (this.state.page <= 1) {
						return;
					}
					this.state.page = this.state.page - 1;
					break;
				case "NextPage":
					if (this.state.page >= this.pdfDoc.numPages) {
						return;
					}
					this.state.page = this.state.page + 1;
					break;
				case "Page":
					switch (data.action) {
						case "sliderRelease":
							break;
						default:
							return;
					}
					break;
				default:
					return;
			}
			this.refresh(date);
		}
	}
});