diff --git a/src/core/annotation.js b/src/core/annotation.js
index 6b3d8e642..c397f68e8 100644
--- a/src/core/annotation.js
+++ b/src/core/annotation.js
@@ -785,6 +785,14 @@ class Annotation {
this._needAppearances = false;
}
+ _getOperatorListNoAppearance() {
+ return {
+ opList: new OperatorList(),
+ separateForm: false,
+ separateCanvas: false,
+ };
+ }
+
/**
* @private
*/
@@ -1213,28 +1221,32 @@ class Annotation {
return resources;
}
+ // Whether the annotation should only be rendered on its own canvas when
+ // interactive forms are enabled. This is the case for checkbox/radio button
+ // widgets, whose checked/unchecked appearances are toggled in forms mode;
+ // other annotations (e.g. push buttons) keep their own canvas in any display
+ // mode.
+ get _ownCanvasRequiresForms() {
+ return false;
+ }
+
async getOperatorList(evaluator, task, intent, annotationStorage) {
const { hasOwnCanvas, id, rect } = this.data;
let appearance = this.appearance;
const isUsingOwnCanvas = !!(
- hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY
+ hasOwnCanvas &&
+ intent & RenderingIntentFlag.DISPLAY &&
+ (!this._ownCanvasRequiresForms ||
+ intent & RenderingIntentFlag.ANNOTATIONS_FORMS)
);
if (isUsingOwnCanvas && (this.width === 0 || this.height === 0)) {
// 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("", new Dict());
}
@@ -1244,7 +1256,12 @@ class Annotation {
RESOURCES_KEYS_OPERATOR_LIST,
appearance
);
- const bbox = lookupRect(appearanceDict.getArray("BBox"), [0, 0, 1, 1]);
+ const bbox = lookupRect(appearanceDict.getArray("BBox"), [
+ 0,
+ 0,
+ this.width,
+ this.height,
+ ]);
const matrix = lookupMatrix(
appearanceDict.getArray("Matrix"),
IDENTITY_MATRIX
@@ -2092,11 +2109,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) {
@@ -3148,20 +3163,58 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
this.data.radioButton = isRadio && !isPushButton;
this.data.pushButton = isPushButton;
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");
}
}
+ get _ownCanvasRequiresForms() {
+ return this.data.checkBox || this.data.radioButton;
+ }
+
+ #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(
@@ -3173,6 +3226,45 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
);
}
+ if (
+ intent & RenderingIntentFlag.DISPLAY &&
+ intent & RenderingIntentFlag.ANNOTATIONS_FORMS &&
+ (this.data.checkBox || this.data.radioButton)
+ ) {
+ // Tag the dedicated canvas with the state it represents. The appearance
+ // may start with other operators (e.g. an optional content marked-content
+ // sequence), so target the `beginAnnotation` operator directly rather
+ // than assuming it's the first one.
+ const setCanvasName = (operatorList, name) => {
+ const index = operatorList.fnArray.indexOf(OPS.beginAnnotation);
+ if (index !== -1) {
+ operatorList.argsArray[index].push(name);
+ }
+ };
+ const checked = await this.#getOperatorListForAppearance(
+ evaluator,
+ task,
+ intent,
+ annotationStorage,
+ null,
+ this.checkedAppearance
+ );
+ setCanvasName(checked.opList, "checked");
+ const unchecked = await this.#getOperatorListForAppearance(
+ evaluator,
+ task,
+ intent,
+ annotationStorage,
+ null,
+ this.uncheckedAppearance
+ );
+ setCanvasName(unchecked.opList, "unchecked");
+ checked.opList.addOpList(unchecked.opList);
+ checked.separateForm ||= unchecked.separateForm;
+ checked.separateCanvas ||= unchecked.separateCanvas;
+ return checked;
+ }
+
let value = null;
let rotation = null;
if (annotationStorage) {
@@ -3195,41 +3287,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, changes) {
@@ -3422,13 +3487,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
_processCheckBox(params) {
const customAppearance = params.dict.get("AP");
- if (!(customAppearance instanceof Dict)) {
- return;
- }
-
- const normalAppearance = customAppearance.get("N");
+ let normalAppearance =
+ customAppearance instanceof Dict ? customAppearance.get("N") : null;
if (!(normalAppearance instanceof Dict)) {
- return;
+ // Synthesize a default appearance below when the field defines none.
+ normalAppearance = null;
}
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1722036.
@@ -3444,7 +3507,9 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
: "Yes";
// Don't decode the keys which are names.
- const exportValues = [...normalAppearance.getKeys()];
+ const exportValues = normalAppearance
+ ? [...normalAppearance.getKeys()]
+ : [];
if (exportValues.length === 0) {
exportValues.push("Off", yes);
} else if (exportValues.length === 1) {
@@ -3470,10 +3535,10 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
this.data.exportValue = exportValues[1];
- const checkedAppearance = normalAppearance.get(this.data.exportValue);
+ const checkedAppearance = normalAppearance?.get(this.data.exportValue);
this.checkedAppearance =
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
- const uncheckedAppearance = normalAppearance.get("Off");
+ const uncheckedAppearance = normalAppearance?.get("Off");
this.uncheckedAppearance =
uncheckedAppearance instanceof BaseStream ? uncheckedAppearance : null;
diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js
index 00408d9f0..e10c89f62 100644
--- a/src/display/annotation_layer.js
+++ b/src/display/annotation_layer.js
@@ -439,9 +439,6 @@ class AnnotationElement {
if (horizontalRadius > 0 || verticalRadius > 0) {
const radius = `calc(${horizontalRadius}px * var(--total-scale-factor)) / calc(${verticalRadius}px * var(--total-scale-factor))`;
style.borderRadius = radius;
- } else if (this instanceof RadioButtonWidgetAnnotationElement) {
- const radius = `calc(${width}px * var(--total-scale-factor)) / calc(${height}px * var(--total-scale-factor))`;
- style.borderRadius = radius;
}
switch (data.borderStyle.style) {
@@ -2009,7 +2006,6 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
);
}
- this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element);
this.container.append(element);
@@ -2028,7 +2024,9 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
const data = this.data;
const id = data.id;
let value = storage.getValue(id, {
- value: data.fieldValue === data.buttonValue,
+ // A radio without an on-state (`buttonValue === null`, e.g. no /AP) must
+ // not be checked by default, otherwise `null === null` would select it.
+ value: data.buttonValue !== null && data.fieldValue === data.buttonValue,
}).value;
if (typeof value === "string") {
// The value has been changed through js and set in annotationStorage.
@@ -2115,7 +2113,6 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
);
}
- this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element);
this.container.append(element);
@@ -4139,18 +4136,44 @@ 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];
+ }
+ }
+ // Drop only the entries we inserted; keep ones whose element isn't in the
+ // DOM yet so a later refresh can still pick them up instead of losing
+ // them.
+ this.#annotationCanvasMap.delete(id);
const editableAnnotation = this.#editableAnnotations.get(id);
if (!editableAnnotation) {
@@ -4168,7 +4191,12 @@ class AnnotationLayer {
editableAnnotation.canvas = canvas;
}
}
- this.#annotationCanvasMap.clear();
+ }
+
+ // Move any pending annotation canvases (e.g. higher-resolution ones rendered
+ // by the detail view) into their elements.
+ refreshCanvases() {
+ this.#setAnnotationCanvasMap();
}
getEditableAnnotations() {
diff --git a/src/display/canvas.js b/src/display/canvas.js
index 8561a4939..b8d8fecb8 100644
--- a/src/display/canvas.js
+++ b/src/display/canvas.js
@@ -3630,7 +3630,15 @@ class CanvasGraphics {
}
}
- beginAnnotation(opIdx, id, rect, transform, matrix, hasOwnCanvas) {
+ beginAnnotation(
+ opIdx,
+ 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...
@@ -3673,7 +3681,26 @@ 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);
+ // Replace any same-named canvas from a previous render so stale
+ // low-resolution canvases don't pile up across zooms.
+ const index = canvases.findIndex(
+ c => c.getAttribute("data-canvas-name") === canvasName
+ );
+ if (index === -1) {
+ canvases.push(canvas);
+ } else {
+ canvases[index] = canvas;
+ }
+ } else {
+ this.annotationCanvasMap.set(id, canvas);
+ }
this.annotationCanvas.savedCtx = this.ctx;
this.ctx = context;
this.ctx.save();
diff --git a/test/annotation_layer_builder_overrides.css b/test/annotation_layer_builder_overrides.css
index 8bcf91d0c..59d88e6f5 100644
--- a/test/annotation_layer_builder_overrides.css
+++ b/test/annotation_layer_builder_overrides.css
@@ -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;
+ }
+ }
+ }
}
diff --git a/test/driver.js b/test/driver.js
index 0ac1b7374..473f1ef81 100644
--- a/test/driver.js
+++ b/test/driver.js
@@ -94,6 +94,7 @@ async function writeSVG(svgElement, ctx) {
setTimeout(resolve, 10);
});
}
+
return loadImage(svg_xml, ctx);
}
@@ -150,21 +151,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;
diff --git a/test/integration/annotation_spec.mjs b/test/integration/annotation_spec.mjs
index 535f68c6a..e448e864c 100644
--- a/test/integration/annotation_spec.mjs
+++ b/test/integration/annotation_spec.mjs
@@ -122,12 +122,12 @@ describe("Checkbox annotation", () => {
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
- `document.querySelector('${selector} > :first-child').checked`
+ `document.querySelector('${selector} input').checked`
);
for (const otherSelector of selectors) {
const checked = await page.$eval(
- `${otherSelector} > :first-child`,
+ `${otherSelector} input`,
el => el.checked
);
expect(checked)
@@ -157,7 +157,7 @@ describe("Checkbox annotation", () => {
const selector = getAnnotationSelector("7R");
await page.click(selector);
await page.waitForFunction(
- `document.querySelector('${selector} > :first-child').checked`
+ `document.querySelector('${selector} input').checked`
);
expect(true).withContext(`In ${browserName}`).toEqual(true);
})
@@ -185,7 +185,7 @@ describe("Checkbox annotation", () => {
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
- `document.querySelector('${selector} > :first-child').checked`
+ `document.querySelector('${selector} input').checked`
);
}
})
diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs
index 82d38af8e..9ed732450 100644
--- a/test/integration/scripting_spec.mjs
+++ b/test/integration/scripting_spec.mjs
@@ -2413,49 +2413,6 @@ describe("Interaction", () => {
});
});
- describe("Change radio property", () => {
- let pages;
-
- beforeEach(async () => {
- pages = await loadAndWait("bug1922766.pdf", getAnnotationSelector("44R"));
- });
-
- afterEach(async () => {
- await closePages(pages);
- });
-
- it("must check that a change on a radio implies the change on all the radio in the group", async () => {
- await Promise.all(
- pages.map(async ([browserName, page]) => {
- await waitForScripting(page);
-
- const checkColor = async color => {
- await waitForSandboxTrip(page);
- for (const i of [40, 41, 42, 43]) {
- const bgColor = await page.$eval(
- `[data-element-id='${i}R']`,
- el => getComputedStyle(el).backgroundColor
- );
- expect(bgColor)
- .withContext(`In ${browserName}`)
- .toEqual(`rgb(${color.join(", ")})`);
- }
- };
- await checkColor([255, 0, 0]);
- await page.click(getAnnotationSelector("44R"));
- await checkColor([0, 0, 255]);
- await page.click(getAnnotationSelector("44R"));
- await checkColor([255, 0, 0]);
-
- await page.click(getAnnotationSelector("43R"));
- await waitForSandboxTrip(page);
- await page.click(getAnnotationSelector("44R"));
- await checkColor([0, 0, 255]);
- })
- );
- });
- });
-
describe("Date creation must be timezone consistent", () => {
let pages;
diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore
index 4d2c9f005..bf14c7859 100644
--- a/test/pdfs/.gitignore
+++ b/test/pdfs/.gitignore
@@ -932,3 +932,5 @@
!cidfont_cmap_overflow.pdf
!jbig2_file_header.pdf
!text_field_own_canvas_calc.pdf
+!bug1802506.pdf
+!checkbox_no_appearance.pdf
diff --git a/test/pdfs/bug1802506.pdf b/test/pdfs/bug1802506.pdf
new file mode 100644
index 000000000..aa2355bbf
Binary files /dev/null and b/test/pdfs/bug1802506.pdf differ
diff --git a/test/pdfs/checkbox_no_appearance.pdf b/test/pdfs/checkbox_no_appearance.pdf
new file mode 100644
index 000000000..416e6aaef
--- /dev/null
+++ b/test/pdfs/checkbox_no_appearance.pdf
@@ -0,0 +1,30 @@
+%PDF-1.7
+%ÿÿÿÿ
+1 0 obj
+<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] /NeedAppearances true >> >>
+endobj
+2 0 obj
+<< /Type /Pages /Kids [3 0 R] /Count 1 >>
+endobj
+3 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [4 0 R 5 0 R] /Resources << >> >>
+endobj
+4 0 obj
+<< /Type /Annot /Subtype /Widget /FT /Btn /T (cbOn) /Rect [30 140 70 180] /V /Yes /AS /Yes >>
+endobj
+5 0 obj
+<< /Type /Annot /Subtype /Widget /FT /Btn /T (cbOff) /Rect [90 140 130 180] /V /Off /AS /Off >>
+endobj
+xref
+0 6
+0000000000 65535 f
+0000000015 00000 n
+0000000124 00000 n
+0000000181 00000 n
+0000000291 00000 n
+0000000400 00000 n
+trailer
+<< /Size 6 /Root 1 0 R >>
+startxref
+511
+%%EOF
diff --git a/test/test_manifest.json b/test/test_manifest.json
index aef2038d3..ec78fd257 100644
--- a/test/test_manifest.json
+++ b/test/test_manifest.json
@@ -14380,5 +14380,23 @@
"rounds": 1,
"link": true,
"type": "eq"
+ },
+ {
+ "id": "bug1802506",
+ "file": "pdfs/bug1802506.pdf",
+ "md5": "ed56da1780b8480262c7329c4419fbb5",
+ "rounds": 1,
+ "type": "eq",
+ "annotations": true,
+ "forms": true
+ },
+ {
+ "id": "checkbox_no_appearance",
+ "file": "pdfs/checkbox_no_appearance.pdf",
+ "md5": "216ff8ea24c470421ff1c528d805caea",
+ "rounds": 1,
+ "type": "eq",
+ "annotations": true,
+ "forms": true
}
]
diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js
index cf622848e..7e3ca6658 100644
--- a/test/unit/annotation_spec.js
+++ b/test/unit/annotation_spec.js
@@ -1776,7 +1776,7 @@ describe("annotation", function () {
expect(opList.argsArray[0]).toEqual([
"271R",
[0, 0, 32, 10],
- [32, 0, 0, 10, 0, 0],
+ [1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
false,
]);
@@ -2607,6 +2607,44 @@ describe("annotation", function () {
expect(opList.argsArray[3][0][0].unicode).toEqual("4");
});
+ it("should synthesize a checked appearance for checkboxes without an /AP", async function () {
+ buttonWidgetDict.set("V", Name.get("Checked"));
+ buttonWidgetDict.set("Rect", [0, 0, 20, 20]);
+ // Note: no /AP entry, so a default appearance must be synthesized.
+
+ const buttonWidgetRef = Ref.get(124, 0);
+ const xref = new XRefMock([
+ { ref: buttonWidgetRef, data: buttonWidgetDict },
+ ]);
+ const task = new WorkerTask("test checkbox without /AP");
+ const checkboxEvaluator = partialEvaluator.clone({ ignoreErrors: true });
+ const annotation = await AnnotationFactory.create(
+ xref,
+ buttonWidgetRef,
+ annotationGlobalsMock,
+ idFactoryMock
+ );
+
+ expect(annotation.data.checkBox).toEqual(true);
+ expect(annotation.data.exportValue).toEqual("Checked");
+
+ const { opList } = await annotation.getOperatorList(
+ checkboxEvaluator,
+ task,
+ RenderingIntentFlag.DISPLAY | RenderingIntentFlag.ANNOTATIONS_FORMS,
+ new Map()
+ );
+
+ // A checkmark is drawn on its own dedicated "checked" canvas.
+ expect(opList.fnArray[0]).toEqual(OPS.beginAnnotation);
+ expect(opList.fnArray.at(-1)).toEqual(OPS.endAnnotation);
+ expect(opList.fnArray).toContain(OPS.showText);
+ const [id, , , , isUsingOwnCanvas, canvasName] = opList.argsArray[0];
+ expect(id).toEqual("124R");
+ expect(isUsingOwnCanvas).toEqual(true);
+ expect(canvasName).toEqual("checked");
+ });
+
it("should render checkboxes for printing", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
@@ -2685,7 +2723,7 @@ describe("annotation", function () {
expect(opList2.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
- [0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
false,
]);
@@ -3084,7 +3122,7 @@ describe("annotation", function () {
expect(opList2.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
- [0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
false,
]);
@@ -3147,7 +3185,7 @@ describe("annotation", function () {
expect(opList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
- [0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
false,
]);
diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css
index 833dc173e..bf4826a60 100644
--- a/web/annotation_layer_builder.css
+++ b/web/annotation_layer_builder.css
@@ -17,6 +17,7 @@
color-scheme: only light;
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,");
+ --annotation-unfocused-field-filter: url("data:image/svg+xml;charset=UTF-8,#pdfjsFillableField");
--input-focus-border-color: Highlight;
--input-focus-outline: 1px solid Canvas;
--input-unfocused-border-color: transparent;
@@ -208,10 +209,6 @@
padding: 0;
}
- .buttonWidgetAnnotation.radioButton input {
- border-radius: 50%;
- }
-
.textWidgetAnnotation textarea {
resize: none;
}
@@ -259,36 +256,34 @@
outline: var(--input-focus-outline);
}
- .buttonWidgetAnnotation.checkBox input:checked::before,
- .buttonWidgetAnnotation.checkBox input:checked::after,
- .buttonWidgetAnnotation.radioButton input:checked::before {
- background-color: CanvasText;
- content: "";
- display: block;
- position: absolute;
- }
+ .buttonWidgetAnnotation:is(.checkBox, .radioButton) {
+ [data-canvas-name] {
+ filter: var(--annotation-unfocused-field-filter);
+ }
- .buttonWidgetAnnotation.checkBox input:checked::before,
- .buttonWidgetAnnotation.checkBox input:checked::after {
- height: 80%;
- left: 45%;
- width: 1px;
- }
+ &:focus-within [data-canvas-name] {
+ filter: none;
+ }
- .buttonWidgetAnnotation.checkBox input:checked::before {
- transform: rotate(45deg);
- }
+ [data-canvas-name="checked"] {
+ &:has(~ input:checked) {
+ display: block;
+ }
- .buttonWidgetAnnotation.checkBox input:checked::after {
- transform: rotate(-45deg);
- }
+ &:has(~ input:not(:checked)) {
+ display: none;
+ }
+ }
- .buttonWidgetAnnotation.radioButton input:checked::before {
- border-radius: 50%;
- height: 50%;
- left: 25%;
- top: 25%;
- width: 50%;
+ [data-canvas-name="unchecked"] {
+ &:has(~ input:checked) {
+ display: none;
+ }
+
+ &:has(~ input:not(:checked)) {
+ display: block;
+ }
+ }
}
.textWidgetAnnotation input.comb {
@@ -317,6 +312,17 @@
appearance: none;
}
+ .buttonWidgetAnnotation:is(.checkBox, .radioButton):has(
+ [data-canvas-name="checked"]
+ )
+ input:checked,
+ .buttonWidgetAnnotation:is(.checkBox, .radioButton):has(
+ [data-canvas-name="unchecked"]
+ )
+ input:not(:checked) {
+ background-image: none;
+ }
+
.fileAttachmentAnnotation .popupTriggerArea {
height: 100%;
width: 100%;
diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js
index bb30dd6bb..4139210fa 100644
--- a/web/annotation_layer_builder.js
+++ b/web/annotation_layer_builder.js
@@ -224,6 +224,10 @@ class AnnotationLayerBuilder {
this.#eventAC = null;
}
+ refreshCanvases() {
+ this.annotationLayer?.refreshCanvases();
+ }
+
hide() {
if (!this.div) {
return;
diff --git a/web/pdf_page_detail_view.js b/web/pdf_page_detail_view.js
index eda8d5cea..3b975f383 100644
--- a/web/pdf_page_detail_view.js
+++ b/web/pdf_page_detail_view.js
@@ -297,6 +297,7 @@ class PDFPageDetailView extends BasePDFPageView {
this.canvas = prevCanvas;
},
() => {
+ this.pageView._refreshAnnotationLayer();
this.dispatchPageRendered(
/* cssTransform */ false,
/* isDetailView */ true
diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js
index 58958f3d1..3ee59520c 100644
--- a/web/pdf_page_view.js
+++ b/web/pdf_page_view.js
@@ -484,6 +484,17 @@ class PDFPageView extends BasePDFPageView {
}
}
+ // The detail view re-renders checkbox/radio appearances at a higher
+ // resolution into the shared `annotationCanvasMap`; move them into the
+ // annotation layer so they replace the lower-resolution ones. This only
+ // consumes pending canvases (no re-render), and is a no-op until their
+ // elements exist, so it's safe to call while the layer is still rendering.
+ _refreshAnnotationLayer() {
+ if (this._annotationCanvasMap?.size) {
+ this.annotationLayer?.refreshCanvases();
+ }
+ }
+
async #renderAnnotationEditorLayer() {
let error = null;
try {