From c973103c31c154f748ab5ec4346f0e7d50c5d28c Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Fri, 24 Apr 2015 10:47:32 +0200 Subject: [PATCH] Run pdf.js rendering inside sandboxed iframe. --- src/styles/components/_presentation.scss | 11 +- static/js/directives/pdfcanvas.js | 211 +++++++---------- static/js/directives/presentation.js | 33 +-- static/js/sandboxes/pdf.js | 281 +++++++++++++++++++++++ static/js/sandboxes/youtube.js | 1 + static/js/services/sandbox.js | 20 ++ static/partials/pdfcanvas_sandbox.html | 33 +++ 7 files changed, 441 insertions(+), 149 deletions(-) create mode 100644 static/js/sandboxes/pdf.js create mode 100644 static/partials/pdfcanvas_sandbox.html diff --git a/src/styles/components/_presentation.scss b/src/styles/components/_presentation.scss index 09583e6c..b5f9a583 100644 --- a/src/styles/components/_presentation.scss +++ b/src/styles/components/_presentation.scss @@ -197,12 +197,13 @@ .canvasContainer { height: 100%; width: 100%; - } + overflow: hidden; - canvas { - display: block; - margin: 0 auto; - position: relative; + iframe { + border: 0; + width: 100%; + height: 100%; + } } .odfcanvas { diff --git a/static/js/directives/pdfcanvas.js b/static/js/directives/pdfcanvas.js index baf922c2..8c51ab15 100644 --- a/static/js/directives/pdfcanvas.js +++ b/static/js/directives/pdfcanvas.js @@ -20,9 +20,9 @@ */ "use strict"; -define(['require', 'underscore', 'jquery'], function(require, _, $) { +define(['require', 'underscore', 'jquery', 'text!partials/pdfcanvas_sandbox.html'], function(require, _, $, sandboxTemplate) { - return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { + return ["$window", "$compile", "$http", "translation", "safeApply", 'restURL', 'sandbox', function($window, $compile, $http, translation, safeApply, restURL, sandbox) { var pdfjs = null; @@ -30,28 +30,69 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { var container = $($element); - var PDFCanvas = function(scope, canvases) { + var pdfCanvas; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__PDFJS_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/pdf') + ".js")); + template = template.replace(/__PDFJS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('pdf') + ".js")); + template = template.replace(/__PDFJS_WORKER_URL__/g, restURL.createAbsoluteUrl(require.toUrl('pdf.worker') + ".js")); + template = template.replace(/__PDFJS_COMPATIBILITY_URL__/g, restURL.createAbsoluteUrl(require.toUrl('libs/pdf/compatibility') + ".js")); + var sandboxApi = sandbox.createSandbox($("iframe", container)[0], template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "pdfjs.loading": + $scope.$apply(function(scope) { + scope.$emit("presentationLoading", data.source); + }); + break; + case "pdfjs.loaded": + pdfCanvas._pdfLoaded(data.source, data.doc); + break; + case "pdfjs.loadError": + pdfCanvas._pdfLoadError(data.source, data.error); + break; + case "pdfjs.pageLoaded": + pdfCanvas._pageLoaded(data.page); + break; + case "pdfjs.pageLoadError": + pdfCanvas._pageLoadError(data.page, data.error); + break; + case "pdfjs.renderingPage": + $scope.$apply(function(scope) { + scope.$emit("presentationPageRendering", data.page); + }); + break; + case "pdfjs.pageRendered": + pdfCanvas._pageRendered(data.page); + break; + case "pdfjs.pageRenderError": + pdfCanvas._pageRenderError(data.page, data.error); + break; + case "pdfjs.keyUp": + $scope.$apply(function(scope) { + scope.$emit("keyUp", data.key); + }); + break; + default: + console.log("Unknown message received", message); + break; + } + }); + + var PDFCanvas = function(scope) { this.scope = scope; - this.canvases = canvases; this.doc = null; - this.currentPage = null; this.currentPageNumber = null; this.pendingPageNumber = null; - this.renderTask = null; this.url = null; }; - PDFCanvas.prototype._close = function() { - this._stopRendering(); - if (this.currentPage) { - this.currentPage.destroy(); - this.currentPage = null; - } - if (this.doc) { - this.doc.cleanup(); - this.doc.destroy(); - this.doc = null; - } + PDFCanvas.prototype.close = function() { + sandboxApi.postMessage("closeFile", {"close": true}); if (this.url) { URL.revokeObjectURL(this.url); this.url = null; @@ -59,20 +100,13 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.pendingPageNumber = null; this.currentPageNumber = -1; this.maxPageNumber = -1; - // clear visible canvas so it's empty when we show the next document - var canvas = this.canvases[this.scope.canvasIndex]; - canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); - }; - - PDFCanvas.prototype.close = function() { - this._close(); }; PDFCanvas.prototype.open = function(presentation) { this.scope.$emit("presentationOpening", presentation); presentation.open(_.bind(function(source) { console.log("Loading PDF from", source); - this._close(); + this.close(); if (typeof source === "string") { // got a url this._openFile(source); @@ -100,7 +134,6 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.doc = doc; this.maxPageNumber = doc.numPages; this.currentPageNumber = -1; - console.log("PDF loaded", doc); scope.$emit("presentationLoaded", source, doc); }, this)); }; @@ -127,43 +160,30 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }, this)); }; - PDFCanvas.prototype._doOpenFile = function(source) { - this.scope.$emit("presentationLoading", source); - pdfjs.getDocument(source).then(_.bind(function(doc) { - this._pdfLoaded(source, doc); - }, this), _.bind(function(error, exception) { - this._pdfLoadError(source, error, exception); - }, this)); - }; - PDFCanvas.prototype._openFile = function(source) { - if (pdfjs === null) { - // load pdf.js lazily - require(['pdf'], _.bind(function(pdf) { - pdf.workerSrc = require.toUrl('pdf.worker') + ".js"; - - console.log("Using pdf.js " + pdf.version + " (build " + pdf.build + ")"); - - pdfjs = pdf; - - this._doOpenFile(source); + if (typeof(source) === "string") { + // we can't load urls from inside the sandbox, do so here and transmit the contents + $http.get(source, { + responseType: "arraybuffer" + }).then(_.bind(function(response) { + this._openFile(response.data); + }, this), _.bind(function(error) { + this._pdfLoadError(source, error); }, this)); - } else { - this._doOpenFile(source); + return; } + + console.log("Opening file", source); + sandboxApi.postMessage("openFile", {"source": source}); }; - PDFCanvas.prototype._pageLoaded = function(page, pageObject) { + PDFCanvas.prototype._pageLoaded = function(page) { this.scope.$apply(_.bind(function(scope) { - console.log("Got page", pageObject); - scope.$emit("presentationPageLoaded", page, pageObject); - this.currentPage = pageObject; - this.drawPage(pageObject); + scope.$emit("presentationPageLoaded", page); }, this)); }; - PDFCanvas.prototype._pageLoadError = function(page, error, exception) { - console.error("Could not load page", page, error, exception); + PDFCanvas.prototype._pageLoadError = function(page, error) { var loadErrorMessage; if (error) { loadErrorMessage = translation._("An error occurred while loading the PDF page (%s).", error); @@ -181,36 +201,19 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } console.log("Showing page", page, "/", this.maxPageNumber); - if (this.currentPage) { - this.currentPage.destroy(); - this.currentPage = null; - } this.currentPageNumber = page; this.scope.$emit("presentationPageLoading", page); - this.doc.getPage(page).then(_.bind(function(pageObject) { - this._pageLoaded(page, pageObject); - }, this), _.bind(function(error, exception) { - this._pageLoadError(page, error, exception); - }, this)); + sandboxApi.postMessage("loadPage", {"page": page}); }; - PDFCanvas.prototype._pageRendered = function(pageObject) { - this.renderTask = null; + PDFCanvas.prototype._pageRendered = function(page) { this.scope.$apply(_.bind(function(scope) { - console.log("Rendered page", pageObject.pageNumber); - this.scope.$emit("presentationPageRendered", pageObject.pageNumber, this.maxPageNumber); - // ...and flip the buffers... - scope.canvasIndex = 1 - scope.canvasIndex; + this.scope.$emit("presentationPageRendered", page, this.maxPageNumber); this.showQueuedPage(); }, this)); }; - PDFCanvas.prototype._pageRenderError = function(pageObject, error, exception) { - if (error === "cancelled") { - return; - } - console.error("Could not render page", pageObject, error, exception); - this.renderTask = null; + PDFCanvas.prototype._pageRenderError = function(page, error) { var loadErrorMessage; if (error) { loadErrorMessage = translation._("An error occurred while rendering the PDF page (%s).", error); @@ -218,59 +221,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { loadErrorMessage = translation._("An unknown error occurred while rendering the PDF page."); } this.scope.$apply(_.bind(function(scope) { - scope.$emit("presentationPageRenderError", pageObject.pageNumber, this.maxPageNumber, loadErrorMessage); - }, this)); - }; - - PDFCanvas.prototype._stopRendering = function() { - if (this.renderTask) { - if (this.renderTask.internalRenderTask && this.renderTask.internalRenderTask.cancel) { - this.renderTask.internalRenderTask.cancel(); - } - this.renderTask = null; - } - } - - PDFCanvas.prototype.drawPage = function(pageObject) { - var pdfView = pageObject.view; - var pdfWidth = pdfView[2] - pdfView[0]; - var pdfHeight = pdfView[3] - pdfView[1]; - var w = container.width(); - var h = container.height(); - var scale = w / pdfWidth; - if (pdfHeight * scale > h) { - scale = container.height() / pdfHeight; - } - - // use double-buffering to avoid flickering while - // the new page is rendered... - var canvas = this.canvases[1 - this.scope.canvasIndex]; - var viewport = pageObject.getViewport(scale); - canvas.width = Math.round(viewport.width); - canvas.height = Math.round(viewport.height); - var renderContext = { - canvasContext: canvas.getContext("2d"), - viewport: viewport - }; - - console.log("Rendering page", pageObject); - this.scope.$emit("presentationPageRendering", pageObject.pageNumber); - - // TODO(fancycode): also render images in different resolutions for subscribed peers and send to them when ready - this._stopRendering(); - var renderTask = pageObject.render(renderContext); - this.renderTask = renderTask; - renderTask.promise.then(_.bind(function() { - this._pageRendered(pageObject); - }, this), _.bind(function(error, exception) { - this._pageRenderError(pageObject, error, exception); + scope.$emit("presentationPageRenderError", page, this.maxPageNumber, loadErrorMessage); }, this)); }; PDFCanvas.prototype.redrawPage = function() { - if (this.currentPage !== null) { - this.drawPage(this.currentPage); - } + sandboxApi.postMessage("redrawPage", {"redraw": true}); }; PDFCanvas.prototype.showPage = function(page) { @@ -290,10 +246,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } }; - $scope.canvasIndex = 0; - - var canvases = container.find("canvas"); - var pdfCanvas = new PDFCanvas($scope, canvases); + pdfCanvas = new PDFCanvas($scope); $scope.$watch("currentPresentation", function(presentation, previousPresentation) { if (presentation) { @@ -333,7 +286,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { return { restrict: 'E', replace: true, - template: '
', + template: '
', controller: controller }; diff --git a/static/js/directives/presentation.js b/static/js/directives/presentation.js index 82ef8709..bc01604d 100644 --- a/static/js/directives/presentation.js +++ b/static/js/directives/presentation.js @@ -715,6 +715,21 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], }); }); + $scope.$on("keyUp", function(event, keyCode) { + switch (keyCode) { + case 37: + // left arrow + $scope.prevPage(); + break; + case 39: + // right arrow + case 32: + // space + $scope.nextPage(); + break; + } + }) + $(document).on("keyup", function(event) { if (!$scope.layout.presentation) { return; @@ -722,22 +737,10 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], if ($(event.target).is("input,textarea,select")) { return; } - $scope.$apply(function() { - switch (event.keyCode) { - case 37: - // left arrow - $scope.prevPage(); - event.preventDefault(); - break; - case 39: - // right arrow - case 32: - // space - $scope.nextPage(); - event.preventDefault(); - break; - } + $scope.$apply(function(scope) { + scope.$emit("keyUp", event.keyCode); }); + event.preventDefault(); }); $scope.$watch("layout.presentation", function(newval, oldval) { diff --git a/static/js/sandboxes/pdf.js b/static/js/sandboxes/pdf.js new file mode 100644 index 00000000..d38f45cd --- /dev/null +++ b/static/js/sandboxes/pdf.js @@ -0,0 +1,281 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * 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 . + * + */ + +"use strict"; +(function () { + + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + var PDFJS_URL = script.getAttribute("data-pdfjs-url"); + var PDFJS_WORKER_URL = script.getAttribute("data-pdfjs-worker-url"); + var PDFJS_COMPATIBILITY_URL = script.getAttribute("data-pdfjs-compatibility-url"); + var container = document.getElementById("container"); + + var pdfScript = null; + var pdfjs = null; + + var PdfJsSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.canvases = document.getElementsByTagName('canvas'); + this.window = window; + this.doc = null; + this.currentPage = null; + this.canvasIndex = 0; + this.renderTask = null; + }; + + PdfJsSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + PdfJsSandbox.prototype.openFile = function(source) { + if (!pdfScript) { + var that = this; + var compat = document.createElement('script'); + compat.type = "text/javascript"; + compat.src = PDFJS_COMPATIBILITY_URL; + this.head.appendChild(compat); + + pdfScript = document.createElement('script'); + pdfScript.type = "text/javascript"; + pdfScript.src = PDFJS_URL; + pdfScript.onerror = function(evt) { + that.postMessage("pdfjs.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(pdfScript); + pdfScript = null; + }; + pdfScript.onload = function(evt) { + pdfjs = that.window.PDFJS; + if (PDFJS_WORKER_URL) { + // NOTE: the worker script won't actually be run inside a + // real Worker object as it can't be loaded cross-domain + // from the sandboxed iframe ("data:" vs. "https"). + pdfjs.workerSrc = PDFJS_WORKER_URL; + } + console.log("Using pdf.js " + pdfjs.version + " (build " + pdfjs.build + ")"); + that._doOpenFile(source); + }; + this.head.appendChild(pdfScript); + } else { + this._doOpenFile(source); + } + }; + + PdfJsSandbox.prototype.closeFile = function() { + this._stopRendering(); + if (this.currentPage) { + this.currentPage.destroy(); + this.currentPage = null; + } + if (this.doc) { + this.doc.cleanup(); + this.doc.destroy(); + this.doc = null; + } + // clear visible canvas so it's empty when we show the next document + var canvas = this.canvases[this.canvasIndex]; + canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + }; + + PdfJsSandbox.prototype._doOpenFile = function(source) { + var that = this; + this.postMessage("pdfjs.loading", {"source": source}); + pdfjs.getDocument(source).then(function(doc) { + that._pdfLoaded(source, doc); + }, function(error, exception) { + that._pdfLoadError(source, error, exception); + }); + }; + + PdfJsSandbox.prototype._pdfLoaded = function(source, doc) { + console.log("PDF loaded", doc); + this.doc = doc; + this.postMessage("pdfjs.loaded", {"source": source, "doc": {"numPages": doc.numPages}}); + }; + + PdfJsSandbox.prototype._pdfLoadError = function(source, error, exception) { + this.postMessage("pdfjs.loadError", {"source": source, "error": error}); + }; + + PdfJsSandbox.prototype.loadPage = function(page) { + if (this.currentPage) { + this.currentPage.destroy(); + this.currentPage = null; + } + var that = this; + this.doc.getPage(page).then(function(pageObject) { + that._pageLoaded(page, pageObject); + }, function(error, exception) { + that._pageLoadError(page, error, exception); + }); + }; + + PdfJsSandbox.prototype._pageLoaded = function(page, pageObject) { + console.log("Got page", pageObject); + this.currentPage = pageObject; + this.postMessage("pdfjs.pageLoaded", {"page": page}); + this.drawPage(pageObject); + }; + + PdfJsSandbox.prototype._pageLoadError = function(page, error, exception) { + console.error("Could not load page", page, error, exception); + this.postMessage("pdfjs.pageLoadError", {"page": page, "error": error}); + }; + + PdfJsSandbox.prototype._stopRendering = function() { + if (this.renderTask) { + if (this.renderTask.internalRenderTask && this.renderTask.internalRenderTask.cancel) { + this.renderTask.internalRenderTask.cancel(); + } + this.renderTask = null; + } + } + + PdfJsSandbox.prototype.drawPage = function(pageObject) { + var pdfView = pageObject.view; + var pdfWidth = pdfView[2] - pdfView[0]; + var pdfHeight = pdfView[3] - pdfView[1]; + var w = container.offsetWidth; + var h = container.offsetHeight; + var scale = w / pdfWidth; + if (pdfHeight * scale > h) { + scale = container.offsetHeight / pdfHeight; + } + + // use double-buffering to avoid flickering while + // the new page is rendered... + var canvas = this.canvases[1 - this.canvasIndex]; + var viewport = pageObject.getViewport(scale); + canvas.width = Math.round(viewport.width); + canvas.height = Math.round(viewport.height); + var renderContext = { + canvasContext: canvas.getContext("2d"), + viewport: viewport + }; + + console.log("Rendering page", pageObject); + this.postMessage("pdfjs.renderingPage", {"page": pageObject.pageNumber}); + + // TODO(fancycode): also render images in different resolutions for subscribed peers and send to them when ready + this._stopRendering(); + var renderTask = pageObject.render(renderContext); + this.renderTask = renderTask; + var that = this; + renderTask.promise.then(function() { + that._pageRendered(pageObject); + }, function(error, exception) { + that._pageRenderError(pageObject, error, exception); + }); + }; + + PdfJsSandbox.prototype._pageRendered = function(pageObject) { + this.renderTask = null; + console.log("Rendered page", pageObject.pageNumber); + this.postMessage("pdfjs.pageRendered", {"page": pageObject.pageNumber}); + // ...and flip the buffers... + this.canvases[this.canvasIndex].style.display = "none"; + this.canvasIndex = 1 - this.canvasIndex; + this.canvases[this.canvasIndex].style.display = "block"; + }; + + PdfJsSandbox.prototype._pageRenderError = function(pageObject, error, exception) { + if (error === "cancelled") { + return; + } + console.error("Could not render page", pageObject, error, exception); + this.renderTask = null; + this.postMessage("pdfjs.pageRenderError", {"page": pageObject.pageNumber, "error": error}); + }; + + PdfJsSandbox.prototype.redrawPage = function() { + if (this.currentPage !== null) { + this.drawPage(this.currentPage); + } + }; + + var sandbox = new PdfJsSandbox(window); + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "openFile": + sandbox.openFile(data.source); + break; + case "closeFile": + sandbox.closeFile(); + break; + case "loadPage": + sandbox.loadPage(data.page); + break; + case "redrawPage": + sandbox.redrawPage(); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + document.addEventListener("keyup", function(event) { + sandbox.postMessage("pdfjs.keyUp", {"key": event.keyCode}); + event.preventDefault(); + }); + + var toggleFullscreen = function(elem) { + var fullScreenElement = document.fullscreenElement || document.msFullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement; + if (fullScreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen(); + } + } + }; + + container.addEventListener("dblclick", function(event) { + toggleFullscreen(container); + }); + + console.log("pdf.js sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); + +})(); diff --git a/static/js/sandboxes/youtube.js b/static/js/sandboxes/youtube.js index b09e7cbf..4a182730 100644 --- a/static/js/sandboxes/youtube.js +++ b/static/js/sandboxes/youtube.js @@ -244,5 +244,6 @@ }, false); console.log("YouTube sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); })(); diff --git a/static/js/services/sandbox.js b/static/js/services/sandbox.js index d3f65a4a..c9a17e01 100644 --- a/static/js/services/sandbox.js +++ b/static/js/services/sandbox.js @@ -30,6 +30,8 @@ define(["jquery", "underscore"], function($, _) { this.target = this.iframe.contentWindow; this.e = $({}); this.handler = _.bind(this.onPostMessageReceived, this); + this.ready = false; + this.pending_messages = []; $window.addEventListener("message", this.handler, false); }; @@ -46,10 +48,28 @@ define(["jquery", "underscore"], function($, _) { return; } + if (event.data.type === "ready") { + this.ready = true; + this._sendPendingMessages(); + } + this.e.triggerHandler("message", [event]); }; + Sandbox.prototype._sendPendingMessages = function() { + var i; + for (i=0; i + + + pdf.js Sandbox + + + +
+ +
+ + +