Skip to content

Commit

Permalink
Use the checkboxes and radio button appearances as defined in the pdf…
Browse files Browse the repository at this point in the history
… to render them in the annotation layer (bug 1802506)

The idea is to generate two operator lists for the Yes/Off states and render them on a separate canvas.
These canvases are then attached the annotation and we modify their display depending on the input state.

It fixes #18021.
  • Loading branch information
calixteman committed Oct 16, 2024
1 parent c10d093 commit b9b6b20
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 110 deletions.
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

0 comments on commit b9b6b20

Please sign in to comment.