From c905191de2d8a8c6f8225c7f380204cd43d7661b Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Tue, 31 Jul 2012 12:21:07 -0500 Subject: [PATCH] Implements loading PDF data by extension/chrome --- .../firefox/components/PdfStreamConverter.js | 140 +++++++++++++++--- web/viewer.js | 41 ++++- 2 files changed, 157 insertions(+), 24 deletions(-) diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index d371e50cd..f53d23285 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -14,6 +14,7 @@ const MOZ_CENTRAL = PDFJSSCRIPT_MOZ_CENTRAL; const PDFJS_EVENT_ID = 'pdf.js.message'; const PDF_CONTENT_TYPE = 'application/pdf'; const PREF_PREFIX = 'PDFJSSCRIPT_PREF_PREFIX'; +const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html'; const MAX_DATABASE_LENGTH = 4096; const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'; const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}'; @@ -122,9 +123,39 @@ function getLocalizedString(strings, id, property) { return id; } +// PDF data storage +function PdfDataListener(length) { + this.length = length; + this.data = new Uint8Array(length); +} + +PdfDataListener.prototype = { + set: function PdfDataListener_set(chunk, offset) { + this.data.set(chunk, offset); + var loaded = offset + chunk.length; + this.onprogress(loaded, this.length); + }, + finish: function PdfDataListener_finish() { + this.isDataReady = true; + if (this.oncompleteCallback) { + this.oncompleteCallback(this.data); + delete this.data; + } + }, + onprogress: function() {}, + set oncomplete(value) { + this.oncompleteCallback = value; + if (this.isDataReady) { + value(this.data); + delete this.data; // releasing temporary storage + } + } +}; + // All the priviledged actions. -function ChromeActions(domWindow) { +function ChromeActions(domWindow, dataListener) { this.domWindow = domWindow; + this.dataListener = dataListener; } ChromeActions.prototype = { @@ -194,6 +225,36 @@ ChromeActions.prototype = { getLocale: function() { return getStringPref('general.useragent.locale', 'en-US'); }, + getLoadingType: function() { + return this.dataListener ? 'passive' : 'active'; + }, + initPassiveLoading: function() { + if (!this.dataListener) + return false; + + var domWindow = this.domWindow; + this.dataListener.onprogress = + function ChromeActions_dataListenerProgress(loaded, total) { + domWindow.postMessage({ + pdfjsLoadAction: 'progress', + loaded: loaded, + total: total + }, '*'); + }; + + this.dataListener.oncomplete = + function ChromeActions_dataListenerComplete(data) { + + domWindow.postMessage({ + pdfjsLoadAction: 'complete', + data: data + }, '*'); + + delete this.dataListener; + }; + + return true; + }, getStrings: function(data) { try { // Lazy initialization of localizedStrings @@ -324,17 +385,21 @@ PdfStreamConverter.prototype = { asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { if (!isEnabled()) throw Cr.NS_ERROR_NOT_IMPLEMENTED; - // Ignoring HTTP POST requests -- pdf.js has to repeat the request. - var skipConversion = false; - try { - var request = aCtxt; - request.QueryInterface(Ci.nsIHttpChannel); - skipConversion = (request.requestMethod !== 'GET'); - } catch (e) { - // Non-HTTP request... continue normally. + + var useFetchByChrome = getBoolPref(PREF_PREFIX + '.fetchByChrome', true); + if (!useFetchByChrome) { + // Ignoring HTTP POST requests -- pdf.js has to repeat the request. + var skipConversion = false; + try { + var request = aCtxt; + request.QueryInterface(Ci.nsIHttpChannel); + skipConversion = (request.requestMethod !== 'GET'); + } catch (e) { + // Non-HTTP request... continue normally. + } + if (skipConversion) + throw Cr.NS_ERROR_NOT_IMPLEMENTED; } - if (skipConversion) - throw Cr.NS_ERROR_NOT_IMPLEMENTED; // Store the listener passed to us this.listener = aListener; @@ -342,8 +407,14 @@ PdfStreamConverter.prototype = { // nsIStreamListener::onDataAvailable onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { - // Do nothing since all the data loading is handled by the viewer. - log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!'); + if (!this.dataListener) { + // Do nothing since all the data loading is handled by the viewer. + return; + } + + var binaryStream = this.binaryStream; + binaryStream.setInputStream(aInputStream); + this.dataListener.set(binaryStream.readByteArray(aCount), aOffset); }, // nsIRequestObserver::onStartRequest @@ -351,15 +422,30 @@ PdfStreamConverter.prototype = { // Setup the request so we can use it below. aRequest.QueryInterface(Ci.nsIChannel); - // Cancel the request so the viewer can handle it. - aRequest.cancel(Cr.NS_BINDING_ABORTED); + var useFetchByChrome = getBoolPref(PREF_PREFIX + '.fetchByChrome', true); + var dataListener; + if (useFetchByChrome) { + // Creating storage for PDF data + var contentLength = aRequest.contentLength; + if (contentLength < 0) + throw new 'Unknown length is not supported'; + + dataListener = new PdfDataListener(contentLength); + this.dataListener = dataListener; + this.binaryStream = Cc['@mozilla.org/binaryinputstream;1'] + .createInstance(Ci.nsIBinaryInputStream); + } else { + // Cancel the request so the viewer can handle it. + aRequest.cancel(Cr.NS_BINDING_ABORTED); + } // Create a new channel that is viewer loaded as a resource. var ioService = Services.io; var channel = ioService.newChannel( - 'resource://pdf.js/web/viewer.html', null, null); + PDF_VIEWER_WEB_PAGE, null, null); var listener = this.listener; + var self = this; // Proxy all the request observer calls, when it gets to onStopRequest // we can get the dom window. var proxy = { @@ -373,8 +459,8 @@ PdfStreamConverter.prototype = { var domWindow = getDOMWindow(channel); // Double check the url is still the correct one. if (domWindow.document.documentURIObject.equals(aRequest.URI)) { - let requestListener = new RequestListener( - new ChromeActions(domWindow)); + let actions = new ChromeActions(domWindow, dataListener); + let requestListener = new RequestListener(actions); domWindow.addEventListener(PDFJS_EVENT_ID, function(event) { requestListener.receive(event); }, false, true); @@ -386,11 +472,27 @@ PdfStreamConverter.prototype = { // Keep the URL the same so the browser sees it as the same. channel.originalURI = aRequest.URI; channel.asyncOpen(proxy, aContext); + if (useFetchByChrome) { + // We can use resource principal when data is fetched by the chrome + // e.g. useful for NoScript + var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1'] + .getService(Ci.nsIScriptSecurityManager); + var uri = ioService.newURI(PDF_VIEWER_WEB_PAGE, null, null); + var resourcePrincipal = securityManager.getCodebasePrincipal(uri); + channel.owner = resourcePrincipal; + } }, // nsIRequestObserver::onStopRequest onStopRequest: function(aRequest, aContext, aStatusCode) { - // Do nothing. + if (!this.dataListener) { + // Do nothing + return; + } + + this.dataListener.finish(); + delete this.dataListener; + delete this.binaryStream; } }; diff --git a/web/viewer.js b/web/viewer.js index e24e694f2..61f52cf60 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -390,11 +390,37 @@ var PDFView = { return value; }, + initPassiveLoading: function pdfViewInitPassiveLoading() { + if (!PDFView.loadingBar) { + PDFView.loadingBar = new ProgressBar('#loadingBar', {}); + } + + window.addEventListener('message', function window_message(e) { + var args = e.data; + + if (!('pdfjsLoadAction' in args)) + return; + switch (args.pdfjsLoadAction) { + case 'progress': + PDFView.progress(args.loaded / args.total); + break; + case 'complete': + PDFView.open(args.data, 0); + break; + } + }); + FirefoxCom.requestSync('initPassiveLoading', null); + }, + + setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { + this.url = url; + document.title = decodeURIComponent(getFileName(url)) || url; + }, + open: function pdfViewOpen(url, scale, password) { var parameters = {password: password}; if (typeof url === 'string') { // URL - this.url = url; - document.title = decodeURIComponent(getFileName(url)) || url; + this.setTitleUsingUrl(url); parameters.url = url; } else if (url && 'byteLength' in url) { // ArrayBuffer parameters.data = url; @@ -1736,7 +1762,7 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) { }; }; -window.addEventListener('load', function webViewerLoad(evt) { +document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) { PDFView.initialize(); var params = PDFView.parseQueryString(document.location.search.substring(1)); @@ -1813,7 +1839,12 @@ window.addEventListener('load', function webViewerLoad(evt) { PDFView.renderHighestPriority(); }); - PDFView.open(file, 0); + if (PDFJS.isFirefoxExtension && + FirefoxCom.requestSync('getLoadingType') == 'passive') { + PDFView.setTitleUsingUrl(file); + PDFView.initPassiveLoading(); + } else + PDFView.open(file, 0); }, true); function updateViewarea() { @@ -1887,7 +1918,7 @@ window.addEventListener('change', function webViewerChange(evt) { // implemented in Firefox. var file = files[0]; fileReader.readAsBinaryString(file); - document.title = file.name; + PDFView.setTitleUsingUrl(file.name); // URL does not reflect proper document location - hiding some icons. document.getElementById('viewBookmark').setAttribute('hidden', 'true');