Merge pull request #13908 from brendandahl/xfa-find

[api-minor] XFA - Support text search in XFA documents.
This commit is contained in:
Brendan Dahl 2021-08-23 08:53:02 -07:00 committed by GitHub
commit bf5a45ce6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 531 additions and 238 deletions

View File

@ -62,6 +62,7 @@ import { MessageHandler } from "../shared/message_handler.js";
import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js";
import { PDFDataTransportStream } from "./transport_stream.js";
import { XfaText } from "./xfa_text.js";
const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
const RENDERING_CANCELLED_TIMEOUT = 100; // ms
@ -1531,6 +1532,13 @@ class PDFPageProxy {
* {@link TextContent} object that represents the page's text content.
*/
getTextContent(params = {}) {
if (this._transport._htmlForXfa) {
// TODO: We need to revisit this once the XFA foreground patch lands and
// only do this for non-foreground XFA.
return this.getXfa().then(xfa => {
return XfaText.textContent(xfa);
});
}
const readableStream = this.streamTextContent(params);
return new Promise(function (resolve, reject) {

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
import { XfaText } from "./xfa_text.js";
class XfaLayer {
static setupStorage(html, id, element, storage, intent) {
const storedData = storage.getValue(id, { value: null });
@ -127,6 +129,9 @@ class XfaLayer {
// Set defaults.
rootDiv.setAttribute("class", "xfaLayer xfaFont");
// Text nodes used for the text highlighter.
const textDivs = [];
while (stack.length > 0) {
const [parent, i, html] = stack[stack.length - 1];
if (i + 1 === parent.children.length) {
@ -141,7 +146,9 @@ class XfaLayer {
const { name } = child;
if (name === "#text") {
html.appendChild(document.createTextNode(child.value));
const node = document.createTextNode(child.value);
textDivs.push(node);
html.appendChild(node);
continue;
}
@ -160,7 +167,11 @@ class XfaLayer {
if (child.children && child.children.length > 0) {
stack.push([child, -1, childHtml]);
} else if (child.value) {
childHtml.appendChild(document.createTextNode(child.value));
const node = document.createTextNode(child.value);
if (XfaText.shouldBuildText(name)) {
textDivs.push(node);
}
childHtml.appendChild(node);
}
}
@ -185,6 +196,10 @@ class XfaLayer {
)) {
el.setAttribute("readOnly", true);
}
return {
textDivs,
};
}
/**

79
src/display/xfa_text.js Normal file
View File

@ -0,0 +1,79 @@
/* Copyright 2021 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.
*/
class XfaText {
/**
* Walk an XFA tree and create an array of text nodes that is compatible
* with a regular PDFs TextContent. Currently, only TextItem.str is supported,
* all other fields and styles haven't been implemented.
*
* @param {Object} xfa - An XFA fake DOM object.
*
* @returns {TextContent}
*/
static textContent(xfa) {
const items = [];
const output = {
items,
styles: Object.create(null),
};
function walk(node) {
if (!node) {
return;
}
let str = null;
const name = node.name;
if (name === "#text") {
str = node.value;
} else if (!XfaText.shouldBuildText(name)) {
return;
} else if (node?.attributes?.textContent) {
str = node.attributes.textContent;
} else if (node.value) {
str = node.value;
}
if (str !== null) {
items.push({
str,
});
}
if (!node.children) {
return;
}
for (const child of node.children) {
walk(child);
}
}
walk(xfa);
return output;
}
/**
* @param {string} name - DOM node name. (lower case)
*
* @returns {boolean} true if the DOM node should have a corresponding text
* node.
*/
static shouldBuildText(name) {
return !(
name === "textarea" ||
name === "input" ||
name === "option" ||
name === "select"
);
}
}
export { XfaText };

View File

@ -72,4 +72,37 @@ describe("find bar", () => {
);
});
});
describe("highlight all", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("xfa_imm5257e.pdf#zoom=100", ".xfaLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must search xfa correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewFind");
await page.waitForSelector("#viewFind", { hidden: false });
await page.type("#findInput", "city");
await page.waitForSelector("#findInput[data-status='']");
await page.waitForSelector(".xfaLayer .highlight");
const resultElement = await page.waitForSelector("#findResultsCount");
const resultText = await resultElement.evaluate(el => el.textContent);
expect(resultText).toEqual("1 of 7 matches");
const selectedElement = await page.waitForSelector(
".highlight.selected"
);
const selectedText = await selectedElement.evaluate(
el => el.textContent
);
expect(selectedText).toEqual("City");
})
);
});
});
});

View File

@ -42,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
import { PDFPageView } from "./pdf_page_view.js";
import { SimpleLinkService } from "./pdf_link_service.js";
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
import { TextHighlighter } from "./text_highlighter.js";
import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
@ -525,7 +526,9 @@ class BaseViewer {
const scale = this.currentScale;
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
const textLayerFactory =
this.textLayerMode !== TextLayerMode.DISABLE ? this : null;
this.textLayerMode !== TextLayerMode.DISABLE && !isPureXfa
? this
: null;
const xfaLayerFactory = isPureXfa ? this : null;
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
@ -541,6 +544,7 @@ class BaseViewer {
textLayerMode: this.textLayerMode,
annotationLayerFactory: this,
xfaLayerFactory,
textHighlighterFactory: this,
structTreeLayerFactory: this,
imageResourcesPath: this.imageResourcesPath,
renderInteractiveForms: this.renderInteractiveForms,
@ -1242,6 +1246,7 @@ class BaseViewer {
* @param {PageViewport} viewport
* @param {boolean} enhanceTextSelection
* @param {EventBus} eventBus
* @param {TextHighlighter} highlighter
* @returns {TextLayerBuilder}
*/
createTextLayerBuilder(
@ -1249,17 +1254,31 @@ class BaseViewer {
pageIndex,
viewport,
enhanceTextSelection = false,
eventBus
eventBus,
highlighter
) {
return new TextLayerBuilder({
textLayerDiv,
eventBus,
pageIndex,
viewport,
findController: this.isInPresentationMode ? null : this.findController,
enhanceTextSelection: this.isInPresentationMode
? false
: enhanceTextSelection,
highlighter,
});
}
/**
* @param {number} pageIndex
* @param {EventBus} eventBus
* @returns {TextHighlighter}
*/
createTextHighlighter(pageIndex, eventBus) {
return new TextHighlighter({
eventBus,
pageIndex,
findController: this.isInPresentationMode ? null : this.findController,
});
}

View File

@ -162,6 +162,7 @@ class IPDFTextLayerFactory {
* @param {PageViewport} viewport
* @param {boolean} enhanceTextSelection
* @param {EventBus} eventBus
* @param {TextHighlighter} highlighter
* @returns {TextLayerBuilder}
*/
createTextLayerBuilder(
@ -169,7 +170,8 @@ class IPDFTextLayerFactory {
pageIndex,
viewport,
enhanceTextSelection = false,
eventBus
eventBus,
highlighter
) {}
}

View File

@ -101,6 +101,11 @@ class PDFPageView {
this.textLayerFactory = options.textLayerFactory;
this.annotationLayerFactory = options.annotationLayerFactory;
this.xfaLayerFactory = options.xfaLayerFactory;
this.textHighlighter =
options.textHighlighterFactory?.createTextHighlighter(
this.id - 1,
this.eventBus
);
this.structTreeLayerFactory = options.structTreeLayerFactory;
this.renderer = options.renderer || RendererType.CANVAS;
this.l10n = options.l10n || NullL10n;
@ -175,7 +180,10 @@ class PDFPageView {
async _renderXfaLayer() {
let error = null;
try {
await this.xfaLayer.render(this.viewport, "display");
const result = await this.xfaLayer.render(this.viewport, "display");
if (this.textHighlighter) {
this._buildXfaTextContentItems(result.textDivs);
}
} catch (ex) {
error = ex;
} finally {
@ -187,6 +195,16 @@ class PDFPageView {
}
}
async _buildXfaTextContentItems(textDivs) {
const text = await this.pdfPage.getTextContent();
const items = [];
for (const item of text.items) {
items.push(item.str);
}
this.textHighlighter.setTextMapping(textDivs, items);
this.textHighlighter.enable();
}
/**
* @private
*/
@ -382,6 +400,7 @@ class PDFPageView {
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel();
this.xfaLayer = null;
this.textHighlighter?.disable();
}
if (this._onTextLayerRendered) {
this.eventBus._off("textlayerrendered", this._onTextLayerRendered);
@ -533,7 +552,8 @@ class PDFPageView {
this.id - 1,
this.viewport,
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE,
this.eventBus
this.eventBus,
this.textHighlighter
);
}
this.textLayer = textLayer;

293
web/text_highlighter.js Normal file
View File

@ -0,0 +1,293 @@
/* Copyright 2021 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.
*/
/**
* @typedef {Object} TextHighlighter
* @property {PDFFindController} findController
* @property {EventBus} eventBus - The application event bus.
* @property {number} pageIndex - The page index.
*/
/**
* TextHighlighter handles highlighting matches from the FindController in
* either the text layer or XFA layer depending on the type of document.
*/
class TextHighlighter {
constructor({ findController, eventBus, pageIndex }) {
this.findController = findController;
this.matches = [];
this.eventBus = eventBus;
this.pageIdx = pageIndex;
this._onUpdateTextLayerMatches = null;
this.textDivs = null;
this.textContentItemsStr = null;
this.enabled = false;
}
/**
* Store two arrays that will map DOM nodes to text they should contain.
* The arrays should be of equal length and the array element at each index
* should correspond to the other. e.g.
* `items[0] = "<span>Item 0</span>" and texts[0] = "Item 0";
*
* @param {Array<Node>} divs
* @param {Array<string>} texts
*/
setTextMapping(divs, texts) {
this.textDivs = divs;
this.textContentItemsStr = texts;
}
/**
* Start listening for events to update the highlighter and check if there are
* any current matches that need be highlighted.
*/
enable() {
if (!this.textDivs || !this.textContentItemsStr) {
throw new Error("Text divs and strings have not been set.");
}
if (this.enabled) {
throw new Error("TextHighlighter is already enabled.");
}
this.enabled = true;
if (!this._onUpdateTextLayerMatches) {
this._onUpdateTextLayerMatches = evt => {
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
this._updateMatches();
}
};
this.eventBus._on(
"updatetextlayermatches",
this._onUpdateTextLayerMatches
);
}
this._updateMatches();
}
disable() {
if (!this.enabled) {
return;
}
this.enabled = false;
if (this._onUpdateTextLayerMatches) {
this.eventBus._off(
"updatetextlayermatches",
this._onUpdateTextLayerMatches
);
this._onUpdateTextLayerMatches = null;
}
}
_convertMatches(matches, matchesLength) {
// Early exit if there is nothing to convert.
if (!matches) {
return [];
}
const { textContentItemsStr } = this;
let i = 0,
iIndex = 0;
const end = textContentItemsStr.length - 1;
const result = [];
for (let m = 0, mm = matches.length; m < mm; m++) {
// Calculate the start position.
let matchIdx = matches[m];
// Loop over the divIdxs.
while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
iIndex += textContentItemsStr[i].length;
i++;
}
if (i === textContentItemsStr.length) {
console.error("Could not find a matching mapping");
}
const match = {
begin: {
divIdx: i,
offset: matchIdx - iIndex,
},
};
// Calculate the end position.
matchIdx += matchesLength[m];
// Somewhat the same array as above, but use > instead of >= to get
// the end position right.
while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
iIndex += textContentItemsStr[i].length;
i++;
}
match.end = {
divIdx: i,
offset: matchIdx - iIndex,
};
result.push(match);
}
return result;
}
_renderMatches(matches) {
// Early exit if there is nothing to render.
if (matches.length === 0) {
return;
}
const { findController, pageIdx } = this;
const { textContentItemsStr, textDivs } = this;
const isSelectedPage = pageIdx === findController.selected.pageIdx;
const selectedMatchIdx = findController.selected.matchIdx;
const highlightAll = findController.state.highlightAll;
let prevEnd = null;
const infinity = {
divIdx: -1,
offset: undefined,
};
function beginText(begin, className) {
const divIdx = begin.divIdx;
textDivs[divIdx].textContent = "";
return appendTextToDiv(divIdx, 0, begin.offset, className);
}
function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
let div = textDivs[divIdx];
if (div.nodeType === 3) {
const span = document.createElement("span");
div.parentNode.insertBefore(span, div);
span.appendChild(div);
textDivs[divIdx] = span;
div = span;
}
const content = textContentItemsStr[divIdx].substring(
fromOffset,
toOffset
);
const node = document.createTextNode(content);
if (className) {
const span = document.createElement("span");
span.className = `${className} appended`;
span.appendChild(node);
div.appendChild(span);
return className.includes("selected") ? span.offsetLeft : 0;
}
div.appendChild(node);
return 0;
}
let i0 = selectedMatchIdx,
i1 = i0 + 1;
if (highlightAll) {
i0 = 0;
i1 = matches.length;
} else if (!isSelectedPage) {
// Not highlighting all and this isn't the selected page, so do nothing.
return;
}
for (let i = i0; i < i1; i++) {
const match = matches[i];
const begin = match.begin;
const end = match.end;
const isSelected = isSelectedPage && i === selectedMatchIdx;
const highlightSuffix = isSelected ? " selected" : "";
let selectedLeft = 0;
// Match inside new div.
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
// If there was a previous div, then add the text at the end.
if (prevEnd !== null) {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
}
// Clear the divs and set the content until the starting point.
beginText(begin);
} else {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
}
if (begin.divIdx === end.divIdx) {
selectedLeft = appendTextToDiv(
begin.divIdx,
begin.offset,
end.offset,
"highlight" + highlightSuffix
);
} else {
selectedLeft = appendTextToDiv(
begin.divIdx,
begin.offset,
infinity.offset,
"highlight begin" + highlightSuffix
);
for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
textDivs[n0].className = "highlight middle" + highlightSuffix;
}
beginText(end, "highlight end" + highlightSuffix);
}
prevEnd = end;
if (isSelected) {
// Attempt to scroll the selected match into view.
findController.scrollMatchIntoView({
element: textDivs[begin.divIdx],
selectedLeft,
pageIndex: pageIdx,
matchIndex: selectedMatchIdx,
});
}
}
if (prevEnd) {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
}
}
_updateMatches() {
if (!this.enabled) {
return;
}
const { findController, matches, pageIdx } = this;
const { textContentItemsStr, textDivs } = this;
let clearedUntilDivIdx = -1;
// Clear all current matches.
for (let i = 0, ii = matches.length; i < ii; i++) {
const match = matches[i];
const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
for (let n = begin, end = match.end.divIdx; n <= end; n++) {
const div = textDivs[n];
div.textContent = textContentItemsStr[n];
div.className = "";
}
clearedUntilDivIdx = match.end.divIdx + 1;
}
if (!findController?.highlightMatches) {
return;
}
// Convert the matches on the `findController` into the match format
// used for the textLayer.
const pageMatches = findController.pageMatches[pageIdx] || null;
const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
this.matches = this._convertMatches(pageMatches, pageMatchesLength);
this._renderMatches(this.matches);
}
}
export { TextHighlighter };

View File

@ -23,7 +23,8 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
* @property {EventBus} eventBus - The application event bus.
* @property {number} pageIndex - The page index.
* @property {PageViewport} viewport - The viewport of the text layer.
* @property {PDFFindController} findController
* @property {TextHighlighter} highlighter - Optional object that will handle
* highlighting text from the find controller.
* @property {boolean} enhanceTextSelection - Option to turn on improved
* text selection.
*/
@ -31,8 +32,7 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
/**
* The text layer builder provides text selection functionality for the PDF.
* It does this by creating overlay divs over the PDF's text. These divs
* contain text that matches the PDF text they are overlaying. This object
* also provides a way to highlight text that is being searched for.
* contain text that matches the PDF text they are overlaying.
*/
class TextLayerBuilder {
constructor({
@ -40,7 +40,7 @@ class TextLayerBuilder {
eventBus,
pageIndex,
viewport,
findController = null,
highlighter = null,
enhanceTextSelection = false,
}) {
this.textLayerDiv = textLayerDiv;
@ -54,11 +54,10 @@ class TextLayerBuilder {
this.matches = [];
this.viewport = viewport;
this.textDivs = [];
this.findController = findController;
this.textLayerRenderTask = null;
this.highlighter = highlighter;
this.enhanceTextSelection = enhanceTextSelection;
this._onUpdateTextLayerMatches = null;
this._bindMouse();
}
@ -94,6 +93,9 @@ class TextLayerBuilder {
this.cancel();
this.textDivs = [];
if (this.highlighter) {
this.highlighter.setTextMapping(this.textDivs, this.textContentItemsStr);
}
const textLayerFrag = document.createDocumentFragment();
this.textLayerRenderTask = renderTextLayer({
textContent: this.textContent,
@ -109,24 +111,12 @@ class TextLayerBuilder {
() => {
this.textLayerDiv.appendChild(textLayerFrag);
this._finishRendering();
this._updateMatches();
this.highlighter?.enable();
},
function (reason) {
// Cancelled or failed to render text layer; skipping errors.
}
);
if (!this._onUpdateTextLayerMatches) {
this._onUpdateTextLayerMatches = evt => {
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
this._updateMatches();
}
};
this.eventBus._on(
"updatetextlayermatches",
this._onUpdateTextLayerMatches
);
}
}
/**
@ -137,13 +127,7 @@ class TextLayerBuilder {
this.textLayerRenderTask.cancel();
this.textLayerRenderTask = null;
}
if (this._onUpdateTextLayerMatches) {
this.eventBus._off(
"updatetextlayermatches",
this._onUpdateTextLayerMatches
);
this._onUpdateTextLayerMatches = null;
}
this.highlighter?.disable();
}
setTextContentStream(readableStream) {
@ -156,198 +140,6 @@ class TextLayerBuilder {
this.textContent = textContent;
}
_convertMatches(matches, matchesLength) {
// Early exit if there is nothing to convert.
if (!matches) {
return [];
}
const { textContentItemsStr } = this;
let i = 0,
iIndex = 0;
const end = textContentItemsStr.length - 1;
const result = [];
for (let m = 0, mm = matches.length; m < mm; m++) {
// Calculate the start position.
let matchIdx = matches[m];
// Loop over the divIdxs.
while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
iIndex += textContentItemsStr[i].length;
i++;
}
if (i === textContentItemsStr.length) {
console.error("Could not find a matching mapping");
}
const match = {
begin: {
divIdx: i,
offset: matchIdx - iIndex,
},
};
// Calculate the end position.
matchIdx += matchesLength[m];
// Somewhat the same array as above, but use > instead of >= to get
// the end position right.
while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
iIndex += textContentItemsStr[i].length;
i++;
}
match.end = {
divIdx: i,
offset: matchIdx - iIndex,
};
result.push(match);
}
return result;
}
_renderMatches(matches) {
// Early exit if there is nothing to render.
if (matches.length === 0) {
return;
}
const { findController, pageIdx, textContentItemsStr, textDivs } = this;
const isSelectedPage = pageIdx === findController.selected.pageIdx;
const selectedMatchIdx = findController.selected.matchIdx;
const highlightAll = findController.state.highlightAll;
let prevEnd = null;
const infinity = {
divIdx: -1,
offset: undefined,
};
function beginText(begin, className) {
const divIdx = begin.divIdx;
textDivs[divIdx].textContent = "";
return appendTextToDiv(divIdx, 0, begin.offset, className);
}
function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
const div = textDivs[divIdx];
const content = textContentItemsStr[divIdx].substring(
fromOffset,
toOffset
);
const node = document.createTextNode(content);
if (className) {
const span = document.createElement("span");
span.className = `${className} appended`;
span.appendChild(node);
div.appendChild(span);
return className.includes("selected") ? span.offsetLeft : 0;
}
div.appendChild(node);
return 0;
}
let i0 = selectedMatchIdx,
i1 = i0 + 1;
if (highlightAll) {
i0 = 0;
i1 = matches.length;
} else if (!isSelectedPage) {
// Not highlighting all and this isn't the selected page, so do nothing.
return;
}
for (let i = i0; i < i1; i++) {
const match = matches[i];
const begin = match.begin;
const end = match.end;
const isSelected = isSelectedPage && i === selectedMatchIdx;
const highlightSuffix = isSelected ? " selected" : "";
let selectedLeft = 0;
// Match inside new div.
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
// If there was a previous div, then add the text at the end.
if (prevEnd !== null) {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
}
// Clear the divs and set the content until the starting point.
beginText(begin);
} else {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
}
if (begin.divIdx === end.divIdx) {
selectedLeft = appendTextToDiv(
begin.divIdx,
begin.offset,
end.offset,
"highlight" + highlightSuffix
);
} else {
selectedLeft = appendTextToDiv(
begin.divIdx,
begin.offset,
infinity.offset,
"highlight begin" + highlightSuffix
);
for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
textDivs[n0].className = "highlight middle" + highlightSuffix;
}
beginText(end, "highlight end" + highlightSuffix);
}
prevEnd = end;
if (isSelected) {
// Attempt to scroll the selected match into view.
findController.scrollMatchIntoView({
element: textDivs[begin.divIdx],
selectedLeft,
pageIndex: pageIdx,
matchIndex: selectedMatchIdx,
});
}
}
if (prevEnd) {
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
}
}
_updateMatches() {
// Only show matches when all rendering is done.
if (!this.renderingDone) {
return;
}
const { findController, matches, pageIdx, textContentItemsStr, textDivs } =
this;
let clearedUntilDivIdx = -1;
// Clear all current matches.
for (let i = 0, ii = matches.length; i < ii; i++) {
const match = matches[i];
const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
for (let n = begin, end = match.end.divIdx; n <= end; n++) {
const div = textDivs[n];
div.textContent = textContentItemsStr[n];
div.className = "";
}
clearedUntilDivIdx = match.end.divIdx + 1;
}
if (!findController?.highlightMatches) {
return;
}
// Convert the matches on the `findController` into the match format
// used for the textLayer.
const pageMatches = findController.pageMatches[pageIdx] || null;
const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
this.matches = this._convertMatches(pageMatches, pageMatchesLength);
this._renderMatches(this.matches);
}
/**
* Improves text selection by adding an additional div where the mouse was
* clicked. This reduces flickering of the content if the mouse is slowly
@ -435,6 +227,7 @@ class DefaultTextLayerFactory {
* @param {PageViewport} viewport
* @param {boolean} enhanceTextSelection
* @param {EventBus} eventBus
* @param {TextHighlighter} highlighter
* @returns {TextLayerBuilder}
*/
createTextLayerBuilder(
@ -442,7 +235,8 @@ class DefaultTextLayerFactory {
pageIndex,
viewport,
enhanceTextSelection = false,
eventBus
eventBus,
highlighter
) {
return new TextLayerBuilder({
textLayerDiv,

View File

@ -17,6 +17,37 @@
--unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
}
.xfaLayer .highlight {
margin: -1px;
padding: 1px;
background-color: rgba(239, 203, 237, 1);
border-radius: 4px;
}
.xfaLayer .highlight.appended {
position: initial;
}
.xfaLayer .highlight.begin {
border-radius: 4px 0 0 4px;
}
.xfaLayer .highlight.end {
border-radius: 0 4px 4px 0;
}
.xfaLayer .highlight.middle {
border-radius: 0;
}
.xfaLayer .highlight.selected {
background-color: rgba(203, 223, 203, 1);
}
.xfaLayer ::selection {
background: rgba(0, 0, 255, 1);
}
.xfaPage {
overflow: hidden;
position: relative;

View File

@ -39,8 +39,9 @@ class XfaLayerBuilder {
/**
* @param {PageViewport} viewport
* @param {string} intent (default value is 'display')
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
* @returns {Promise<Object | void>} A promise that is resolved when rendering
* of the XFA layer is complete. The first rendering will return an object
* with a `textDivs` property that can be used with the TextHighlighter.
*/
render(viewport, intent = "display") {
if (intent === "print") {
@ -67,7 +68,7 @@ class XfaLayerBuilder {
.getXfa()
.then(xfa => {
if (this._cancelled) {
return;
return Promise.resolve();
}
const parameters = {
viewport: viewport.clone({ dontFlip: true }),
@ -79,15 +80,13 @@ class XfaLayerBuilder {
};
if (this.div) {
XfaLayer.update(parameters);
} else {
// Create an xfa layer div and render the form
this.div = document.createElement("div");
this.pageDiv.appendChild(this.div);
parameters.div = this.div;
XfaLayer.render(parameters);
return XfaLayer.update(parameters);
}
// Create an xfa layer div and render the form
this.div = document.createElement("div");
this.pageDiv.appendChild(this.div);
parameters.div = this.div;
return XfaLayer.render(parameters);
})
.catch(error => {
console.error(error);