From 8daf2f1eb1ded5a91294ada50357a4e7e9a5f01f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 30 May 2023 12:36:35 +0200 Subject: [PATCH] [Annotation] Use the clip-path property when an annotation has some quad points This way it'll avoid to split a div in multiple divs having the same id (which is supposed to be unique). --- src/display/annotation_layer.js | 172 +++++++++++--------- test/annotation_layer_builder_overrides.css | 12 +- test/driver.js | 3 + web/annotation_layer_builder.css | 22 ++- 4 files changed, 128 insertions(+), 81 deletions(-) diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 89700d1bc..eca676aa0 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -22,7 +22,6 @@ import { AnnotationBorderStyleType, AnnotationEditorType, AnnotationType, - assert, FeatureTest, LINE_FACTOR, shadow, @@ -156,6 +155,8 @@ class AnnotationElementFactory { } class AnnotationElement { + #hasBorder = false; + constructor( parameters, { @@ -182,7 +183,7 @@ class AnnotationElement { this.container = this._createContainer(ignoreBorder); } if (createQuadrilaterals) { - this.quadrilaterals = this._createQuadrilaterals(ignoreBorder); + this._createQuadrilaterals(); } } @@ -279,6 +280,7 @@ class AnnotationElement { const borderColor = data.borderColor || null; if (borderColor) { + this.#hasBorder = true; container.style.borderColor = Util.makeHexColor( borderColor[0] | 0, borderColor[1] | 0, @@ -441,31 +443,90 @@ class AnnotationElement { * Create quadrilaterals from the annotation's quadpoints. * * @private - * @param {boolean} ignoreBorder * @memberof AnnotationElement - * @returns {Array} An array of section elements. */ - _createQuadrilaterals(ignoreBorder = false) { - if (!this.data.quadPoints) { - return null; + _createQuadrilaterals() { + if (!this.container) { + return; + } + const { quadPoints } = this.data; + if (!quadPoints) { + return; } - const quadrilaterals = []; - const savedRect = this.data.rect; - let firstQuadRect = null; - for (const quadPoint of this.data.quadPoints) { - this.data.rect = [ - quadPoint[2].x, - quadPoint[2].y, - quadPoint[1].x, - quadPoint[1].y, - ]; - quadrilaterals.push(this._createContainer(ignoreBorder)); - firstQuadRect ||= this.data.rect; + const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect; + + if (quadPoints.length === 1) { + const [, { x: trX, y: trY }, { x: blX, y: blY }] = quadPoints[0]; + if ( + rectTrX === trX && + rectTrY === trY && + rectBlX === blX && + rectBlY === blY + ) { + // The quadpoints cover the whole annotation rectangle, so no need to + // create a quadrilateral. + return; + } } - this.data.rect = savedRect; - this.firstQuadRect = firstQuadRect; - return quadrilaterals; + + const { style } = this.container; + let svgBuffer; + if (this.#hasBorder) { + const { borderColor, borderWidth } = style; + style.borderWidth = 0; + svgBuffer = [ + "url('data:image/svg+xml;utf8,", + ``, + ``, + ]; + this.container.classList.add("hasBorder"); + } + + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + this.container.classList.add("hasClipPath"); + } + + const width = rectTrX - rectBlX; + const height = rectTrY - rectBlY; + + const { svgFactory } = this; + const svg = svgFactory.createElement("svg"); + svg.classList.add("quadrilateralsContainer"); + svg.setAttribute("width", 0); + svg.setAttribute("height", 0); + const defs = svgFactory.createElement("defs"); + svg.append(defs); + const clipPath = svgFactory.createElement("clipPath"); + const id = `clippath_${this.data.id}`; + clipPath.setAttribute("id", id); + clipPath.setAttribute("clipPathUnits", "objectBoundingBox"); + defs.append(clipPath); + + for (const [, { x: trX, y: trY }, { x: blX, y: blY }] of quadPoints) { + const rect = svgFactory.createElement("rect"); + const x = (blX - rectBlX) / width; + const y = (rectTrY - trY) / height; + const rectWidth = (trX - blX) / width; + const rectHeight = (trY - blY) / height; + rect.setAttribute("x", x); + rect.setAttribute("y", y); + rect.setAttribute("width", rectWidth); + rect.setAttribute("height", rectHeight); + clipPath.append(rect); + svgBuffer?.push( + `` + ); + } + + if (this.#hasBorder) { + svgBuffer.push(`')`); + style.backgroundImage = svgBuffer.join(""); + } + + this.container.append(svg); + this.container.style.clipPath = `url(#${id})`; } /** @@ -487,7 +548,7 @@ class AnnotationElement { modificationDate: data.modificationDate, contentsObj: data.contentsObj, richText: data.richText, - parentRect: this.firstQuadRect || data.rect, + parentRect: data.rect, borderStyle: 0, id: `popup_${data.id}`, rotation: data.rotation, @@ -498,32 +559,11 @@ class AnnotationElement { this.parent.div.append(popup.render()); } - /** - * Render the quadrilaterals of the annotation. - * - * @private - * @param {string} className - * @memberof AnnotationElement - * @returns {Array} An array of section elements. - */ - _renderQuadrilaterals(className) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - assert(this.quadrilaterals, "Missing quadrilaterals during rendering"); - } - - for (const quadrilateral of this.quadrilaterals) { - quadrilateral.classList.add(className); - } - return this.quadrilaterals; - } - /** * Render the annotation's HTML element(s). * * @public * @memberof AnnotationElement - * @returns {HTMLElement|Array|undefined} A section element or - * an array of section elements. */ render() { unreachable("Abstract method `AnnotationElement.render` called"); @@ -591,8 +631,16 @@ class AnnotationElement { this.popup?.forceHide(); } + /** + * Get the HTML element(s) which can trigger a popup when clicked or hovered. + * + * @public + * @memberof AnnotationElement + * @returns {Array|HTMLElement} An array of elements or an + * element. + */ getElementsToTriggerPopup() { - return this.quadrilaterals || this.container; + return this.container; } addHighlightArea() { @@ -674,16 +722,6 @@ class LinkAnnotationElement extends AnnotationElement { } } - if (this.quadrilaterals) { - return this._renderQuadrilaterals("linkAnnotation").map( - (quadrilateral, index) => { - const linkElement = index === 0 ? link : link.cloneNode(); - quadrilateral.append(linkElement); - return quadrilateral; - } - ); - } - this.container.classList.add("linkAnnotation"); if (isBound) { this.container.append(link); @@ -2632,10 +2670,6 @@ class HighlightAnnotationElement extends AnnotationElement { this._createPopup(); } - if (this.quadrilaterals) { - return this._renderQuadrilaterals("highlightAnnotation"); - } - this.container.classList.add("highlightAnnotation"); return this.container; } @@ -2661,10 +2695,6 @@ class UnderlineAnnotationElement extends AnnotationElement { this._createPopup(); } - if (this.quadrilaterals) { - return this._renderQuadrilaterals("underlineAnnotation"); - } - this.container.classList.add("underlineAnnotation"); return this.container; } @@ -2690,10 +2720,6 @@ class SquigglyAnnotationElement extends AnnotationElement { this._createPopup(); } - if (this.quadrilaterals) { - return this._renderQuadrilaterals("squigglyAnnotation"); - } - this.container.classList.add("squigglyAnnotation"); return this.container; } @@ -2719,10 +2745,6 @@ class StrikeOutAnnotationElement extends AnnotationElement { this._createPopup(); } - if (this.quadrilaterals) { - return this._renderQuadrilaterals("strikeoutAnnotation"); - } - this.container.classList.add("strikeoutAnnotation"); return this.container; } @@ -2971,13 +2993,7 @@ class AnnotationLayer { if (data.hidden) { rendered.style.visibility = "hidden"; } - if (Array.isArray(rendered)) { - for (const renderedElement of rendered) { - this.#appendElement(renderedElement, data.id); - } - } else { - this.#appendElement(rendered, data.id); - } + this.#appendElement(rendered, data.id); } this.#setAnnotationCanvasMap(); diff --git a/test/annotation_layer_builder_overrides.css b/test/annotation_layer_builder_overrides.css index 4efeb8dbe..ac3ee7b3f 100644 --- a/test/annotation_layer_builder_overrides.css +++ b/test/annotation_layer_builder_overrides.css @@ -29,7 +29,9 @@ -webkit-appearance: none; } -.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a, +.annotationLayer + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) + > a, .annotationLayer .popupTriggerArea::after, .annotationLayer .fileAttachmentAnnotation .popupTriggerArea { opacity: 0.2; @@ -37,6 +39,14 @@ box-shadow: 0 2px 10px rgba(255, 255, 0, 1); } +.annotationLayer .hasClipPath::after { + box-shadow: none; +} + +.annotationLayer .linkAnnotation.hasBorder { + background-color: rgba(255, 255, 0, 0.2); +} + .annotationLayer .popupTriggerArea::after { display: block; width: 100%; diff --git a/test/driver.js b/test/driver.js index 1df6232de..6ec0e5a4f 100644 --- a/test/driver.js +++ b/test/driver.js @@ -84,6 +84,9 @@ async function writeSVG(svgElement, ctx) { // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1844414 // we load the image two times. await loadImage(svg_xml, null); + await new Promise(resolve => { + setTimeout(resolve, 10); + }); } return loadImage(svg_xml, ctx); } diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 1219fd3af..4a5b4f1f1 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -120,13 +120,21 @@ } .annotationLayer - :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover { opacity: 0.2; - background: rgba(255, 255, 0, 1); + background-color: rgba(255, 255, 0, 1); box-shadow: 0 2px 10px rgba(255, 255, 0, 1); } +.annotationLayer .linkAnnotation.hasBorder:hover { + background-color: rgba(255, 255, 0, 0.2); +} + +.annotationLayer .hasBorder { + background-size: 100% 100%; +} + .annotationLayer .textAnnotation img { position: absolute; cursor: pointer; @@ -368,3 +376,13 @@ width: 100%; display: inline-block; } + +.annotationLayer svg.quadrilateralsContainer { + contain: strict; + width: 0; + height: 0; + position: absolute; + top: 0; + left: 0; + z-index: -1; +}