Skip to content
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

Use the checkboxes and radio button appearances as defined in the pdf to render them in the annotation layer (bug 1802506) #18907

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
144 changes: 91 additions & 53 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,14 @@ class Annotation {
this._needAppearances = false;
}

_getOperatorListNoAppearance() {
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
}

/**
* @private
*/
Expand Down Expand Up @@ -1155,24 +1163,18 @@ class Annotation {
const { hasOwnCanvas, id, rect } = this.data;
let appearance = this.appearance;
const isUsingOwnCanvas = !!(
hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY
hasOwnCanvas &&
intent & RenderingIntentFlag.DISPLAY &&
intent & RenderingIntentFlag.ANNOTATIONS_FORMS
);
if (isUsingOwnCanvas && (rect[0] === rect[2] || rect[1] === rect[3])) {
// Empty annotation, don't draw anything.
this.data.hasOwnCanvas = false;
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
return this._getOperatorListNoAppearance();
}
if (!appearance) {
if (!isUsingOwnCanvas) {
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
return this._getOperatorListNoAppearance();
}
appearance = new StringStream("");
appearance.dict = new Dict();
Expand Down Expand Up @@ -2020,11 +2022,9 @@ class WidgetAnnotation extends Annotation {
!this.data.noHTML &&
!this.data.hasOwnCanvas
) {
return {
opList: new OperatorList(),
separateForm: true,
separateCanvas: false,
};
const list = this._getOperatorListNoAppearance();
list.separateForm = true;
return list;
}

if (!this._hasText) {
Expand Down Expand Up @@ -2994,20 +2994,54 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
!this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON);
this.data.pushButton = this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON);
this.data.isTooltipOnly = false;
this.data.hasOwnCanvas = true;
this.data.noHTML = false;

if (this.data.checkBox) {
this._processCheckBox(params);
} else if (this.data.radioButton) {
this._processRadioButton(params);
} else if (this.data.pushButton) {
this.data.hasOwnCanvas = true;
this.data.noHTML = false;
this._processPushButton(params);
} else {
warn("Invalid field flags for button widget annotation");
}
}

#getOperatorListForAppearance(
evaluator,
task,
intent,
annotationStorage,
rotation,
appearance
) {
if (!appearance) {
return this._getOperatorListNoAppearance();
}

const savedAppearance = this.appearance;
const savedMatrix = lookupMatrix(
appearance.dict.getArray("Matrix"),
IDENTITY_MATRIX
);

if (rotation) {
appearance.dict.set("Matrix", this.getRotationMatrix(annotationStorage));
}

this.appearance = appearance;
const operatorList = super.getOperatorList(
evaluator,
task,
intent,
annotationStorage
);
this.appearance = savedAppearance;
appearance.dict.set("Matrix", savedMatrix);
return operatorList;
}

async getOperatorList(evaluator, task, intent, annotationStorage) {
if (this.data.pushButton) {
return super.getOperatorList(
Expand All @@ -3019,6 +3053,37 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
);
}

if (
intent & RenderingIntentFlag.DISPLAY &&
intent & RenderingIntentFlag.ANNOTATIONS_FORMS &&
(this.data.checkBox || this.data.radioButton)
) {
const checked = await this.#getOperatorListForAppearance(
evaluator,
task,
intent,
annotationStorage,
null,
this.checkedAppearance
);
if (checked.opList.argsArray?.[0]) {
checked.opList.argsArray[0].push("checked");
}
const unchecked = await this.#getOperatorListForAppearance(
evaluator,
task,
intent,
annotationStorage,
null,
this.uncheckedAppearance
);
if (unchecked.opList.argsArray?.[0]) {
unchecked.opList.argsArray[0].push("unchecked");
}
checked.opList.addOpList(unchecked.opList);
return checked;
}

let value = null;
let rotation = null;
if (annotationStorage) {
Expand All @@ -3041,41 +3106,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
: this.data.fieldValue === this.data.buttonValue;
}

const appearance = value
? this.checkedAppearance
: this.uncheckedAppearance;
if (appearance) {
const savedAppearance = this.appearance;
const savedMatrix = lookupMatrix(
appearance.dict.getArray("Matrix"),
IDENTITY_MATRIX
);

if (rotation) {
appearance.dict.set(
"Matrix",
this.getRotationMatrix(annotationStorage)
);
}

this.appearance = appearance;
const operatorList = super.getOperatorList(
evaluator,
task,
intent,
annotationStorage
);
this.appearance = savedAppearance;
appearance.dict.set("Matrix", savedMatrix);
return operatorList;
}

// No appearance
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
return this.#getOperatorListForAppearance(
evaluator,
task,
intent,
annotationStorage,
rotation,
value ? this.checkedAppearance : this.uncheckedAppearance
);
}

async save(evaluator, task, annotationStorage) {
Expand Down
39 changes: 29 additions & 10 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,6 @@ class AnnotationElement {
if (horizontalRadius > 0 || verticalRadius > 0) {
const radius = `calc(${horizontalRadius}px * var(--scale-factor)) / calc(${verticalRadius}px * var(--scale-factor))`;
style.borderRadius = radius;
} else if (this instanceof RadioButtonWidgetAnnotationElement) {
const radius = `calc(${width}px * var(--scale-factor)) / calc(${height}px * var(--scale-factor))`;
style.borderRadius = radius;
}

switch (data.borderStyle.style) {
Expand Down Expand Up @@ -3240,17 +3237,39 @@ class AnnotationLayer {
if (!element) {
continue;
}

canvas.className = "annotationContent";
if (Array.isArray(canvas)) {
for (const cvs of canvas) {
cvs.className = "annotationContent";
cvs.ariaHidden = true;
}
} else {
canvas.className = "annotationContent";
canvas.ariaHidden = true;
}
const toRemove = [];
for (const child of element.children) {
if (child.nodeName === "CANVAS") {
toRemove.push(child);
}
}
for (const child of toRemove) {
child.remove();
}
const firstCanvas = Array.isArray(canvas) ? canvas[0] : canvas;
const { firstChild } = element;
if (!firstChild) {
element.append(canvas);
} else if (firstChild.nodeName === "CANVAS") {
firstChild.replaceWith(canvas);
element.append(firstCanvas);
} else if (!firstChild.classList.contains("annotationContent")) {
firstChild.before(canvas);
firstChild.before(firstCanvas);
} else {
firstChild.after(canvas);
firstChild.after(firstCanvas);
}
if (Array.isArray(canvas)) {
let lastCanvas = firstCanvas;
for (let i = 1, ii = canvas.length; i < ii; i++) {
lastCanvas.after(canvas[i]);
lastCanvas = canvas[i];
}
}
}
this.#annotationCanvasMap.clear();
Expand Down
14 changes: 12 additions & 2 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -2646,7 +2646,7 @@ class CanvasGraphics {
}
}

beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) {
beginAnnotation(id, rect, transform, matrix, hasOwnCanvas, canvasName) {
// The annotations are drawn just after the page content.
// The page content drawing can potentially have set a transform,
// a clipping path, whatever...
Expand Down Expand Up @@ -2691,7 +2691,17 @@ class CanvasGraphics {
canvasHeight
);
const { canvas, context } = this.annotationCanvas;
this.annotationCanvasMap.set(id, canvas);
if (canvasName) {
let canvases = this.annotationCanvasMap.get(id);
if (!canvases) {
canvases = [];
this.annotationCanvasMap.set(id, canvases);
}
canvas.setAttribute("data-canvas-name", canvasName);
canvases.push(canvas);
} else {
this.annotationCanvasMap.set(id, canvas);
}
this.annotationCanvas.savedCtx = this.ctx;
this.ctx = context;
this.ctx.save();
Expand Down
22 changes: 22 additions & 0 deletions test/annotation_layer_builder_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,26 @@
color: red;
font-size: 10px;
}

.buttonWidgetAnnotation:is(.checkBox, .radioButton) {
img[data-canvas-name="checked"] {
&:has(~ input:checked) {
display: block;
}

&:has(~ input:not(:checked)) {
display: none;
}
}

img[data-canvas-name="unchecked"] {
&:has(~ input:checked) {
display: none;
}

&:has(~ input:not(:checked)) {
display: block;
}
}
}
}
48 changes: 34 additions & 14 deletions test/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ async function writeSVG(svgElement, ctx) {
setTimeout(resolve, 10);
});
}

return loadImage(svg_xml, ctx);
}

Expand Down Expand Up @@ -144,21 +145,40 @@ async function inlineImages(node, silentErrors = false) {
async function convertCanvasesToImages(annotationCanvasMap, outputScale) {
const results = new Map();
const promises = [];
const canvasToImage = (canvas, key) => {
const { promise, resolve } = Promise.withResolvers();
promises.push(promise);
canvas.toBlob(blob => {
const image = document.createElement("img");
image.classList.add("wasCanvas");
image.onload = function () {
image.style.width = Math.floor(image.width / outputScale) + "px";
resolve();
};
const canvasName = canvas.getAttribute("data-canvas-name");
if (canvasName) {
image.setAttribute("data-canvas-name", canvasName);
let images = results.get(key);
if (!images) {
images = [];
results.set(key, images);
}
images.push(image);
} else {
results.set(key, image);
}
image.src = URL.createObjectURL(blob);
});
};

for (const [key, canvas] of annotationCanvasMap) {
promises.push(
new Promise(resolve => {
canvas.toBlob(blob => {
const image = document.createElement("img");
image.classList.add("wasCanvas");
image.onload = function () {
image.style.width = Math.floor(image.width / outputScale) + "px";
resolve();
};
results.set(key, image);
image.src = URL.createObjectURL(blob);
});
})
);
if (Array.isArray(canvas)) {
for (const canvasItem of canvas) {
canvasToImage(canvasItem, key);
}
} else {
canvasToImage(canvas, key);
}
}
await Promise.all(promises);
return results;
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -676,3 +676,4 @@
!issue15096.pdf
!issue18036.pdf
!issue18894.pdf
!bug1802506.pdf
Binary file added test/pdfs/bug1802506.pdf
Binary file not shown.
Loading
Loading