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,
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<HTMLElement>} 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,",
`<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,
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<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).
*
* @public
* @memberof AnnotationElement
* @returns {HTMLElement|Array<HTMLElement>|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>|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();

View File

@ -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%;

View File

@ -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);
}

View File

@ -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;
}