Skip to content

[Annotation] Use the clip-path property when an annotation has some quad points #16492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 95 additions & 79 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
AnnotationBorderStyleType,
AnnotationEditorType,
AnnotationType,
assert,
FeatureTest,
LINE_FACTOR,
shadow,
Expand Down Expand Up @@ -156,6 +155,8 @@ class AnnotationElementFactory {
}

class AnnotationElement {
#hasBorder = false;

constructor(
parameters,
{
Expand All @@ -182,7 +183,7 @@ class AnnotationElement {
this.container = this._createContainer(ignoreBorder);
}
if (createQuadrilaterals) {
this.quadrilaterals = this._createQuadrilaterals(ignoreBorder);
this._createQuadrilaterals();
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

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,
_createQuadrilaterals() {
if (!this.container) {
return;
}
const { quadPoints } = this.data;
if (!quadPoints) {
return;
}

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

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}">`,
];
quadrilaterals.push(this._createContainer(ignoreBorder));
firstQuadRect ||= this.data.rect;
this.container.classList.add("hasBorder");
}
this.data.rect = savedRect;
this.firstQuadRect = firstQuadRect;
return quadrilaterals;

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

/**
Expand All @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 11 additions & 1 deletion test/annotation_layer_builder_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,24 @@
-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;
background: 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 {
display: block;
width: 100%;
Expand Down
3 changes: 3 additions & 0 deletions test/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
22 changes: 20 additions & 2 deletions web/annotation_layer_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}