Browse Source

Creates PDFWorker, separates fetchDocument from transport.

Yury Delendik 10 years ago
parent
commit
09772e1e15
  1. 9
      src/core/worker.js
  2. 381
      src/display/api.js
  3. 70
      test/unit/api_spec.js

9
src/core/worker.js

@ -85,6 +85,8 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = {
}); });
}, },
createDocumentHandler: function wphCreateDocumentHandler(data, port) { createDocumentHandler: function wphCreateDocumentHandler(data, port) {
// This context is actually holds references on pdfManager and handler,
// until the latter is destroyed.
var pdfManager; var pdfManager;
var terminated = false; var terminated = false;
var cancelXHRs = null; var cancelXHRs = null;
@ -555,7 +557,12 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = {
task.terminate(); task.terminate();
}); });
return Promise.all(waitOn).then(function () {}); return Promise.all(waitOn).then(function () {
// Notice that even if we destroying handler, resolved response promise
// must be sent back.
handler.destroy();
handler = null;
});
}); });
setupDoc(data); setupDoc(data);

381
src/display/api.js

@ -222,6 +222,8 @@ PDFJS.isEvalSupported = (PDFJS.isEvalSupported === undefined ?
* @property {number} rangeChunkSize - Optional parameter to specify * @property {number} rangeChunkSize - Optional parameter to specify
* maximum number of bytes fetched per range request. The default value is * maximum number of bytes fetched per range request. The default value is
* 2^16 = 65536. * 2^16 = 65536.
* @property {PDFWorker} worker - The worker that will be used for the loading
* and parsing of the PDF data.
*/ */
/** /**
@ -284,7 +286,6 @@ PDFJS.getDocument = function getDocument(src,
task.onPassword = passwordCallback || null; task.onPassword = passwordCallback || null;
task.onProgress = progressCallback || null; task.onProgress = progressCallback || null;
var workerInitializedCapability, transport;
var source; var source;
if (typeof src === 'string') { if (typeof src === 'string') {
source = { url: src }; source = { url: src };
@ -305,12 +306,18 @@ PDFJS.getDocument = function getDocument(src,
} }
var params = {}; var params = {};
var rangeTransport = null;
var worker = null;
for (var key in source) { for (var key in source) {
if (key === 'url' && typeof window !== 'undefined') { if (key === 'url' && typeof window !== 'undefined') {
// The full path is required in the 'url' field. // The full path is required in the 'url' field.
params[key] = combineUrl(window.location.href, source[key]); params[key] = combineUrl(window.location.href, source[key]);
continue; continue;
} else if (key === 'range') { } else if (key === 'range') {
rangeTransport = source[key];
continue;
} else if (key === 'worker') {
worker = source[key];
continue; continue;
} else if (key === 'data' && !(source[key] instanceof Uint8Array)) { } else if (key === 'data' && !(source[key] instanceof Uint8Array)) {
// Converting string or array-like data to Uint8Array. // Converting string or array-like data to Uint8Array.
@ -331,27 +338,91 @@ PDFJS.getDocument = function getDocument(src,
params[key] = source[key]; params[key] = source[key];
} }
params.rangeChunkSize = source.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE; params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
workerInitializedCapability = createPromiseCapability(); if (!worker) {
transport = new WorkerTransport(workerInitializedCapability, source.range); // Worker was not provided -- creating and owning our own.
workerInitializedCapability.promise.then(function transportInitialized() { worker = new PDFWorker();
transport.fetchDocument(task, params); task._worker = worker;
}); }
var docId = task.docId;
worker.promise.then(function () {
if (task.destroyed) {
throw new Error('Loading aborted');
}
return _fetchDocument(worker, params, rangeTransport, docId).then(
function (workerId) {
if (task.destroyed) {
throw new Error('Loading aborted');
}
var messageHandler = new MessageHandler(docId, workerId, worker.port);
var transport = new WorkerTransport(messageHandler, task, rangeTransport);
task._transport = transport; task._transport = transport;
});
}, task._capability.reject);
return task; return task;
}; };
/**
* Starts fetching of specified PDF document/data.
* @param {PDFWorker} worker
* @param {Object} source
* @param {PDFDataRangeTransport} pdfDataRangeTransport
* @param {string} docId Unique document id, used as MessageHandler id.
* @returns {Promise} The promise, which is resolved when worker id of
* MessageHandler is known.
* @private
*/
function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
if (worker.destroyed) {
return Promise.reject(new Error('Worker was destroyed'));
}
source.disableAutoFetch = PDFJS.disableAutoFetch;
source.disableStream = PDFJS.disableStream;
source.chunkedViewerLoading = !!pdfDataRangeTransport;
if (pdfDataRangeTransport) {
source.length = pdfDataRangeTransport.length;
source.initialData = pdfDataRangeTransport.initialData;
}
return worker.messageHandler.sendWithPromise('GetDocRequest', {
docId: docId,
source: source,
disableRange: PDFJS.disableRange,
maxImageSize: PDFJS.maxImageSize,
cMapUrl: PDFJS.cMapUrl,
cMapPacked: PDFJS.cMapPacked,
disableFontFace: PDFJS.disableFontFace,
disableCreateObjectURL: PDFJS.disableCreateObjectURL,
verbosity: PDFJS.verbosity
}).then(function (workerId) {
if (worker.destroyed) {
throw new Error('Worker was destroyed');
}
return workerId;
});
}
/** /**
* PDF document loading operation. * PDF document loading operation.
* @class * @class
* @alias PDFDocumentLoadingTask * @alias PDFDocumentLoadingTask
*/ */
var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() {
var nextDocumentId = 0;
/** @constructs PDFDocumentLoadingTask */
function PDFDocumentLoadingTask() { function PDFDocumentLoadingTask() {
this._capability = createPromiseCapability(); this._capability = createPromiseCapability();
this._transport = null; this._transport = null;
this._worker = null;
/**
* Unique document loading task id -- used in MessageHandlers.
* @type {string}
*/
this.docId = 'd' + (nextDocumentId++);
/** /**
* Shows if loading task is destroyed. * Shows if loading task is destroyed.
@ -390,10 +461,16 @@ var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() {
*/ */
destroy: function () { destroy: function () {
this.destroyed = true; this.destroyed = true;
if (!this._transport) {
return Promise.resolve(); var transportDestroyed = !this._transport ? Promise.resolve() :
this._transport.destroy();
return transportDestroyed.then(function () {
this._transport = null;
if (this._worker) {
this._worker.destroy();
this._worker = null;
} }
return this._transport.destroy(); }.bind(this));
}, },
/** /**
@ -622,7 +699,7 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
* Destroys current document instance and terminates worker. * Destroys current document instance and terminates worker.
*/ */
destroy: function PDFDocumentProxy_destroy() { destroy: function PDFDocumentProxy_destroy() {
return this.transport.destroy(); return this.loadingTask.destroy();
} }
}; };
return PDFDocumentProxy; return PDFDocumentProxy;
@ -1019,28 +1096,66 @@ var PDFPageProxy = (function PDFPageProxyClosure() {
})(); })();
/** /**
* For internal use only. * PDF.js web worker abstraction, it controls instantiation of PDF documents and
* @ignore * WorkerTransport for them. If creation of a web worker is not possible,
* a "fake" worker will be used instead.
* @class
*/ */
var WorkerTransport = (function WorkerTransportClosure() { var PDFWorker = (function PDFWorkerClosure() {
function WorkerTransport(workerInitializedCapability, pdfDataRangeTransport) { var nextFakeWorkerId = 0;
this.pdfDataRangeTransport = pdfDataRangeTransport;
this.workerInitializedCapability = workerInitializedCapability;
this.commonObjs = new PDFObjects();
this.loadingTask = null; // Loads worker code into main thread.
function setupFakeWorkerGlobal() {
if (!PDFJS.fakeWorkerFilesLoadedCapability) {
PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability();
// In the developer build load worker_loader which in turn loads all the
// other files and resolves the promise. In production only the
// pdf.worker.js file is needed.
//#if !PRODUCTION
Util.loadScript(PDFJS.workerSrc);
//#endif
//#if PRODUCTION && SINGLE_FILE
// PDFJS.fakeWorkerFilesLoadedCapability.resolve();
//#endif
//#if PRODUCTION && !SINGLE_FILE
// Util.loadScript(PDFJS.workerSrc, function() {
// PDFJS.fakeWorkerFilesLoadedCapability.resolve();
// });
//#endif
}
return PDFJS.fakeWorkerFilesLoadedCapability.promise;
}
function PDFWorker(name) {
this.name = name;
this.destroyed = false; this.destroyed = false;
this.destroyCapability = null;
this.pageCache = []; this._readyCapability = createPromiseCapability();
this.pagePromises = []; this._port = null;
this.downloadInfoCapability = createPromiseCapability(); this._webWorker = null;
this._messageHandler = null;
this._initialize();
}
PDFWorker.prototype = /** @lends PDFWorker.prototype */ {
get promise() {
return this._readyCapability.promise;
},
get port() {
return this._port;
},
get messageHandler() {
return this._messageHandler;
},
_initialize: function PDFWorker_initialize() {
// If worker support isn't disabled explicit and the browser has worker // If worker support isn't disabled explicit and the browser has worker
// support, create a new web worker and test if it/the browser fullfills // support, create a new web worker and test if it/the browser fullfills
// all requirements to run parts of pdf.js in a web worker. // all requirements to run parts of pdf.js in a web worker.
// Right now, the requirement is, that an Uint8Array is still an Uint8Array // Right now, the requirement is, that an Uint8Array is still an
// as it arrives on the worker. Chrome added this with version 15. // Uint8Array as it arrives on the worker. (Chrome added this with v.15.)
//#if !SINGLE_FILE //#if !SINGLE_FILE
if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') {
var workerSrc = PDFJS.workerSrc; var workerSrc = PDFJS.workerSrc;
@ -1054,17 +1169,26 @@ var WorkerTransport = (function WorkerTransportClosure() {
var worker = new Worker(workerSrc); var worker = new Worker(workerSrc);
var messageHandler = new MessageHandler('main', 'worker', worker); var messageHandler = new MessageHandler('main', 'worker', worker);
messageHandler.on('test', function transportTest(data) { messageHandler.on('test', function PDFWorker_test(data) {
if (this.destroyed) {
this._readyCapability.reject(new Error('Worker was destroyed'));
messageHandler.destroy();
worker.terminate();
return; // worker was destroyed
}
var supportTypedArray = data && data.supportTypedArray; var supportTypedArray = data && data.supportTypedArray;
if (supportTypedArray) { if (supportTypedArray) {
this.worker = worker; this._messageHandler = messageHandler;
this._port = worker;
this._webWorker = worker;
if (!data.supportTransfers) { if (!data.supportTransfers) {
PDFJS.postMessageTransfers = false; PDFJS.postMessageTransfers = false;
} }
this.setupMainMessageHandler(messageHandler, worker); this._readyCapability.resolve();
workerInitializedCapability.resolve();
} else { } else {
this.setupFakeWorker(); this._setupFakeWorker();
messageHandler.destroy();
worker.terminate();
} }
}.bind(this)); }.bind(this));
@ -1096,7 +1220,94 @@ var WorkerTransport = (function WorkerTransportClosure() {
//#endif //#endif
// Either workers are disabled, not supported or have thrown an exception. // Either workers are disabled, not supported or have thrown an exception.
// Thus, we fallback to a faked worker. // Thus, we fallback to a faked worker.
this.setupFakeWorker(); this._setupFakeWorker();
},
_setupFakeWorker: function PDFWorker_setupFakeWorker() {
warn('Setting up fake worker.');
globalScope.PDFJS.disableWorker = true;
setupFakeWorkerGlobal().then(function () {
if (this.destroyed) {
this._readyCapability.reject(new Error('Worker was destroyed'));
return;
}
// If we don't use a worker, just post/sendMessage to the main thread.
var port = {
_listeners: [],
postMessage: function (obj) {
var e = {data: obj};
this._listeners.forEach(function (listener) {
listener.call(this, e);
}, this);
},
addEventListener: function (name, listener) {
this._listeners.push(listener);
},
removeEventListener: function (name, listener) {
var i = this._listeners.indexOf(listener);
this._listeners.splice(i, 1);
},
terminate: function () {}
};
this._port = port;
// All fake workers use the same port, making id unique.
var id = 'fake' + (nextFakeWorkerId++);
// If the main thread is our worker, setup the handling for the
// messages -- the main thread sends to it self.
var workerHandler = new MessageHandler(id + '_worker', id, port);
PDFJS.WorkerMessageHandler.setup(workerHandler, port);
var messageHandler = new MessageHandler(id, id + '_worker', port);
this._messageHandler = messageHandler;
this._readyCapability.resolve();
}.bind(this));
},
/**
* Destroys the worker instance.
*/
destroy: function PDFWorker_destroy() {
this.destroyed = true;
if (this._webWorker) {
// We need to terminate only web worker created resource.
this._webWorker.terminate();
this._webWorker = null;
}
this._port = null;
if (this._messageHandler) {
this._messageHandler.destroy();
this._messageHandler = null;
}
}
};
return PDFWorker;
})();
PDFJS.PDFWorker = PDFWorker;
/**
* For internal use only.
* @ignore
*/
var WorkerTransport = (function WorkerTransportClosure() {
function WorkerTransport(messageHandler, loadingTask, pdfDataRangeTransport) {
this.messageHandler = messageHandler;
this.loadingTask = loadingTask;
this.pdfDataRangeTransport = pdfDataRangeTransport;
this.commonObjs = new PDFObjects();
this.destroyed = false;
this.destroyCapability = null;
this.pageCache = [];
this.pagePromises = [];
this.downloadInfoCapability = createPromiseCapability();
this.setupMessageHandler();
} }
WorkerTransport.prototype = { WorkerTransport.prototype = {
destroy: function WorkerTransport_destroy() { destroy: function WorkerTransport_destroy() {
@ -1123,81 +1334,22 @@ var WorkerTransport = (function WorkerTransportClosure() {
waitOn.push(terminated); waitOn.push(terminated);
Promise.all(waitOn).then(function () { Promise.all(waitOn).then(function () {
FontLoader.clear(); FontLoader.clear();
if (self.worker) {
self.worker.terminate();
}
if (self.pdfDataRangeTransport) { if (self.pdfDataRangeTransport) {
self.pdfDataRangeTransport.abort(); self.pdfDataRangeTransport.abort();
self.pdfDataRangeTransport = null; self.pdfDataRangeTransport = null;
} }
if (self.messageHandler) {
self.messageHandler.destroy();
self.messageHandler = null; self.messageHandler = null;
}
self.destroyCapability.resolve(); self.destroyCapability.resolve();
}, this.destroyCapability.reject); }, this.destroyCapability.reject);
return this.destroyCapability.promise; return this.destroyCapability.promise;
}, },
setupFakeWorker: function WorkerTransport_setupFakeWorker() {
globalScope.PDFJS.disableWorker = true;
if (!PDFJS.fakeWorkerFilesLoadedCapability) {
PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability();
// In the developer build load worker_loader which in turn loads all the
// other files and resolves the promise. In production only the
// pdf.worker.js file is needed.
//#if !PRODUCTION
Util.loadScript(PDFJS.workerSrc);
//#endif
//#if PRODUCTION && SINGLE_FILE
// PDFJS.fakeWorkerFilesLoadedCapability.resolve();
//#endif
//#if PRODUCTION && !SINGLE_FILE
// Util.loadScript(PDFJS.workerSrc, function() {
// PDFJS.fakeWorkerFilesLoadedCapability.resolve();
// });
//#endif
}
PDFJS.fakeWorkerFilesLoadedCapability.promise.then(function () {
warn('Setting up fake worker.');
// If we don't use a worker, just post/sendMessage to the main thread.
var fakeWorker = {
_listeners: [],
postMessage: function WorkerTransport_postMessage(obj) {
var e = {data: obj};
this._listeners.forEach(function (listener) {
listener.call(this, e);
}, this);
},
addEventListener: function (name, listener) {
this._listeners.push(listener);
},
removeEventListener: function (name, listener) {
var i = this._listeners.indexOf(listener);
this._listeners.splice(i, 1);
},
terminate: function WorkerTransport_terminate() {}
};
var messageHandler = new MessageHandler('main', 'worker', fakeWorker);
this.setupMainMessageHandler(messageHandler, fakeWorker);
// If the main thread is our worker, setup the handling for the messages
// the main thread sends to it self.
var workerHandler = new MessageHandler('worker', 'main', fakeWorker);
PDFJS.WorkerMessageHandler.setup(workerHandler, fakeWorker);
this.workerInitializedCapability.resolve();
}.bind(this));
},
setupMainMessageHandler:
function WorkerTransport_setupMainMessageHandler(messageHandler, port) {
this.mainMessageHandler = messageHandler;
this.port = port;
},
setupMessageHandler: setupMessageHandler:
function WorkerTransport_setupMessageHandler(messageHandler) { function WorkerTransport_setupMessageHandler() {
this.messageHandler = messageHandler; var messageHandler = this.messageHandler;
function updatePassword(password) { function updatePassword(password) {
messageHandler.send('UpdatePassword', password); messageHandler.send('UpdatePassword', password);
@ -1462,45 +1614,6 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, this); }, this);
}, },
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;
source.disableStream = PDFJS.disableStream;
source.chunkedViewerLoading = !!this.pdfDataRangeTransport;
if (this.pdfDataRangeTransport) {
source.length = this.pdfDataRangeTransport.length;
source.initialData = this.pdfDataRangeTransport.initialData;
}
var docId = 'doc';
this.mainMessageHandler.sendWithPromise('GetDocRequest', {
docId: docId,
source: source,
disableRange: PDFJS.disableRange,
maxImageSize: PDFJS.maxImageSize,
cMapUrl: PDFJS.cMapUrl,
cMapPacked: PDFJS.cMapPacked,
disableFontFace: PDFJS.disableFontFace,
disableCreateObjectURL: PDFJS.disableCreateObjectURL,
verbosity: PDFJS.verbosity
}).then(function (workerId) {
if (this.destroyed) {
loadingTask._capability.reject(new Error('Loading aborted'));
this.destroyCapability.resolve();
return;
}
var messageHandler = new MessageHandler(docId, workerId, this.port);
this.setupMessageHandler(messageHandler);
}.bind(this), loadingTask._capability.reject);
},
getData: function WorkerTransport_getData() { getData: function WorkerTransport_getData() {
return this.messageHandler.sendWithPromise('GetData', null); return this.messageHandler.sendWithPromise('GetData', null);
}, },

70
test/unit/api_spec.js

@ -77,8 +77,7 @@ describe('api', function() {
var loadingTask = PDFJS.getDocument(basicApiUrl); var loadingTask = PDFJS.getDocument(basicApiUrl);
// This can be somewhat random -- we cannot guarantee perfect // This can be somewhat random -- we cannot guarantee perfect
// 'Terminate' message to the worker before/after setting up pdfManager. // 'Terminate' message to the worker before/after setting up pdfManager.
var destroyed = loadingTask._transport.workerInitializedCapability. var destroyed = loadingTask._worker.promise.then(function () {
promise.then(function () {
return loadingTask.destroy(); return loadingTask.destroy();
}); });
waitsForPromiseResolved(destroyed, function (data) { waitsForPromiseResolved(destroyed, function (data) {
@ -208,6 +207,73 @@ describe('api', function() {
}); });
}); });
}); });
describe('PDFWorker', function() {
it('worker created or destroyed', function () {
var worker = new PDFJS.PDFWorker('test1');
waitsForPromiseResolved(worker.promise, function () {
expect(worker.name).toEqual('test1');
expect(!!worker.port).toEqual(true);
expect(worker.destroyed).toEqual(false);
expect(!!worker._webWorker).toEqual(true);
expect(worker.port === worker._webWorker).toEqual(true);
worker.destroy();
expect(!!worker.port).toEqual(false);
expect(worker.destroyed).toEqual(true);
});
});
it('worker created or destroyed by getDocument', function () {
var loadingTask = PDFJS.getDocument(basicApiUrl);
var worker;
waitsForPromiseResolved(loadingTask.promise, function () {
worker = loadingTask._worker;
expect(!!worker).toEqual(true);
});
var destroyPromise = loadingTask.promise.then(function () {
return loadingTask.destroy();
});
waitsForPromiseResolved(destroyPromise, function () {
var destroyedWorker = loadingTask._worker;
expect(!!destroyedWorker).toEqual(false);
expect(worker.destroyed).toEqual(true);
});
});
it('worker created and can be used in getDocument', function () {
var worker = new PDFJS.PDFWorker('test1');
var loadingTask = PDFJS.getDocument({url: basicApiUrl, worker: worker});
waitsForPromiseResolved(loadingTask.promise, function () {
var docWorker = loadingTask._worker;
expect(!!docWorker).toEqual(false);
// checking is the same port is used in the MessageHandler
var messageHandlerPort = loadingTask._transport.messageHandler.comObj;
expect(messageHandlerPort === worker.port).toEqual(true);
});
var destroyPromise = loadingTask.promise.then(function () {
return loadingTask.destroy();
});
waitsForPromiseResolved(destroyPromise, function () {
expect(worker.destroyed).toEqual(false);
worker.destroy();
});
});
it('creates more than one worker', function () {
var worker1 = new PDFJS.PDFWorker('test1');
var worker2 = new PDFJS.PDFWorker('test2');
var worker3 = new PDFJS.PDFWorker('test3');
var ready = Promise.all([worker1.promise, worker2.promise,
worker3.promise]);
waitsForPromiseResolved(ready, function () {
expect(worker1.port !== worker2.port &&
worker1.port !== worker3.port &&
worker2.port !== worker3.port).toEqual(true);
worker1.destroy();
worker2.destroy();
worker3.destroy();
});
});
});
describe('PDFDocument', function() { describe('PDFDocument', function() {
var promise = PDFJS.getDocument(basicApiUrl); var promise = PDFJS.getDocument(basicApiUrl);
var doc; var doc;

Loading…
Cancel
Save