diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index 7fb42c0cd..d5b12dbde 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,68 @@ function getLocalizedString(strings, id, property) { return id; } +// PDF data storage +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) { + 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 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); + } + }, + 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.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.getData()); + } + if (this.errorCode) { + value(null, this.errorCode); + } + } +}; + // All the priviledged actions. -function ChromeActions(domWindow) { +function ChromeActions(domWindow, dataListener) { this.domWindow = domWindow; + this.dataListener = dataListener; } ChromeActions.prototype = { @@ -194,6 +254,38 @@ 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, errorCode) { + + domWindow.postMessage({ + pdfjsLoadAction: 'complete', + data: data, + errorCode: errorCode + }, '*'); + + delete this.dataListener; + }; + + return true; + }, getStrings: function(data) { try { // Lazy initialization of localizedStrings @@ -341,17 +433,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; @@ -359,8 +455,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 @@ -368,15 +470,27 @@ 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; + 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 = { @@ -390,8 +504,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); @@ -403,11 +517,33 @@ 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); + // FF16 and below had getCodebasePrincipal (bug 774585) + var resourcePrincipal = 'getSimpleCodebasePrincipal' in securityManager ? + securityManager.getSimpleCodebasePrincipal(uri) : + securityManager.getCodebasePrincipal(uri); + channel.owner = resourcePrincipal; + } }, // nsIRequestObserver::onStopRequest onStopRequest: function(aRequest, aContext, aStatusCode) { - // Do nothing. + if (!this.dataListener) { + // Do nothing + return; + } + + if (Components.isSuccessCode(aStatusCode)) + this.dataListener.finish(); + else + this.dataListener.error(aStatusCode); + delete this.dataListener; + delete this.binaryStream; } }; diff --git a/src/image.js b/src/image.js index 3495483e7..59f70f99a 100644 --- a/src/image.js +++ b/src/image.js @@ -347,6 +347,10 @@ var PDFImage = (function PDFImageClosure() { var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; var imgArray = this.getImageBytes(originalHeight * rowBytes); + // imgArray can be incomplete (e.g. after CCITT fax encoding) + var actualHeight = 0 | (imgArray.length / rowBytes * + height / originalHeight); + var comps = this.colorSpace.getRgbBuffer( this.getComponents(imgArray), bpc); if (originalWidth != width || originalHeight != height) @@ -355,7 +359,7 @@ var PDFImage = (function PDFImageClosure() { var compsPos = 0; var opacity = this.getOpacity(width, height); var opacityPos = 0; - var length = width * height * 4; + var length = width * actualHeight * 4; for (var i = 0; i < length; i += 4) { buffer[i] = comps[compsPos++]; diff --git a/test/pdfs/issue1658.pdf.link b/test/pdfs/issue1658.pdf.link new file mode 100644 index 000000000..f77372073 --- /dev/null +++ b/test/pdfs/issue1658.pdf.link @@ -0,0 +1 @@ +https://www.ads.tuwien.ac.at/teaching/archiv/praktika/CherkasskyGoldberg-1995-MaxFlow.pdf \ No newline at end of file diff --git a/test/test_manifest.json b/test/test_manifest.json index 12761bd33..a80c7bb4f 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -539,6 +539,15 @@ "link": true, "type": "eq" }, + { "id": "issue1658", + "file": "pdfs/issue1658.pdf", + "md5": "b71a0f641e83ad427b8bcfc180899a05", + "rounds": 1, + "skipPages": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "pageLimit": 10, + "link": true, + "type": "eq" + }, { "id": "issue1049", "file": "pdfs/issue1049.pdf", "md5": "15473fffcdde9fb8f3756a4cf1aab347", diff --git a/web/viewer.js b/web/viewer.js index 48bbf6eec..9cbdc6e4c 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -348,11 +348,42 @@ var PDFView = { return support; }, + initPassiveLoading: function pdfViewInitPassiveLoading() { + if (!PDFView.loadingBar) { + PDFView.loadingBar = new ProgressBar('#loadingBar', {}); + } + + window.addEventListener('message', function window_message(e) { + var args = e.data; + + if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) + return; + switch (args.pdfjsLoadAction) { + case 'progress': + 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; + } + }); + 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; @@ -1768,7 +1799,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)); @@ -1870,6 +1901,15 @@ window.addEventListener('load', function webViewerLoad(evt) { PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen'); PDFView.renderHighestPriority(); }); + +//#if (FIREFOX || MOZCENTRAL) +//if (FirefoxCom.requestSync('getLoadingType') == 'passive') { +// PDFView.setTitleUsingUrl(file); +// PDFView.initPassiveLoading(); +// return; +//} +//#endif + //#if !B2G PDFView.open(file, 0); //#endif @@ -1961,7 +2001,7 @@ window.addEventListener('change', function webViewerChange(evt) { var file = files[0]; fileReader.readAsArrayBuffer(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');