diff --git a/extensions/firefox/content/PdfStreamConverter.jsm b/extensions/firefox/content/PdfStreamConverter.jsm index b331444ca..9e12f10f2 100644 --- a/extensions/firefox/content/PdfStreamConverter.jsm +++ b/extensions/firefox/content/PdfStreamConverter.jsm @@ -662,8 +662,8 @@ var RangedChromeActions = (function RangedChromeActionsClosure() { // If we are in range request mode, this means we manually issued xhr // requests, which we need to abort when we leave the page domWindow.addEventListener('unload', function unload(e) { - self.networkManager.abortAllRequests(); domWindow.removeEventListener(e.type, unload); + self.abortLoading(); }); } @@ -691,7 +691,7 @@ var RangedChromeActions = (function RangedChromeActionsClosure() { }, '*'); }; this.dataListener.oncomplete = function () { - delete self.dataListener; + self.dataListener = null; }; } @@ -735,6 +735,15 @@ var RangedChromeActions = (function RangedChromeActionsClosure() { }); }; + proto.abortLoading = function RangedChromeActions_abortLoading() { + this.networkManager.abortAllRequests(); + if (this.originalRequest) { + this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); + this.originalRequest = null; + } + this.dataListener = null; + }; + return RangedChromeActions; })(); @@ -744,9 +753,10 @@ var StandardChromeActions = (function StandardChromeActionsClosure() { * This is for a single network stream */ function StandardChromeActions(domWindow, contentDispositionFilename, - dataListener) { + originalRequest, dataListener) { ChromeActions.call(this, domWindow, contentDispositionFilename); + this.originalRequest = originalRequest; this.dataListener = dataListener; } @@ -772,20 +782,29 @@ var StandardChromeActions = (function StandardChromeActionsClosure() { }, '*'); }; - this.dataListener.oncomplete = function ChromeActions_dataListenerComplete( - data, errorCode) { + this.dataListener.oncomplete = + function StandardChromeActions_dataListenerComplete(data, errorCode) { self.domWindow.postMessage({ pdfjsLoadAction: 'complete', data: data, errorCode: errorCode }, '*'); - delete self.dataListener; + self.dataListener = null; + self.originalRequest = null; }; return true; }; + proto.abortLoading = function StandardChromeActions_abortLoading() { + if (this.originalRequest) { + this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); + this.originalRequest = null; + } + this.dataListener = null; + }; + return StandardChromeActions; })(); @@ -1029,7 +1048,7 @@ PdfStreamConverter.prototype = { rangeRequest, streamRequest, dataListener); } else { actions = new StandardChromeActions( - domWindow, contentDispositionFilename, dataListener); + domWindow, contentDispositionFilename, aRequest, dataListener); } var requestListener = new RequestListener(actions); domWindow.addEventListener(PDFJS_EVENT_ID, function(event) { diff --git a/src/core/annotation.js b/src/core/annotation.js index 5844f2e4d..f08d22d3b 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -281,7 +281,7 @@ var Annotation = (function AnnotationClosure() { }.bind(this)); }, - getOperatorList: function Annotation_getOperatorList(evaluator) { + getOperatorList: function Annotation_getOperatorList(evaluator, task) { if (!this.appearance) { return Promise.resolve(new OperatorList()); @@ -308,7 +308,8 @@ var Annotation = (function AnnotationClosure() { return resourcesPromise.then(function(resources) { var opList = new OperatorList(); opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); - return evaluator.getOperatorList(self.appearance, resources, opList). + return evaluator.getOperatorList(self.appearance, task, + resources, opList). then(function () { opList.addOp(OPS.endAnnotation, []); self.appearance.reset(); @@ -319,7 +320,7 @@ var Annotation = (function AnnotationClosure() { }; Annotation.appendToOperatorList = function Annotation_appendToOperatorList( - annotations, opList, pdfManager, partialEvaluator, intent) { + annotations, opList, pdfManager, partialEvaluator, task, intent) { function reject(e) { annotationsReadyCapability.reject(e); @@ -332,7 +333,7 @@ var Annotation = (function AnnotationClosure() { if (intent === 'display' && annotations[i].isViewable() || intent === 'print' && annotations[i].isPrintable()) { annotationPromises.push( - annotations[i].getOperatorList(partialEvaluator)); + annotations[i].getOperatorList(partialEvaluator, task)); } } Promise.all(annotationPromises).then(function(datas) { @@ -564,9 +565,10 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { } Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { - getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { + getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator, + task) { if (this.appearance) { - return Annotation.prototype.getOperatorList.call(this, evaluator); + return Annotation.prototype.getOperatorList.call(this, evaluator, task); } var opList = new OperatorList(); @@ -579,7 +581,8 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { } var stream = new Stream(stringToBytes(data.defaultAppearance)); - return evaluator.getOperatorList(stream, this.fieldResources, opList). + return evaluator.getOperatorList(stream, task, + this.fieldResources, opList). then(function () { return opList; }); diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index b0a7b46fe..de20ba101 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -301,7 +301,7 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { this.chunksNeededByRequest = {}; this.requestsByChunk = {}; - this.callbacksByRequest = {}; + this.promisesByRequest = {}; this.progressiveDataLength = 0; this._loadedStreamCapability = createPromiseCapability(); @@ -320,12 +320,11 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { // contiguous ranges to load in as few requests as possible requestAllChunks: function ChunkedStreamManager_requestAllChunks() { var missingChunks = this.stream.getMissingChunks(); - this.requestChunks(missingChunks); + this._requestChunks(missingChunks); return this._loadedStreamCapability.promise; }, - requestChunks: function ChunkedStreamManager_requestChunks(chunks, - callback) { + _requestChunks: function ChunkedStreamManager_requestChunks(chunks) { var requestId = this.currRequestId++; var chunksNeeded; @@ -338,13 +337,11 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { } if (isEmptyObj(chunksNeeded)) { - if (callback) { - callback(); - } - return; + return Promise.resolve(); } - this.callbacksByRequest[requestId] = callback; + var capability = createPromiseCapability(); + this.promisesByRequest[requestId] = capability; var chunksToRequest = []; for (var chunk in chunksNeeded) { @@ -357,7 +354,7 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { } if (!chunksToRequest.length) { - return; + return capability.promise; } var groupedChunksToRequest = this.groupChunks(chunksToRequest); @@ -368,6 +365,8 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length); this.sendRequest(begin, end); } + + return capability.promise; }, getStream: function ChunkedStreamManager_getStream() { @@ -375,8 +374,7 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { }, // Loads any chunks in the requested range that are not yet loaded - requestRange: function ChunkedStreamManager_requestRange( - begin, end, callback) { + requestRange: function ChunkedStreamManager_requestRange(begin, end) { end = Math.min(end, this.length); @@ -388,11 +386,10 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { chunks.push(chunk); } - this.requestChunks(chunks, callback); + return this._requestChunks(chunks); }, - requestRanges: function ChunkedStreamManager_requestRanges(ranges, - callback) { + requestRanges: function ChunkedStreamManager_requestRanges(ranges) { ranges = ranges || []; var chunksToRequest = []; @@ -407,7 +404,7 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { } chunksToRequest.sort(function(a, b) { return a - b; }); - this.requestChunks(chunksToRequest, callback); + return this._requestChunks(chunksToRequest); }, // Groups a sorted array of chunks into as few contiguous larger @@ -506,17 +503,15 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); } if (isInt(nextEmptyChunk)) { - this.requestChunks([nextEmptyChunk]); + this._requestChunks([nextEmptyChunk]); } } for (i = 0; i < loadedRequests.length; ++i) { requestId = loadedRequests[i]; - var callback = this.callbacksByRequest[requestId]; - delete this.callbacksByRequest[requestId]; - if (callback) { - callback(); - } + var capability = this.promisesByRequest[requestId]; + delete this.promisesByRequest[requestId]; + capability.resolve(); } this.msgHandler.send('DocProgress', { @@ -537,6 +532,16 @@ var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { getEndChunk: function ChunkedStreamManager_getEndChunk(end) { var chunk = Math.floor((end - 1) / this.chunkSize) + 1; return chunk; + }, + + abort: function ChunkedStreamManager_abort() { + if (this.networkManager) { + this.networkManager.abortAllRequests(); + } + for(var requestId in this.promisesByRequest) { + var capability = this.promisesByRequest[requestId]; + capability.reject(new Error('Request was aborted')); + } } }; diff --git a/src/core/core.js b/src/core/core.js index e18824877..2342e3199 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -161,7 +161,7 @@ var Page = (function PageClosure() { }.bind(this)); }, - getOperatorList: function Page_getOperatorList(handler, intent) { + getOperatorList: function Page_getOperatorList(handler, task, intent) { var self = this; var pdfManager = this.pdfManager; @@ -194,8 +194,8 @@ var Page = (function PageClosure() { pageIndex: self.pageIndex, intent: intent }); - return partialEvaluator.getOperatorList(contentStream, self.resources, - opList).then(function () { + return partialEvaluator.getOperatorList(contentStream, task, + self.resources, opList).then(function () { return opList; }); }); @@ -212,7 +212,7 @@ var Page = (function PageClosure() { } var annotationsReadyPromise = Annotation.appendToOperatorList( - annotations, pageOpList, pdfManager, partialEvaluator, intent); + annotations, pageOpList, pdfManager, partialEvaluator, task, intent); return annotationsReadyPromise.then(function () { pageOpList.flush(true); return pageOpList; @@ -220,7 +220,7 @@ var Page = (function PageClosure() { }); }, - extractTextContent: function Page_extractTextContent() { + extractTextContent: function Page_extractTextContent(task) { var handler = { on: function nullHandlerOn() {}, send: function nullHandlerSend() {} @@ -249,6 +249,7 @@ var Page = (function PageClosure() { self.fontCache); return partialEvaluator.getTextContent(contentStream, + task, self.resources); }); }, diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 973f93eea..41f6571a7 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -125,6 +125,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { buildFormXObject: function PartialEvaluator_buildFormXObject(resources, xobj, smask, operatorList, + task, initialState) { var matrix = xobj.dict.getArray('Matrix'); var bbox = xobj.dict.getArray('BBox'); @@ -157,7 +158,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]); - return this.getOperatorList(xobj, + return this.getOperatorList(xobj, task, (xobj.dict.get('Resources') || resources), operatorList, initialState). then(function () { operatorList.addOp(OPS.paintFormXObjectEnd, []); @@ -269,7 +270,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, - operatorList, + operatorList, task, stateManager) { var smaskContent = smask.get('G'); var smaskOptions = { @@ -277,13 +278,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { backdrop: smask.get('BC') }; return this.buildFormXObject(resources, smaskContent, smaskOptions, - operatorList, stateManager.state.clone()); + operatorList, task, stateManager.state.clone()); }, handleTilingType: function PartialEvaluator_handleTilingType(fn, args, resources, pattern, patternDict, - operatorList) { + operatorList, task) { // Create an IR of the pattern code. var tilingOpList = new OperatorList(); // Merge the available resources, to prevent issues when the patternDict @@ -291,8 +292,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var resourcesArray = [patternDict.get('Resources'), resources]; var patternResources = Dict.merge(this.xref, resourcesArray); - return this.getOperatorList(pattern, patternResources, tilingOpList).then( - function () { + return this.getOperatorList(pattern, task, patternResources, + tilingOpList).then(function () { // Add the dependencies to the parent operator list so they are // resolved before sub operator list is executed synchronously. operatorList.addDependencies(tilingOpList.dependencies); @@ -305,7 +306,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { handleSetFont: function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef, - operatorList, state) { + operatorList, task, state) { // TODO(mack): Not needed? var fontName; if (fontArgs) { @@ -319,8 +320,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (!translated.font.isType3Font) { return translated; } - return translated.loadType3Data(self, resources, operatorList).then( - function () { + return translated.loadType3Data(self, resources, operatorList, task). + then(function () { return translated; }); }).then(function (translated) { @@ -367,8 +368,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, setGState: function PartialEvaluator_setGState(resources, gState, - operatorList, xref, - stateManager) { + operatorList, task, + xref, stateManager) { // This array holds the converted/processed state data. var gStateObj = []; var gStateMap = gState.map; @@ -392,8 +393,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { break; case 'Font': promise = promise.then(function () { - return self.handleSetFont(resources, null, value[0], - operatorList, stateManager.state). + return self.handleSetFont(resources, null, value[0], operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); gStateObj.push([key, [loadedName, value[1]]]); @@ -412,7 +413,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (isDict(dict)) { promise = promise.then(function () { return self.handleSMask(dict, resources, operatorList, - stateManager); + task, stateManager); }); gStateObj.push([key, true]); } else { @@ -593,7 +594,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handleColorN: function PartialEvaluator_handleColorN(operatorList, fn, args, - cs, patterns, resources, xref) { + cs, patterns, resources, task, xref) { // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors @@ -606,7 +607,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (typeNum === TILING_PATTERN) { var color = cs.base ? cs.base.getRgb(args, 0) : null; return this.handleTilingType(fn, color, resources, pattern, - dict, operatorList); + dict, operatorList, task); } else if (typeNum === SHADING_PATTERN) { var shading = dict.get('Shading'); var matrix = dict.get('Matrix'); @@ -623,6 +624,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, getOperatorList: function PartialEvaluator_getOperatorList(stream, + task, resources, operatorList, initialState) { @@ -641,6 +643,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, i, ii, cs; while (!(stop = timeSlotManager.check())) { @@ -683,7 +686,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (type.name === 'Form') { stateManager.save(); return self.buildFormXObject(resources, xobj, null, - operatorList, + operatorList, task, stateManager.state.clone()). then(function () { stateManager.restore(); @@ -707,8 +710,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { case OPS.setFont: var fontSize = args[1]; // eagerly collect all fonts - return self.handleSetFont(resources, args, null, - operatorList, stateManager.state). + return self.handleSetFont(resources, args, null, operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); operatorList.addOp(OPS.setFont, [loadedName, fontSize]); @@ -814,7 +817,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs = stateManager.state.fillColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setFillColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } @@ -825,7 +828,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs = stateManager.state.strokeColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setStrokeColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } @@ -859,8 +862,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } var gState = extGState.get(dictName.name); - return self.setGState(resources, gState, operatorList, xref, - stateManager).then(function() { + return self.setGState(resources, gState, operatorList, task, + xref, stateManager).then(function() { next(resolve, reject); }, reject); case OPS.moveTo: @@ -898,7 +901,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } // Some PDFs don't close all restores inside object/form. @@ -910,7 +913,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }); }, - getTextContent: function PartialEvaluator_getTextContent(stream, resources, + getTextContent: function PartialEvaluator_getTextContent(stream, task, + resources, stateManager) { stateManager = (stateManager || new StateManager(new TextState())); @@ -1088,6 +1092,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, args = []; while (!(stop = timeSlotManager.check())) { @@ -1243,7 +1248,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { stateManager.transform(matrix); } - return self.getTextContent(xobj, + return self.getTextContent(xobj, task, xobj.dict.get('Resources') || resources, stateManager). then(function (formTextContent) { Util.appendToArray(bidiTexts, formTextContent.items); @@ -1283,7 +1288,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } resolve(textContent); @@ -1848,7 +1853,7 @@ var TranslatedFont = (function TranslatedFontClosure() { ]); this.sent = true; }, - loadType3Data: function (evaluator, resources, parentOperatorList) { + loadType3Data: function (evaluator, resources, parentOperatorList, task) { assert(this.font.isType3Font); if (this.type3Loaded) { @@ -1865,7 +1870,7 @@ var TranslatedFont = (function TranslatedFontClosure() { loadCharProcsPromise = loadCharProcsPromise.then(function (key) { var glyphStream = charProcs[key]; var operatorList = new OperatorList(); - return evaluator.getOperatorList(glyphStream, fontResources, + return evaluator.getOperatorList(glyphStream, task, fontResources, operatorList).then(function () { charProcOperatorList[key] = operatorList.getIR(); diff --git a/src/core/obj.js b/src/core/obj.js index fe5135fc5..ef8766e46 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -1378,9 +1378,9 @@ var XRef = (function XRefClosure() { resolve(xref.fetch(ref, suppressEncryption)); } catch (e) { if (e instanceof MissingDataException) { - streamManager.requestRange(e.begin, e.end, function () { + streamManager.requestRange(e.begin, e.end).then(function () { tryFetch(resolve, reject); - }); + }, reject); return; } reject(e); @@ -1656,6 +1656,7 @@ var ObjectLoader = (function() { this.keys = keys; this.xref = xref; this.refSet = null; + this.capability = null; } ObjectLoader.prototype = { @@ -1676,11 +1677,11 @@ var ObjectLoader = (function() { nodesToVisit.push(this.obj[keys[i]]); } - this.walk(nodesToVisit); + this._walk(nodesToVisit); return this.capability.promise; }, - walk: function ObjectLoader_walk(nodesToVisit) { + _walk: function ObjectLoader_walk(nodesToVisit) { var nodesToRevisit = []; var pendingRequests = []; // DFS walk of the object graph. @@ -1727,7 +1728,7 @@ var ObjectLoader = (function() { } if (pendingRequests.length) { - this.xref.stream.manager.requestRanges(pendingRequests, + this.xref.stream.manager.requestRanges(pendingRequests).then( function pendingRequestCallback() { nodesToVisit = nodesToRevisit; for (var i = 0; i < nodesToRevisit.length; i++) { @@ -1738,8 +1739,8 @@ var ObjectLoader = (function() { this.refSet.remove(node); } } - this.walk(nodesToVisit); - }.bind(this)); + this._walk(nodesToVisit); + }.bind(this), this.capability.reject); return; } // Everything is loaded. diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index 3101e4edf..b264fdef9 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -183,7 +183,8 @@ var NetworkPdfManager = (function NetworkPdfManagerClosure() { reject(e); return; } - pdfManager.streamManager.requestRange(e.begin, e.end, ensureHelper); + pdfManager.streamManager.requestRange(e.begin, e.end). + then(ensureHelper, reject); } } @@ -193,11 +194,7 @@ var NetworkPdfManager = (function NetworkPdfManagerClosure() { NetworkPdfManager.prototype.requestRange = function NetworkPdfManager_requestRange(begin, end) { - return new Promise(function (resolve) { - this.streamManager.requestRange(begin, end, function() { - resolve(); - }); - }.bind(this)); + return this.streamManager.requestRange(begin, end); }; NetworkPdfManager.prototype.requestLoadedStream = @@ -217,7 +214,7 @@ var NetworkPdfManager = (function NetworkPdfManagerClosure() { NetworkPdfManager.prototype.terminate = function NetworkPdfManager_terminate() { - this.streamManager.networkManager.abortAllRequests(); + this.streamManager.abort(); }; return NetworkPdfManager; diff --git a/src/core/worker.js b/src/core/worker.js index 40d7d899e..7440534e7 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -22,9 +22,58 @@ 'use strict'; +var WorkerTask = (function WorkerTaskClosure() { + function WorkerTask(name) { + this.name = name; + this.terminated = false; + this._capability = createPromiseCapability(); + } + + WorkerTask.prototype = { + get finished() { + return this._capability.promise; + }, + + finish: function () { + this._capability.resolve(); + }, + + terminate: function () { + this.terminated = true; + }, + + ensureNotTerminated: function () { + if (this.terminated) { + throw new Error('Worker task was terminated'); + } + } + }; + + return WorkerTask; +})(); + var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { setup: function wphSetup(handler) { var pdfManager; + var terminated = false; + var cancelXHRs = null; + var WorkerTasks = []; + + function ensureNotTerminated() { + if (terminated) { + throw new Error('Worker was terminated'); + } + } + + function startWorkerTask(task) { + WorkerTasks.push(task); + } + + function finishWorkerTask(task) { + task.finish(); + var i = WorkerTasks.indexOf(task); + WorkerTasks.splice(i, 1); + } function loadDocument(recoveryMode) { var loadDocumentCapability = createPromiseCapability(); @@ -61,13 +110,14 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { function getPdfManager(data) { var pdfManagerCapability = createPromiseCapability(); + var pdfManager; var source = data.source; var disableRange = data.disableRange; if (source.data) { try { pdfManager = new LocalPdfManager(source.data, source.password); - pdfManagerCapability.resolve(); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } @@ -75,7 +125,7 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } else if (source.chunkedViewerLoading) { try { pdfManager = new NetworkPdfManager(source, handler); - pdfManagerCapability.resolve(); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } @@ -136,6 +186,7 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } catch (ex) { pdfManagerCapability.reject(ex); } + cancelXHRs = null; }, onProgressiveData: source.disableStream ? null : @@ -176,10 +227,11 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { // the data is array, instantiating directly from it try { pdfManager = new LocalPdfManager(pdfFile, source.password); - pdfManagerCapability.resolve(); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } + cancelXHRs = null; }, onError: function onError(status) { @@ -194,6 +246,7 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { ') while retrieving PDF "' + source.url + '".', status); handler.send('UnexpectedResponse', exception); } + cancelXHRs = null; }, onProgress: function onProgress(evt) { @@ -204,6 +257,10 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } }); + cancelXHRs = function () { + networkManager.abortRequest(fullRequestXhrId); + }; + return pdfManagerCapability.promise; } @@ -236,8 +293,8 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }); handler.on('GetDocRequest', function wphSetupDoc(data) { - var onSuccess = function(doc) { + ensureNotTerminated(); handler.send('GetDoc', { pdfInfo: doc }); }; @@ -260,6 +317,8 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } }; + ensureNotTerminated(); + PDFJS.maxImageSize = data.maxImageSize === undefined ? -1 : data.maxImageSize; PDFJS.disableFontFace = data.disableFontFace; @@ -269,13 +328,26 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { null : data.cMapUrl; PDFJS.cMapPacked = data.cMapPacked === true; - getPdfManager(data).then(function () { + getPdfManager(data).then(function (newPdfManager) { + if (terminated) { + // We were in a process of setting up the manager, but it got + // terminated in the middle. + newPdfManager.terminate(); + throw new Error('Worker was terminated'); + } + + pdfManager = newPdfManager; + handler.send('PDFManagerReady', null); pdfManager.onLoadedStream().then(function(stream) { handler.send('DataLoaded', { length: stream.bytes.byteLength }); }); }).then(function pdfManagerReady() { + ensureNotTerminated(); + loadDocument(false).then(onSuccess, function loadFailure(ex) { + ensureNotTerminated(); + // Try again with recoveryMode == true if (!(ex instanceof XRefParseException)) { if (ex instanceof PasswordException) { @@ -290,6 +362,8 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { pdfManager.requestLoadedStream(); pdfManager.onLoadedStream().then(function() { + ensureNotTerminated(); + loadDocument(true).then(onSuccess, onFailure); }); }, onFailure); @@ -380,17 +454,25 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }); handler.on('RenderPageRequest', function wphSetupRenderPage(data) { - pdfManager.getPage(data.pageIndex).then(function(page) { + var pageIndex = data.pageIndex; + pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('RenderPageRequest: page ' + pageIndex); + startWorkerTask(task); - var pageNum = data.pageIndex + 1; + var pageNum = pageIndex + 1; var start = Date.now(); // Pre compile the pdf page and fetch the fonts/images. - page.getOperatorList(handler, data.intent).then(function(operatorList) { + page.getOperatorList(handler, task, data.intent).then( + function(operatorList) { + finishWorkerTask(task); info('page=' + pageNum + ' - getOperatorList: time=' + (Date.now() - start) + 'ms, len=' + operatorList.fnArray.length); - }, function(e) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } var minimumStackMessage = 'worker.js: while trying to getPage() and getOperatorList()'; @@ -425,13 +507,23 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }, this); handler.on('GetTextContent', function wphExtractText(data) { - return pdfManager.getPage(data.pageIndex).then(function(page) { - var pageNum = data.pageIndex + 1; + var pageIndex = data.pageIndex; + return pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('GetTextContent: page ' + pageIndex); + startWorkerTask(task); + var pageNum = pageIndex + 1; var start = Date.now(); - return page.extractTextContent().then(function(textContent) { + return page.extractTextContent(task).then(function(textContent) { + finishWorkerTask(task); info('text indexing: page=' + pageNum + ' - time=' + (Date.now() - start) + 'ms'); return textContent; + }, function (reason) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } + throw reason; }); }); }); @@ -441,7 +533,22 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { }); handler.on('Terminate', function wphTerminate(data) { - pdfManager.terminate(); + terminated = true; + if (pdfManager) { + pdfManager.terminate(); + pdfManager = null; + } + if (cancelXHRs) { + cancelXHRs(); + } + + var waitOn = []; + WorkerTasks.forEach(function (task) { + waitOn.push(task.finished); + task.terminate(); + }); + + return Promise.all(waitOn).then(function () {}); }); } }; diff --git a/src/display/api.js b/src/display/api.js index c89c931a8..fb47469e0 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -19,7 +19,7 @@ Promise, PasswordResponses, PasswordException, InvalidPDFException, MissingPDFException, UnknownErrorException, FontFaceObject, loadJpegStream, createScratchCanvas, CanvasGraphics, stringToBytes, - UnexpectedResponseException */ + UnexpectedResponseException, deprecated */ 'use strict'; @@ -259,12 +259,19 @@ PDFJS.getDocument = function getDocument(src, var task = new PDFDocumentLoadingTask(); // Support of the obsolete arguments (for compatibility with API v1.0) + if (arguments.length > 1) { + deprecated('getDocument is called with pdfDataRangeTransport, ' + + 'passwordCallback or progressCallback argument'); + } if (pdfDataRangeTransport) { if (!(pdfDataRangeTransport instanceof PDFDataRangeTransport)) { // Not a PDFDataRangeTransport instance, trying to add missing properties. pdfDataRangeTransport = Object.create(pdfDataRangeTransport); pdfDataRangeTransport.length = src.length; pdfDataRangeTransport.initialData = src.initialData; + if (!pdfDataRangeTransport.abort) { + pdfDataRangeTransport.abort = function () {}; + } } src = Object.create(src); src.range = pdfDataRangeTransport; @@ -324,6 +331,7 @@ PDFJS.getDocument = function getDocument(src, workerInitializedCapability.promise.then(function transportInitialized() { transport.fetchDocument(task, params); }); + task._transport = transport; return task; }; @@ -336,6 +344,7 @@ var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { /** @constructs PDFDocumentLoadingTask */ function PDFDocumentLoadingTask() { this._capability = createPromiseCapability(); + this._transport = null; /** * Callback to request a password if wrong or no password was provided. @@ -361,7 +370,14 @@ var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { return this._capability.promise; }, - // TODO add cancel or abort method + /** + * Aborts all network requests and destroys worker. + * @return {Promise} A promise that is resolved after destruction activity + * is completed. + */ + destroy: function () { + return this._transport.destroy(); + }, /** * Registers callbacks to indicate the document loading completion. @@ -448,6 +464,9 @@ var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() { requestDataRange: function PDFDataRangeTransport_requestDataRange(begin, end) { throw new Error('Abstract method PDFDataRangeTransport.requestDataRange'); + }, + + abort: function PDFDataRangeTransport_abort() { } }; return PDFDataRangeTransport; @@ -461,9 +480,10 @@ PDFJS.PDFDataRangeTransport = PDFDataRangeTransport; * @class */ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { - function PDFDocumentProxy(pdfInfo, transport) { + function PDFDocumentProxy(pdfInfo, transport, loadingTask) { this.pdfInfo = pdfInfo; this.transport = transport; + this.loadingTask = loadingTask; } PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ { /** @@ -586,7 +606,7 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { * Destroys current document instance and terminates worker. */ destroy: function PDFDocumentProxy_destroy() { - this.transport.destroy(); + return this.transport.destroy(); } }; return PDFDocumentProxy; @@ -663,8 +683,9 @@ var PDFPageProxy = (function PDFPageProxyClosure() { this.commonObjs = transport.commonObjs; this.objs = new PDFObjects(); this.cleanupAfterRender = false; - this.pendingDestroy = false; + this.pendingCleanup = false; this.intentStates = {}; + this.destroyed = false; } PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ { /** @@ -728,7 +749,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { // If there was a pending destroy cancel it so no cleanup happens during // this call to render. - this.pendingDestroy = false; + this.pendingCleanup = false; var renderingIntent = (params.intent === 'print' ? 'print' : 'display'); @@ -769,13 +790,14 @@ var PDFPageProxy = (function PDFPageProxyClosure() { // Obsolete parameter support if (params.continueCallback) { + deprecated('render is used with continueCallback parameter'); renderTask.onContinue = params.continueCallback; } var self = this; intentState.displayReadyCapability.promise.then( function pageDisplayReadyPromise(transparency) { - if (self.pendingDestroy) { + if (self.pendingCleanup) { complete(); return; } @@ -795,9 +817,9 @@ var PDFPageProxy = (function PDFPageProxyClosure() { } if (self.cleanupAfterRender) { - self.pendingDestroy = true; + self.pendingCleanup = true; } - self._tryDestroy(); + self._tryCleanup(); if (error) { internalRenderTask.capability.reject(error); @@ -858,20 +880,52 @@ var PDFPageProxy = (function PDFPageProxyClosure() { pageIndex: this.pageNumber - 1 }); }, + /** - * Destroys resources allocated by the page. + * Destroys page object. */ - destroy: function PDFPageProxy_destroy() { - this.pendingDestroy = true; - this._tryDestroy(); + _destroy: function PDFPageProxy_destroy() { + this.destroyed = true; + this.transport.pageCache[this.pageIndex] = null; + + var waitOn = []; + Object.keys(this.intentStates).forEach(function(intent) { + var intentState = this.intentStates[intent]; + intentState.renderTasks.forEach(function(renderTask) { + var renderCompleted = renderTask.capability.promise. + catch(function () {}); // ignoring failures + waitOn.push(renderCompleted); + renderTask.cancel(); + }); + }, this); + this.objs.clear(); + this.annotationsPromise = null; + this.pendingCleanup = false; + return Promise.all(waitOn); + }, + + /** + * Cleans up resources allocated by the page. (deprecated) + */ + destroy: function() { + deprecated('page destroy method, use cleanup() instead'); + this.cleanup(); + }, + + /** + * Cleans up resources allocated by the page. + */ + cleanup: function PDFPageProxy_cleanup() { + this.pendingCleanup = true; + this._tryCleanup(); }, /** * For internal use only. Attempts to clean up if rendering is in a state * where that's possible. * @ignore */ - _tryDestroy: function PDFPageProxy__destroy() { - if (!this.pendingDestroy || + _tryCleanup: function PDFPageProxy_tryCleanup() { + if (!this.pendingCleanup || Object.keys(this.intentStates).some(function(intent) { var intentState = this.intentStates[intent]; return (intentState.renderTasks.length !== 0 || @@ -885,7 +939,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { }, this); this.objs.clear(); this.annotationsPromise = null; - this.pendingDestroy = false; + this.pendingCleanup = false; }, /** * For internal use only. @@ -923,7 +977,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { if (operatorListChunk.lastChunk) { intentState.receivingOperatorList = false; - this._tryDestroy(); + this._tryCleanup(); } } }; @@ -941,6 +995,8 @@ var WorkerTransport = (function WorkerTransportClosure() { this.commonObjs = new PDFObjects(); this.loadingTask = null; + this.destroyed = false; + this.destroyCapability = null; this.pageCache = []; this.pagePromises = []; @@ -1001,15 +1057,40 @@ var WorkerTransport = (function WorkerTransportClosure() { } WorkerTransport.prototype = { destroy: function WorkerTransport_destroy() { + if (this.destroyCapability) { + return this.destroyCapability.promise; + } + + this.destroyed = true; + this.destroyCapability = createPromiseCapability(); + + var waitOn = []; + // We need to wait for all renderings to be completed, e.g. + // timeout/rAF can take a long time. + this.pageCache.forEach(function (page) { + if (page) { + waitOn.push(page._destroy()); + } + }); this.pageCache = []; this.pagePromises = []; var self = this; - this.messageHandler.sendWithPromise('Terminate', null).then(function () { + // We also need to wait for the worker to finish its long running tasks. + var terminated = this.messageHandler.sendWithPromise('Terminate', null); + waitOn.push(terminated); + Promise.all(waitOn).then(function () { FontLoader.clear(); if (self.worker) { self.worker.terminate(); } - }); + if (self.pdfDataRangeTransport) { + self.pdfDataRangeTransport.abort(); + self.pdfDataRangeTransport = null; + } + self.messageHandler = null; + self.destroyCapability.resolve(); + }, this.destroyCapability.reject); + return this.destroyCapability.promise; }, setupFakeWorker: function WorkerTransport_setupFakeWorker() { @@ -1091,9 +1172,10 @@ var WorkerTransport = (function WorkerTransportClosure() { messageHandler.on('GetDoc', function transportDoc(data) { var pdfInfo = data.pdfInfo; this.numPages = data.pdfInfo.numPages; - var pdfDocument = new PDFDocumentProxy(pdfInfo, this); + var loadingTask = this.loadingTask; + var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask); this.pdfDocument = pdfDocument; - this.loadingTask._capability.resolve(pdfDocument); + loadingTask._capability.resolve(pdfDocument); }, this); messageHandler.on('NeedPassword', @@ -1294,6 +1376,12 @@ var WorkerTransport = (function WorkerTransportClosure() { }, fetchDocument: function WorkerTransport_fetchDocument(loadingTask, source) { + if (this.destroyed) { + loadingTask._capability.reject(new Error('Loading aborted')); + this.destroyCapability.resolve(); + return; + } + this.loadingTask = loadingTask; source.disableAutoFetch = PDFJS.disableAutoFetch; @@ -1332,6 +1420,9 @@ var WorkerTransport = (function WorkerTransportClosure() { var promise = this.messageHandler.sendWithPromise('GetPage', { pageIndex: pageIndex }).then(function (pageInfo) { + if (this.destroyed) { + throw new Error('Transport destroyed'); + } var page = new PDFPageProxy(pageIndex, pageInfo, this); this.pageCache[pageIndex] = page; return page; @@ -1389,7 +1480,7 @@ var WorkerTransport = (function WorkerTransportClosure() { for (var i = 0, ii = this.pageCache.length; i < ii; i++) { var page = this.pageCache[i]; if (page) { - page.destroy(); + page.cleanup(); } } this.commonObjs.clear(); diff --git a/src/shared/util.js b/src/shared/util.js index f4821bcc9..1adeb82c4 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -213,6 +213,11 @@ function warn(msg) { } } +// Deprecated API function -- treated as warnings. +function deprecated(details) { + warn('Deprecated API usage: ' + details); +} + // Fatal errors that should trigger the fallback UI and halt execution by // throwing an exception. function error(msg) { diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index b183b9793..36f97ab4e 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -11,31 +11,31 @@ describe('api', function() { var basicApiUrl = combineUrl(window.location.href, '../pdfs/basicapi.pdf'); var basicApiFileLength = 105779; // bytes function waitsForPromiseResolved(promise, successCallback) { - var data; + var resolved = false; promise.then(function(val) { - data = val; - successCallback(data); + resolved = true; + successCallback(val); }, function(error) { // Shouldn't get here. expect(false).toEqual(true); }); waitsFor(function() { - return data !== undefined; + return resolved; }, 20000); } function waitsForPromiseRejected(promise, failureCallback) { - var data; + var rejected = false; promise.then(function(val) { // Shouldn't get here. expect(false).toEqual(true); }, function(error) { - data = error; - failureCallback(data); + rejected = true; + failureCallback(error); }); waitsFor(function() { - return data !== undefined; + return rejected; }, 20000); } @@ -63,6 +63,28 @@ describe('api', function() { waitsForPromiseResolved(Promise.all(promises), function (data) { expect((data[0].loaded / data[0].total) > 0).toEqual(true); expect(data[1] instanceof PDFDocumentProxy).toEqual(true); + expect(loadingTask).toEqual(data[1].loadingTask); + }); + }); + it('creates pdf doc from URL and aborts before worker initialized', + function() { + var loadingTask = PDFJS.getDocument(basicApiUrl); + loadingTask.destroy(); + waitsForPromiseRejected(loadingTask.promise, function(reason) { + expect(true).toEqual(true); + }); + }); + it('creates pdf doc from URL and aborts loading after worker initialized', + function() { + var loadingTask = PDFJS.getDocument(basicApiUrl); + // This can be somewhat random -- we cannot guarantee perfect + // 'Terminate' message to the worker before/after setting up pdfManager. + var destroyed = loadingTask._transport.workerInitializedCapability. + promise.then(function () { + return loadingTask.destroy(); + }); + waitsForPromiseResolved(destroyed, function (data) { + expect(true).toEqual(true); }); }); it('creates pdf doc from typed array', function() { diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js index 5bf007d0b..5e21122c5 100644 --- a/test/unit/evaluator_spec.js +++ b/test/unit/evaluator_spec.js @@ -1,13 +1,13 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* globals expect, it, describe, PartialEvaluator, StringStream, OPS, - OperatorList, waitsFor, runs, Dict, Name, Stream */ + OperatorList, waitsFor, runs, Dict, Name, Stream, WorkerTask */ 'use strict'; describe('evaluator', function() { function XrefMock(queue) { - this.queue = queue; + this.queue = queue || []; } XrefMock.prototype = { fetchIfRef: function() { @@ -35,7 +35,9 @@ describe('evaluator', function() { var done = false; runs(function () { var result = new OperatorList(); - evaluator.getOperatorList(stream, resources, result).then(function () { + var task = new WorkerTask('OperatorListCheck'); + evaluator.getOperatorList(stream, task, resources, result).then( + function () { check(result); done = true; }); @@ -259,4 +261,48 @@ describe('evaluator', function() { }); }); }); + + describe('thread control', function() { + it('should abort operator list parsing', function () { + var evaluator = new PartialEvaluator(new PdfManagerMock(), + new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('qqQQ'); + var resources = new ResourcesMock(); + var done = false; + runs(function () { + var result = new OperatorList(); + var task = new WorkerTask('OperatorListAbort'); + task.terminate(); + evaluator.getOperatorList(stream, task, resources, result).catch( + function () { + done = true; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(0); + }); + }); + waitsFor(function () { + return done; + }); + }); + it('should abort text parsing parsing', function () { + var resources = new ResourcesMock(); + var evaluator = new PartialEvaluator(new PdfManagerMock(), + new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('qqQQ'); + var done = false; + runs(function () { + var task = new WorkerTask('TextContentAbort'); + task.terminate(); + evaluator.getTextContent(stream, task, resources).catch( + function () { + done = true; + }); + }); + waitsFor(function () { + return done; + }); + }); + }); }); diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 9193dbcd7..5fe1f254b 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -35,6 +35,7 @@ + diff --git a/web/chromecom.js b/web/chromecom.js index 6823dd3a1..134dd6f00 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -64,7 +64,7 @@ var ChromeCom = (function ChromeComClosure() { var streamUrl = response.streamUrl; if (streamUrl) { console.log('Found data stream for ' + file); - PDFViewerApplication.open(streamUrl, 0, undefined, undefined, { + PDFViewerApplication.open(streamUrl, { length: response.contentLength }); PDFViewerApplication.setTitleUsingUrl(file); @@ -91,7 +91,7 @@ var ChromeCom = (function ChromeComClosure() { resolveLocalFileSystemURL(file, function onResolvedFSURL(fileEntry) { fileEntry.file(function(fileObject) { var blobUrl = URL.createObjectURL(fileObject); - PDFViewerApplication.open(blobUrl, 0, undefined, undefined, { + PDFViewerApplication.open(blobUrl, { length: fileObject.size }); }); @@ -100,7 +100,7 @@ var ChromeCom = (function ChromeComClosure() { // usual way of getting the File's data (via the Web worker). console.warn('Cannot resolve file ' + file + ', ' + error.name + ' ' + error.message); - PDFViewerApplication.open(file, 0); + PDFViewerApplication.open(file); }); return; } @@ -109,7 +109,7 @@ var ChromeCom = (function ChromeComClosure() { // There is no UI to input a different URL, so this assumption will hold // for now. setReferer(file, function() { - PDFViewerApplication.open(file, 0); + PDFViewerApplication.open(file); }); return; } @@ -122,14 +122,14 @@ var ChromeCom = (function ChromeComClosure() { } isAllowedFileSchemeAccess(function(isAllowedAccess) { if (isAllowedAccess) { - PDFViewerApplication.open(file, 0); + PDFViewerApplication.open(file); } else { requestAccessToLocalFile(file); } }); return; } - PDFViewerApplication.open(file, 0); + PDFViewerApplication.open(file); }); }; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index bec6d1d95..34345a3af 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -101,7 +101,7 @@ var PDFPageView = (function PDFPageViewClosure() { this.zoomLayer = null; this.reset(); if (this.pdfPage) { - this.pdfPage.destroy(); + this.pdfPage.cleanup(); } }, @@ -457,9 +457,9 @@ var PDFPageView = (function PDFPageViewClosure() { canvasContext: ctx, viewport: this.viewport, // intent: 'default', // === 'display' - continueCallback: renderContinueCallback }; var renderTask = this.renderTask = this.pdfPage.render(renderContext); + renderTask.onContinue = renderContinueCallback; this.renderTask.promise.then( function pdfPageRenderCallback() { diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 88c92c738..24e4cf5f3 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -285,10 +285,10 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() { var renderContext = { canvasContext: ctx, - viewport: drawViewport, - continueCallback: renderContinueCallback + viewport: drawViewport }; var renderTask = this.renderTask = this.pdfPage.render(renderContext); + renderTask.onContinue = renderContinueCallback; renderTask.promise.then( function pdfPageRenderCallback() { diff --git a/web/viewer.js b/web/viewer.js index 4b25ffe3c..947b42241 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -92,6 +92,7 @@ var PDFViewerApplication = { initialized: false, fellback: false, pdfDocument: null, + pdfLoadingTask: null, sidebarOpen: false, printing: false, /** @type {PDFViewer} */ @@ -410,6 +411,11 @@ var PDFViewerApplication = { function FirefoxComDataRangeTransport_requestDataRange(begin, end) { FirefoxCom.request('requestDataRange', { begin: begin, end: end }); }; + FirefoxComDataRangeTransport.prototype.abort = + function FirefoxComDataRangeTransport_abort() { + // Sync call to ensure abort is really started. + FirefoxCom.requestSync('abortLoading', null); + }; var pdfDataRangeTransport; @@ -429,8 +435,8 @@ var PDFViewerApplication = { pdfDataRangeTransport = new FirefoxComDataRangeTransport(args.length, args.data); - PDFViewerApplication.open(args.pdfUrl, 0, undefined, - pdfDataRangeTransport); + PDFViewerApplication.open(args.pdfUrl, + {range: pdfDataRangeTransport}); if (args.length) { PDFViewerApplication.pdfDocumentProperties @@ -455,7 +461,7 @@ var PDFViewerApplication = { 'An error occurred while loading the PDF.'), e); break; } - PDFViewerApplication.open(args.data, 0); + PDFViewerApplication.open(args.data); break; } }); @@ -482,36 +488,76 @@ var PDFViewerApplication = { document.title = title; }, + /** + * Closes opened PDF document. + * @returns {Promise} - Returns the promise, which is resolved when all + * destruction is completed. + */ close: function pdfViewClose() { var errorWrapper = document.getElementById('errorWrapper'); errorWrapper.setAttribute('hidden', 'true'); - if (!this.pdfDocument) { - return; + if (!this.pdfLoadingTask) { + return Promise.resolve(); } - this.pdfDocument.destroy(); - this.pdfDocument = null; + var promise = this.pdfLoadingTask.destroy(); + this.pdfLoadingTask = null; - this.pdfThumbnailViewer.setDocument(null); - this.pdfViewer.setDocument(null); - this.pdfLinkService.setDocument(null, null); + if (this.pdfDocument) { + this.pdfDocument = null; + + this.pdfThumbnailViewer.setDocument(null); + this.pdfViewer.setDocument(null); + this.pdfLinkService.setDocument(null, null); + } if (typeof PDFBug !== 'undefined') { PDFBug.cleanup(); } + return promise; }, - // TODO(mack): This function signature should really be pdfViewOpen(url, args) - open: function pdfViewOpen(file, scale, password, - pdfDataRangeTransport, args) { - if (this.pdfDocument) { - // Reload the preferences if a document was previously opened. - Preferences.reload(); + /** + * Opens PDF document specified by URL or array with additional arguments. + * @param {string|TypedArray|ArrayBuffer} file - PDF location or binary data. + * @param {Object} args - (optional) Additional arguments for the getDocument + * call, e.g. HTTP headers ('httpHeaders') or + * alternative data transport ('range'). + * @returns {Promise} - Returns the promise, which is resolved when document + * is opened. + */ + open: function pdfViewOpen(file, args) { + var scale = 0; + if (arguments.length > 2 || typeof args === 'number') { + console.warn('Call of open() with obsolete signature.'); + if (typeof args === 'number') { + scale = args; // scale argument was found + } + args = arguments[4] || null; + if (arguments[3] && typeof arguments[3] === 'object') { + // The pdfDataRangeTransport argument is present. + args = Object.create(args); + args.range = arguments[3]; + } + if (typeof arguments[2] === 'string') { + // The password argument is present. + args = Object.create(args); + args.password = arguments[2]; + } } - this.close(); - var parameters = {password: password}; + if (this.pdfLoadingTask) { + // We need to destroy already opened document. + return this.close().then(function () { + // Reload the preferences if a document was previously opened. + Preferences.reload(); + // ... and repeat the open() call. + return this.open(file, args); + }.bind(this)); + } + + var parameters = Object.create(null); if (typeof file === 'string') { // URL this.setTitleUsingUrl(file); parameters.url = file; @@ -530,18 +576,20 @@ var PDFViewerApplication = { var self = this; self.downloadComplete = false; - var passwordNeeded = function passwordNeeded(updatePassword, reason) { + var loadingTask = PDFJS.getDocument(parameters); + this.pdfLoadingTask = loadingTask; + + loadingTask.onPassword = function passwordNeeded(updatePassword, reason) { PasswordPrompt.updatePassword = updatePassword; PasswordPrompt.reason = reason; PasswordPrompt.open(); }; - function getDocumentProgress(progressData) { + loadingTask.onProgress = function getDocumentProgress(progressData) { self.progress(progressData.loaded / progressData.total); - } + }; - PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded, - getDocumentProgress).then( + var result = loadingTask.promise.then( function getDocumentCallback(pdfDocument) { self.load(pdfDocument, scale); }, @@ -567,12 +615,15 @@ var PDFViewerApplication = { message: message }; self.error(loadingErrorMessage, moreInfo); + + throw new Error(loadingErrorMessage); } ); if (args && args.length) { PDFViewerApplication.pdfDocumentProperties.setFileSize(args.length); } + return result; }, download: function pdfViewDownload() { @@ -999,6 +1050,9 @@ var PDFViewerApplication = { }, cleanup: function pdfViewCleanup() { + if (!this.pdfDocument) { + return; // run cleanup when document is loaded + } this.pdfViewer.cleanup(); this.pdfThumbnailViewer.cleanup(); this.pdfDocument.cleanup(); @@ -1493,7 +1547,7 @@ function webViewerInitialized() { PDFViewerApplication.setTitleUsingUrl(file); var xhr = new XMLHttpRequest(); xhr.onload = function() { - PDFViewerApplication.open(new Uint8Array(xhr.response), 0); + PDFViewerApplication.open(new Uint8Array(xhr.response)); }; try { xhr.open('GET', file); @@ -1507,7 +1561,7 @@ function webViewerInitialized() { } if (file) { - PDFViewerApplication.open(file, 0); + PDFViewerApplication.open(file); } //#endif //#if CHROME @@ -1722,14 +1776,14 @@ window.addEventListener('change', function webViewerChange(evt) { if (!PDFJS.disableCreateObjectURL && typeof URL !== 'undefined' && URL.createObjectURL) { - PDFViewerApplication.open(URL.createObjectURL(file), 0); + PDFViewerApplication.open(URL.createObjectURL(file)); } else { // Read the local file into a Uint8Array. var fileReader = new FileReader(); fileReader.onload = function webViewerChangeFileReaderOnload(evt) { var buffer = evt.target.result; var uint8Array = new Uint8Array(buffer); - PDFViewerApplication.open(uint8Array, 0); + PDFViewerApplication.open(uint8Array); }; fileReader.readAsArrayBuffer(file); }