|
@@ -1495,6 +1495,412 @@ exports.binarySearchFirstItem = binarySearchFirstItem;
|
|
|
}));
|
|
|
|
|
|
|
|
|
+(function (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsWebPDFFindController = {}), root.pdfjsWebUIUtils);
|
|
|
+ }
|
|
|
+}(this, function (exports, uiUtils, firefoxCom) {
|
|
|
+
|
|
|
+var scrollIntoView = uiUtils.scrollIntoView;
|
|
|
+
|
|
|
+var FindStates = {
|
|
|
+ FIND_FOUND: 0,
|
|
|
+ FIND_NOTFOUND: 1,
|
|
|
+ FIND_WRAPPED: 2,
|
|
|
+ FIND_PENDING: 3
|
|
|
+};
|
|
|
+
|
|
|
+var FIND_SCROLL_OFFSET_TOP = -50;
|
|
|
+var FIND_SCROLL_OFFSET_LEFT = -400;
|
|
|
+
|
|
|
+var CHARACTERS_TO_NORMALIZE = {
|
|
|
+ '\u2018': '\'', // Left single quotation mark
|
|
|
+ '\u2019': '\'', // Right single quotation mark
|
|
|
+ '\u201A': '\'', // Single low-9 quotation mark
|
|
|
+ '\u201B': '\'', // Single high-reversed-9 quotation mark
|
|
|
+ '\u201C': '"', // Left double quotation mark
|
|
|
+ '\u201D': '"', // Right double quotation mark
|
|
|
+ '\u201E': '"', // Double low-9 quotation mark
|
|
|
+ '\u201F': '"', // Double high-reversed-9 quotation mark
|
|
|
+ '\u00BC': '1/4', // Vulgar fraction one quarter
|
|
|
+ '\u00BD': '1/2', // Vulgar fraction one half
|
|
|
+ '\u00BE': '3/4', // Vulgar fraction three quarters
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * Provides "search" or "find" functionality for the PDF.
|
|
|
+ * This object actually performs the search for a given string.
|
|
|
+ */
|
|
|
+var PDFFindController = (function PDFFindControllerClosure() {
|
|
|
+ function PDFFindController(options) {
|
|
|
+ this.pdfViewer = options.pdfViewer || null;
|
|
|
+
|
|
|
+ this.onUpdateResultsCount = null;
|
|
|
+ this.onUpdateState = null;
|
|
|
+
|
|
|
+ this.reset();
|
|
|
+
|
|
|
+ // Compile the regular expression for text normalization once.
|
|
|
+ var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join('');
|
|
|
+ this.normalizationRegex = new RegExp('[' + replace + ']', 'g');
|
|
|
+ }
|
|
|
+
|
|
|
+ PDFFindController.prototype = {
|
|
|
+ listenWindowEvents: function PDFFindController_listenWindowEvents() {
|
|
|
+ var events = [
|
|
|
+ 'find',
|
|
|
+ 'findagain',
|
|
|
+ 'findhighlightallchange',
|
|
|
+ 'findcasesensitivitychange'
|
|
|
+ ];
|
|
|
+ var handleEvent = function (e) {
|
|
|
+ this.executeCommand(e.type, e.detail);
|
|
|
+ }.bind(this);
|
|
|
+
|
|
|
+ for (var i = 0, len = events.length; i < len; i++) {
|
|
|
+ window.addEventListener(events[i], handleEvent);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ reset: function PDFFindController_reset() {
|
|
|
+ this.startedTextExtraction = false;
|
|
|
+ this.extractTextPromises = [];
|
|
|
+ this.pendingFindMatches = Object.create(null);
|
|
|
+ this.active = false; // If active, find results will be highlighted.
|
|
|
+ this.pageContents = []; // Stores the text for each page.
|
|
|
+ this.pageMatches = [];
|
|
|
+ this.matchCount = 0;
|
|
|
+ this.selected = { // Currently selected match.
|
|
|
+ pageIdx: -1,
|
|
|
+ matchIdx: -1
|
|
|
+ };
|
|
|
+ this.offset = { // Where the find algorithm currently is in the document.
|
|
|
+ pageIdx: null,
|
|
|
+ matchIdx: null
|
|
|
+ };
|
|
|
+ this.pagesToSearch = null;
|
|
|
+ this.resumePageIdx = null;
|
|
|
+ this.state = null;
|
|
|
+ this.dirtyMatch = false;
|
|
|
+ this.findTimeout = null;
|
|
|
+
|
|
|
+ this.firstPagePromise = new Promise(function (resolve) {
|
|
|
+ this.resolveFirstPage = resolve;
|
|
|
+ }.bind(this));
|
|
|
+ },
|
|
|
+
|
|
|
+ normalize: function PDFFindController_normalize(text) {
|
|
|
+ return text.replace(this.normalizationRegex, function (ch) {
|
|
|
+ return CHARACTERS_TO_NORMALIZE[ch];
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) {
|
|
|
+ var pageContent = this.normalize(this.pageContents[pageIndex]);
|
|
|
+ var query = this.normalize(this.state.query);
|
|
|
+ var caseSensitive = this.state.caseSensitive;
|
|
|
+ var queryLen = query.length;
|
|
|
+
|
|
|
+ if (queryLen === 0) {
|
|
|
+ // Do nothing: the matches should be wiped out already.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!caseSensitive) {
|
|
|
+ pageContent = pageContent.toLowerCase();
|
|
|
+ query = query.toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ var matches = [];
|
|
|
+ var matchIdx = -queryLen;
|
|
|
+ while (true) {
|
|
|
+ matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
|
|
|
+ if (matchIdx === -1) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ matches.push(matchIdx);
|
|
|
+ }
|
|
|
+ this.pageMatches[pageIndex] = matches;
|
|
|
+ this.updatePage(pageIndex);
|
|
|
+ if (this.resumePageIdx === pageIndex) {
|
|
|
+ this.resumePageIdx = null;
|
|
|
+ this.nextPageMatch();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update the matches count
|
|
|
+ if (matches.length > 0) {
|
|
|
+ this.matchCount += matches.length;
|
|
|
+ this.updateUIResultsCount();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ extractText: function PDFFindController_extractText() {
|
|
|
+ if (this.startedTextExtraction) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.startedTextExtraction = true;
|
|
|
+
|
|
|
+ this.pageContents = [];
|
|
|
+ var extractTextPromisesResolves = [];
|
|
|
+ var numPages = this.pdfViewer.pagesCount;
|
|
|
+ for (var i = 0; i < numPages; i++) {
|
|
|
+ this.extractTextPromises.push(new Promise(function (resolve) {
|
|
|
+ extractTextPromisesResolves.push(resolve);
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ var self = this;
|
|
|
+ function extractPageText(pageIndex) {
|
|
|
+ self.pdfViewer.getPageTextContent(pageIndex).then(
|
|
|
+ function textContentResolved(textContent) {
|
|
|
+ var textItems = textContent.items;
|
|
|
+ var str = [];
|
|
|
+
|
|
|
+ for (var i = 0, len = textItems.length; i < len; i++) {
|
|
|
+ str.push(textItems[i].str);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Store the pageContent as a string.
|
|
|
+ self.pageContents.push(str.join(''));
|
|
|
+
|
|
|
+ extractTextPromisesResolves[pageIndex](pageIndex);
|
|
|
+ if ((pageIndex + 1) < self.pdfViewer.pagesCount) {
|
|
|
+ extractPageText(pageIndex + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ extractPageText(0);
|
|
|
+ },
|
|
|
+
|
|
|
+ executeCommand: function PDFFindController_executeCommand(cmd, state) {
|
|
|
+ if (this.state === null || cmd !== 'findagain') {
|
|
|
+ this.dirtyMatch = true;
|
|
|
+ }
|
|
|
+ this.state = state;
|
|
|
+ this.updateUIState(FindStates.FIND_PENDING);
|
|
|
+
|
|
|
+ this.firstPagePromise.then(function() {
|
|
|
+ this.extractText();
|
|
|
+
|
|
|
+ clearTimeout(this.findTimeout);
|
|
|
+ if (cmd === 'find') {
|
|
|
+ // Only trigger the find action after 250ms of silence.
|
|
|
+ this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
|
|
|
+ } else {
|
|
|
+ this.nextMatch();
|
|
|
+ }
|
|
|
+ }.bind(this));
|
|
|
+ },
|
|
|
+
|
|
|
+ updatePage: function PDFFindController_updatePage(index) {
|
|
|
+ if (this.selected.pageIdx === index) {
|
|
|
+ // If the page is selected, scroll the page into view, which triggers
|
|
|
+ // rendering the page, which adds the textLayer. Once the textLayer is
|
|
|
+ // build, it will scroll onto the selected match.
|
|
|
+ this.pdfViewer.scrollPageIntoView(index + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ var page = this.pdfViewer.getPageView(index);
|
|
|
+ if (page.textLayer) {
|
|
|
+ page.textLayer.updateMatches();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ nextMatch: function PDFFindController_nextMatch() {
|
|
|
+ var previous = this.state.findPrevious;
|
|
|
+ var currentPageIndex = this.pdfViewer.currentPageNumber - 1;
|
|
|
+ var numPages = this.pdfViewer.pagesCount;
|
|
|
+
|
|
|
+ this.active = true;
|
|
|
+
|
|
|
+ if (this.dirtyMatch) {
|
|
|
+ // Need to recalculate the matches, reset everything.
|
|
|
+ this.dirtyMatch = false;
|
|
|
+ this.selected.pageIdx = this.selected.matchIdx = -1;
|
|
|
+ this.offset.pageIdx = currentPageIndex;
|
|
|
+ this.offset.matchIdx = null;
|
|
|
+ this.hadMatch = false;
|
|
|
+ this.resumePageIdx = null;
|
|
|
+ this.pageMatches = [];
|
|
|
+ this.matchCount = 0;
|
|
|
+ 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.
|
|
|
+ if (!(i in this.pendingFindMatches)) {
|
|
|
+ this.pendingFindMatches[i] = true;
|
|
|
+ this.extractTextPromises[i].then(function(pageIdx) {
|
|
|
+ delete self.pendingFindMatches[pageIdx];
|
|
|
+ self.calcFindMatch(pageIdx);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If there's no query there's no point in searching.
|
|
|
+ if (this.state.query === '') {
|
|
|
+ this.updateUIState(FindStates.FIND_FOUND);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we're waiting on a page, we return since we can't do anything else.
|
|
|
+ if (this.resumePageIdx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var offset = this.offset;
|
|
|
+ // Keep track of how many pages we should maximally iterate through.
|
|
|
+ this.pagesToSearch = numPages;
|
|
|
+ // 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();
|
|
|
+ },
|
|
|
+
|
|
|
+ matchesReady: function PDFFindController_matchesReady(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);
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ // No matches, so attempt to search the next page.
|
|
|
+ this.advanceOffsetPage(previous);
|
|
|
+ if (offset.wrapped) {
|
|
|
+ offset.matchIdx = null;
|
|
|
+ if (this.pagesToSearch < 0) {
|
|
|
+ // No point in wrapping again, there were no matches.
|
|
|
+ this.updateMatch(false);
|
|
|
+ // while matches were not found, searching for a page
|
|
|
+ // with matches should nevertheless halt.
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Matches were not found (and searching is not done).
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The method is called back from the text layer when match presentation
|
|
|
+ * is updated.
|
|
|
+ * @param {number} pageIndex - page index.
|
|
|
+ * @param {number} index - match index.
|
|
|
+ * @param {Array} elements - text layer div elements array.
|
|
|
+ * @param {number} beginIdx - start index of the div array for the match.
|
|
|
+ * @param {number} endIdx - end index of the div array for the match.
|
|
|
+ */
|
|
|
+ updateMatchPosition: function PDFFindController_updateMatchPosition(
|
|
|
+ pageIndex, index, elements, beginIdx, endIdx) {
|
|
|
+ if (this.selected.matchIdx === index &&
|
|
|
+ this.selected.pageIdx === pageIndex) {
|
|
|
+ var spot = {
|
|
|
+ top: FIND_SCROLL_OFFSET_TOP,
|
|
|
+ left: FIND_SCROLL_OFFSET_LEFT
|
|
|
+ };
|
|
|
+ scrollIntoView(elements[beginIdx], spot,
|
|
|
+ /* skipOverflowHiddenElements = */ true);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ nextPageMatch: function PDFFindController_nextPageMatch() {
|
|
|
+ if (this.resumePageIdx !== null) {
|
|
|
+ console.error('There can only be one pending page.');
|
|
|
+ }
|
|
|
+ do {
|
|
|
+ var pageIdx = this.offset.pageIdx;
|
|
|
+ var matches = this.pageMatches[pageIdx];
|
|
|
+ if (!matches) {
|
|
|
+ // The matches don't exist yet for processing by "matchesReady",
|
|
|
+ // so set a resume point for when they do exist.
|
|
|
+ this.resumePageIdx = pageIdx;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } while (!this.matchesReady(matches));
|
|
|
+ },
|
|
|
+
|
|
|
+ advanceOffsetPage: function PDFFindController_advanceOffsetPage(previous) {
|
|
|
+ var offset = this.offset;
|
|
|
+ var numPages = this.extractTextPromises.length;
|
|
|
+ offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1);
|
|
|
+ offset.matchIdx = null;
|
|
|
+
|
|
|
+ this.pagesToSearch--;
|
|
|
+
|
|
|
+ if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
|
|
|
+ offset.pageIdx = (previous ? numPages - 1 : 0);
|
|
|
+ offset.wrapped = true;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMatch: function PDFFindController_updateMatch(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);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ updateUIResultsCount:
|
|
|
+ function PDFFindController_updateUIResultsCount() {
|
|
|
+ if (this.onUpdateResultsCount) {
|
|
|
+ this.onUpdateResultsCount(this.matchCount);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ updateUIState: function PDFFindController_updateUIState(state, previous) {
|
|
|
+ if (this.onUpdateState) {
|
|
|
+ this.onUpdateState(state, previous, this.matchCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return PDFFindController;
|
|
|
+})();
|
|
|
+
|
|
|
+exports.FindStates = FindStates;
|
|
|
+exports.PDFFindController = PDFFindController;
|
|
|
+}));
|
|
|
+
|
|
|
+
|
|
|
(function (root, factory) {
|
|
|
{
|
|
|
factory((root.pdfjsWebPDFLinkService = {}), root.pdfjsWebUIUtils);
|
|
@@ -3325,6 +3731,8 @@ exports.PDFViewer = PDFViewer;
|
|
|
PDFJS.DefaultAnnotationLayerFactory =
|
|
|
pdfViewerLibs.pdfjsWebAnnotationLayerBuilder.DefaultAnnotationLayerFactory;
|
|
|
PDFJS.PDFHistory = pdfViewerLibs.pdfjsWebPDFHistory.PDFHistory;
|
|
|
+ PDFJS.PDFFindController =
|
|
|
+ pdfViewerLibs.pdfjsWebPDFFindController.PDFFindController;
|
|
|
|
|
|
PDFJS.DownloadManager = pdfViewerLibs.pdfjsWebDownloadManager.DownloadManager;
|
|
|
PDFJS.ProgressBar = pdfViewerLibs.pdfjsWebUIUtils.ProgressBar;
|