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 {