Merge pull request #13908 from brendandahl/xfa-find
[api-minor] XFA - Support text search in XFA documents.
This commit is contained in:
commit
bf5a45ce6d
@ -62,6 +62,7 @@ import { MessageHandler } from "../shared/message_handler.js";
|
|||||||
import { Metadata } from "./metadata.js";
|
import { Metadata } from "./metadata.js";
|
||||||
import { OptionalContentConfig } from "./optional_content_config.js";
|
import { OptionalContentConfig } from "./optional_content_config.js";
|
||||||
import { PDFDataTransportStream } from "./transport_stream.js";
|
import { PDFDataTransportStream } from "./transport_stream.js";
|
||||||
|
import { XfaText } from "./xfa_text.js";
|
||||||
|
|
||||||
const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
|
const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
|
||||||
const RENDERING_CANCELLED_TIMEOUT = 100; // ms
|
const RENDERING_CANCELLED_TIMEOUT = 100; // ms
|
||||||
@ -1531,6 +1532,13 @@ class PDFPageProxy {
|
|||||||
* {@link TextContent} object that represents the page's text content.
|
* {@link TextContent} object that represents the page's text content.
|
||||||
*/
|
*/
|
||||||
getTextContent(params = {}) {
|
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);
|
const readableStream = this.streamTextContent(params);
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { XfaText } from "./xfa_text.js";
|
||||||
|
|
||||||
class XfaLayer {
|
class XfaLayer {
|
||||||
static setupStorage(html, id, element, storage, intent) {
|
static setupStorage(html, id, element, storage, intent) {
|
||||||
const storedData = storage.getValue(id, { value: null });
|
const storedData = storage.getValue(id, { value: null });
|
||||||
@ -127,6 +129,9 @@ class XfaLayer {
|
|||||||
// Set defaults.
|
// Set defaults.
|
||||||
rootDiv.setAttribute("class", "xfaLayer xfaFont");
|
rootDiv.setAttribute("class", "xfaLayer xfaFont");
|
||||||
|
|
||||||
|
// Text nodes used for the text highlighter.
|
||||||
|
const textDivs = [];
|
||||||
|
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const [parent, i, html] = stack[stack.length - 1];
|
const [parent, i, html] = stack[stack.length - 1];
|
||||||
if (i + 1 === parent.children.length) {
|
if (i + 1 === parent.children.length) {
|
||||||
@ -141,7 +146,9 @@ class XfaLayer {
|
|||||||
|
|
||||||
const { name } = child;
|
const { name } = child;
|
||||||
if (name === "#text") {
|
if (name === "#text") {
|
||||||
html.appendChild(document.createTextNode(child.value));
|
const node = document.createTextNode(child.value);
|
||||||
|
textDivs.push(node);
|
||||||
|
html.appendChild(node);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +167,11 @@ class XfaLayer {
|
|||||||
if (child.children && child.children.length > 0) {
|
if (child.children && child.children.length > 0) {
|
||||||
stack.push([child, -1, childHtml]);
|
stack.push([child, -1, childHtml]);
|
||||||
} else if (child.value) {
|
} 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);
|
el.setAttribute("readOnly", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
textDivs,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
79
src/display/xfa_text.js
Normal file
79
src/display/xfa_text.js
Normal 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 };
|
@ -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");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -42,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
|
|||||||
import { PDFPageView } from "./pdf_page_view.js";
|
import { PDFPageView } from "./pdf_page_view.js";
|
||||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
||||||
|
import { TextHighlighter } from "./text_highlighter.js";
|
||||||
import { TextLayerBuilder } from "./text_layer_builder.js";
|
import { TextLayerBuilder } from "./text_layer_builder.js";
|
||||||
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||||
|
|
||||||
@ -525,7 +526,9 @@ class BaseViewer {
|
|||||||
const scale = this.currentScale;
|
const scale = this.currentScale;
|
||||||
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
|
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
|
||||||
const textLayerFactory =
|
const textLayerFactory =
|
||||||
this.textLayerMode !== TextLayerMode.DISABLE ? this : null;
|
this.textLayerMode !== TextLayerMode.DISABLE && !isPureXfa
|
||||||
|
? this
|
||||||
|
: null;
|
||||||
const xfaLayerFactory = isPureXfa ? this : null;
|
const xfaLayerFactory = isPureXfa ? this : null;
|
||||||
|
|
||||||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||||
@ -541,6 +544,7 @@ class BaseViewer {
|
|||||||
textLayerMode: this.textLayerMode,
|
textLayerMode: this.textLayerMode,
|
||||||
annotationLayerFactory: this,
|
annotationLayerFactory: this,
|
||||||
xfaLayerFactory,
|
xfaLayerFactory,
|
||||||
|
textHighlighterFactory: this,
|
||||||
structTreeLayerFactory: this,
|
structTreeLayerFactory: this,
|
||||||
imageResourcesPath: this.imageResourcesPath,
|
imageResourcesPath: this.imageResourcesPath,
|
||||||
renderInteractiveForms: this.renderInteractiveForms,
|
renderInteractiveForms: this.renderInteractiveForms,
|
||||||
@ -1242,6 +1246,7 @@ class BaseViewer {
|
|||||||
* @param {PageViewport} viewport
|
* @param {PageViewport} viewport
|
||||||
* @param {boolean} enhanceTextSelection
|
* @param {boolean} enhanceTextSelection
|
||||||
* @param {EventBus} eventBus
|
* @param {EventBus} eventBus
|
||||||
|
* @param {TextHighlighter} highlighter
|
||||||
* @returns {TextLayerBuilder}
|
* @returns {TextLayerBuilder}
|
||||||
*/
|
*/
|
||||||
createTextLayerBuilder(
|
createTextLayerBuilder(
|
||||||
@ -1249,17 +1254,31 @@ class BaseViewer {
|
|||||||
pageIndex,
|
pageIndex,
|
||||||
viewport,
|
viewport,
|
||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus
|
eventBus,
|
||||||
|
highlighter
|
||||||
) {
|
) {
|
||||||
return new TextLayerBuilder({
|
return new TextLayerBuilder({
|
||||||
textLayerDiv,
|
textLayerDiv,
|
||||||
eventBus,
|
eventBus,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
viewport,
|
viewport,
|
||||||
findController: this.isInPresentationMode ? null : this.findController,
|
|
||||||
enhanceTextSelection: this.isInPresentationMode
|
enhanceTextSelection: this.isInPresentationMode
|
||||||
? false
|
? false
|
||||||
: enhanceTextSelection,
|
: enhanceTextSelection,
|
||||||
|
highlighter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pageIndex
|
||||||
|
* @param {EventBus} eventBus
|
||||||
|
* @returns {TextHighlighter}
|
||||||
|
*/
|
||||||
|
createTextHighlighter(pageIndex, eventBus) {
|
||||||
|
return new TextHighlighter({
|
||||||
|
eventBus,
|
||||||
|
pageIndex,
|
||||||
|
findController: this.isInPresentationMode ? null : this.findController,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +162,7 @@ class IPDFTextLayerFactory {
|
|||||||
* @param {PageViewport} viewport
|
* @param {PageViewport} viewport
|
||||||
* @param {boolean} enhanceTextSelection
|
* @param {boolean} enhanceTextSelection
|
||||||
* @param {EventBus} eventBus
|
* @param {EventBus} eventBus
|
||||||
|
* @param {TextHighlighter} highlighter
|
||||||
* @returns {TextLayerBuilder}
|
* @returns {TextLayerBuilder}
|
||||||
*/
|
*/
|
||||||
createTextLayerBuilder(
|
createTextLayerBuilder(
|
||||||
@ -169,7 +170,8 @@ class IPDFTextLayerFactory {
|
|||||||
pageIndex,
|
pageIndex,
|
||||||
viewport,
|
viewport,
|
||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus
|
eventBus,
|
||||||
|
highlighter
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,11 @@ class PDFPageView {
|
|||||||
this.textLayerFactory = options.textLayerFactory;
|
this.textLayerFactory = options.textLayerFactory;
|
||||||
this.annotationLayerFactory = options.annotationLayerFactory;
|
this.annotationLayerFactory = options.annotationLayerFactory;
|
||||||
this.xfaLayerFactory = options.xfaLayerFactory;
|
this.xfaLayerFactory = options.xfaLayerFactory;
|
||||||
|
this.textHighlighter =
|
||||||
|
options.textHighlighterFactory?.createTextHighlighter(
|
||||||
|
this.id - 1,
|
||||||
|
this.eventBus
|
||||||
|
);
|
||||||
this.structTreeLayerFactory = options.structTreeLayerFactory;
|
this.structTreeLayerFactory = options.structTreeLayerFactory;
|
||||||
this.renderer = options.renderer || RendererType.CANVAS;
|
this.renderer = options.renderer || RendererType.CANVAS;
|
||||||
this.l10n = options.l10n || NullL10n;
|
this.l10n = options.l10n || NullL10n;
|
||||||
@ -175,7 +180,10 @@ class PDFPageView {
|
|||||||
async _renderXfaLayer() {
|
async _renderXfaLayer() {
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
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) {
|
} catch (ex) {
|
||||||
error = ex;
|
error = ex;
|
||||||
} finally {
|
} 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
|
* @private
|
||||||
*/
|
*/
|
||||||
@ -382,6 +400,7 @@ class PDFPageView {
|
|||||||
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
||||||
this.xfaLayer.cancel();
|
this.xfaLayer.cancel();
|
||||||
this.xfaLayer = null;
|
this.xfaLayer = null;
|
||||||
|
this.textHighlighter?.disable();
|
||||||
}
|
}
|
||||||
if (this._onTextLayerRendered) {
|
if (this._onTextLayerRendered) {
|
||||||
this.eventBus._off("textlayerrendered", this._onTextLayerRendered);
|
this.eventBus._off("textlayerrendered", this._onTextLayerRendered);
|
||||||
@ -533,7 +552,8 @@ class PDFPageView {
|
|||||||
this.id - 1,
|
this.id - 1,
|
||||||
this.viewport,
|
this.viewport,
|
||||||
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE,
|
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE,
|
||||||
this.eventBus
|
this.eventBus,
|
||||||
|
this.textHighlighter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.textLayer = textLayer;
|
this.textLayer = textLayer;
|
||||||
|
293
web/text_highlighter.js
Normal file
293
web/text_highlighter.js
Normal 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 };
|
@ -23,7 +23,8 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
|
|||||||
* @property {EventBus} eventBus - The application event bus.
|
* @property {EventBus} eventBus - The application event bus.
|
||||||
* @property {number} pageIndex - The page index.
|
* @property {number} pageIndex - The page index.
|
||||||
* @property {PageViewport} viewport - The viewport of the text layer.
|
* @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
|
* @property {boolean} enhanceTextSelection - Option to turn on improved
|
||||||
* text selection.
|
* text selection.
|
||||||
*/
|
*/
|
||||||
@ -31,8 +32,7 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
|
|||||||
/**
|
/**
|
||||||
* The text layer builder provides text selection functionality for the PDF.
|
* 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
|
* 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
|
* contain text that matches the PDF text they are overlaying.
|
||||||
* also provides a way to highlight text that is being searched for.
|
|
||||||
*/
|
*/
|
||||||
class TextLayerBuilder {
|
class TextLayerBuilder {
|
||||||
constructor({
|
constructor({
|
||||||
@ -40,7 +40,7 @@ class TextLayerBuilder {
|
|||||||
eventBus,
|
eventBus,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
viewport,
|
viewport,
|
||||||
findController = null,
|
highlighter = null,
|
||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
}) {
|
}) {
|
||||||
this.textLayerDiv = textLayerDiv;
|
this.textLayerDiv = textLayerDiv;
|
||||||
@ -54,11 +54,10 @@ class TextLayerBuilder {
|
|||||||
this.matches = [];
|
this.matches = [];
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
this.textDivs = [];
|
this.textDivs = [];
|
||||||
this.findController = findController;
|
|
||||||
this.textLayerRenderTask = null;
|
this.textLayerRenderTask = null;
|
||||||
|
this.highlighter = highlighter;
|
||||||
this.enhanceTextSelection = enhanceTextSelection;
|
this.enhanceTextSelection = enhanceTextSelection;
|
||||||
|
|
||||||
this._onUpdateTextLayerMatches = null;
|
|
||||||
this._bindMouse();
|
this._bindMouse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +93,9 @@ class TextLayerBuilder {
|
|||||||
this.cancel();
|
this.cancel();
|
||||||
|
|
||||||
this.textDivs = [];
|
this.textDivs = [];
|
||||||
|
if (this.highlighter) {
|
||||||
|
this.highlighter.setTextMapping(this.textDivs, this.textContentItemsStr);
|
||||||
|
}
|
||||||
const textLayerFrag = document.createDocumentFragment();
|
const textLayerFrag = document.createDocumentFragment();
|
||||||
this.textLayerRenderTask = renderTextLayer({
|
this.textLayerRenderTask = renderTextLayer({
|
||||||
textContent: this.textContent,
|
textContent: this.textContent,
|
||||||
@ -109,24 +111,12 @@ class TextLayerBuilder {
|
|||||||
() => {
|
() => {
|
||||||
this.textLayerDiv.appendChild(textLayerFrag);
|
this.textLayerDiv.appendChild(textLayerFrag);
|
||||||
this._finishRendering();
|
this._finishRendering();
|
||||||
this._updateMatches();
|
this.highlighter?.enable();
|
||||||
},
|
},
|
||||||
function (reason) {
|
function (reason) {
|
||||||
// Cancelled or failed to render text layer; skipping errors.
|
// 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.cancel();
|
||||||
this.textLayerRenderTask = null;
|
this.textLayerRenderTask = null;
|
||||||
}
|
}
|
||||||
if (this._onUpdateTextLayerMatches) {
|
this.highlighter?.disable();
|
||||||
this.eventBus._off(
|
|
||||||
"updatetextlayermatches",
|
|
||||||
this._onUpdateTextLayerMatches
|
|
||||||
);
|
|
||||||
this._onUpdateTextLayerMatches = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextContentStream(readableStream) {
|
setTextContentStream(readableStream) {
|
||||||
@ -156,198 +140,6 @@ class TextLayerBuilder {
|
|||||||
this.textContent = textContent;
|
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
|
* Improves text selection by adding an additional div where the mouse was
|
||||||
* clicked. This reduces flickering of the content if the mouse is slowly
|
* clicked. This reduces flickering of the content if the mouse is slowly
|
||||||
@ -435,6 +227,7 @@ class DefaultTextLayerFactory {
|
|||||||
* @param {PageViewport} viewport
|
* @param {PageViewport} viewport
|
||||||
* @param {boolean} enhanceTextSelection
|
* @param {boolean} enhanceTextSelection
|
||||||
* @param {EventBus} eventBus
|
* @param {EventBus} eventBus
|
||||||
|
* @param {TextHighlighter} highlighter
|
||||||
* @returns {TextLayerBuilder}
|
* @returns {TextLayerBuilder}
|
||||||
*/
|
*/
|
||||||
createTextLayerBuilder(
|
createTextLayerBuilder(
|
||||||
@ -442,7 +235,8 @@ class DefaultTextLayerFactory {
|
|||||||
pageIndex,
|
pageIndex,
|
||||||
viewport,
|
viewport,
|
||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus
|
eventBus,
|
||||||
|
highlighter
|
||||||
) {
|
) {
|
||||||
return new TextLayerBuilder({
|
return new TextLayerBuilder({
|
||||||
textLayerDiv,
|
textLayerDiv,
|
||||||
|
@ -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>");
|
--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 {
|
.xfaPage {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -39,8 +39,9 @@ class XfaLayerBuilder {
|
|||||||
/**
|
/**
|
||||||
* @param {PageViewport} viewport
|
* @param {PageViewport} viewport
|
||||||
* @param {string} intent (default value is 'display')
|
* @param {string} intent (default value is 'display')
|
||||||
* @returns {Promise<void>} A promise that is resolved when rendering of the
|
* @returns {Promise<Object | void>} A promise that is resolved when rendering
|
||||||
* annotations is complete.
|
* 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") {
|
render(viewport, intent = "display") {
|
||||||
if (intent === "print") {
|
if (intent === "print") {
|
||||||
@ -67,7 +68,7 @@ class XfaLayerBuilder {
|
|||||||
.getXfa()
|
.getXfa()
|
||||||
.then(xfa => {
|
.then(xfa => {
|
||||||
if (this._cancelled) {
|
if (this._cancelled) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const parameters = {
|
const parameters = {
|
||||||
viewport: viewport.clone({ dontFlip: true }),
|
viewport: viewport.clone({ dontFlip: true }),
|
||||||
@ -79,15 +80,13 @@ class XfaLayerBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.div) {
|
if (this.div) {
|
||||||
XfaLayer.update(parameters);
|
return 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);
|
|
||||||
}
|
}
|
||||||
|
// 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 => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user