From 3488d756e8c9798741fedf113a6b949551a4adab Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 19 Oct 2012 16:28:17 -0700 Subject: [PATCH] Allow find to highlight matches without extracting all text. --- web/viewer.js | 276 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 112 deletions(-) diff --git a/web/viewer.js b/web/viewer.js index 7c5ff7b5f..fa885b9f1 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -230,7 +230,9 @@ var cache = new Cache(kCacheSize); var currentPageNumber = 1; var PDFFindController = { - extractTextPromise: null, + startedTextExtraction: false, + + extractTextPromises: [], // If active, find results will be highlighted. active: false, @@ -240,11 +242,22 @@ var PDFFindController = { pageMatches: [], + // Currently selected match. selected: { - pageIdx: 0, - matchIdx: 0 + pageIdx: -1, + matchIdx: -1 + }, + + // Where find algorithm currently is in the document. + offset: { + pageIdx: null, + matchIdx: null }, + resumePageIdx: null, + + resumeCallback: null, + state: null, dirtyMatch: false, @@ -266,13 +279,16 @@ var PDFFindController = { } }, - calcFindMatch: function(pageContent) { + calcFindMatch: function(pageIndex) { + var pageContent = this.pageContents[pageIndex]; var query = this.state.query; var caseSensitive = this.state.caseSensitive; var queryLen = query.length; - if (queryLen === 0) - return []; + if (queryLen === 0) { + // Do nothing the matches should be wiped out already. + return; + } if (!caseSensitive) { pageContent = pageContent.toLowerCase(); @@ -290,14 +306,26 @@ var PDFFindController = { matches.push(matchIdx); } - return matches; + this.pageMatches[pageIndex] = matches; + this.updatePage(pageIndex); + if (this.resumePageIdx === pageIndex) { + var callback = this.resumeCallback; + this.resumePageIdx = null; + this.resumeCallback = null; + callback(); + } }, extractText: function() { - if (this.extractTextPromise) { - return this.extractTextPromise; + if (this.startedTextExtraction) { + return; + } + this.startedTextExtraction = true; + + this.pageContents = []; + for (var i = 0, ii = PDFView.pdfDocument.numPages; i < ii; i++) { + this.extractTextPromises.push(new PDFJS.Promise()); } - this.extractTextPromise = new PDFJS.Promise(); var self = this; function extractPageText(pageIndex) { @@ -313,13 +341,10 @@ var PDFFindController = { // Store the pageContent as a string. self.pageContents.push(str); - // Ensure there is a empty array of matches. - self.pageMatches.push([]); + self.extractTextPromises[pageIndex].resolve(pageIndex); if ((pageIndex + 1) < PDFView.pages.length) extractPageText(pageIndex + 1); - else - self.extractTextPromise.resolve(); } ); } @@ -334,16 +359,14 @@ var PDFFindController = { this.state = e.detail; this.updateUIState(FindStates.FIND_PENDING); - var promise = this.extractText(); + this.extractText(); clearTimeout(this.findTimeout); if (e.type === 'find') { // Only trigger the find action after 250ms of silence. - this.findTimeout = setTimeout(function() { - promise.then(this.performFind.bind(this)); - }.bind(this), 250); + this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); } else { - promise.then(this.performFind.bind(this)); + this.nextMatch(); } }, @@ -362,118 +385,147 @@ var PDFFindController = { } }, - performFind: function() { - // Recalculate all the matches. - // TODO: Make one match show up as the current match - + nextMatch: function() { var pages = PDFView.pages; - var pageContents = this.pageContents; - var pageMatches = this.pageMatches; + var previous = this.state.findPrevious; + var numPages = PDFView.pages.length; this.active = true; if (this.dirtyMatch) { - // Need to recalculate the matches. + // Need to recalculate the matches, reset everything. this.dirtyMatch = false; + this.selected.pageIdx = this.selected.matchIdx = -1; + this.offset.pageIdx = previous ? numPages - 1 : 0; + this.offset.matchIdx = null; + this.hadMatch = false; + this.resumeCallback = null; + this.resumePageIdx = null; + this.pageMatches = []; + var self = this; + + for (var i = 0; i < numPages; i++) { + // Wipe out any previous highlighted matches. + this.updatePage(i); + + // As soon as the text is extracted start finding the matches. + this.extractTextPromises[i].onData(function(pageIdx) { + // Use a timeout since all the pages may already be extracted and we + // want to start highlighting before finding all the matches. + setTimeout(function() { + self.calcFindMatch(pageIdx); + }); + }); + } + } - this.selected = { - pageIdx: -1, - matchIdx: -1 - }; + // If there's no query there's no point in searching. + if (this.state.query === '') { + this.updateUIState(FindStates.FIND_FOUND); + return; + } - // TODO: Make this way more lasily (aka. efficient) - e.g. calculate only - // the matches for the current visible pages. - var firstMatch = true; - for (var i = 0; i < pageContents.length; i++) { - var matches = pageMatches[i] = this.calcFindMatch(pageContents[i]); - if (firstMatch && matches.length !== 0) { - firstMatch = false; - this.selected = { - pageIdx: i, - matchIdx: 0 - }; - } - this.updatePage(i, true); - } - if (!firstMatch || !this.state.query) { - this.updateUIState(FindStates.FIND_FOUND); - } else { - this.updateUIState(FindStates.FIND_NOTFOUND); - } - } else { - // If there is NO selection, then there is no match at all -> no sense to - // handle previous/next action. - if (this.selected.pageIdx === -1) { - this.updateUIState(FindStates.FIND_NOTFOUND); + // If we're waiting on a page, we return since we can't do anything else. + if (this.resumeCallback) { + return; + } + + var offset = this.offset; + // If there's already a matchIdx that means we are iterating through a + // page's matches. + if (offset.matchIdx !== null) { + var numPageMatches = this.pageMatches[offset.pageIdx].length; + if ((!previous && offset.matchIdx + 1 < numPageMatches) || + (previous && offset.matchIdx > 0)) { + // The simple case, we just have advance the matchIdx to select the next + // match on the page. + this.hadMatch = true; + offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; + this.updateMatch(true); return; } + // We went beyond the current page's matches, so we advance to the next + // page. + this.advanceOffsetPage(previous); + } + // Start searching through the page. + this.nextPageMatch(); + }, - // Handle findAgain case. - var previous = this.state.findPrevious; - var sPageIdx = this.selected.pageIdx; - var sMatchIdx = this.selected.matchIdx; - var findState = FindStates.FIND_FOUND; - - if (previous) { - // Select previous match. + nextPageMatch: function() { + if (this.resumePageIdx !== null) + console.error('There can only be one pending page.'); - if (sMatchIdx !== 0) { - this.selected.matchIdx -= 1; - } else { - var len = pageMatches.length; - for (var i = sPageIdx - 1; i != sPageIdx; i--) { - if (i < 0) - i += len; - - if (pageMatches[i].length !== 0) { - this.selected = { - pageIdx: i, - matchIdx: pageMatches[i].length - 1 - }; - break; - } - } - // If pageIdx stayed the same, select last match on the page. - if (this.selected.pageIdx === sPageIdx) { - this.selected.matchIdx = pageMatches[sPageIdx].length - 1; - findState = FindStates.FIND_WRAPPED; - } else if (this.selected.pageIdx > sPageIdx) { - findState = FindStates.FIND_WRAPPED; - } - } + var matchesReady = function(matches) { + var offset = this.offset; + var numMatches = matches.length; + var previous = this.state.findPrevious; + if (numMatches) { + // There were matches for the page, so initialize the matchIdx. + this.hadMatch = true; + offset.matchIdx = previous ? numMatches - 1 : 0; + this.updateMatch(true); } else { - // Select next match. - - if (pageMatches[sPageIdx].length !== sMatchIdx + 1) { - this.selected.matchIdx += 1; - } else { - var len = pageMatches.length; - for (var i = sPageIdx + 1; i < len + sPageIdx; i++) { - if (pageMatches[i % len].length !== 0) { - this.selected = { - pageIdx: i % len, - matchIdx: 0 - }; - break; - } - } - - // If pageIdx stayed the same, select first match on the page. - if (this.selected.pageIdx === sPageIdx) { - this.selected.matchIdx = 0; - findState = FindStates.FIND_WRAPPED; - } else if (this.selected.pageIdx < sPageIdx) { - findState = FindStates.FIND_WRAPPED; + // No matches attempt to search the next page. + this.advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (!this.hadMatch) { + // No point in wrapping there were no matches. + this.updateMatch(false); + return; } } + // Search the next page. + this.nextPageMatch(); } + }.bind(this); - this.updateUIState(findState, previous); - this.updatePage(sPageIdx, sPageIdx === this.selected.pageIdx); - if (sPageIdx !== this.selected.pageIdx) { - this.updatePage(this.selected.pageIdx, true); + var pageIdx = this.offset.pageIdx; + var pageMatches = this.pageMatches; + if (!pageMatches[pageIdx]) { + // The matches aren't ready setup a callback so we can be notified, + // when they are ready. + this.resumeCallback = function() { + matchesReady(pageMatches[pageIdx]); + }; + this.resumePageIdx = pageIdx; + return; + } + // The matches are finished already. + matchesReady(pageMatches[pageIdx]); + }, + + advanceOffsetPage: function(previous) { + var offset = this.offset; + var numPages = this.extractTextPromises.length; + offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; + offset.matchIdx = null; + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = previous ? numPages - 1 : 0; + offset.wrapped = true; + return; + } + }, + + updateMatch: function(found) { + var state = FindStates.FIND_NOTFOUND; + var wrapped = this.offset.wrapped; + this.offset.wrapped = false; + if (found) { + var previousPage = this.selected.pageIdx; + this.selected.pageIdx = this.offset.pageIdx; + this.selected.matchIdx = this.offset.matchIdx; + state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND; + // Update the currently selected page to wipe out any selected matches. + if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { + this.updatePage(previousPage); } } + this.updateUIState(state, this.state.findPrevious); + if (this.selected.pageIdx !== -1) { + this.updatePage(this.selected.pageIdx, true); + } }, updateUIState: function(state, previous) {