diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 6a2b35147..2495d7a0d 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -210,7 +210,7 @@ class AnnotationElement { container.style.zIndex = this.parent.zIndex++; if (this.data.popupRef) { - container.setAttribute("aria-haspopup", true); + container.setAttribute("aria-haspopup", "dialog"); } if (data.noRotate) { @@ -474,7 +474,7 @@ class AnnotationElement { */ _createPopup() { const { container, data } = this; - container.setAttribute("aria-haspopup", true); + container.setAttribute("aria-haspopup", "dialog"); const popup = new PopupAnnotationElement({ data: { @@ -590,6 +590,17 @@ class AnnotationElement { getElementsToTriggerPopup() { return this.quadrilaterals || this.container; } + + addHighlightArea() { + const triggers = this.getElementsToTriggerPopup(); + if (Array.isArray(triggers)) { + for (const element of triggers) { + element.classList.add("highlightArea"); + } + } else { + triggers.classList.add("highlightArea"); + } + } } class LinkAnnotationElement extends AnnotationElement { @@ -1881,6 +1892,7 @@ class PopupAnnotationElement extends AnnotationElement { for (const element of this.elements) { element.popup = popup; elementIds.push(element.data.id); + element.addHighlightArea(); } this.container.setAttribute("aria-controls", elementIds.join(",")); @@ -2264,6 +2276,10 @@ class LineAnnotationElement extends AnnotationElement { getElementsToTriggerPopup() { return this.#line; } + + addHighlightArea() { + this.container.classList.add("highlightArea"); + } } class SquareAnnotationElement extends AnnotationElement { @@ -2323,6 +2339,10 @@ class SquareAnnotationElement extends AnnotationElement { getElementsToTriggerPopup() { return this.#square; } + + addHighlightArea() { + this.container.classList.add("highlightArea"); + } } class CircleAnnotationElement extends AnnotationElement { @@ -2383,6 +2403,10 @@ class CircleAnnotationElement extends AnnotationElement { getElementsToTriggerPopup() { return this.#circle; } + + addHighlightArea() { + this.container.classList.add("highlightArea"); + } } class PolylineAnnotationElement extends AnnotationElement { @@ -2452,6 +2476,10 @@ class PolylineAnnotationElement extends AnnotationElement { getElementsToTriggerPopup() { return this.#polyline; } + + addHighlightArea() { + this.container.classList.add("highlightArea"); + } } class PolygonAnnotationElement extends PolylineAnnotationElement { @@ -2556,6 +2584,10 @@ class InkAnnotationElement extends AnnotationElement { getElementsToTriggerPopup() { return this.#polylines; } + + addHighlightArea() { + this.container.classList.add("highlightArea"); + } } class HighlightAnnotationElement extends AnnotationElement { @@ -2750,6 +2782,10 @@ class FileAttachmentAnnotationElement extends AnnotationElement { return this.#trigger; } + addHighlightArea() { + this.container.classList.add("highlightArea"); + } + /** * Download the file attachment associated with this annotation. * diff --git a/src/display/base_factory.js b/src/display/base_factory.js index 696cc15c9..c6da0791c 100644 --- a/src/display/base_factory.js +++ b/src/display/base_factory.js @@ -30,6 +30,10 @@ class BaseFilterFactory { return "none"; } + addHighlightHCMFilter(fgColor, bgColor, newFgColor, newBgColor) { + return "none"; + } + destroy(keepHCM = false) {} } diff --git a/src/display/display_utils.js b/src/display/display_utils.js index fe51836ff..84b61c536 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -64,6 +64,12 @@ class DOMFilterFactory extends BaseFilterFactory { #hcmUrl; + #hcmHighlightFilter; + + #hcmHighlightKey; + + #hcmHighlightUrl; + #id = 0; constructor({ docId, ownerDocument = globalThis.document } = {}) { @@ -98,13 +104,6 @@ class DOMFilterFactory extends BaseFilterFactory { return this.#_defs; } - #appendFeFunc(feComponentTransfer, func, table) { - const feFunc = this.#document.createElementNS(SVG_NS, func); - feFunc.setAttribute("type", "discrete"); - feFunc.setAttribute("tableValues", table); - feComponentTransfer.append(feFunc); - } - addFilter(maps) { if (!maps) { return "none"; @@ -155,20 +154,8 @@ class DOMFilterFactory extends BaseFilterFactory { this.#cache.set(maps, url); this.#cache.set(key, url); - const filter = this.#document.createElementNS(SVG_NS, "filter", SVG_NS); - filter.setAttribute("id", id); - filter.setAttribute("color-interpolation-filters", "sRGB"); - const feComponentTransfer = this.#document.createElementNS( - SVG_NS, - "feComponentTransfer" - ); - filter.append(feComponentTransfer); - - this.#appendFeFunc(feComponentTransfer, "feFuncR", tableR); - this.#appendFeFunc(feComponentTransfer, "feFuncG", tableG); - this.#appendFeFunc(feComponentTransfer, "feFuncB", tableB); - - this.#defs.append(filter); + const filter = this.#createFilter(id); + this.#addTransferMapConversion(tableR, tableG, tableB, filter); return url; } @@ -187,13 +174,9 @@ class DOMFilterFactory extends BaseFilterFactory { return this.#hcmUrl; } - this.#defs.style.color = fgColor; - fgColor = getComputedStyle(this.#defs).getPropertyValue("color"); - const fgRGB = getRGB(fgColor); + const fgRGB = this.#getRGB(fgColor); fgColor = Util.makeHexColor(...fgRGB); - this.#defs.style.color = bgColor; - bgColor = getComputedStyle(this.#defs).getPropertyValue("color"); - const bgRGB = getRGB(bgColor); + const bgRGB = this.#getRGB(bgColor); bgColor = Util.makeHexColor(...bgRGB); this.#defs.style.color = ""; @@ -221,39 +204,9 @@ class DOMFilterFactory extends BaseFilterFactory { const table = map.join(","); const id = `g_${this.#docId}_hcm_filter`; - const filter = (this.#hcmFilter = this.#document.createElementNS( - SVG_NS, - "filter", - SVG_NS - )); - filter.setAttribute("id", id); - filter.setAttribute("color-interpolation-filters", "sRGB"); - let feComponentTransfer = this.#document.createElementNS( - SVG_NS, - "feComponentTransfer" - ); - filter.append(feComponentTransfer); - - this.#appendFeFunc(feComponentTransfer, "feFuncR", table); - this.#appendFeFunc(feComponentTransfer, "feFuncG", table); - this.#appendFeFunc(feComponentTransfer, "feFuncB", table); - - const feColorMatrix = this.#document.createElementNS( - SVG_NS, - "feColorMatrix" - ); - feColorMatrix.setAttribute("type", "matrix"); - feColorMatrix.setAttribute( - "values", - "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0" - ); - filter.append(feColorMatrix); - - feComponentTransfer = this.#document.createElementNS( - SVG_NS, - "feComponentTransfer" - ); - filter.append(feComponentTransfer); + const filter = (this.#hcmHighlightFilter = this.#createFilter(id)); + this.#addTransferMapConversion(table, table, table, filter); + this.#addGrayConversion(filter); const getSteps = (c, n) => { const start = fgRGB[c] / 255; @@ -264,18 +217,101 @@ class DOMFilterFactory extends BaseFilterFactory { } return arr.join(","); }; - this.#appendFeFunc(feComponentTransfer, "feFuncR", getSteps(0, 5)); - this.#appendFeFunc(feComponentTransfer, "feFuncG", getSteps(1, 5)); - this.#appendFeFunc(feComponentTransfer, "feFuncB", getSteps(2, 5)); - - this.#defs.append(filter); + this.#addTransferMapConversion( + getSteps(0, 5), + getSteps(1, 5), + getSteps(2, 5), + filter + ); this.#hcmUrl = `url(#${id})`; return this.#hcmUrl; } + addHighlightHCMFilter(fgColor, bgColor, newFgColor, newBgColor) { + const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`; + if (this.#hcmHighlightKey === key) { + return this.#hcmHighlightUrl; + } + + this.#hcmHighlightKey = key; + this.#hcmHighlightUrl = "none"; + this.#hcmHighlightFilter?.remove(); + + if (!fgColor || !bgColor) { + return this.#hcmHighlightUrl; + } + + const [fgRGB, bgRGB] = [fgColor, bgColor].map(this.#getRGB.bind(this)); + let fgGray = Math.round( + 0.2126 * fgRGB[0] + 0.7152 * fgRGB[1] + 0.0722 * fgRGB[2] + ); + let bgGray = Math.round( + 0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2] + ); + let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map( + this.#getRGB.bind(this) + ); + if (bgGray < fgGray) { + [fgGray, bgGray, newFgRGB, newBgRGB] = [ + bgGray, + fgGray, + newBgRGB, + newFgRGB, + ]; + } + this.#defs.style.color = ""; + + // Now we can create the filters to highlight some canvas parts. + // The colors in the pdf will almost be Canvas and CanvasText, hence we + // want to filter them to finally get Highlight and HighlightText. + // Since we're in HCM the background color and the foreground color should + // be really different when converted to grayscale (if they're not then it + // means that we've a poor contrast). Once the canvas colors are converted + // to grayscale we can easily map them on their new colors. + // The grayscale step is important because if we've something like: + // fgColor = #FF.... + // bgColor = #FF.... + // then we are enable to map the red component on the new red components + // which can be different. + + const getSteps = (fg, bg, n) => { + const arr = new Array(256); + const step = (bgGray - fgGray) / n; + const newStart = fg / 255; + const newStep = (bg - fg) / (255 * n); + let prev = 0; + for (let i = 0; i <= n; i++) { + const k = Math.round(fgGray + i * step); + const value = newStart + i * newStep; + for (let j = prev; j <= k; j++) { + arr[j] = value; + } + prev = k + 1; + } + for (let i = prev; i < 256; i++) { + arr[i] = arr[prev - 1]; + } + return arr.join(","); + }; + + const id = `g_${this.#docId}_hcm_highlight_filter`; + const filter = (this.#hcmHighlightFilter = this.#createFilter(id)); + + this.#addGrayConversion(filter); + this.#addTransferMapConversion( + getSteps(newFgRGB[0], newBgRGB[0], 5), + getSteps(newFgRGB[1], newBgRGB[1], 5), + getSteps(newFgRGB[2], newBgRGB[2], 5), + filter + ); + + this.#hcmHighlightUrl = `url(#${id})`; + return this.#hcmHighlightUrl; + } + destroy(keepHCM = false) { - if (keepHCM && this.#hcmUrl) { + if (keepHCM && (this.#hcmUrl || this.#hcmHighlightUrl)) { return; } if (this.#_defs) { @@ -288,6 +324,51 @@ class DOMFilterFactory extends BaseFilterFactory { } this.#id = 0; } + + #addGrayConversion(filter) { + const feColorMatrix = this.#document.createElementNS( + SVG_NS, + "feColorMatrix" + ); + feColorMatrix.setAttribute("type", "matrix"); + feColorMatrix.setAttribute( + "values", + "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0" + ); + filter.append(feColorMatrix); + } + + #createFilter(id) { + const filter = this.#document.createElementNS(SVG_NS, "filter"); + filter.setAttribute("color-interpolation-filters", "sRGB"); + filter.setAttribute("id", id); + this.#defs.append(filter); + + return filter; + } + + #appendFeFunc(feComponentTransfer, func, table) { + const feFunc = this.#document.createElementNS(SVG_NS, func); + feFunc.setAttribute("type", "discrete"); + feFunc.setAttribute("tableValues", table); + feComponentTransfer.append(feFunc); + } + + #addTransferMapConversion(rTable, gTable, bTable, filter) { + const feComponentTransfer = this.#document.createElementNS( + SVG_NS, + "feComponentTransfer" + ); + filter.append(feComponentTransfer); + this.#appendFeFunc(feComponentTransfer, "feFuncR", rTable); + this.#appendFeFunc(feComponentTransfer, "feFuncG", gTable); + this.#appendFeFunc(feComponentTransfer, "feFuncB", bTable); + } + + #getRGB(color) { + this.#defs.style.color = color; + return getRGB(getComputedStyle(this.#defs).getPropertyValue("color")); + } } class DOMCanvasFactory extends BaseCanvasFactory { diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index f22194d46..1219fd3af 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -30,6 +30,7 @@ --input-disabled-border-color: GrayText; --input-hover-border-color: Highlight; --link-outline: 1.5px solid LinkText; + --hcm-highligh-filter: invert(100%); } .annotationLayer .textWidgetAnnotation :is(input, textarea):required, .annotationLayer .choiceWidgetAnnotation select:required, @@ -40,7 +41,30 @@ } .annotationLayer .linkAnnotation:hover { - backdrop-filter: invert(100%); + backdrop-filter: var(--hcm-highligh-filter); + } + + .annotationLayer .linkAnnotation > a:hover { + opacity: 0 !important; + background: none !important; + box-shadow: none; + } + + .annotationLayer .popupAnnotation .popup { + outline: calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color: ButtonFace !important; + color: ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + backdrop-filter: var(--hcm-highligh-filter); + content: ""; + pointer-events: none; } .annotationLayer .popupAnnotation.focused .popup { diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 4825d1ef7..b31408767 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -13,6 +13,8 @@ * limitations under the License. */ +// eslint-disable-next-line max-len +/** @typedef {import("../src/display/base_factory.js").BaseFilterFactory} BaseFilterFactory */ // eslint-disable-next-line max-len /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ // eslint-disable-next-line max-len @@ -84,6 +86,8 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js"; * @property {IL10n} [l10n] - Localization service. * @property {function} [layerProperties] - The function that is used to lookup * the necessary layer-properties. + * @property {BaseFilterFactory} [filterFactory] - Factory to create some SVG + * filters. */ const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216; @@ -203,6 +207,21 @@ class PDFPageView { "--scale-factor", this.scale * PixelsPerInch.PDF_TO_CSS_UNITS ); + if ( + options.filterFactory && + (this.pageColors?.foreground === "CanvasText" || + this.pageColors?.background === "Canvas") + ) { + container?.style.setProperty( + "--hcm-highligh-filter", + options.filterFactory.addHighlightHCMFilter( + "CanvasText", + "Canvas", + "HighlightText", + "Highlight" + ) + ); + } const { optionalContentConfigPromise } = options; if (optionalContentConfigPromise) { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 103937a50..363000fd8 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -883,6 +883,20 @@ class PDFViewer { // Ensure that the various layers always get the correct initial size, // see issue 15795. this.viewer.style.setProperty("--scale-factor", viewport.scale); + if ( + this.pageColors?.foreground === "CanvasText" || + this.pageColors?.background === "Canvas" + ) { + this.viewer.style.setProperty( + "--hcm-highligh-filter", + pdfDocument.filterFactory.addHighlightHCMFilter( + "CanvasText", + "Canvas", + "HighlightText", + "Highlight" + ) + ); + } for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { const pageView = new PDFPageView({ @@ -902,6 +916,7 @@ class PDFViewer { pageColors: this.pageColors, l10n: this.l10n, layerProperties, + filterFactory: pdfDocument.filterFactory, }); this._pages.push(pageView); }