From 7f6cde6f6ed4960cffeff4f794ebc45c461a55f4 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 15 Oct 2024 19:22:57 +0200 Subject: [PATCH] Use the checkboxes and radio button appearances as defined in the pdf 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. --- src/core/annotation.js | 140 ++++++++++++-------- src/display/annotation_layer.js | 39 ++++-- src/display/canvas.js | 14 +- test/annotation_layer_builder_overrides.css | 22 +++ test/driver.js | 48 +++++-- test/pdfs/.gitignore | 1 + test/pdfs/bug1802506.pdf | Bin 0 -> 9380 bytes test/test_manifest.json | 10 ++ web/annotation_layer_builder.css | 48 +++---- 9 files changed, 213 insertions(+), 109 deletions(-) create mode 100755 test/pdfs/bug1802506.pdf 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 0000000000000000000000000000000000000000..aa2355bbfeda8b8c2b0eeb6c08d775d1194db7cf GIT binary patch literal 9380 zcmeHNd011|wvT`sL%^X9D1wI+M3CeRnPZ3n0tzOEhDk+)kQ^WkNle0Al`x33IMO=T zA_`V(9ngxFA_~f_0|JU#QE@`CIN(^sq23b)i8Xz%-skQ2{xIK1IA@==)?VwketVz2 zSGJSCz=sS|7*hlY$^)^#Q@9zy#X4P z#ssHh&4BR0w9n41|UVe0X7?uTqYtgkpgs zkl`ju1;A9yK?x{CmC?EwfC(GCNiqZqgA$cW2f*~M=K&Zr0F?#?rm9hJh6IfQLqcHy z0bmRQtxkhV6RZd^&5@DvArTRg5wT{&h`afjbNerPW=Xp4Ve#oy#%RIaesi5OX5{j> z9|O)g0S~1k%s=_GTC%JkG5_|F1vT>);LM0P^D{&|;gf2Hc83HAK*pMpM{fsM31lV>7X=vS2U(pdnx|z-GSXeMH37sE7zeiZkQc z4+uPDRx|b!7Kg(bv+T%$jjQ#Cm+8->?P;5NN|E-$S$R^OF_nry_?MaW#4`QrJiW&Q z*X%l-%~UP2ODIwW4%ixnM)@ImP zl~RW)4fh5S3FxI!sXbK~FJOp*&@pl&6dIciP?;VN}b{Htacq%mrLKFxv z{4rnz`)0DS?=Z|I4Q900Ckd)mC2C|SMs_+LDVo)1H%BAA$r50?Pf5Y1&X9PjRK3N$4P^uM%Q7Wk!u*+Rk8q77Ybr?14d;q zATd&Bpt!+s581_$v8YVfN0g~~t}RI5)KO*%pQNPh?3Ana zD=t|SkIK#tT_-nKTlA?McD^XJwKf8nq^r^yer*`0iMM_Ki}gD%BU zkNUZ;51b^qF)P$zL7~(AIL4zhmA;Pa=1=>hrs2j>aYjE}UXlaX6ubt)e=v9%$a|q{ z4CLR&5aU)o#;8(S@EeAiJ+jIl;a2-pXqBP+%r#_;|KhD?c-t{Ho@njW(xbEWvvK_f z;C`#NYBvD@f(_IU2LMc^z83-?oL{wsDn2p0{qb;i8!Ad#_v(OOMe0zqOs4n5lnH@X z#tEmnc;O2`N2=bd;n!gmhmSapy{s(?&MfU{x6EpvQ0wG2vDWt0wah`Dlv`i7`Rufr zOB<`~_yfMuBB5TuU0Ws~~q9`o9WM8wNxIr}EZ+1?# zr@1@6VvJzrZ$eqhA@heJ-yt#g#@$NdjAzKM+cyRfX6FQyBe(ix&#@v+ltqeWUhOyd zNchI%g+IP-fh#QSS3wuzHvC%N5>Vr(zcF|3!e6&b+ggzdwd^sp#I!}$anN1iLyzw|6bx(H^+|L}oAT@SrOI4}Gj;gI;&7p%mcMe#d5VwQ2 za3FWcf*jYZbMw=k!m;6z`0(f!Q@Jy|KtE{iAow51o&O|jEO(dL{YOEi4jG!vQ$b6H zZ2g84SWy{RX0Z+@DR#{gHRpB!=}fWL$j(p8<*Pn_@-N5o$-=OPw)+K^Hl@Owi|Y9X zu)c=FV$8%Tg@2fHZ1(=n)V`Wouy(D^!u#7z!kdjhG;;Oz-+Q3N1V;ap3}`0y`R z#O(WR?H8xk4t7jqUP?OG^3zRUvF^c3y`5Rpvn%N$KfhVL_2x@=?zta&=2g{-Guym_ zx5T+}eoF2Vw`@n*OLo4VG34ID-y=eUe*#Q z{-=``DsEZqSUhF5AcWHR!nue@Hg_Dq(A%=TI^9M-XSwrW8`gDKNkZMabk?KQ)}1q} zs+M>7&Mz-7Eh_tF6?Dy!vv`x|Kr;S^F~XI%{Q|Ig@o{_oslt7gKRvEn8+dlJ`-D=# zH#IJyJ5q5QgR+ikM6NDW?p1wfa!j=U#ma+p81I+%i}{lCpLG%OyI{&AEYsDPg5Bznw z=ktUE$Um3XwckBSn8T>AkyllDuL-#mcHr6#mT!UULJnq&w$W59{wWuk2*e z^yPqF^e!tV9Gbzci!rl7JnQtOAqc`|p zhxTTB$bhL>XvklMCj9E6@*+fB7=Ba?)hHTD_(Km}T~Gg%r{7V7t8W`}#)-eJ`{pOSUEylcWN^ z;PH$$dO2zT;hCTJ7Y8}SZrb%zYDQTLX@F+*cKiDg53XkiRW$Qgb*x~6Tx<~X~xBgtjxGUtHxp39y1cF*HZpy06U0+-*d}1fs zW;42Cj44LC&0j@CW482dd{;)^WFu^E)%7)%~S>pw-C;{21ai zJI|}XRQ??J^fIBT#jfH~rf@zllR7$CXK{Z-f-`rf?S#DINgEEZTZ5MFl_n^5bi_f* z!pF>W-p}}hbKFzL>I!QMGkt%#G{o9|=#ugMHw3-B;aQ|tv^p#qK5^wT7m{%6HYRh~ zyjb4z^`fHFHTXg;nAR3O%wv3kZBzWMtD(a-Eb2@gUeZb!75!aXE}hmH$*(@U_9ze*FA|`h>bX}bdEWh)Xfu3N|J&l6wY z3P1hy^^ceT7!`q=0-pcO1ci+jJ-u@kWUS3H86moY`2wen;)Lv+eVrrTO4M>|FC`*fzvE`)6JOVd`laioLzzR%2N%E7J;%^(Tr64AxElROL$fEglxz)Ph;0X79D%M4W^CY!=w(&bPT(+#U8 zB2*A!fG|vk*&GDsV6~^74~b_e)NwWPD2_zn)6*Yz=T3^z>C_w$OioUwBvUCWO*9B& zRVENZKm;LU5@c5)=46QOruED{;(zGK%{iB-atGPKvOoW*2C zLNt^N$x+N2!eo=Bh!o2qm5Q?12!n~DXs=x(T_d|#BpeJEd7=!}{9U--(lN5tLr4zh zBSt2e3%=c8LNW=}XfZr_*of+r?rrRm=YobYZZzh*CzP&MY(~LmGw{uHX2k7ohW}T- z`vUo|nC~|Fv6J@#_aj^%;d(Cw-b?r+yFSA8UI@IG@JDuiFt`YP8zSuJfIBG}+wPdy za2eGo|FUL4@2!W?q3+WkhM$GsEr%gA304i+-H?0=teyB7)Yx%K=&}SWWDo`%ZOaWD zxw&niW67407cUro18#*ZFPZS>@091>cmpHI|Ljh1w>uO+k=wD8by{CHtjw|-!It(g z4WiL$Od5l6mH|nakpKDY?~a3~)xu+9zaX`ca$M}^_8ES{@sLFg5Vb*c+%2ym5JDQL z=dfcAd!hQqj#}|yT0`R@GbPDi<)_fNKJ;no3XAv>GF5gos!{#FIQjCInf(=DV literal 0 HcmV?d00001 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 {