Merge pull request #16492 from calixteman/clip_path

[Annotation] Use the clip-path property when an annotation has some quad points
This commit is contained in:
calixteman 2023-07-20 15:01:27 +02:00 committed by GitHub
commit 0702663b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 81 deletions

View File

@ -22,7 +22,6 @@ import {
AnnotationBorderStyleType, AnnotationBorderStyleType,
AnnotationEditorType, AnnotationEditorType,
AnnotationType, AnnotationType,
assert,
FeatureTest, FeatureTest,
LINE_FACTOR, LINE_FACTOR,
shadow, shadow,
@ -156,6 +155,8 @@ class AnnotationElementFactory {
} }
class AnnotationElement { class AnnotationElement {
#hasBorder = false;
constructor( constructor(
parameters, parameters,
{ {
@ -182,7 +183,7 @@ class AnnotationElement {
this.container = this._createContainer(ignoreBorder); this.container = this._createContainer(ignoreBorder);
} }
if (createQuadrilaterals) { if (createQuadrilaterals) {
this.quadrilaterals = this._createQuadrilaterals(ignoreBorder); this._createQuadrilaterals();
} }
} }
@ -279,6 +280,7 @@ class AnnotationElement {
const borderColor = data.borderColor || null; const borderColor = data.borderColor || null;
if (borderColor) { if (borderColor) {
this.#hasBorder = true;
container.style.borderColor = Util.makeHexColor( container.style.borderColor = Util.makeHexColor(
borderColor[0] | 0, borderColor[0] | 0,
borderColor[1] | 0, borderColor[1] | 0,
@ -441,31 +443,90 @@ class AnnotationElement {
* Create quadrilaterals from the annotation's quadpoints. * Create quadrilaterals from the annotation's quadpoints.
* *
* @private * @private
* @param {boolean} ignoreBorder
* @memberof AnnotationElement * @memberof AnnotationElement
* @returns {Array<HTMLElement>} An array of section elements.
*/ */
_createQuadrilaterals(ignoreBorder = false) { _createQuadrilaterals() {
if (!this.data.quadPoints) { if (!this.container) {
return null; return;
}
const { quadPoints } = this.data;
if (!quadPoints) {
return;
} }
const quadrilaterals = []; const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect;
const savedRect = this.data.rect;
let firstQuadRect = null; if (quadPoints.length === 1) {
for (const quadPoint of this.data.quadPoints) { const [, { x: trX, y: trY }, { x: blX, y: blY }] = quadPoints[0];
this.data.rect = [ if (
quadPoint[2].x, rectTrX === trX &&
quadPoint[2].y, rectTrY === trY &&
quadPoint[1].x, rectBlX === blX &&
quadPoint[1].y, rectBlY === blY
]; ) {
quadrilaterals.push(this._createContainer(ignoreBorder)); // The quadpoints cover the whole annotation rectangle, so no need to
firstQuadRect ||= this.data.rect; // create a quadrilateral.
return;
}
} }
this.data.rect = savedRect;
this.firstQuadRect = firstQuadRect; const { style } = this.container;
return quadrilaterals; let svgBuffer;
if (this.#hasBorder) {
const { borderColor, borderWidth } = style;
style.borderWidth = 0;
svgBuffer = [
"url('data:image/svg+xml;utf8,",
`<svg xmlns="http://www.w3.org/2000/svg"`,
` preserveAspectRatio="none" viewBox="0 0 1 1">`,
`<g fill="transparent" stroke="${borderColor}" stroke-width="${borderWidth}">`,
];
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(
`<rect vector-effect="non-scaling-stroke" x="${x}" y="${y}" width="${rectWidth}" height="${rectHeight}"/>`
);
}
if (this.#hasBorder) {
svgBuffer.push(`</g></svg>')`);
style.backgroundImage = svgBuffer.join("");
}
this.container.append(svg);
this.container.style.clipPath = `url(#${id})`;
} }
/** /**
@ -487,7 +548,7 @@ class AnnotationElement {
modificationDate: data.modificationDate, modificationDate: data.modificationDate,
contentsObj: data.contentsObj, contentsObj: data.contentsObj,
richText: data.richText, richText: data.richText,
parentRect: this.firstQuadRect || data.rect, parentRect: data.rect,
borderStyle: 0, borderStyle: 0,
id: `popup_${data.id}`, id: `popup_${data.id}`,
rotation: data.rotation, rotation: data.rotation,
@ -498,32 +559,11 @@ class AnnotationElement {
this.parent.div.append(popup.render()); this.parent.div.append(popup.render());
} }
/**
* Render the quadrilaterals of the annotation.
*
* @private
* @param {string} className
* @memberof AnnotationElement
* @returns {Array<HTMLElement>} 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). * Render the annotation's HTML element(s).
* *
* @public * @public
* @memberof AnnotationElement * @memberof AnnotationElement
* @returns {HTMLElement|Array<HTMLElement>|undefined} A section element or
* an array of section elements.
*/ */
render() { render() {
unreachable("Abstract method `AnnotationElement.render` called"); unreachable("Abstract method `AnnotationElement.render` called");
@ -591,8 +631,16 @@ class AnnotationElement {
this.popup?.forceHide(); this.popup?.forceHide();
} }
/**
* Get the HTML element(s) which can trigger a popup when clicked or hovered.
*
* @public
* @memberof AnnotationElement
* @returns {Array<HTMLElement>|HTMLElement} An array of elements or an
* element.
*/
getElementsToTriggerPopup() { getElementsToTriggerPopup() {
return this.quadrilaterals || this.container; return this.container;
} }
addHighlightArea() { 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"); this.container.classList.add("linkAnnotation");
if (isBound) { if (isBound) {
this.container.append(link); this.container.append(link);
@ -2632,10 +2670,6 @@ class HighlightAnnotationElement extends AnnotationElement {
this._createPopup(); this._createPopup();
} }
if (this.quadrilaterals) {
return this._renderQuadrilaterals("highlightAnnotation");
}
this.container.classList.add("highlightAnnotation"); this.container.classList.add("highlightAnnotation");
return this.container; return this.container;
} }
@ -2661,10 +2695,6 @@ class UnderlineAnnotationElement extends AnnotationElement {
this._createPopup(); this._createPopup();
} }
if (this.quadrilaterals) {
return this._renderQuadrilaterals("underlineAnnotation");
}
this.container.classList.add("underlineAnnotation"); this.container.classList.add("underlineAnnotation");
return this.container; return this.container;
} }
@ -2690,10 +2720,6 @@ class SquigglyAnnotationElement extends AnnotationElement {
this._createPopup(); this._createPopup();
} }
if (this.quadrilaterals) {
return this._renderQuadrilaterals("squigglyAnnotation");
}
this.container.classList.add("squigglyAnnotation"); this.container.classList.add("squigglyAnnotation");
return this.container; return this.container;
} }
@ -2719,10 +2745,6 @@ class StrikeOutAnnotationElement extends AnnotationElement {
this._createPopup(); this._createPopup();
} }
if (this.quadrilaterals) {
return this._renderQuadrilaterals("strikeoutAnnotation");
}
this.container.classList.add("strikeoutAnnotation"); this.container.classList.add("strikeoutAnnotation");
return this.container; return this.container;
} }
@ -2971,13 +2993,7 @@ class AnnotationLayer {
if (data.hidden) { if (data.hidden) {
rendered.style.visibility = "hidden"; rendered.style.visibility = "hidden";
} }
if (Array.isArray(rendered)) { this.#appendElement(rendered, data.id);
for (const renderedElement of rendered) {
this.#appendElement(renderedElement, data.id);
}
} else {
this.#appendElement(rendered, data.id);
}
} }
this.#setAnnotationCanvasMap(); this.#setAnnotationCanvasMap();

View File

@ -29,7 +29,9 @@
-webkit-appearance: none; -webkit-appearance: none;
} }
.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a, .annotationLayer
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a,
.annotationLayer .popupTriggerArea::after, .annotationLayer .popupTriggerArea::after,
.annotationLayer .fileAttachmentAnnotation .popupTriggerArea { .annotationLayer .fileAttachmentAnnotation .popupTriggerArea {
opacity: 0.2; opacity: 0.2;
@ -37,6 +39,14 @@
box-shadow: 0 2px 10px rgba(255, 255, 0, 1); 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 { .annotationLayer .popupTriggerArea::after {
display: block; display: block;
width: 100%; width: 100%;

View File

@ -84,6 +84,9 @@ async function writeSVG(svgElement, ctx) {
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1844414 // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1844414
// we load the image two times. // we load the image two times.
await loadImage(svg_xml, null); await loadImage(svg_xml, null);
await new Promise(resolve => {
setTimeout(resolve, 10);
});
} }
return loadImage(svg_xml, ctx); return loadImage(svg_xml, ctx);
} }

View File

@ -120,13 +120,21 @@
} }
.annotationLayer .annotationLayer
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a:hover { > a:hover {
opacity: 0.2; 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); 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 { .annotationLayer .textAnnotation img {
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
@ -368,3 +376,13 @@
width: 100%; width: 100%;
display: inline-block; display: inline-block;
} }
.annotationLayer svg.quadrilateralsContainer {
contain: strict;
width: 0;
height: 0;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}