From c905191de2d8a8c6f8225c7f380204cd43d7661b Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Tue, 31 Jul 2012 12:21:07 -0500 Subject: [PATCH 1/6] 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'); From fc3efa9b164548e3429ec6507d3b61fedd360231 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Wed, 1 Aug 2012 16:39:51 -0500 Subject: [PATCH 2/6] FF17 getSimpleCodebasePrincipal name change --- extensions/firefox/components/PdfStreamConverter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index f53d23285..2872da3fc 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -235,6 +235,7 @@ ChromeActions.prototype = { var domWindow = this.domWindow; this.dataListener.onprogress = function ChromeActions_dataListenerProgress(loaded, total) { + domWindow.postMessage({ pdfjsLoadAction: 'progress', loaded: loaded, @@ -478,7 +479,10 @@ PdfStreamConverter.prototype = { 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); + // FF16 and below had getCodebasePrincipal (bug 774585) + var resourcePrincipal = 'getSimpleCodebasePrincipal' in securityManager ? + securityManager.getSimpleCodebasePrincipal(uri) : + securityManager.getCodebasePrincipal(uri); channel.owner = resourcePrincipal; } }, From 68c298a4097b70e7a13dbe182db042debec36ff4 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Wed, 15 Aug 2012 10:28:26 -0500 Subject: [PATCH 3/6] Merge fix: don't open the file if passive mode is selected --- web/viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/viewer.js b/web/viewer.js index ad327924f..2399e9d91 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -1890,6 +1890,7 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) { //if (FirefoxCom.requestSync('getLoadingType') == 'passive') { // PDFView.setTitleUsingUrl(file); // PDFView.initPassiveLoading(); +// return; //} //#endif From ec8fdb60fcdbda93b56d29d7212cda086a8c8b22 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Wed, 15 Aug 2012 10:38:15 -0500 Subject: [PATCH 4/6] Fixes "TypeError: invalid 'in' operand args" --- web/viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/viewer.js b/web/viewer.js index 2399e9d91..92d517909 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -356,7 +356,7 @@ var PDFView = { window.addEventListener('message', function window_message(e) { var args = e.data; - if (!('pdfjsLoadAction' in args)) + if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) return; switch (args.pdfjsLoadAction) { case 'progress': From df4fadeaf5ae4007807d18cd9892ec7ba0cf36ba Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 20 Aug 2012 17:16:04 -0500 Subject: [PATCH 5/6] Unknown length support; reports download error --- .../firefox/components/PdfStreamConverter.js | 56 ++++++++++++++----- web/viewer.js | 5 ++ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index bac546c87..10af5f99c 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -125,29 +125,56 @@ function getLocalizedString(strings, id, property) { // PDF data storage function PdfDataListener(length) { - this.length = length; - this.data = new Uint8Array(length); + this.length = length; // less than 0, if length is unknown + this.data = new Uint8Array(length >= 0 ? length : 0x10000); } PdfDataListener.prototype = { set: function PdfDataListener_set(chunk, offset) { - this.data.set(chunk, offset); var loaded = offset + chunk.length; - this.onprogress(loaded, this.length); + if (this.length < 0 && this.data.length < loaded) { + // data length is unknown and new chunk will not fit in the existing + // buffer, resizing the buffer by doubling the last its length + var newLength = this.data.length; + for (; newLength < loaded; newLength *= 2) {} + var newData = new Uint8Array(newLength); + newData.set(this.data); + this.data = newData; + } + + this.data.set(chunk, offset); + this.loaded = loaded; + + // not reporting the progress if data length is unknown + if (this.length >= 0) + this.onprogress(loaded, this.length); + }, + getData: function PdfDataListener_getData() { + var data = this.length >= 0 ? this.data : + this.data.subarray(0, this.loaded); + delete this.data; // releasing temporary storage + return data; }, finish: function PdfDataListener_finish() { this.isDataReady = true; if (this.oncompleteCallback) { - this.oncompleteCallback(this.data); - delete this.data; + this.oncompleteCallback(this.getData()); + } + }, + error: function PdfDataListener_error(errorCode) { + this.errorCode = errorCode; + if (this.oncompleteCallback) { + this.oncompleteCallback(null, errorCode); } }, onprogress: function() {}, set oncomplete(value) { this.oncompleteCallback = value; if (this.isDataReady) { - value(this.data); - delete this.data; // releasing temporary storage + value(this.getData()); + } + if (this.errorCode) { + value(null, this.errorCode); } } }; @@ -244,11 +271,12 @@ ChromeActions.prototype = { }; this.dataListener.oncomplete = - function ChromeActions_dataListenerComplete(data) { + function ChromeActions_dataListenerComplete(data, errorCode) { domWindow.postMessage({ pdfjsLoadAction: 'complete', - data: data + data: data, + errorCode: errorCode }, '*'); delete this.dataListener; @@ -445,9 +473,6 @@ PdfStreamConverter.prototype = { 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'] @@ -511,7 +536,10 @@ PdfStreamConverter.prototype = { return; } - this.dataListener.finish(); + if (Components.isSuccessCode(aStatusCode)) + this.dataListener.finish(); + else + this.dataListener.error(aStatusCode); delete this.dataListener; delete this.binaryStream; } diff --git a/web/viewer.js b/web/viewer.js index 92d517909..d1fbeb4ed 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -363,6 +363,11 @@ var PDFView = { PDFView.progress(args.loaded / args.total); break; case 'complete': + if (!args.data) { + PDFView.error(mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'), e); + break; + } PDFView.open(args.data, 0); break; } From 077b19fca14e0f92a64a33b599ee799eb83321bf Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 20 Aug 2012 17:53:26 -0500 Subject: [PATCH 6/6] Fixes reported offset when length is unknown --- .../firefox/components/PdfStreamConverter.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index 10af5f99c..d5b12dbde 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -127,27 +127,29 @@ function getLocalizedString(strings, id, property) { function PdfDataListener(length) { this.length = length; // less than 0, if length is unknown this.data = new Uint8Array(length >= 0 ? length : 0x10000); + this.loaded = 0; } PdfDataListener.prototype = { set: function PdfDataListener_set(chunk, offset) { - var loaded = offset + chunk.length; - if (this.length < 0 && this.data.length < loaded) { + if (this.length < 0) { + var willBeLoaded = this.loaded + chunk.length; // data length is unknown and new chunk will not fit in the existing - // buffer, resizing the buffer by doubling the last its length - var newLength = this.data.length; - for (; newLength < loaded; newLength *= 2) {} - var newData = new Uint8Array(newLength); - newData.set(this.data); - this.data = newData; + // buffer, resizing the buffer by doubling the its last length + if (this.data.length < willBeLoaded) { + var newLength = this.data.length; + for (; newLength < willBeLoaded; newLength *= 2) {} + var newData = new Uint8Array(newLength); + newData.set(this.data); + this.data = newData; + } + this.data.set(chunk, this.loaded); + this.loaded = willBeLoaded; + } else { + this.data.set(chunk, offset); + this.loaded = offset + chunk.length; + this.onprogress(this.loaded, this.length); } - - this.data.set(chunk, offset); - this.loaded = loaded; - - // not reporting the progress if data length is unknown - if (this.length >= 0) - this.onprogress(loaded, this.length); }, getData: function PdfDataListener_getData() { var data = this.length >= 0 ? this.data :