/** * @licstart The following is the entire license notice for the * Javascript code in this page * * Copyright 2020 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @licend The above is the entire license notice for the * Javascript code in this page */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PDFFindController = exports.FindState = void 0; var _pdf = require("../pdf"); var _pdf_find_utils = require("./pdf_find_utils.js"); var _ui_utils = require("./ui_utils.js"); const FindState = { FOUND: 0, NOT_FOUND: 1, WRAPPED: 2, PENDING: 3 }; exports.FindState = FindState; const FIND_TIMEOUT = 250; const MATCH_SCROLL_OFFSET_TOP = -50; const MATCH_SCROLL_OFFSET_LEFT = -400; const CHARACTERS_TO_NORMALIZE = { "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"', "\u00BC": "1/4", "\u00BD": "1/2", "\u00BE": "3/4" }; let normalizationRegex = null; function normalize(text) { if (!normalizationRegex) { const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); normalizationRegex = new RegExp(`[${replace}]`, "g"); } let diffs = null; const normalizedText = text.replace(normalizationRegex, function (ch, index) { const normalizedCh = CHARACTERS_TO_NORMALIZE[ch], diff = normalizedCh.length - ch.length; if (diff !== 0) { (diffs || (diffs = [])).push([index, diff]); } return normalizedCh; }); return [normalizedText, diffs]; } function getOriginalIndex(matchIndex, diffs = null) { if (!diffs) { return matchIndex; } let totalDiff = 0; for (const [index, diff] of diffs) { const currentIndex = index + totalDiff; if (currentIndex >= matchIndex) { break; } if (currentIndex + diff > matchIndex) { totalDiff += matchIndex - currentIndex; break; } totalDiff += diff; } return matchIndex - totalDiff; } class PDFFindController { constructor({ linkService, eventBus }) { this._linkService = linkService; this._eventBus = eventBus; this._reset(); eventBus._on("findbarclose", this._onFindBarClose.bind(this)); } get highlightMatches() { return this._highlightMatches; } get pageMatches() { return this._pageMatches; } get pageMatchesLength() { return this._pageMatchesLength; } get selected() { return this._selected; } get state() { return this._state; } setDocument(pdfDocument) { if (this._pdfDocument) { this._reset(); } if (!pdfDocument) { return; } this._pdfDocument = pdfDocument; this._firstPageCapability.resolve(); } executeCommand(cmd, state) { if (!state) { return; } const pdfDocument = this._pdfDocument; if (this._state === null || this._shouldDirtyMatch(cmd, state)) { this._dirtyMatch = true; } this._state = state; if (cmd !== "findhighlightallchange") { this._updateUIState(FindState.PENDING); } this._firstPageCapability.promise.then(() => { if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { return; } this._extractText(); const findbarClosed = !this._highlightMatches; const pendingTimeout = !!this._findTimeout; if (this._findTimeout) { clearTimeout(this._findTimeout); this._findTimeout = null; } if (cmd === "find") { this._findTimeout = setTimeout(() => { this._nextMatch(); this._findTimeout = null; }, FIND_TIMEOUT); } else if (this._dirtyMatch) { this._nextMatch(); } else if (cmd === "findagain") { this._nextMatch(); if (findbarClosed && this._state.highlightAll) { this._updateAllPages(); } } else if (cmd === "findhighlightallchange") { if (pendingTimeout) { this._nextMatch(); } else { this._highlightMatches = true; } this._updateAllPages(); } else { this._nextMatch(); } }); } scrollMatchIntoView({ element = null, pageIndex = -1, matchIndex = -1 }) { if (!this._scrollMatches || !element) { return; } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { return; } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) { return; } this._scrollMatches = false; const spot = { top: MATCH_SCROLL_OFFSET_TOP, left: MATCH_SCROLL_OFFSET_LEFT }; (0, _ui_utils.scrollIntoView)(element, spot, true); } _reset() { this._highlightMatches = false; this._scrollMatches = false; this._pdfDocument = null; this._pageMatches = []; this._pageMatchesLength = []; this._state = null; this._selected = { pageIdx: -1, matchIdx: -1 }; this._offset = { pageIdx: null, matchIdx: null, wrapped: false }; this._extractTextPromises = []; this._pageContents = []; this._pageDiffs = []; this._matchesCountTotal = 0; this._pagesToSearch = null; this._pendingFindMatches = Object.create(null); this._resumePageIdx = null; this._dirtyMatch = false; clearTimeout(this._findTimeout); this._findTimeout = null; this._firstPageCapability = (0, _pdf.createPromiseCapability)(); } get _query() { if (this._state.query !== this._rawQuery) { this._rawQuery = this._state.query; [this._normalizedQuery] = normalize(this._state.query); } return this._normalizedQuery; } _shouldDirtyMatch(cmd, state) { if (state.query !== this._state.query) { return true; } switch (cmd) { case "findagain": const pageNumber = this._selected.pageIdx + 1; const linkService = this._linkService; if (pageNumber >= 1 && pageNumber <= linkService.pagesCount && pageNumber !== linkService.page && !linkService.isPageVisible(pageNumber)) { return true; } return false; case "findhighlightallchange": return false; } return true; } _prepareMatches(matchesWithLength, matches, matchesLength) { function isSubTerm(currentIndex) { const currentElem = matchesWithLength[currentIndex]; const nextElem = matchesWithLength[currentIndex + 1]; if (currentIndex < matchesWithLength.length - 1 && currentElem.match === nextElem.match) { currentElem.skipped = true; return true; } for (let i = currentIndex - 1; i >= 0; i--) { const prevElem = matchesWithLength[i]; if (prevElem.skipped) { continue; } if (prevElem.match + prevElem.matchLength < currentElem.match) { break; } if (prevElem.match + prevElem.matchLength >= currentElem.match + currentElem.matchLength) { currentElem.skipped = true; return true; } } return false; } matchesWithLength.sort(function (a, b) { return a.match === b.match ? a.matchLength - b.matchLength : a.match - b.match; }); for (let i = 0, len = matchesWithLength.length; i < len; i++) { if (isSubTerm(i)) { continue; } matches.push(matchesWithLength[i].match); matchesLength.push(matchesWithLength[i].matchLength); } } _isEntireWord(content, startIdx, length) { if (startIdx > 0) { const first = content.charCodeAt(startIdx); const limit = content.charCodeAt(startIdx - 1); if ((0, _pdf_find_utils.getCharacterType)(first) === (0, _pdf_find_utils.getCharacterType)(limit)) { return false; } } const endIdx = startIdx + length - 1; if (endIdx < content.length - 1) { const last = content.charCodeAt(endIdx); const limit = content.charCodeAt(endIdx + 1); if ((0, _pdf_find_utils.getCharacterType)(last) === (0, _pdf_find_utils.getCharacterType)(limit)) { return false; } } return true; } _calculatePhraseMatch(query, pageIndex, pageContent, pageDiffs, entireWord) { const matches = [], matchesLength = []; const queryLen = query.length; let matchIdx = -queryLen; while (true) { matchIdx = pageContent.indexOf(query, matchIdx + queryLen); if (matchIdx === -1) { break; } if (entireWord && !this._isEntireWord(pageContent, matchIdx, queryLen)) { continue; } const originalMatchIdx = getOriginalIndex(matchIdx, pageDiffs), matchEnd = matchIdx + queryLen - 1, originalQueryLen = getOriginalIndex(matchEnd, pageDiffs) - originalMatchIdx + 1; matches.push(originalMatchIdx); matchesLength.push(originalQueryLen); } this._pageMatches[pageIndex] = matches; this._pageMatchesLength[pageIndex] = matchesLength; } _calculateWordMatch(query, pageIndex, pageContent, pageDiffs, entireWord) { const matchesWithLength = []; const queryArray = query.match(/\S+/g); for (let i = 0, len = queryArray.length; i < len; i++) { const subquery = queryArray[i]; const subqueryLen = subquery.length; let matchIdx = -subqueryLen; while (true) { matchIdx = pageContent.indexOf(subquery, matchIdx + subqueryLen); if (matchIdx === -1) { break; } if (entireWord && !this._isEntireWord(pageContent, matchIdx, subqueryLen)) { continue; } const originalMatchIdx = getOriginalIndex(matchIdx, pageDiffs), matchEnd = matchIdx + subqueryLen - 1, originalQueryLen = getOriginalIndex(matchEnd, pageDiffs) - originalMatchIdx + 1; matchesWithLength.push({ match: originalMatchIdx, matchLength: originalQueryLen, skipped: false }); } } this._pageMatchesLength[pageIndex] = []; this._pageMatches[pageIndex] = []; this._prepareMatches(matchesWithLength, this._pageMatches[pageIndex], this._pageMatchesLength[pageIndex]); } _calculateMatch(pageIndex) { let pageContent = this._pageContents[pageIndex]; const pageDiffs = this._pageDiffs[pageIndex]; let query = this._query; const { caseSensitive, entireWord, phraseSearch } = this._state; if (query.length === 0) { return; } if (!caseSensitive) { pageContent = pageContent.toLowerCase(); query = query.toLowerCase(); } if (phraseSearch) { this._calculatePhraseMatch(query, pageIndex, pageContent, pageDiffs, entireWord); } else { this._calculateWordMatch(query, pageIndex, pageContent, pageDiffs, entireWord); } if (this._state.highlightAll) { this._updatePage(pageIndex); } if (this._resumePageIdx === pageIndex) { this._resumePageIdx = null; this._nextPageMatch(); } const pageMatchesCount = this._pageMatches[pageIndex].length; if (pageMatchesCount > 0) { this._matchesCountTotal += pageMatchesCount; this._updateUIResultsCount(); } } _extractText() { if (this._extractTextPromises.length > 0) { return; } let promise = Promise.resolve(); for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { const extractTextCapability = (0, _pdf.createPromiseCapability)(); this._extractTextPromises[i] = extractTextCapability.promise; promise = promise.then(() => { return this._pdfDocument.getPage(i + 1).then(pdfPage => { return pdfPage.getTextContent({ normalizeWhitespace: true }); }).then(textContent => { const textItems = textContent.items; const strBuf = []; for (let j = 0, jj = textItems.length; j < jj; j++) { strBuf.push(textItems[j].str); } [this._pageContents[i], this._pageDiffs[i]] = normalize(strBuf.join("")); extractTextCapability.resolve(i); }, reason => { console.error(`Unable to get text content for page ${i + 1}`, reason); this._pageContents[i] = ""; this._pageDiffs[i] = null; extractTextCapability.resolve(i); }); }); } } _updatePage(index) { if (this._scrollMatches && this._selected.pageIdx === index) { this._linkService.page = index + 1; } this._eventBus.dispatch("updatetextlayermatches", { source: this, pageIndex: index }); } _updateAllPages() { this._eventBus.dispatch("updatetextlayermatches", { source: this, pageIndex: -1 }); } _nextMatch() { const previous = this._state.findPrevious; const currentPageIndex = this._linkService.page - 1; const numPages = this._linkService.pagesCount; this._highlightMatches = true; if (this._dirtyMatch) { this._dirtyMatch = false; this._selected.pageIdx = this._selected.matchIdx = -1; this._offset.pageIdx = currentPageIndex; this._offset.matchIdx = null; this._offset.wrapped = false; this._resumePageIdx = null; this._pageMatches.length = 0; this._pageMatchesLength.length = 0; this._matchesCountTotal = 0; this._updateAllPages(); for (let i = 0; i < numPages; i++) { if (this._pendingFindMatches[i] === true) { continue; } this._pendingFindMatches[i] = true; this._extractTextPromises[i].then(pageIdx => { delete this._pendingFindMatches[pageIdx]; this._calculateMatch(pageIdx); }); } } if (this._query === "") { this._updateUIState(FindState.FOUND); return; } if (this._resumePageIdx) { return; } const offset = this._offset; this._pagesToSearch = numPages; if (offset.matchIdx !== null) { const numPageMatches = this._pageMatches[offset.pageIdx].length; if (!previous && offset.matchIdx + 1 < numPageMatches || previous && offset.matchIdx > 0) { offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; this._updateMatch(true); return; } this._advanceOffsetPage(previous); } this._nextPageMatch(); } _matchesReady(matches) { const offset = this._offset; const numMatches = matches.length; const previous = this._state.findPrevious; if (numMatches) { offset.matchIdx = previous ? numMatches - 1 : 0; this._updateMatch(true); return true; } this._advanceOffsetPage(previous); if (offset.wrapped) { offset.matchIdx = null; if (this._pagesToSearch < 0) { this._updateMatch(false); return true; } } return false; } _nextPageMatch() { if (this._resumePageIdx !== null) { console.error("There can only be one pending page."); } let matches = null; do { const pageIdx = this._offset.pageIdx; matches = this._pageMatches[pageIdx]; if (!matches) { this._resumePageIdx = pageIdx; break; } } while (!this._matchesReady(matches)); } _advanceOffsetPage(previous) { const offset = this._offset; const numPages = this._linkService.pagesCount; 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(found = false) { let state = FindState.NOT_FOUND; const wrapped = this._offset.wrapped; this._offset.wrapped = false; if (found) { const previousPage = this._selected.pageIdx; this._selected.pageIdx = this._offset.pageIdx; this._selected.matchIdx = this._offset.matchIdx; state = wrapped ? FindState.WRAPPED : FindState.FOUND; if (previousPage !== -1 && previousPage !== this._selected.pageIdx) { this._updatePage(previousPage); } } this._updateUIState(state, this._state.findPrevious); if (this._selected.pageIdx !== -1) { this._scrollMatches = true; this._updatePage(this._selected.pageIdx); } } _onFindBarClose(evt) { const pdfDocument = this._pdfDocument; this._firstPageCapability.promise.then(() => { if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { return; } if (this._findTimeout) { clearTimeout(this._findTimeout); this._findTimeout = null; } if (this._resumePageIdx) { this._resumePageIdx = null; this._dirtyMatch = true; } this._updateUIState(FindState.FOUND); this._highlightMatches = false; this._updateAllPages(); }); } _requestMatchesCount() { const { pageIdx, matchIdx } = this._selected; let current = 0, total = this._matchesCountTotal; if (matchIdx !== -1) { for (let i = 0; i < pageIdx; i++) { current += this._pageMatches[i] && this._pageMatches[i].length || 0; } current += matchIdx + 1; } if (current < 1 || current > total) { current = total = 0; } return { current, total }; } _updateUIResultsCount() { this._eventBus.dispatch("updatefindmatchescount", { source: this, matchesCount: this._requestMatchesCount() }); } _updateUIState(state, previous) { this._eventBus.dispatch("updatefindcontrolstate", { source: this, state, previous, matchesCount: this._requestMatchesCount(), rawQuery: this._state ? this._state.query : null }); } } exports.PDFFindController = PDFFindController;