From b215af30d3ad06c60b98af035fe12c1cd90abf69 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Oct 2014 15:56:40 +0200 Subject: [PATCH 1/2] Require destinations when they are needed and do not fetch all of them in advance --- src/core/obj.js | 40 ++++++++++++++++++++++++++++++++++++++++ src/core/worker.js | 6 ++++++ src/display/api.js | 14 ++++++++++++++ test/unit/api_spec.js | 7 +++++++ web/viewer.js | 29 +++++++++++++---------------- 5 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/core/obj.js b/src/core/obj.js index f1de50f59..cc2e36dd6 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -457,6 +457,46 @@ var Catalog = (function CatalogClosure() { } return shadow(this, 'destinations', dests); }, + getDestination: function Catalog_getDestination(destinationId) { + function fetchDestination(dest) { + return isDict(dest) ? dest.get('D') : dest; + } + + var xref = this.xref; + var dest, nameTreeRef, nameDictionaryRef; + var obj = this.catDict.get('Names'); + if (obj && obj.has('Dests')) { + nameTreeRef = obj.getRaw('Dests'); + } else if (this.catDict.has('Dests')) { + nameDictionaryRef = this.catDict.get('Dests'); + } + + if (nameDictionaryRef) { + // reading simple destination dictionary + obj = nameDictionaryRef; + obj.forEach(function catalogForEach(key, value) { + if (!value) { + return; + } + if (key === destinationId) { + dest = fetchDestination(value); + } + }); + } + if (nameTreeRef) { + var nameTree = new NameTree(nameTreeRef, xref); + var names = nameTree.getAll(); + for (var name in names) { + if (!names.hasOwnProperty(name)) { + continue; + } + if (name === destinationId) { + dest = fetchDestination(names[name]); + } + } + } + return dest; + }, get attachments() { var xref = this.xref; var attachments = null, nameTreeRef; diff --git a/src/core/worker.js b/src/core/worker.js index d4deb0c8c..40d7d899e 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -325,6 +325,12 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } ); + handler.on('GetDestination', + function wphSetupGetDestination(data) { + return pdfManager.ensureCatalog('getDestination', [ data.id ]); + } + ); + handler.on('GetAttachments', function wphSetupGetAttachments(data) { return pdfManager.ensureCatalog('attachments'); diff --git a/src/display/api.js b/src/display/api.js index b20472231..8fc68c2ef 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -293,10 +293,20 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { /** * @return {Promise} A promise that is resolved with a lookup table for * mapping named destinations to reference numbers. + * + * This can be slow for large documents: use getDestination instead */ getDestinations: function PDFDocumentProxy_getDestinations() { return this.transport.getDestinations(); }, + /** + * @param {string} id The named destination to get. + * @return {Promise} A promise that is resolved with all information + * of the given named destination. + */ + getDestination: function PDFDocumentProxy_getDestination(id) { + return this.transport.getDestination(id); + }, /** * @return {Promise} A promise that is resolved with a lookup table for * mapping named attachments to their content. @@ -1128,6 +1138,10 @@ var WorkerTransport = (function WorkerTransportClosure() { return this.messageHandler.sendWithPromise('GetDestinations', null); }, + getDestination: function WorkerTransport_getDestination(id) { + return this.messageHandler.sendWithPromise('GetDestination', { id: id } ); + }, + getAttachments: function WorkerTransport_getAttachments() { return this.messageHandler.sendWithPromise('GetAttachments', null); }, diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index a8ba8382d..0044764e2 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -123,6 +123,13 @@ describe('api', function() { 0, 841.89, null] }); }); }); + it('gets a destination', function() { + var promise = doc.getDestination('chapter1'); + waitsForPromiseResolved(promise, function(data) { + expect(data).toEqual([{ gen: 0, num: 17 }, { name: 'XYZ' }, + 0, 841.89, null]); + }); + }); it('gets attachments', function() { var promise = doc.getAttachments(); waitsForPromiseResolved(promise, function (data) { diff --git a/web/viewer.js b/web/viewer.js index d4b449df9..95614693d 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -675,15 +675,19 @@ var PDFViewerApplication = { } }; - this.destinationsPromise.then(function() { - if (typeof dest === 'string') { - destString = dest; - dest = self.destinations[dest]; - } - if (!(dest instanceof Array)) { + var destinationPromise; + if (typeof dest === 'string') { + destString = dest; + destinationPromise = this.pdfDocument.getDestination(dest); + } else { + destinationPromise = Promise.resolve(dest); + } + destinationPromise.then(function(destination) { + dest = destination; + if (!(destination instanceof Array)) { return; // invalid destination } - goToDestination(dest[0]); + goToDestination(destination[0]); }); }, @@ -984,15 +988,8 @@ var PDFViewerApplication = { } }); - var destinationsPromise = - this.destinationsPromise = pdfDocument.getDestinations(); - destinationsPromise.then(function(destinations) { - self.destinations = destinations; - }); - - // outline depends on destinations and pagesRefMap - var promises = [pagesPromise, destinationsPromise, - this.animationStartedPromise]; + // outline depends on pagesRefMap + var promises = [pagesPromise, this.animationStartedPromise]; Promise.all(promises).then(function() { pdfDocument.getOutline().then(function(outline) { var outlineView = document.getElementById('outlineView'); From aaa1f2cb11826bc0070cad79400a874c59e6046f Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Oct 2014 17:34:49 +0200 Subject: [PATCH 2/2] Implemented NameTree.get() using binary search --- src/core/obj.js | 80 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/src/core/obj.js b/src/core/obj.js index cc2e36dd6..babfdaf8a 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -485,15 +485,7 @@ var Catalog = (function CatalogClosure() { } if (nameTreeRef) { var nameTree = new NameTree(nameTreeRef, xref); - var names = nameTree.getAll(); - for (var name in names) { - if (!names.hasOwnProperty(name)) { - continue; - } - if (name === destinationId) { - dest = fetchDestination(names[name]); - } - } + dest = fetchDestination(nameTree.get(destinationId)); } return dest; }, @@ -1398,6 +1390,76 @@ var NameTree = (function NameTreeClosure() { } } return dict; + }, + + get: function NameTree_get(destinationId) { + if (!this.root) { + return null; + } + + var xref = this.xref; + var kidsOrNames = xref.fetchIfRef(this.root); + var loopCount = 0; + var MAX_NAMES_LEVELS = 10; + var l, r, m; + + // Perform a binary search to quickly find the entry that + // contains the named destination we are looking for. + while (kidsOrNames.has('Kids')) { + loopCount++; + if (loopCount > MAX_NAMES_LEVELS) { + warn('Search depth limit for named destionations has been reached.'); + return null; + } + + var kids = kidsOrNames.get('Kids'); + if (!isArray(kids)) { + return null; + } + + l = 0; + r = kids.length - 1; + while (l <= r) { + m = (l + r) >> 1; + var kid = xref.fetchIfRef(kids[m]); + var limits = kid.get('Limits'); + + if (destinationId < limits[0]) { + r = m - 1; + } else if (destinationId > limits[1]) { + l = m + 1; + } else { + kidsOrNames = xref.fetchIfRef(kids[m]); + break; + } + } + if (l > r) { + return null; + } + } + + // If we get here, then we have found the right entry. Now + // go through the named destinations in the Named dictionary + // until we find the exact destination we're looking for. + var names = kidsOrNames.get('Names'); + if (isArray(names)) { + // Perform a binary search to reduce the lookup time. + l = 0; + r = names.length - 2; + while (l <= r) { + // Check only even indices (0, 2, 4, ...) because the + // odd indices contain the actual D array. + m = (l + r) & ~1; + if (destinationId < names[m]) { + r = m - 2; + } else if (destinationId > names[m]) { + l = m + 2; + } else { + return xref.fetchIfRef(names[m + 1]); + } + } + } + return null; } }; return NameTree;