From 4ce6cb8b0fa9db948516b2b738fa1503cf0ef90e Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 11 Nov 2013 21:30:26 -0600 Subject: [PATCH 1/2] Uses postMessage transfers --- src/core/evaluator.js | 27 +++++++++++++++++++++++---- src/core/worker.js | 8 +++++++- src/display/api.js | 26 +++++++++++++++++++++----- src/shared/util.js | 10 ++++++++-- test/features/tests.js | 33 +++++++++++++++++++++++++++++++++ test/features/worker-stub.js | 3 +++ 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 80099f5dd..bf1679880 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -301,7 +301,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { PDFImage.buildImage(function(imageObj) { var imgData = imageObj.getImageData(); - self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData]); + self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], + null, [imgData.data.buffer]); }, self.handler, self.xref, resources, image, inline); operatorList.addOp(OPS.paintImageXObject, args); @@ -1457,14 +1458,30 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } }; - return PartialEvaluator; })(); var OperatorList = (function OperatorListClosure() { var CHUNK_SIZE = 100; - function OperatorList(messageHandler, pageIndex) { + function getTransfers(queue) { + var transfers = []; + var fnArray = queue.fnArray, argsArray = queue.argsArray; + for (var i = 0, ii = queue.length; i < ii; i++) { + switch (fnArray[i]) { + case OPS.paintInlineImageXObject: + case OPS.paintInlineImageXObjectGroup: + case OPS.paintImageMaskXObject: + var arg = argsArray[i][0]; // first param in imgData + transfers.push(arg.data.buffer); + break; + } + } + return transfers; + } + + + function OperatorList(messageHandler, pageIndex) { this.messageHandler = messageHandler; // When there isn't a message handler the fn array needs to be able to grow // since we can't flush the operators. @@ -1529,6 +1546,7 @@ var OperatorList = (function OperatorListClosure() { flush: function(lastChunk) { PartialEvaluator.optimizeQueue(this); + var transfers = getTransfers(this); this.messageHandler.send('RenderPageChunk', { operatorList: { fnArray: this.fnArray, @@ -1537,7 +1555,7 @@ var OperatorList = (function OperatorListClosure() { length: this.length }, pageIndex: this.pageIndex - }); + }, null, transfers); this.dependencies = []; this.fnIndex = 0; this.argsArray = []; @@ -1546,6 +1564,7 @@ var OperatorList = (function OperatorListClosure() { return OperatorList; })(); + var TextState = (function TextStateClosure() { function TextState() { this.fontSize = 0; diff --git a/src/core/worker.js b/src/core/worker.js index 9d93fc7d7..b72d1c721 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -173,6 +173,9 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { handler.send('test', false); return; } + // making sure postMessage transfers are working + var supportTransfers = data[0] === 255; + handler.postMessageTransfers = supportTransfers; // check if the response property is supported by xhr var xhr = new XMLHttpRequest(); var responseExists = 'response' in xhr; @@ -186,7 +189,10 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { handler.send('test', false); return; } - handler.send('test', true); + handler.send('test', { + supportTypedArray: true, + supportTransfers: supportTransfers + }); }); handler.on('GetDocRequest', function wphSetupDoc(data) { diff --git a/src/display/api.js b/src/display/api.js index 28e0a8480..4b88ee91d 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -85,6 +85,12 @@ PDFJS.disableAutoFetch = PDFJS.disableAutoFetch === undefined ? */ PDFJS.pdfBug = PDFJS.pdfBug === undefined ? false : PDFJS.pdfBug; +/** + * Enables transfer usage in postMessage for ArrayBuffers. + * @var {boolean} + */ +PDFJS.postMessageTransfers = PDFJS.postMessageTransfers === undefined ? + true : PDFJS.postMessageTransfers; /** * This is the main entry point for loading a PDF and interacting with it. * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR) @@ -544,9 +550,13 @@ var WorkerTransport = (function WorkerTransportClosure() { var messageHandler = new MessageHandler('main', worker); this.messageHandler = messageHandler; - messageHandler.on('test', function transportTest(supportTypedArray) { + messageHandler.on('test', function transportTest(data) { + var supportTypedArray = data && data.supportTypedArray; if (supportTypedArray) { this.worker = worker; + if (!data.supportTransfers) { + PDFJS.postMessageTransfers = false; + } this.setupMessageHandler(messageHandler); workerInitializedPromise.resolve(); } else { @@ -558,10 +568,16 @@ var WorkerTransport = (function WorkerTransportClosure() { } }.bind(this)); - var testObj = new Uint8Array(1); - // Some versions of Opera throw a DATA_CLONE_ERR on - // serializing the typed array. - messageHandler.send('test', testObj); + var testObj = new Uint8Array([PDFJS.postMessageTransfers ? 255 : 0]); + // Some versions of Opera throw a DATA_CLONE_ERR on serializing the + // typed array. Also, checking if we can use transfers. + try { + messageHandler.send('test', testObj, null, [testObj.buffer]); + } catch (ex) { + info('Cannot use postMessage transfers'); + testObj[0] = 0; + messageHandler.send('test', testObj); + } return; } catch (e) { info('The worker has been disabled.'); diff --git a/src/shared/util.js b/src/shared/util.js index 6400c5d7a..f845c0f62 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1104,6 +1104,7 @@ function MessageHandler(name, comObj) { this.name = name; this.comObj = comObj; this.callbackIndex = 1; + this.postMessageTransfers = true; var callbacks = this.callbacks = {}; var ah = this.actionHandler = {}; @@ -1170,8 +1171,9 @@ MessageHandler.prototype = { * @param {String} actionName Action to call. * @param {JSON} data JSON data to send. * @param {function} [callback] Optional callback that will handle a reply. + * @param {Array} [transfers] Optional list of transfers/ArrayBuffers */ - send: function messageHandlerSend(actionName, data, callback) { + send: function messageHandlerSend(actionName, data, callback, transfers) { var message = { action: actionName, data: data @@ -1181,7 +1183,11 @@ MessageHandler.prototype = { this.callbacks[callbackId] = callback; message.callbackId = callbackId; } - this.comObj.postMessage(message); + if (transfers && this.postMessageTransfers) { + this.comObj.postMessage(message, transfers); + } else { + this.comObj.postMessage(message); + } } }; diff --git a/test/features/tests.js b/test/features/tests.js index 934760b99..3c0b60c32 100644 --- a/test/features/tests.js +++ b/test/features/tests.js @@ -540,6 +540,39 @@ var tests = [ impact: 'Important', area: 'Core' }, + { + id: 'Worker-transfers', + name: 'Worker can use transfers for postMessage', + run: function () { + if (typeof Worker == 'undefined') + return { output: 'Skipped', emulated: '' }; + + try { + var worker = new Worker('worker-stub.js'); + + var promise = new Promise(); + var timeout = setTimeout(function () { + promise.resolve({ output: 'Failed', emulated: '?' }); + }, 5000); + + worker.addEventListener('message', function (e) { + var data = e.data; + if (data.action == 'test-transfers' && data.result) + promise.resolve({ output: 'Success', emulated: '' }); + else + promise.resolve({ output: 'Failed', emulated: 'Yes' }); + }, false); + var testObj = new Uint8Array([255]); + worker.postMessage({action: 'test-transfers', + data: testObj}, [testObj.buffer]); + return promise; + } catch (e) { + return { output: 'Failed', emulated: 'Yes' }; + } + }, + impact: 'Normal', + area: 'Core' + }, { id: 'Worker-xhr-response', name: 'XMLHttpRequest supports the reponse property in web workers', diff --git a/test/features/worker-stub.js b/test/features/worker-stub.js index c23f3d52d..6dc01f9fa 100644 --- a/test/features/worker-stub.js +++ b/test/features/worker-stub.js @@ -21,6 +21,9 @@ onmessage = function (e) { case 'test': postMessage({action: 'test', result: data.data instanceof Uint8Array}); break; + case 'test-transfers': + postMessage({action: 'test-transfers', result: data.data[0] === 255}); + break; case 'xhr': var xhr = new XMLHttpRequest(); var responseExists = 'response' in xhr; From c8af2565f18f7f22caa3f870a15a3afa1969743d Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 11 Nov 2013 22:25:03 -0600 Subject: [PATCH 2/2] Uses blob URL instead of data when possible --- src/core/stream.js | 4 ++-- src/display/api.js | 5 ++--- src/shared/util.js | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/core/stream.js b/src/core/stream.js index c46d55e63..61f1af544 100644 --- a/src/core/stream.js +++ b/src/core/stream.js @@ -15,7 +15,7 @@ * limitations under the License. */ /* globals bytesToString, ColorSpace, Dict, EOF, error, info, Jbig2Image, - JpegImage, JpxImage, Lexer, Util */ + JpegImage, JpxImage, Lexer, Util, PDFJS */ 'use strict'; @@ -832,7 +832,7 @@ var JpegStream = (function JpegStreamClosure() { } }; JpegStream.prototype.getIR = function JpegStream_getIR() { - return bytesToString(this.bytes); + return PDFJS.createObjectURL(this.bytes, 'image/jpeg'); }; /** * Checks if the image can be decoded and displayed by the browser without any diff --git a/src/display/api.js b/src/display/api.js index 4b88ee91d..44b24c3de 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -815,7 +815,7 @@ var WorkerTransport = (function WorkerTransportClosure() { }, this); messageHandler.on('JpegDecode', function(data, promise) { - var imageData = data[0]; + var imageUrl = data[0]; var components = data[1]; if (components != 3 && components != 1) error('Only 3 component or 1 component can be returned'); @@ -845,8 +845,7 @@ var WorkerTransport = (function WorkerTransportClosure() { } promise.resolve({ data: buf, width: width, height: height}); }).bind(this); - var src = 'data:image/jpeg;base64,' + window.btoa(imageData); - img.src = src; + img.src = imageUrl; }); }, diff --git a/src/shared/util.js b/src/shared/util.js index f845c0f62..964948b8d 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref */ +/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL */ 'use strict'; @@ -1100,6 +1100,33 @@ PDFJS.createBlob = function createBlob(data, contentType) { return bb.getBlob(contentType); }; +PDFJS.createObjectURL = (function createObjectURLClosure() { + if (typeof URL !== 'undefined' && URL.createObjectURL) { + return function createObjectURL(data, contentType) { + var blob = PDFJS.createBlob(data, contentType); + return URL.createObjectURL(blob); + }; + } + + // Blob/createObjectURL is not available, falling back to data schema. + var digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + return function createObjectURL(data, contentType) { + var buffer = 'data:' + contentType + ';base64,'; + for (var i = 0, ii = data.length; i < ii; i += 3) { + var b1 = data[i] & 0xFF; + var b2 = data[i + 1] & 0xFF; + var b3 = data[i + 2] & 0xFF; + var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); + var d3 = i + 1 < ii ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; + var d4 = i + 2 < ii ? (b3 & 0x3F) : 64; + buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4]; + } + return buffer; + }; +})(); + function MessageHandler(name, comObj) { this.name = name; this.comObj = comObj; @@ -1191,10 +1218,10 @@ MessageHandler.prototype = { } }; -function loadJpegStream(id, imageData, objs) { +function loadJpegStream(id, imageUrl, objs) { var img = new Image(); img.onload = (function loadJpegStream_onloadClosure() { objs.resolve(id, img); }); - img.src = 'data:image/jpeg;base64,' + window.btoa(imageData); + img.src = imageUrl; }