diff --git a/src/core/annotation.js b/src/core/annotation.js index fe48f13872d52f..1c0f13698fefe9 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -725,6 +725,14 @@ class Annotation { this._needAppearances = false; } + _getOperatorListNoAppearance() { + return { + opList: new OperatorList(), + separateForm: false, + separateCanvas: false, + }; + } + /** * @private */ @@ -1160,19 +1168,11 @@ class Annotation { 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(); @@ -2020,11 +2020,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) { @@ -2994,20 +2992,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( @@ -3019,6 +3051,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) { @@ -3041,41 +3104,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) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 955b16f363303a..0f659e91a81365 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -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) { @@ -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(); diff --git a/src/display/canvas.js b/src/display/canvas.js index 13f0790a9123b6..1d77405aa801f2 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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... @@ -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(); diff --git a/test/annotation_layer_builder_overrides.css b/test/annotation_layer_builder_overrides.css index 8bcf91d0c95eec..59d88e6f58103f 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 8ba30b0bfd3ced..5869c1f4e3dd0c 100644 --- a/test/driver.js +++ b/test/driver.js @@ -88,6 +88,7 @@ async function writeSVG(svgElement, ctx) { setTimeout(resolve, 10); }); } + return loadImage(svg_xml, ctx); } @@ -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; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index fa61a789c7d193..89f40b441fe663 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -676,3 +676,4 @@ !issue15096.pdf !issue18036.pdf !issue18894.pdf +!bug1802506.pdf diff --git a/test/pdfs/bug1802506.pdf b/test/pdfs/bug1802506.pdf new file mode 100755 index 00000000000000..aa2355bbfeda8b Binary files /dev/null and b/test/pdfs/bug1802506.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index dcf9307fbf03cb..cfe7bed66a30b8 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10709,5 +10709,15 @@ "type": "eq", "link": true, "talos": false + }, + { + "id": "bug1802506", + "file": "pdfs/bug1802506.pdf", + "md5": "ed56da1780b8480262c7329c4419fbb5", + "rounds": 1, + "type": "eq", + "annotations": true, + "forms": true, + "talos": false } ] diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 3047adbb2ecfd3..46e5d889b6f040 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -186,10 +186,6 @@ padding: 0; } - .buttonWidgetAnnotation.radioButton input { - border-radius: 50%; - } - .textWidgetAnnotation textarea { resize: none; } @@ -237,36 +233,26 @@ 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.checkBox input:checked::before, - .buttonWidgetAnnotation.checkBox input:checked::after { - height: 80%; - left: 45%; - width: 1px; - } + .buttonWidgetAnnotation:is(.checkBox, .radioButton) { + canvas[data-canvas-name="checked"] { + &:has(~ input:checked) { + display: block; + } - .buttonWidgetAnnotation.checkBox input:checked::before { - transform: rotate(45deg); - } + &:has(~ input:not(:checked)) { + display: none; + } + } - .buttonWidgetAnnotation.checkBox input:checked::after { - transform: rotate(-45deg); - } + canvas[data-canvas-name="unchecked"] { + &:has(~ input:checked) { + display: none; + } - .buttonWidgetAnnotation.radioButton input:checked::before { - border-radius: 50%; - height: 50%; - left: 25%; - top: 25%; - width: 50%; + &:has(~ input:not(:checked)) { + display: block; + } + } } .textWidgetAnnotation input.comb {