From 6dae2677d5fabe3116065c6ada5e50aec451dcba Mon Sep 17 00:00:00 2001 From: Ross Johnson Date: Tue, 12 Jan 2021 15:21:19 +0100 Subject: [PATCH] [api-minor] Highlight search results correctly for normalized text (PR 9448) This patch is a rebased *and* refactored version of PR 9448, such that it applies cleanly given that `PDFFindController` has changed since that PR was opened; obviously keeping the original author information intact. This patch will thus ensure that e.g. fractions, and other things that we normalize before searching, will still be highlighted correctly in the textLayer. Furthermore, this patch also adds basic unit-tests for this functionality. *Note:* The `[api-minor]` tag is added, since third-party implementations of the `PDFFindController` must now always use the `pageMatchesLength` property to get accurate length information (see the `web/text_layer_builder.js` changes). Co-authored-by: Ross Johnson Co-authored-by: Jonas Jenwald --- test/pdfs/.gitignore | 1 + test/pdfs/fraction-highlight.pdf | Bin 0 -> 34393 bytes test/unit/clitests_helper.js | 3 + test/unit/pdf_find_controller_spec.js | 226 ++++++++++++++++---------- web/pdf_find_controller.js | 85 ++++++++-- web/text_layer_builder.js | 11 +- 6 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 test/pdfs/fraction-highlight.pdf diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index bc70eeb44..a827ae72c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -6,6 +6,7 @@ !TrueType_without_cmap.pdf !franz.pdf !franz_2.pdf +!fraction-highlight.pdf !german-umlaut-r.pdf !xref_command_missing.pdf !issue1155r.pdf diff --git a/test/pdfs/fraction-highlight.pdf b/test/pdfs/fraction-highlight.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09b7c474a25ea03731e763880d826815e996143e GIT binary patch literal 34393 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 literal 0 HcmV?d00001 diff --git a/test/unit/clitests_helper.js b/test/unit/clitests_helper.js index 398d6a336..981af09a4 100644 --- a/test/unit/clitests_helper.js +++ b/test/unit/clitests_helper.js @@ -18,6 +18,9 @@ import { isNodeJS } from "../../src/shared/is_node.js"; import { PDFNodeStream } from "../../src/display/node_stream.js"; import { setPDFNetworkStreamFactory } from "../../src/display/api.js"; +// Sets longer timeout, similar to `jasmine-boot.js`. +jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + // Ensure that this script only runs in Node.js environments. if (!isNodeJS) { throw new Error( diff --git a/test/unit/pdf_find_controller_spec.js b/test/unit/pdf_find_controller_spec.js index f4ae3a35b..1b97f47e4 100644 --- a/test/unit/pdf_find_controller_spec.js +++ b/test/unit/pdf_find_controller_spec.js @@ -19,6 +19,8 @@ import { getDocument } from "../../src/display/api.js"; import { PDFFindController } from "../../web/pdf_find_controller.js"; import { SimpleLinkService } from "../../web/pdf_link_service.js"; +const tracemonkeyFileName = "tracemonkey.pdf"; + class MockLinkService extends SimpleLinkService { constructor() { super(); @@ -44,89 +46,102 @@ class MockLinkService extends SimpleLinkService { } } -describe("pdf_find_controller", function () { - let eventBus; - let pdfFindController; +async function initPdfFindController(filename) { + const loadingTask = getDocument( + buildGetDocumentParams(filename || tracemonkeyFileName) + ); + const pdfDocument = await loadingTask.promise; - beforeEach(function (done) { - const loadingTask = getDocument(buildGetDocumentParams("tracemonkey.pdf")); - loadingTask.promise.then(function (pdfDocument) { - eventBus = new EventBus(); + const eventBus = new EventBus(); - const linkService = new MockLinkService(); - linkService.setDocument(pdfDocument); + const linkService = new MockLinkService(); + linkService.setDocument(pdfDocument); - pdfFindController = new PDFFindController({ - linkService, - eventBus, - }); - pdfFindController.setDocument(pdfDocument); // Enable searching. - - done(); - }); + const pdfFindController = new PDFFindController({ + linkService, + eventBus, }); + pdfFindController.setDocument(pdfDocument); // Enable searching. - afterEach(function () { - eventBus = null; - pdfFindController = null; - }); + return { eventBus, pdfFindController }; +} - function testSearch({ parameters, matchesPerPage, selectedMatch }) { - return new Promise(function (resolve) { - pdfFindController.executeCommand("find", parameters); +function testSearch({ + eventBus, + pdfFindController, + parameters, + matchesPerPage, + selectedMatch, + pageMatches = null, + pageMatchesLength = null, +}) { + return new Promise(function (resolve) { + pdfFindController.executeCommand("find", parameters); - // The `updatefindmatchescount` event is only emitted if the page contains - // at least one match for the query, so the last non-zero item in the - // matches per page array corresponds to the page for which the final - // `updatefindmatchescount` event is emitted. If this happens, we know - // that any subsequent pages won't trigger the event anymore and we - // can start comparing the matches per page. This logic is necessary - // because we call the `pdfFindController.pageMatches` getter directly - // after receiving the event and the underlying `_pageMatches` array - // is only extended when a page is processed, so it will only contain - // entries for the pages processed until the time when the final event - // was emitted. - let totalPages = matchesPerPage.length; - for (let i = totalPages - 1; i >= 0; i--) { - if (matchesPerPage[i] > 0) { - totalPages = i + 1; - break; - } + // The `updatefindmatchescount` event is only emitted if the page contains + // at least one match for the query, so the last non-zero item in the + // matches per page array corresponds to the page for which the final + // `updatefindmatchescount` event is emitted. If this happens, we know + // that any subsequent pages won't trigger the event anymore and we + // can start comparing the matches per page. This logic is necessary + // because we call the `pdfFindController.pageMatches` getter directly + // after receiving the event and the underlying `_pageMatches` array + // is only extended when a page is processed, so it will only contain + // entries for the pages processed until the time when the final event + // was emitted. + let totalPages = matchesPerPage.length; + for (let i = totalPages - 1; i >= 0; i--) { + if (matchesPerPage[i] > 0) { + totalPages = i + 1; + break; } + } - const totalMatches = matchesPerPage.reduce((a, b) => { - return a + b; - }); - - eventBus.on( - "updatefindmatchescount", - function onUpdateFindMatchesCount(evt) { - if (pdfFindController.pageMatches.length !== totalPages) { - return; - } - eventBus.off("updatefindmatchescount", onUpdateFindMatchesCount); - - expect(evt.matchesCount.total).toBe(totalMatches); - for (let i = 0; i < totalPages; i++) { - expect(pdfFindController.pageMatches[i].length).toEqual( - matchesPerPage[i] - ); - } - expect(pdfFindController.selected.pageIdx).toEqual( - selectedMatch.pageIndex - ); - expect(pdfFindController.selected.matchIdx).toEqual( - selectedMatch.matchIndex - ); - - resolve(); - } - ); + const totalMatches = matchesPerPage.reduce((a, b) => { + return a + b; }); - } - it("performs a normal search", function (done) { - testSearch({ + eventBus.on( + "updatefindmatchescount", + function onUpdateFindMatchesCount(evt) { + if (pdfFindController.pageMatches.length !== totalPages) { + return; + } + eventBus.off("updatefindmatchescount", onUpdateFindMatchesCount); + + expect(evt.matchesCount.total).toBe(totalMatches); + for (let i = 0; i < totalPages; i++) { + expect(pdfFindController.pageMatches[i].length).toEqual( + matchesPerPage[i] + ); + } + expect(pdfFindController.selected.pageIdx).toEqual( + selectedMatch.pageIndex + ); + expect(pdfFindController.selected.matchIdx).toEqual( + selectedMatch.matchIndex + ); + + if (pageMatches) { + expect(pdfFindController.pageMatches).toEqual(pageMatches); + expect(pdfFindController.pageMatchesLength).toEqual( + pageMatchesLength + ); + } + + resolve(); + } + ); + }); +} + +describe("pdf_find_controller", function () { + it("performs a normal search", async function () { + const { eventBus, pdfFindController } = await initPdfFindController(); + + await testSearch({ + eventBus, + pdfFindController, parameters: { query: "Dynamic", caseSensitive: false, @@ -139,14 +154,18 @@ describe("pdf_find_controller", function () { pageIndex: 0, matchIndex: 0, }, - }).then(done); + }); }); - it("performs a normal search and finds the previous result", function (done) { + it("performs a normal search and finds the previous result", async function () { // Page 14 (with page index 13) contains five results. By default, the // first result (match index 0) is selected, so the previous result // should be the fifth result (match index 4). - testSearch({ + const { eventBus, pdfFindController } = await initPdfFindController(); + + await testSearch({ + eventBus, + pdfFindController, parameters: { query: "conference", caseSensitive: false, @@ -159,11 +178,15 @@ describe("pdf_find_controller", function () { pageIndex: 13, matchIndex: 4, }, - }).then(done); + }); }); - it("performs a case sensitive search", function (done) { - testSearch({ + it("performs a case sensitive search", async function () { + const { eventBus, pdfFindController } = await initPdfFindController(); + + await testSearch({ + eventBus, + pdfFindController, parameters: { query: "Dynamic", caseSensitive: true, @@ -176,13 +199,17 @@ describe("pdf_find_controller", function () { pageIndex: 0, matchIndex: 0, }, - }).then(done); + }); }); - it("performs an entire word search", function (done) { + it("performs an entire word search", async function () { // Page 13 contains both 'Government' and 'Governmental', so the latter // should not be found with entire word search. - testSearch({ + const { eventBus, pdfFindController } = await initPdfFindController(); + + await testSearch({ + eventBus, + pdfFindController, parameters: { query: "Government", caseSensitive: false, @@ -195,13 +222,17 @@ describe("pdf_find_controller", function () { pageIndex: 12, matchIndex: 0, }, - }).then(done); + }); }); - it("performs a multiple term (no phrase) search", function (done) { + it("performs a multiple term (no phrase) search", async function () { // Page 9 contains 'alternate' and pages 6 and 9 contain 'solution'. // Both should be found for multiple term (no phrase) search. - testSearch({ + const { eventBus, pdfFindController } = await initPdfFindController(); + + await testSearch({ + eventBus, + pdfFindController, parameters: { query: "alternate solution", caseSensitive: false, @@ -214,6 +245,31 @@ describe("pdf_find_controller", function () { pageIndex: 5, matchIndex: 0, }, - }).then(done); + }); + }); + + it("performs a normal search, where the text is normalized", async function () { + const { eventBus, pdfFindController } = await initPdfFindController( + "fraction-highlight.pdf" + ); + + await testSearch({ + eventBus, + pdfFindController, + parameters: { + query: "fraction", + caseSensitive: false, + entireWord: false, + phraseSearch: true, + findPrevious: false, + }, + matchesPerPage: [3], + selectedMatch: { + pageIndex: 0, + matchIndex: 0, + }, + pageMatches: [[19, 48, 66]], + pageMatchesLength: [[8, 8, 8]], + }); }); }); diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index a306b7e84..100dee201 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -49,9 +49,40 @@ function normalize(text) { const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); normalizationRegex = new RegExp(`[${replace}]`, "g"); } - return text.replace(normalizationRegex, function (ch) { - return CHARACTERS_TO_NORMALIZE[ch]; + 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 ||= []).push([index, diff]); + } + return normalizedCh; }); + + return [normalizedText, diffs]; +} + +// Determine the original, non-normalized, match index such that highlighting of +// search results is correct in the `textLayer` for strings containing e.g. "½" +// characters; essentially "inverting" the result of the `normalize` function. +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; } /** @@ -215,6 +246,7 @@ class PDFFindController { }; this._extractTextPromises = []; this._pageContents = []; // Stores the normalized text for each page. + this._pageDiffs = []; this._matchesCountTotal = 0; this._pagesToSearch = null; this._pendingFindMatches = Object.create(null); @@ -232,7 +264,7 @@ class PDFFindController { get _query() { if (this._state.query !== this._rawQuery) { this._rawQuery = this._state.query; - this._normalizedQuery = normalize(this._state.query); + [this._normalizedQuery] = normalize(this._state.query); } return this._normalizedQuery; } @@ -349,8 +381,9 @@ class PDFFindController { return true; } - _calculatePhraseMatch(query, pageIndex, pageContent, entireWord) { - const matches = []; + _calculatePhraseMatch(query, pageIndex, pageContent, pageDiffs, entireWord) { + const matches = [], + matchesLength = []; const queryLen = query.length; let matchIdx = -queryLen; @@ -362,12 +395,19 @@ class PDFFindController { if (entireWord && !this._isEntireWord(pageContent, matchIdx, queryLen)) { continue; } - matches.push(matchIdx); + 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, entireWord) { + _calculateWordMatch(query, pageIndex, pageContent, pageDiffs, entireWord) { const matchesWithLength = []; // Divide the query into pieces and search for text in each piece. @@ -388,10 +428,15 @@ class PDFFindController { ) { continue; } + const originalMatchIdx = getOriginalIndex(matchIdx, pageDiffs), + matchEnd = matchIdx + subqueryLen - 1, + originalQueryLen = + getOriginalIndex(matchEnd, pageDiffs) - originalMatchIdx + 1; + // Other searches do not, so we store the length. matchesWithLength.push({ - match: matchIdx, - matchLength: subqueryLen, + match: originalMatchIdx, + matchLength: originalQueryLen, skipped: false, }); } @@ -412,6 +457,7 @@ class PDFFindController { _calculateMatch(pageIndex) { let pageContent = this._pageContents[pageIndex]; + const pageDiffs = this._pageDiffs[pageIndex]; let query = this._query; const { caseSensitive, entireWord, phraseSearch } = this._state; @@ -426,9 +472,21 @@ class PDFFindController { } if (phraseSearch) { - this._calculatePhraseMatch(query, pageIndex, pageContent, entireWord); + this._calculatePhraseMatch( + query, + pageIndex, + pageContent, + pageDiffs, + entireWord + ); } else { - this._calculateWordMatch(query, pageIndex, pageContent, entireWord); + this._calculateWordMatch( + query, + pageIndex, + pageContent, + pageDiffs, + entireWord + ); } // When `highlightAll` is set, ensure that the matches on previously @@ -478,7 +536,9 @@ class PDFFindController { } // Store the normalized page content (text items) as one string. - this._pageContents[i] = normalize(strBuf.join("")); + [this._pageContents[i], this._pageDiffs[i]] = normalize( + strBuf.join("") + ); extractTextCapability.resolve(i); }, reason => { @@ -488,6 +548,7 @@ class PDFFindController { ); // Page error -- assuming no text content. this._pageContents[i] = ""; + this._pageDiffs[i] = null; extractTextCapability.resolve(i); } ); diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 183241b60..f2a59edf7 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -161,12 +161,11 @@ class TextLayerBuilder { if (!matches) { return []; } - const { findController, textContentItemsStr } = this; + const { textContentItemsStr } = this; let i = 0, iIndex = 0; const end = textContentItemsStr.length - 1; - const queryLen = findController.state.query.length; const result = []; for (let m = 0, mm = matches.length; m < mm; m++) { @@ -191,13 +190,7 @@ class TextLayerBuilder { }; // Calculate the end position. - if (matchesLength) { - // Multiterm search. - matchIdx += matchesLength[m]; - } else { - // Phrase search. - matchIdx += queryLen; - } + matchIdx += matchesLength[m]; // Somewhat the same array as above, but use > instead of >= to get // the end position right.