From f3d177e3e42c678b069b14315d4c7fe4a537cdae Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 30 Jun 2024 15:16:00 +0200 Subject: [PATCH 0001/1133] [api-minor] Remove the deprecated `renderTextLayer` and `updateTextLayer` functions (PR 18104 follow-up) --- src/display/text_layer.js | 40 ++------------------------------------- src/pdf.js | 8 +------- test/unit/pdf_spec.js | 8 +------- web/pdfjs.js | 4 ---- 4 files changed, 4 insertions(+), 56 deletions(-) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index d7b96eff78c5e..e6a20ef655106 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -17,7 +17,7 @@ /** @typedef {import("./api").TextContent} TextContent */ import { AbortException, Util, warn } from "../shared/util.js"; -import { deprecated, setLayerDimensions } from "./display_utils.js"; +import { setLayerDimensions } from "./display_utils.js"; /** * @typedef {Object} TextLayerParameters @@ -557,40 +557,4 @@ class TextLayer { } } -function renderTextLayer() { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - return; - } - deprecated("`renderTextLayer`, please use `TextLayer` instead."); - - const { textContentSource, container, viewport, ...rest } = arguments[0]; - const restKeys = Object.keys(rest); - if (restKeys.length > 0) { - warn("Ignoring `renderTextLayer` parameters: " + restKeys.join(", ")); - } - - const textLayer = new TextLayer({ - textContentSource, - container, - viewport, - }); - - const { textDivs, textContentItemsStr } = textLayer; - const promise = textLayer.render(); - - // eslint-disable-next-line consistent-return - return { - promise, - textDivs, - textContentItemsStr, - }; -} - -function updateTextLayer() { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - return; - } - deprecated("`updateTextLayer`, please use `TextLayer` instead."); -} - -export { renderTextLayer, TextLayer, updateTextLayer }; +export { TextLayer }; diff --git a/src/pdf.js b/src/pdf.js index cb24267617494..9921e324b7d25 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -63,11 +63,6 @@ import { RenderingCancelledException, setLayerDimensions, } from "./display/display_utils.js"; -import { - renderTextLayer, - TextLayer, - updateTextLayer, -} from "./display/text_layer.js"; import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; @@ -75,6 +70,7 @@ import { ColorPicker } from "./display/editor/color_picker.js"; import { DrawLayer } from "./display/draw_layer.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; import { Outliner } from "./display/editor/outliner.js"; +import { TextLayer } from "./display/text_layer.js"; import { XfaLayer } from "./display/xfa_layer.js"; /* eslint-disable-next-line no-unused-vars */ @@ -121,12 +117,10 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 6dfc1584fc478..accced5554d23 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -55,11 +55,6 @@ import { RenderingCancelledException, setLayerDimensions, } from "../../src/display/display_utils.js"; -import { - renderTextLayer, - TextLayer, - updateTextLayer, -} from "../../src/display/text_layer.js"; import { AnnotationEditorLayer } from "../../src/display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js"; import { AnnotationLayer } from "../../src/display/annotation_layer.js"; @@ -67,6 +62,7 @@ import { ColorPicker } from "../../src/display/editor/color_picker.js"; import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; import { Outliner } from "../../src/display/editor/outliner.js"; +import { TextLayer } from "../../src/display/text_layer.js"; import { XfaLayer } from "../../src/display/xfa_layer.js"; const expectedAPI = Object.freeze({ @@ -106,12 +102,10 @@ const expectedAPI = Object.freeze({ PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, diff --git a/web/pdfjs.js b/web/pdfjs.js index dd85896fd4d57..8b8cde0169d0c 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -50,12 +50,10 @@ const { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, @@ -99,12 +97,10 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, From 2f4d5d25a8da8affa3f9768358dfc47f4a2dcba4 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Mon, 1 Jul 2024 20:10:35 +0200 Subject: [PATCH 0002/1133] Bump the stable version in `pdfjs.config` --- pdfjs.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdfjs.config b/pdfjs.config index 5d402b9712acc..b69212ec5c953 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { - "stableVersion": "4.3.136", + "stableVersion": "4.4.168", "baseVersion": "fdb3617e0f7ebbefce9a65a87971251e92653d07", "versionPrefix": "4.4." } From 576aaf7cc1e56c17da4a350ddb277d1c96e44544 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 1 Jul 2024 23:59:37 +0200 Subject: [PATCH 0003/1133] [Editor] Take into account the page translation when computing the quadpoints when saving an highlight It fixes #18360. --- src/display/editor/highlight.js | 5 +-- test/integration/highlight_editor_spec.mjs | 35 +++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/issue18360.pdf | Bin 0 -> 10075 bytes 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100755 test/pdfs/issue18360.pdf diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index 9887b0778320f..36ca75cc51463 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -671,12 +671,13 @@ class HighlightEditor extends AnnotationEditor { return null; } const [pageWidth, pageHeight] = this.pageDimensions; + const [pageX, pageY] = this.pageTranslation; const boxes = this.#boxes; const quadPoints = new Float32Array(boxes.length * 8); let i = 0; for (const { x, y, width, height } of boxes) { - const sx = x * pageWidth; - const sy = (1 - y - height) * pageHeight; + const sx = x * pageWidth + pageX; + const sy = (1 - y - height) * pageHeight + pageY; // The specifications say that the rectangle should start from the bottom // left corner and go counter-clockwise. // But when opening the file in Adobe Acrobat it appears that this isn't diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index a3c6c6d084519..6c47bbe7e0af1 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1302,6 +1302,41 @@ describe("Highlight Editor", () => { }); }); + describe("Quadpoints must be correct when they're in a translated page", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue18360.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the quadpoints for an highlight are almost correct", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Hello World"); + await page.mouse.click( + rect.x + rect.width / 4, + rect.y + rect.height / 2, + { count: 2, delay: 100 } + ); + + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + const quadPoints = await getFirstSerialized(page, e => e.quadPoints); + const expected = [148, 624, 176, 624, 148, 637, 176, 637]; + expect(quadPoints.every((x, i) => Math.abs(x - expected[i]) <= 5)) + .withContext(`In ${browserName} (got ${quadPoints})`) + .toBeTrue(); + }) + ); + }); + }); + describe("Editor must be unselected when the color picker is Escaped", () => { let pages; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index b92b8e3915b02..98c3f4ace1aa7 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -652,3 +652,4 @@ !bug1539074.pdf !bug1539074.1.pdf !issue18305.pdf +!issue18360.pdf diff --git a/test/pdfs/issue18360.pdf b/test/pdfs/issue18360.pdf new file mode 100755 index 0000000000000000000000000000000000000000..43a96575ecf4f9078fecb061c8e1fe00da23349a GIT binary patch literal 10075 zcmeHNc{G&m->0%9dy4cM8$d=>&xgf`D~E5CA|S!1^Ev46Fu1U?3Q<9{ss72!ldF z!5Sb-5F7~sK@botB_$4uJ9$G0Xp@Vs1M^D0@lP^DnO7hH2`xMr;3N8Fi<#D6@!PV0}zNB zOcf8qLlH^}7!+C!p^n6005lW{ha=I4(dsBX6o*Em00mUPDaV2 zD&14MfdsG)#odACNOyI^8lMe6VPFE4N~4?fZQO_s4G4+^8~b}uz~%&sJ=n|=0)m1z z5b&aTQiyIGY>vZrc4Tf_TRU4eMqYL$l~c!+&pc%n^@FO~C8(TQ5{vhHn0PWOfr!+I zImvVBW9PkjE|pM52HrZ>ISw{Dia+H1z|)IH^rZQDQtUa{01h@WF*SX44z@oq=3s*& zegV4~Vjwj752uhQ`b^U2%?JcT|H)cgTU(N?Z6cXrOF|&C@gvqc8TNe~Y#Mg125pbH zn;+FCb03dCS#y3AWn6ks!FWKiNRPSSLtL-$PU=c^_!UtQ*e@4xRCgN1o#q9Cz`t?eJgFXs zsr0FY0&)O-+F)`R3z8!?Mb3+gr+)O*+zr{HUJ3vO&>ih4LqqNBMQwDOkYJ{V~QV*KJ)Ytq0yrh z1bp{k!=>NM-{AZK#U?ZKBjO+c>erlTM9D;HvxNiPOe;arN++xEJK4|dP~IIsd=|aO z(J&m8X`{M>jqS#+Ez(9x4Q@(H(L=>Tnk#)?KGjQ5Q_wY{msJk=wD6U}F=aFcj?ner$S<7E=CYV)X0uTcx4>y>{&@S!37V#5L9`TUYrl zuEm9`Sqz8^D~81<@FQxR9y;#=NG-S+6qL0SW{-PY;nJs#mIX?@{3x+fay$6+XsXj zu0lp~px(7zm4v;snh0+gX+};%OfR>34t*XoKYQ%-Xr<9!ZE4R`VXm2={8Aa&3%PxA zaXqNsc`4NrSaa!=&*pk~iuUjN-uQlkktudBm`M_OmjCYGl++$q0n0I$}N)eOje&bRfbye5CM zNk!7YtHjo>G)MMJq9GM6M~BqU+dN2XFuxU}yL{fKy+WoAQH~t+mWj&3xajnvw)Yo> zCq?)0K4#gTU#&y~wk4~q3hmtTI?mhsW4C$5114neb{)Z1*No>_zUE8qXwB_Cb6wjs zWBI+c2AGo#l2Ee7UCw1rCh6DRG)?m=auMr2nb#fMm#dtf1<8G0dKn~($HYqt+zf~; zEqfAev{e7#ex!NaYI=ZWO4#IGixj&T@_ttpBwZx(D45E}%xgo6k8?cy>-iV9%*O7n zGZ@&X8&r~1@Fp{~cW{o$)YKyQY{XTTl~4h<3lrr6j5SU{N%{g=J1}3Pm(D71$JM4< zWbL99Nl?|fpj|DP$Vn4_`&hRbZvDyfEZDgFdougk(d|5v@He4`x6R=9ZCO{sU&*C; z1W4waifdfO9h&ZOu5HmUb_SUQi4UJrPS?=FRrc=f@_Vj8jsPkeSxq7@I)tv@7}ifc zBnfJX&viWa=;3LIP*dY6bxK6BNF9#PLYgywDf-j%*99{{7M+cNDUVu6GPGoQUd3Qs zr(cL+VyoCLk^{S$cmXTB_G5p_3mxEOQ04i(+V|uvR+O0ZhlM*r?xcK}fUb;JpJfce zI|*=Bux9YGBwufl2>0PHo5&i0s<<5Ynqf4PC=g8C#h}tHg)Qwgqnt&|7!BWDjZ@NX{~5C!7YHA+B1Y1 z6lC(YU~AiR+aMHi=}`N;SV%g{TyojU3WiJJ!f22+)5?l>dQf^~qW9NlHj|YR?*}sm zy6UUk=SLo|BA$!*v2j;zo#_^umRVa5(=$5|v>qS@z}K)a@Z0|oc>9&f3W zHqW@*bk4iwrth%}$>q^HB?)0Zr$3EPc3F7YOxOwnFH~(|r8y==C72zFgl31*__&f) z_-#E2-te>dJgzfho&F>7wV@K;2j^?6Y?VtV5XdWrkK->7l5x8Z&H_g(mM`?;DqITB z@s*bM?@5d?Wb!qMm_kL|I*1*~d-VRb`xmLFd|V#dj2`fo;@hmB0ITEIMemQ6UT4*X`cIgEkgp`=A6m3D<(pD+<){hL-Dobt?&fTaDP3}NhoP7Y>xTTp7* z#gXcV3oOr5q8}x|JFkedGU$AmSACKA`AM!|o}hMoi=Oo^zehU*nbsT%x4qcia<1dG zN!3$KZHHQbF5A0nN{rIQSMERDlK5fT)y-7e>cMl9 z!f}u!=9{S<-PWO~8CGj(J$-(rL9<;H?p)XJ!pzI{RXv+cvi3R!l%75-UF4twm1fFk1+ANxf+FXZjy?6g@@{($bfjhp zAt%os(ig1TuG#dcL6SH+c1?aJH!ieM<|z#*9z z?BF}a*lkzJS@w1=hRQv74*|)|Ks?f#h~(7sT@2Se6Hj(1Rkonc^djy|u?801+;3Vm zBiv~S3gS^yU4iQ>5zTlT*H3N17(Y zQ#4OQJhr^+(iR*|Z4BG`PzG;WG}Qk z3O1~tbCgfW3GzKW(rdfGd{9^+iki`6G}2dDMNu})szJ%!OHI70EKW2CqdMLQz-iaK zJR67q^bC9>CIa91^fuW!KYY%LzmWG~g;DI6kT>(nHy(n$Q7Q0-*~klM$#lVP{|t=r zC!rT_<@6?EL|R((R$~qi=5-8-pFqdKq$JZRNxlx(G3pSCvF>fR7V)PU38o+J$y|)w zi5-h*t4yvDl#2O!w)&z?u>o_I%vH_@256kD>GQUF=b|hl?6UKibR0}YC(d8lX))k( zWqV#I$V>ei9J)H(p5K@mE)n}7^@DC~)q&(=hPCiS!G?ns`7z-)8&l_Sx;aNOqS9xY zX4K~Uu*T1B;kc|xRf6R=az^`mVBMSS7VH+nqhpWs^hAr$2?A}}UZYCJ@p9qFPkZ`b( zw;gRGm&a-1H`2Y0ydJ>)t}xKpO~=Sgpem(jSVY%QtG)8{az~Wc9udKCpR+nV0+(t} zGSw8`*8)%IP1g^KsM>;ZL%Fj`O{!=gAx|XTuXM}0Z@u2eVU>IlGgW}Qq|D#UzjJ5G z%1ffxicd(g%(Ld^^NR}e{+Cw*C*twsZK{dml^i!=?a%zdlgByF`Ht`S^2)p5p>w23 z{HbP*$XD{lCz4dM9YhkWbL$U`UG!6X*{t`;sy~o#cNl-CYSfLok4^I?J6P|(){tOD zEF>8xY`^LH1^L94BqY~iawn~@fCn9L|Ez^GG*?vf9z3S?ajIa$2SD(ADn(LKl6Z{2 z!C3rly}5IST-oF1RH58TgXSHkyIr*HO=q9hgx|Up^9XA}OcDKhM<~JzDl;YJz|_icz4BqNOHVX6qrA;?*6zDqIvL`#te??$C^125Ey zc@)6>HP7W1T}!0I#Kg*eo*#{l6}1M%;E@Tqah^}%YT|5mXGOAejATGhcy>2?lO^oD z@!oY)B~#~oVzNh$X!7iiX)zdlG5zIYnew6}Ipc}TapvVQ_IIrbc9W+}1y9!5i!u`< z#3Dq++xadY<#3Qc_3jx4%`GZCw_j)Iysnvs^~1_nrgy6AHOAlyOiq15)hm<1LEOHJ zedIgUh)bCk_}mIh>*d$}uek`~mj`_#Biez}D5GgzS*OHcQH_V=UIFtxjtNQ1_iYN* z132f>OqrU4n+}K69Da5_F#oBZHIMU*b3w>e-wYS77R}OfRs=)Z-#ZK z?wjKgdDPRNkX=y^-CZxQM}|VrSo%9Z9-0d7q-~#y87lyL+9$DSOgt*Cup9}=;% za5P&RZPIs$_}WDRz1$up&$wP1N>F_6VdAIN75j=*isC&O+u!B!aZq`A-z>p7Vj;2cE6c~8J(20rgB(ly^<#sns>BMl6|a#)Qyp*m?%g1HVDF?= z9!ON}eWB_c>*qDn^BPi*7@w6}J`+&!{FO_{prE zx@Vg~0+OlmR-L#^dL2*vJ8t{6ZLLI~_c(>YqA9o8HU0A*e0mbk5*?&6PMOEsRC?B5 zvBxrxHe5Ax_c3i$VsjFI#9r-$P*fqL`?$G1eQj!LX!GFVEqlQf!lb=^rn!I*7hz8o z|8f5`E?W*nFzt2SmD|;WMguNgIv*8IF1r`LA5!Uj#1QlX)8uxdwIu6sVX{(zMbnj| ze&R~+tzE4SKvxdDJzbYMTs0cT7Wk4oS2?lwfyu7^tmyq|yP_jz(4vkTJE$PPRQP5a z9r{1n=%A=SZK1~!I-P3)_J;XjHlab>@kDVq7GZ$EhIz}?y3BCy7CE9Ga?Y!1urooM zW#`j(xK(lxv@qR;beHApocOcsNb9{59+~`yc(D8K5}lZ}v_lR(n2?YeQ+;v!PGd1- zt*(Xl;}MOH;af!krbdwg4I>$cueUQ)6h0W*O5N=e;(;F%K6zuy=^)1sB5!XB@SvMH zW7+&?2XI|qOagE73m?=Q13U?6OX2SAFp%_l?p;ac+`DjBzfOP+`!e&@}$BytwxBKYb`+Ad` zXY2`k0|Ub^$!zQQd0NP@m4OO5m9-hB{}$5k)i21;>K6oxrWe0p|6TD5Y6KVnW39R*f#xH^zlXE3rzW|UvtGU7ya|=eSB0n&Z*o2 zT@0DbV*xZ_6K9#qN6E}I8sp{p3K)|r+x5rWlu(i`I>Fe z%9r>+dHGPfcQ0xWb#LO`Mh*7IEB8m`5e5Z-pRYl4U7hc){86<1^(s_Q_VXZ;Tqrb< z9mT=PT~TDdqFMyxL{=0rL+Ap!9yp4llg0^8iqQ!@W6}v15{4|Iti-0^hxK#waHE%i zL4Izo?p|0wMG+$Xpn!s<|K4~E76E-Dp}8oEsBR2^%ykVwII1TFgpq^Dk^lfgFVxB* zQ3x_%kCFjF0VoU%Ai)rbEChpvLa_9I5kD>wrH#V4f+yJ?OTerD&_};h6mg`{Jg{J} zudlD1FHDZ==>Ue%>%?FH3Wh>u=@hbF{_ZrQpRBvr!S6!;kb|dqkvyF|Xiijj(1u*1 z9o3trC?fK+0>7d9hhuIYKNY!q$$fKDjzo0>`w>0B5IF$6855cGlgGo`)AgHaWD=O- zN^#pLbJO`CoAS{y)z$sUx~V!hw@o=ce#7HWeDndjIvDsT02>(4^C^EbLD*exv*s8GnT2 zW>WrQ$&WLKZ?yj+=bHwqr!kf4s`NeLR7svxJ0cB4ukC{*kZ@T$Cz>R^;x9)(MF7wU z)OY88XZuYenS`~cdb$y5N<i*9A7rC15^z$Nj66FuM z6exsZhk+wxVQ@5B77iehvKRyuBMXo*L?o05qmW?V-J{F>jrTXXbWgB!T*%%eisyd{ z*PnQP$Wj1rte@Z3)Az;t?J0e=(sv5*&#mIy(8kL8KfQiA`G2V9A0>Ya+<)Nu2d=+` zz~3VNC%XQD>u(|Ow}}6VuKzW-*nTA&^s1qvh%Y^r*d&?xtubVi&Wi7;+Rsc4{-b$m zquTe^E`<_fz~Hy^Y^6g-@*EP*%*I?l6KTw!Gs7Z!?K=P9GsbNK3@;Ho3O6z4-?w>u zHwN)X(*y*%(KA7BrP|yYDBR$?IdWRMGR&h&&2`s;LWzoyXKl`%& E3*|c}Pyhe` literal 0 HcmV?d00001 From 64635f3b35a64d1f941aadaf2bdcfd114b7053cc Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 21 May 2024 14:41:07 +0200 Subject: [PATCH 0004/1133] [api-minor][Editor] When switching to editing mode, redraw pages containing editable annotations Right now, editable annotations are using their own canvas when they're drawn, but it induces several issues: - if the annotation has to be composed with the page then the canvas must be correctly composed with its parent. That means we should move the canvas under canvasWrapper and we should extract composing info from the drawing instructions... Currently it's the case with highlight annotations. - we use some extra memory for those canvas even if the user will never edit them, which the case for example when opening a pdf in Fenix. So with this patch, all the editable annotations are drawn on the canvas. When the user switches to editing mode, then the pages with some editable annotations are redrawn but without them: they'll be replaced by their counterpart in the annotation editor layer. --- src/core/annotation.js | 8 +- src/core/document.js | 6 +- src/core/worker.js | 2 + src/display/annotation_layer.js | 18 +- src/display/annotation_storage.js | 39 ++++- src/display/api.js | 39 ++++- test/integration/annotation_spec.mjs | 8 + test/integration/freetext_editor_spec.mjs | 200 ++++++++++++++-------- test/integration/stamp_editor_spec.mjs | 4 +- test/integration/test_utils.mjs | 23 ++- web/annotation_layer_builder.js | 4 + web/pdf_page_view.js | 21 +++ web/pdf_viewer.js | 89 +++++++++- 13 files changed, 358 insertions(+), 103 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 4e821b98730c3..632112d967ef7 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -680,6 +680,7 @@ class Annotation { hasOwnCanvas: false, noRotate: !!(this.flags & AnnotationFlag.NOROTATE), noHTML: isLocked && isContentLocked, + isEditable: false, }; if (params.collectFields) { @@ -776,6 +777,10 @@ class Annotation { return this.printable; } + mustBeViewedWhenEditing() { + return !this.data.isEditable; + } + /** * @type {boolean} */ @@ -3802,7 +3807,8 @@ class FreeTextAnnotation extends MarkupAnnotation { // It uses its own canvas in order to be hidden if edited. // But if it has the noHTML flag, it means that we don't want to be able // to modify it so we can just draw it on the main canvas. - this.data.hasOwnCanvas = !this.data.noHTML; + this.data.hasOwnCanvas = this.data.noRotate; + this.data.isEditable = !this.data.noHTML; // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; diff --git a/src/core/document.js b/src/core/document.js index 505cd9a75834b..ae07d8a8bec3b 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -411,6 +411,8 @@ class Page { intent, cacheKey, annotationStorage = null, + isEditing = false, + modifiedIds = null, }) { const contentStreamPromise = this.getContentStream(); const resourcesPromise = this.loadResources([ @@ -579,7 +581,9 @@ class Page { if ( intentAny || (intentDisplay && - annotation.mustBeViewed(annotationStorage, renderForms)) || + annotation.mustBeViewed(annotationStorage, renderForms) && + ((isEditing && annotation.mustBeViewedWhenEditing()) || + (!isEditing && !modifiedIds?.has(annotation.data.id)))) || (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( diff --git a/src/core/worker.js b/src/core/worker.js index 804936229bcc9..8d223c2738aca 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -752,6 +752,8 @@ class WorkerMessageHandler { intent: data.intent, cacheKey: data.cacheKey, annotationStorage: data.annotationStorage, + isEditing: data.isEditing, + modifiedIds: data.modifiedIds, }) .then( function (operatorListInfo) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 57c074ab4db80..dff737b6ca6d7 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -198,6 +198,10 @@ class AnnotationElement { return !!(titleObj?.str || contentsObj?.str || richText?.str); } + get _isEditable() { + return this.data.isEditable; + } + get hasPopupData() { return AnnotationElement._hasPopupData(this.data); } @@ -734,10 +738,6 @@ class AnnotationElement { } } - get _isEditable() { - return false; - } - _editOnDoubleClick() { if (!this._isEditable) { return; @@ -2530,10 +2530,6 @@ class FreeTextAnnotationElement extends AnnotationElement { return this.container; } - - get _isEditable() { - return this.data.hasOwnCanvas; - } } class LineAnnotationElement extends AnnotationElement { @@ -3107,6 +3103,10 @@ class AnnotationLayer { } } + hasEditableAnnotations() { + return this.#editableAnnotations.size > 0; + } + #appendElement(element, id) { const contentElement = element.firstChild || element; contentElement.id = `${AnnotationPrefix}${id}`; @@ -3188,7 +3188,7 @@ class AnnotationLayer { } this.#appendElement(rendered, data.id); - if (element.annotationEditorType > 0) { + if (element._isEditable) { this.#editableAnnotations.set(element.data.id, element); this._annotationEditorUIManager?.renderAnnotationElement(element); } diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index 8154453c3b48b..9999f3b52b025 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { objectFromMap, unreachable } from "../shared/util.js"; +import { objectFromMap, shadow, unreachable } from "../shared/util.js"; import { AnnotationEditor } from "./editor/editor.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js"; @@ -29,6 +29,8 @@ const SerializableEmpty = Object.freeze({ class AnnotationStorage { #modified = false; + #modifiedIds = null; + #storage = new Map(); constructor() { @@ -248,6 +250,34 @@ class AnnotationStorage { } return stats; } + + resetModifiedIds() { + this.#modifiedIds = null; + } + + /** + * @returns {{ids: Set, hash: string}} + */ + get modifiedIds() { + if (this.#modifiedIds) { + return this.#modifiedIds; + } + const ids = []; + for (const value of this.#storage.values()) { + if ( + !(value instanceof AnnotationEditor) || + !value.annotationElementId || + !value.serialize() + ) { + continue; + } + ids.push(value.annotationElementId); + } + return (this.#modifiedIds = { + ids: new Set(ids), + hash: ids.join(","), + }); + } } /** @@ -282,6 +312,13 @@ class PrintAnnotationStorage extends AnnotationStorage { get serializable() { return this.#serializable; } + + get modifiedIds() { + return shadow(this, "modifiedIds", { + ids: new Set(), + hash: "", + }); + } } export { AnnotationStorage, PrintAnnotationStorage, SerializableEmpty }; diff --git a/src/display/api.js b/src/display/api.js index e008a75eee8c7..6244a0611eff4 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1227,6 +1227,7 @@ class PDFDocumentProxy { * @property {Map} [annotationCanvasMap] - Map some * annotation ids with canvases used to render them. * @property {PrintAnnotationStorage} [printAnnotationStorage] + * @property {boolean} [isEditing] - Render the page in editing mode. */ /** @@ -1248,6 +1249,7 @@ class PDFDocumentProxy { * from the {@link AnnotationStorage}-instance; useful e.g. for printing. * The default value is `AnnotationMode.ENABLE`. * @property {PrintAnnotationStorage} [printAnnotationStorage] + * @property {boolean} [isEditing] - Render the page in editing mode. */ /** @@ -1420,13 +1422,15 @@ class PDFPageProxy { annotationCanvasMap = null, pageColors = null, printAnnotationStorage = null, + isEditing = false, }) { this._stats?.time("Overall"); const intentArgs = this._transport.getRenderingIntent( intent, annotationMode, - printAnnotationStorage + printAnnotationStorage, + isEditing ); const { renderingIntent, cacheKey } = intentArgs; // If there was a pending destroy, cancel it so no cleanup happens during @@ -1560,6 +1564,7 @@ class PDFPageProxy { intent = "display", annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, + isEditing = false, } = {}) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { throw new Error("Not implemented: getOperatorList"); @@ -1576,6 +1581,7 @@ class PDFPageProxy { intent, annotationMode, printAnnotationStorage, + isEditing, /* isOpList = */ true ); let intentState = this._intentStates.get(intentArgs.cacheKey); @@ -1812,6 +1818,8 @@ class PDFPageProxy { renderingIntent, cacheKey, annotationStorageSerializable, + isEditing, + modifiedIds, }) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { assert( @@ -1828,6 +1836,8 @@ class PDFPageProxy { intent: renderingIntent, cacheKey, annotationStorage: map, + isEditing, + modifiedIds, }, transfer ); @@ -2420,6 +2430,7 @@ class WorkerTransport { intent, annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, + isEditing = false, isOpList = false ) { let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. @@ -2438,6 +2449,12 @@ class WorkerTransport { warn(`getRenderingIntent - invalid intent: ${intent}`); } + const annotationStorage = + renderingIntent & RenderingIntentFlag.PRINT && + printAnnotationStorage instanceof PrintAnnotationStorage + ? printAnnotationStorage + : this.annotationStorage; + switch (annotationMode) { case AnnotationMode.DISABLE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE; @@ -2450,12 +2467,6 @@ class WorkerTransport { case AnnotationMode.ENABLE_STORAGE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE; - const annotationStorage = - renderingIntent & RenderingIntentFlag.PRINT && - printAnnotationStorage instanceof PrintAnnotationStorage - ? printAnnotationStorage - : this.annotationStorage; - annotationStorageSerializable = annotationStorage.serializable; break; default: @@ -2466,10 +2477,22 @@ class WorkerTransport { renderingIntent += RenderingIntentFlag.OPLIST; } + const { ids: modifiedIds, hash: modifiedIdsHash } = + annotationStorage.modifiedIds; + + const cacheKeyBuf = [ + renderingIntent, + annotationStorageSerializable.hash, + isEditing ? 1 : 0, + modifiedIdsHash, + ]; + return { renderingIntent, - cacheKey: `${renderingIntent}_${annotationStorageSerializable.hash}`, + cacheKey: cacheKeyBuf.join("_"), annotationStorageSerializable, + isEditing, + modifiedIds, }; } diff --git a/test/integration/annotation_spec.mjs b/test/integration/annotation_spec.mjs index 579ee2d0ce1f9..a15a777e8a78d 100644 --- a/test/integration/annotation_spec.mjs +++ b/test/integration/annotation_spec.mjs @@ -503,6 +503,14 @@ describe("ResetForm action", () => { it("must check that the Ink annotation has a popup", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + if (browserName) { + // TODO + pending( + "Re-enable this test when the Ink annotation has been made editable." + ); + return; + } + await page.waitForFunction( `document.querySelector("[data-annotation-id='25R']").hidden === false` ); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 18ea77b789b86..3150eb0b99ad0 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -45,6 +45,7 @@ import { scrollIntoView, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForSelectedEditor, waitForSerialized, waitForStorageEntries, @@ -987,6 +988,29 @@ describe("FreeText Editor", () => { pages.map(async ([browserName, page]) => { await switchToFreeText(page); + const isEditorWhite = editorRect => + page.evaluate(rect => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + rect ||= { + x: 0, + y: 0, + width: canvas.width, + height: canvas.height, + }; + const { data } = ctx.getImageData( + rect.x, + rect.y, + rect.width, + rect.height + ); + return data.every(x => x === 0xff); + }, editorRect); + + // The page has been re-rendered but with no freetext annotations. + let isWhite = await isEditorWhite(); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + let editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6); @@ -1041,11 +1065,9 @@ describe("FreeText Editor", () => { // canvas. editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); - const hidden = await page.$eval( - "[data-annotation-id='26R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); + + isWhite = await isEditorWhite(editorRect); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); // Check we've now a div containing the text. const newDivText = await page.$eval( @@ -1288,10 +1310,12 @@ describe("FreeText Editor", () => { await closePages(pages); }); - it("must move an annotation", async () => { + it("must edit an annotation", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); await page.click("[data-annotation-id='26R']", { count: 2 }); + await awaitPromise(modeChangedHandle); await page.waitForSelector(`${getEditorSelector(0)}-editor`); const [focusedId, editable] = await page.evaluate(() => { @@ -1347,6 +1371,7 @@ describe("FreeText Editor", () => { // TODO: remove this when we switch to BiDi. await hover(page, "[data-annotation-id='23R']"); + // Wait for the popup to be displayed. await page.waitForFunction( () => @@ -1588,12 +1613,6 @@ describe("FreeText Editor", () => { it("must open an existing annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1646,8 +1665,12 @@ describe("FreeText Editor", () => { return null; }; - for (const n of [0, 1, 2, 3, 4]) { - const rect = await getRect(page, getEditorSelector(n)); + const firstPixelsAnnotations = new Map(); + + // [26, 32, ...] are the annotation ids + for (const n of [26, 32, 42, 57, 35, 1]) { + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1658,33 +1681,33 @@ describe("FreeText Editor", () => { editorImage.width, editorImage.height ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + for (const n of [0, 1, 2, 3, 4]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height ); expect( @@ -1719,12 +1742,6 @@ describe("FreeText Editor", () => { it("must open an existing rotated annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1806,13 +1823,15 @@ describe("FreeText Editor", () => { return null; }; + const firstPixelsAnnotations = new Map(); for (const [n, start] of [ - [0, "BL"], - [1, "BR"], - [2, "TR"], - [3, "TL"], + [17, "BL"], + [18, "BR"], + [19, "TR"], + [20, "TL"], ]) { - const rect = await getRect(page, getEditorSelector(n)); + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1824,33 +1843,38 @@ describe("FreeText Editor", () => { editorImage.height, start ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const [n, start] of [ + [0, "BL"], + [1, "BR"], + [2, "TR"], + [3, "TL"], + ]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height, + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height, start ); @@ -3552,13 +3576,6 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - let hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); - // Check we've now a div containing the text. await page.waitForSelector( "[data-annotation-id='998R'] div.annotationContent" @@ -3571,6 +3588,24 @@ describe("FreeText Editor", () => { .withContext(`In ${browserName}`) .toEqual("Hello World and edited in Firefox"); + // Check that the canvas has nothing drawn at the annotation position. + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = true) + ); + let editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = false) + ); + let editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeTrue(); + const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2); for (const pageNumber of oneToThirteen) { await scrollIntoView( @@ -3587,6 +3622,19 @@ describe("FreeText Editor", () => { await switchToFreeText(page, /* disable = */ true); const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + const handlePromise = await createPromise(page, resolve => { + const callback = e => { + if (e.source.id === 1) { + window.PDFViewerApplication.eventBus.off( + "pagerendered", + callback + ); + resolve(); + } + }; + window.PDFViewerApplication.eventBus.on("pagerendered", callback); + }); + for (const pageNumber of thirteenToOne) { await scrollIntoView( page, @@ -3594,12 +3642,16 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeFalse(); + await awaitPromise(handlePromise); + + editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeFalse(); }) ); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index 42c8187aa657e..fed856d8627a2 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -564,14 +564,14 @@ describe("Stamp Editor", () => { for (let i = 0; i < pages1.length; i++) { const [, page1] = pages1[i]; await page1.bringToFront(); - await page1.click("#editorStamp"); + await switchToStamp(page1); await copyImage(page1, "../images/firefox_logo.png", 0); await copy(page1); const [, page2] = pages2[i]; await page2.bringToFront(); - await page2.click("#editorStamp"); + await switchToStamp(page2); await paste(page2); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 1c8d39d0de64b..e8b4ef5c139b6 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -447,11 +447,30 @@ function waitForAnnotationEditorLayer(page) { return createPromise(page, resolve => { window.PDFViewerApplication.eventBus.on( "annotationeditorlayerrendered", - resolve + resolve, + { once: true } + ); + }); +} + +function waitForAnnotationModeChanged(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on( + "annotationeditormodechanged", + resolve, + { once: true } ); }); } +function waitForPageRendered(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on("pagerendered", resolve, { + once: true, + }); + }); +} + async function scrollIntoView(page, selector) { const handle = await page.evaluateHandle( sel => [ @@ -695,8 +714,10 @@ export { serializeBitmapDimensions, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEntryInStorage, waitForEvent, + waitForPageRendered, waitForSandboxTrip, waitForSelectedEditor, waitForSerialized, diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 481a3e1e42c8a..2b56d506cd86f 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -182,6 +182,10 @@ class AnnotationLayerBuilder { this.div.hidden = true; } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { if (!this.div) { return; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 31ec394ebc157..ddeca8d0b75ff 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -119,6 +119,8 @@ class PDFPageView { #hasRestrictedScaling = false; + #isEditing = false; + #layerProperties = null; #loadingId = null; @@ -354,6 +356,10 @@ class PDFPageView { this.pdfPage?.cleanup(); } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + get _textHighlighter() { return shadow( this, @@ -582,6 +588,20 @@ class PDFPageView { } } + toggleEditingMode(isEditing) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#isEditing = isEditing; + this.reset({ + keepZoomLayer: true, + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + }); + } + /** * @typedef {Object} PDFPageViewUpdateParameters * @property {number} [scale] The new scale, if specified. @@ -1037,6 +1057,7 @@ class PDFPageView { optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, pageColors, + isEditing: this.#isEditing, }; const renderTask = (this.renderTask = pdfPage.render(renderContext)); renderTask.onContinue = renderContinueCallback; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 570fca03fdd56..23664ab9e1950 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -223,6 +223,10 @@ class PDFViewer { #mlManager = null; + #onPageRenderedCallback = null; + + #switchAnnotationEditorModeTimeoutId = null; + #getAllTextInProgress = false; #hiddenCopyElement = null; @@ -1117,6 +1121,10 @@ class PDFViewer { this.#hiddenCopyElement?.remove(); this.#hiddenCopyElement = null; + + this.#onPageRenderedCallback = null; + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; } #ensurePageViewVisible() { @@ -1653,6 +1661,32 @@ class PDFViewer { }); } + #switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { ids, views } = visible; + for (const page of views) { + const { view } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + } + + if (pagesToRefresh.length === 0) { + return null; + } + this.renderingQueue.renderHighestPriority({ + first: pagesToRefresh[0], + last: pagesToRefresh.at(-1), + views: pagesToRefresh, + ids, + }); + + return ids; + } + containsElement(element) { return this.container.contains(element); } @@ -2259,13 +2293,56 @@ class PDFViewer { if (!this.pdfDocument) { return; } - this.#annotationEditorMode = mode; - this.eventBus.dispatch("annotationeditormodechanged", { - source: this, - mode, - }); - this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + const { eventBus } = this; + const updater = () => { + if (this.#onPageRenderedCallback) { + eventBus._off("pagerendered", this.#onPageRenderedCallback); + this.#onPageRenderedCallback = null; + } + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + this.#annotationEditorMode = mode; + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode, + }); + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + }; + + if ( + mode === AnnotationEditorType.NONE || + this.#annotationEditorMode === AnnotationEditorType.NONE + ) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.annotationStorage.resetModifiedIds(); + } + for (const pageView of this._pages) { + pageView.toggleEditingMode(isEditing); + } + // We must call #switchToEditAnnotationMode unconditionally to ensure that + // page is rendered if it's useful or not. + const idsToRefresh = this.#switchToEditAnnotationMode(); + if (isEditing && editId && idsToRefresh) { + // We're editing an existing annotation so we must switch to editing + // mode when the rendering is done. + const { signal } = this.#eventAbortController; + this.#onPageRenderedCallback = ({ pageNumber }) => { + idsToRefresh.delete(pageNumber); + if (idsToRefresh.size === 0) { + eventBus._off("pagerendered", this.#onPageRenderedCallback); + this.#onPageRenderedCallback = null; + this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0); + } + }; + eventBus._on("pagerendered", this.#onPageRenderedCallback, { signal }); + return; + } + } + updater(); } // eslint-disable-next-line accessor-pairs From ecb39a75ed89bc6ed0a9067edfb96310b8005db0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 22 Jul 2022 11:56:20 +0200 Subject: [PATCH 0005/1133] [Firefox] Generate a PDF.js default-prefs file that can be used directly in mozilla-central (bug 1905864) --- extensions/firefox/.eslintrc | 22 ---------- .../content/PdfJsDefaultPreferences.sys.mjs | 18 -------- gulpfile.mjs | 44 +++++++++---------- 3 files changed, 21 insertions(+), 63 deletions(-) delete mode 100644 extensions/firefox/.eslintrc delete mode 100644 extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs diff --git a/extensions/firefox/.eslintrc b/extensions/firefox/.eslintrc deleted file mode 100644 index e5ae68046fee7..0000000000000 --- a/extensions/firefox/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Note: The root .eslintrc file will define the base rules, - // but mozilla/recommended will override them for the rules it sets. Finally, - // the rules in this file will take precedence. - "extends": [ - "plugin:mozilla/recommended", - ], - - "plugins": [ - "mozilla" - ], - - "rules": { - // Other rules mozilla/recommended hasn't enabled yet. - "no-shadow": "error", - "arrow-body-style": ["error", "as-needed"], - "arrow-parens": ["error", "always"], - "constructor-super": "error", - "no-confusing-arrow": "error", - "no-useless-constructor": "error", - }, -} diff --git a/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs b/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs deleted file mode 100644 index 738adad80b6da..0000000000000 --- a/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2018 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const PdfJsDefaultPreferences = Object.freeze( - PDFJSDev.eval("DEFAULT_PREFERENCES") -); diff --git a/gulpfile.mjs b/gulpfile.mjs index 78dfa005e7a98..45fc2d92c94aa 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -53,7 +53,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const BUILD_DIR = "build/"; const L10N_DIR = "l10n/"; const TEST_DIR = "test/"; -const EXTENSION_SRC_DIR = "extensions/"; const BASELINE_DIR = BUILD_DIR + "baseline/"; const MOZCENTRAL_BASELINE_DIR = BUILD_DIR + "mozcentral.baseline/"; @@ -1278,26 +1277,31 @@ gulp.task( ) ); -function preprocessDefaultPreferences(content) { +function createDefaultPrefsFile() { + const defaultFileName = "PdfJsDefaultPrefs.js", + overrideFileName = "PdfJsOverridePrefs.js"; const licenseHeader = fs.readFileSync("./src/license_header.js").toString(); const MODIFICATION_WARNING = - "//\n// THIS FILE IS GENERATED AUTOMATICALLY, DO NOT EDIT MANUALLY!\n//\n"; + "// THIS FILE IS GENERATED AUTOMATICALLY, DO NOT EDIT MANUALLY!\n//\n" + + `// Any overrides should be placed in \`${overrideFileName}\`.\n`; - const bundleDefines = { - ...DEFINES, - DEFAULT_PREFERENCES: getDefaultPreferences("mozcentral/"), - }; + const prefs = getDefaultPreferences("mozcentral/"); + const buf = []; - content = preprocessPDFJSCode( - { - rootPath: __dirname, - defines: bundleDefines, - }, - content - ); + for (const name in prefs) { + let value = prefs[name]; - return licenseHeader + "\n" + MODIFICATION_WARNING + "\n" + content + "\n"; + if (typeof value === "string") { + value = `"${value}"`; + } + buf.push(`pref("pdfjs.${name}", ${value});`); + } + buf.sort(); + buf.unshift(licenseHeader, MODIFICATION_WARNING); + buf.push(`\n#include ${overrideFileName}\n`); + + return createStringSource(defaultFileName, buf.join("\n")); } function replaceMozcentralCSS() { @@ -1325,8 +1329,7 @@ gulp.task( MOZCENTRAL_EXTENSION_DIR = MOZCENTRAL_DIR + "browser/extensions/pdfjs/", MOZCENTRAL_CONTENT_DIR = MOZCENTRAL_EXTENSION_DIR + "content/", MOZCENTRAL_L10N_DIR = - MOZCENTRAL_DIR + "browser/locales/en-US/pdfviewer/", - FIREFOX_CONTENT_DIR = EXTENSION_SRC_DIR + "/firefox/content/"; + MOZCENTRAL_DIR + "browser/locales/en-US/pdfviewer/"; const MOZCENTRAL_WEB_FILES = [ ...COMMON_WEB_FILES, @@ -1401,12 +1404,7 @@ gulp.task( gulp .src("LICENSE", { encoding: false }) .pipe(gulp.dest(MOZCENTRAL_EXTENSION_DIR)), - gulp - .src(FIREFOX_CONTENT_DIR + "PdfJsDefaultPreferences.sys.mjs", { - encoding: false, - }) - .pipe(transform("utf8", preprocessDefaultPreferences)) - .pipe(gulp.dest(MOZCENTRAL_CONTENT_DIR)), + createDefaultPrefsFile().pipe(gulp.dest(MOZCENTRAL_EXTENSION_DIR)), ]); } ) From 033623c8d64caa51c0035df3814d498783473b03 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 2 Jul 2024 14:34:59 +0200 Subject: [PATCH 0006/1133] Update the year in the `license_header` files --- src/license_header.js | 2 +- src/license_header_libre.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/license_header.js b/src/license_header.js index 22d9d8a176bcb..c113cdc898a84 100644 --- a/src/license_header.js +++ b/src/license_header.js @@ -1,4 +1,4 @@ -/* Copyright 2023 Mozilla Foundation +/* Copyright 2024 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/license_header_libre.js b/src/license_header_libre.js index 4832396898ef3..90a33e6980cb8 100644 --- a/src/license_header_libre.js +++ b/src/license_header_libre.js @@ -2,7 +2,7 @@ * @licstart The following is the entire license notice for the * JavaScript code in this page * - * Copyright 2023 Mozilla Foundation + * Copyright 2024 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 2060944bdac8b4804c1a6231c5f57a59b9137356 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 2 Jul 2024 15:08:43 +0200 Subject: [PATCH 0007/1133] Bump library version to 4.5 --- pdfjs.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pdfjs.config b/pdfjs.config index b69212ec5c953..4144df06cc443 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { "stableVersion": "4.4.168", - "baseVersion": "fdb3617e0f7ebbefce9a65a87971251e92653d07", - "versionPrefix": "4.4." + "baseVersion": "64635f3b35a64d1f941aadaf2bdcfd114b7053cc", + "versionPrefix": "4.5." } From c154b1da2302f63ed40bf2089d249d4b00085146 Mon Sep 17 00:00:00 2001 From: calixteman Date: Tue, 2 Jul 2024 15:46:05 +0200 Subject: [PATCH 0008/1133] Update pdfjs.config Co-authored-by: Jonas Jenwald --- pdfjs.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdfjs.config b/pdfjs.config index 4144df06cc443..0f48eda4929cc 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { "stableVersion": "4.4.168", - "baseVersion": "64635f3b35a64d1f941aadaf2bdcfd114b7053cc", + "baseVersion": "bdcc4a0febc02aec5be79da67507972e3c7be280", "versionPrefix": "4.5." } From 68175323f1aa27436cd0e710094582dd99030dae Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 2 Jul 2024 15:49:54 +0200 Subject: [PATCH 0009/1133] [Editor] Make sure everything is cleaned up when we switch to annotation editor mode --- test/integration/test_utils.mjs | 1 + web/pdf_viewer.js | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index e8b4ef5c139b6..c49cb1daa9d9f 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -135,6 +135,7 @@ function getSelector(id) { async function getRect(page, selector) { // In Chrome something is wrong when serializing a `DomRect`, // so we extract the values and return them ourselves. + await page.waitForSelector(selector); return page.$eval(selector, el => { const { x, y, width, height } = el.getBoundingClientRect(); return { x, y, width, height }; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 23664ab9e1950..256ad0e3a37f7 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -1122,9 +1122,7 @@ class PDFViewer { this.#hiddenCopyElement?.remove(); this.#hiddenCopyElement = null; - this.#onPageRenderedCallback = null; - clearTimeout(this.#switchAnnotationEditorModeTimeoutId); - this.#switchAnnotationEditorModeTimeoutId = null; + this.#cleanupSwitchAnnotationEditorMode(); } #ensurePageViewVisible() { @@ -2263,6 +2261,17 @@ class PDFViewer { ]); } + #cleanupSwitchAnnotationEditorMode() { + if (this.#onPageRenderedCallback) { + this.eventBus._off("pagerendered", this.#onPageRenderedCallback); + this.#onPageRenderedCallback = null; + } + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + } + get annotationEditorMode() { return this.#annotationEditorUIManager ? this.#annotationEditorMode @@ -2296,14 +2305,7 @@ class PDFViewer { const { eventBus } = this; const updater = () => { - if (this.#onPageRenderedCallback) { - eventBus._off("pagerendered", this.#onPageRenderedCallback); - this.#onPageRenderedCallback = null; - } - if (this.#switchAnnotationEditorModeTimeoutId !== null) { - clearTimeout(this.#switchAnnotationEditorModeTimeoutId); - this.#switchAnnotationEditorModeTimeoutId = null; - } + this.#cleanupSwitchAnnotationEditorMode(); this.#annotationEditorMode = mode; eventBus.dispatch("annotationeditormodechanged", { source: this, @@ -2329,15 +2331,14 @@ class PDFViewer { if (isEditing && editId && idsToRefresh) { // We're editing an existing annotation so we must switch to editing // mode when the rendering is done. - const { signal } = this.#eventAbortController; + this.#cleanupSwitchAnnotationEditorMode(); this.#onPageRenderedCallback = ({ pageNumber }) => { idsToRefresh.delete(pageNumber); if (idsToRefresh.size === 0) { - eventBus._off("pagerendered", this.#onPageRenderedCallback); - this.#onPageRenderedCallback = null; this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0); } }; + const { signal } = this.#eventAbortController; eventBus._on("pagerendered", this.#onPageRenderedCallback, { signal }); return; } From bb54e7e64cc46792e6271c8deb714373cb570c3c Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Tue, 2 Jul 2024 18:20:49 +0200 Subject: [PATCH 0010/1133] Update dependencies to the most recent versions --- gulpfile.mjs | 2 +- package-lock.json | 98 ++++++++++++++++++++++++----------------------- package.json | 12 +++--- 3 files changed, 57 insertions(+), 55 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 78dfa005e7a98..b415f21cf34d2 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -2215,7 +2215,7 @@ function packageJson() { license: DIST_LICENSE, optionalDependencies: { canvas: "^2.11.2", - path2d: "^0.2.0", + path2d: "^0.2.1", }, browser: { canvas: false, diff --git a/package-lock.json b/package-lock.json index 4c0fb2980582e..632b03cb3c601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001636", + "caniuse-lite": "^1.0.30001639", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -34,7 +34,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-sort-exports": "^0.9.1", "eslint-plugin-unicorn": "^54.0.0", - "globals": "^15.6.0", + "globals": "^15.8.0", "gulp": "^5.0.0", "gulp-cli": "^3.0.0", "gulp-postcss": "^10.0.0", @@ -48,22 +48,22 @@ "metalsmith": "^2.6.3", "metalsmith-html-relative": "^2.0.1", "ordered-read-streams": "^2.0.0", - "path2d": "^0.2.0", + "path2d": "^0.2.1", "pngjs": "^7.0.0", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-dark-theme-class": "^1.3.0", "postcss-dir-pseudo-class": "^8.0.1", "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", "prettier": "^3.3.2", - "puppeteer": "^22.12.0", + "puppeteer": "^22.12.1", "streamqueue": "^1.1.2", "stylelint": "^16.6.1", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", - "typescript": "^5.5.2", + "typescript": "^5.5.3", "vinyl": "^3.0.0", "webpack": "^5.92.1", "webpack-stream": "^7.0.0", @@ -3869,9 +3869,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", "dev": true, "funding": [ { @@ -7395,9 +7395,9 @@ } }, "node_modules/globals": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz", - "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "license": "MIT", "engines": { @@ -10505,9 +10505,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", "dev": true, "license": "MIT", "dependencies": { @@ -10516,9 +10516,9 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" }, "engines": { "node": ">= 14" @@ -10538,9 +10538,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -10738,10 +10738,11 @@ } }, "node_modules/path2d": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.0.tgz", - "integrity": "sha512-KdPAykQX6kmLSOO6Jpu2KNcCED7CKjmaBNGGNuctOsG0hgYO1OdYQaan6cYXJiG0WmXOwZZPILPBimu5QAIw3A==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -10830,9 +10831,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -10848,9 +10849,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -11277,9 +11279,9 @@ } }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -11342,17 +11344,17 @@ } }, "node_modules/puppeteer": { - "version": "22.12.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.12.0.tgz", - "integrity": "sha512-kyUYI12SyJIjf9UGTnHfhNMYv4oVK321Jb9QZDBiGVNx5453SplvbdKI7UrF+S//3RtCneuUFCyHxnvQXQjpxg==", + "version": "22.12.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.12.1.tgz", + "integrity": "sha512-1GxY8dnEnHr1SLzdSDr0FCjM6JQfAh2E2I/EqzeF8a58DbGVk9oVjj4lFdqNoVbpgFSpAbz7VER9St7S1wDpNg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.2.3", - "cosmiconfig": "9.0.0", + "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.12.0" + "puppeteer-core": "22.12.1" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" @@ -11362,17 +11364,17 @@ } }, "node_modules/puppeteer-core": { - "version": "22.12.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.12.0.tgz", - "integrity": "sha512-9gY+JwBW/Fp3/x9+cOGK7ZcwqjvtvY2xjqRqsAA0B3ZFMzBauVTSZ26iWTmvOQX2sk78TN/rd5rnetxVxmK5CQ==", + "version": "22.12.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.12.1.tgz", + "integrity": "sha512-XmqeDPVdC5/3nGJys1jbgeoZ02wP0WV1GBlPtr/ULRbGXJFuqgXMcKQ3eeNtFpBzGRbpeoCGWHge1ZWKWl0Exw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.2.3", "chromium-bidi": "0.5.24", - "debug": "4.3.5", + "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", - "ws": "8.17.1" + "ws": "^8.17.1" }, "engines": { "node": ">=18" @@ -12269,15 +12271,15 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -13696,9 +13698,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 5c880187a48f0..f121039c2b3a8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001636", + "caniuse-lite": "^1.0.30001639", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -28,7 +28,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-sort-exports": "^0.9.1", "eslint-plugin-unicorn": "^54.0.0", - "globals": "^15.6.0", + "globals": "^15.8.0", "gulp": "^5.0.0", "gulp-cli": "^3.0.0", "gulp-postcss": "^10.0.0", @@ -42,22 +42,22 @@ "metalsmith": "^2.6.3", "metalsmith-html-relative": "^2.0.1", "ordered-read-streams": "^2.0.0", - "path2d": "^0.2.0", + "path2d": "^0.2.1", "pngjs": "^7.0.0", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-dark-theme-class": "^1.3.0", "postcss-dir-pseudo-class": "^8.0.1", "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", "prettier": "^3.3.2", - "puppeteer": "^22.12.0", + "puppeteer": "^22.12.1", "streamqueue": "^1.1.2", "stylelint": "^16.6.1", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", - "typescript": "^5.5.2", + "typescript": "^5.5.3", "vinyl": "^3.0.0", "webpack": "^5.92.1", "webpack-stream": "^7.0.0", From 4e8a015a78e8c372244fc119757b55c3ad13388f Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Tue, 2 Jul 2024 18:29:19 +0200 Subject: [PATCH 0011/1133] Update translations to the most recent versions --- l10n/zh-TW/viewer.ftl | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/l10n/zh-TW/viewer.ftl b/l10n/zh-TW/viewer.ftl index f8614a9f35f7d..98ef060abc8f0 100644 --- a/l10n/zh-TW/viewer.ftl +++ b/l10n/zh-TW/viewer.ftl @@ -51,12 +51,6 @@ pdfjs-download-button-label = 下載 pdfjs-bookmark-button = .title = 目前頁面(含目前檢視頁面的網址) pdfjs-bookmark-button-label = 目前頁面 -# Used in Firefox for Android. -pdfjs-open-in-app-button = - .title = 在應用程式中開啟 -# Used in Firefox for Android. -# Length of the translation matters since we are in a mobile context, with limited screen estate. -pdfjs-open-in-app-button-label = 用程式開啟 ## Secondary toolbar and context menu @@ -82,8 +76,8 @@ pdfjs-cursor-hand-tool-button = .title = 開啟頁面移動工具 pdfjs-cursor-hand-tool-button-label = 頁面移動工具 pdfjs-scroll-page-button = - .title = 使用頁面捲動功能 -pdfjs-scroll-page-button-label = 頁面捲動功能 + .title = 使用單頁捲動版面 +pdfjs-scroll-page-button-label = 單頁捲動 pdfjs-scroll-vertical-button = .title = 使用垂直捲動版面 pdfjs-scroll-vertical-button-label = 垂直捲動 @@ -108,8 +102,8 @@ pdfjs-spread-even-button-label = 偶數跨頁 pdfjs-document-properties-button = .title = 文件內容… pdfjs-document-properties-button-label = 文件內容… -pdfjs-document-properties-file-name = 檔案名稱: -pdfjs-document-properties-file-size = 檔案大小: +pdfjs-document-properties-file-name = 檔案名稱: +pdfjs-document-properties-file-size = 檔案大小: # Variables: # $size_kb (Number) - the PDF file size in kilobytes # $size_b (Number) - the PDF file size in bytes @@ -118,21 +112,21 @@ pdfjs-document-properties-kb = { $size_kb } KB({ $size_b } 位元組) # $size_mb (Number) - the PDF file size in megabytes # $size_b (Number) - the PDF file size in bytes pdfjs-document-properties-mb = { $size_mb } MB({ $size_b } 位元組) -pdfjs-document-properties-title = 標題: -pdfjs-document-properties-author = 作者: -pdfjs-document-properties-subject = 主旨: -pdfjs-document-properties-keywords = 關鍵字: -pdfjs-document-properties-creation-date = 建立日期: -pdfjs-document-properties-modification-date = 修改日期: +pdfjs-document-properties-title = 標題: +pdfjs-document-properties-author = 作者: +pdfjs-document-properties-subject = 主旨: +pdfjs-document-properties-keywords = 關鍵字: +pdfjs-document-properties-creation-date = 建立日期: +pdfjs-document-properties-modification-date = 修改日期: # Variables: # $date (Date) - the creation/modification date of the PDF file # $time (Time) - the creation/modification time of the PDF file pdfjs-document-properties-date-string = { $date } { $time } -pdfjs-document-properties-creator = 建立者: -pdfjs-document-properties-producer = PDF 產生器: -pdfjs-document-properties-version = PDF 版本: -pdfjs-document-properties-page-count = 頁數: -pdfjs-document-properties-page-size = 頁面大小: +pdfjs-document-properties-creator = 建立者: +pdfjs-document-properties-producer = PDF 產生器: +pdfjs-document-properties-version = PDF 版本: +pdfjs-document-properties-page-count = 頁數: +pdfjs-document-properties-page-size = 頁面大小: pdfjs-document-properties-page-size-unit-inches = in pdfjs-document-properties-page-size-unit-millimeters = mm pdfjs-document-properties-page-size-orientation-portrait = 垂直 @@ -156,7 +150,7 @@ pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $hei # The linearization status of the document; usually called "Fast Web View" in # English locales of Adobe software. -pdfjs-document-properties-linearized = 快速 Web 檢視: +pdfjs-document-properties-linearized = 快速 Web 檢視: pdfjs-document-properties-linearized-yes = 是 pdfjs-document-properties-linearized-no = 否 pdfjs-document-properties-close-button = 關閉 @@ -296,8 +290,6 @@ pdfjs-editor-stamp-button-label = 新增或編輯圖片 pdfjs-editor-highlight-button = .title = 強調 pdfjs-editor-highlight-button-label = 強調 -pdfjs-highlight-floating-button = - .title = 強調 pdfjs-highlight-floating-button1 = .title = 強調 .aria-label = 強調 @@ -331,7 +323,7 @@ pdfjs-editor-free-highlight-thickness-title = .title = 更改強調文字以外的項目時的線條粗細 pdfjs-free-text = .aria-label = 文本編輯器 -pdfjs-free-text-default-content = 開始打字… +pdfjs-free-text-default-content = 在此打字… pdfjs-ink = .aria-label = 圖形編輯器 pdfjs-ink-canvas = From 832fc93aa4b882ff304a2c78d91418afbc0465fc Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 2 Jul 2024 22:38:10 +0200 Subject: [PATCH 0012/1133] Use vertical variant of a char when it's in a missing vertical font (bug 1905623) --- src/core/fonts.js | 8 +++++++ src/core/fonts_utils.js | 40 +++++++++++++++++++++++++++++++++++ test/pdfs/bug1905623.pdf.link | 1 + test/test_manifest.json | 10 +++++++++ 4 files changed, 59 insertions(+) create mode 100644 test/pdfs/bug1905623.pdf.link diff --git a/src/core/fonts.js b/src/core/fonts.js index 52994267da462..0301d49ad35e5 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -26,6 +26,7 @@ import { import { CFFCompiler, CFFParser } from "./cff_parser.js"; import { FontFlags, + getVerticalPresentationForm, MacStandardGlyphOrdering, normalizeFontName, recoverGlyphName, @@ -3366,6 +3367,13 @@ class Font { } } + if (this.missingFile && this.vertical && fontChar.length === 1) { + const vertical = getVerticalPresentationForm()[fontChar.charCodeAt(0)]; + if (vertical) { + fontChar = unicode = String.fromCharCode(vertical); + } + } + glyph = new Glyph( charcode, fontChar, diff --git a/src/core/fonts_utils.js b/src/core/fonts_utils.js index e5067d8e6ca8f..20c8e87e81905 100644 --- a/src/core/fonts_utils.js +++ b/src/core/fonts_utils.js @@ -15,6 +15,7 @@ import { getEncoding, StandardEncoding } from "./encodings.js"; import { getGlyphsUnicode } from "./glyphlist.js"; +import { getLookupTableFactory } from "./core_utils.js"; import { getUnicodeForGlyph } from "./unicode.js"; import { info } from "../shared/util.js"; @@ -168,8 +169,47 @@ function normalizeFontName(name) { return name.replaceAll(/[,_]/g, "-").replaceAll(/\s/g, ""); } +const getVerticalPresentationForm = getLookupTableFactory(t => { + // This table has been found at + // https://searchfox.org/mozilla-central/rev/cbdfa503a87597b20719aae5f6a1efccd6cb3b7b/gfx/thebes/gfxHarfBuzzShaper.cpp#251-294 + t[0x2013] = 0xfe32; // EN DASH + t[0x2014] = 0xfe31; // EM DASH + t[0x2025] = 0xfe30; // TWO DOT LEADER + t[0x2026] = 0xfe19; // HORIZONTAL ELLIPSIS + t[0x3001] = 0xfe11; // IDEOGRAPHIC COMMA + t[0x3002] = 0xfe12; // IDEOGRAPHIC FULL STOP + t[0x3008] = 0xfe3f; // LEFT ANGLE BRACKET + t[0x3009] = 0xfe40; // RIGHT ANGLE BRACKET + t[0x300a] = 0xfe3d; // LEFT DOUBLE ANGLE BRACKET + t[0x300b] = 0xfe3e; // RIGHT DOUBLE ANGLE BRACKET + t[0x300c] = 0xfe41; // LEFT CORNER BRACKET + t[0x300d] = 0xfe42; // RIGHT CORNER BRACKET + t[0x300e] = 0xfe43; // LEFT WHITE CORNER BRACKET + t[0x300f] = 0xfe44; // RIGHT WHITE CORNER BRACKET + t[0x3010] = 0xfe3b; // LEFT BLACK LENTICULAR BRACKET + t[0x3011] = 0xfe3c; // RIGHT BLACK LENTICULAR BRACKET + t[0x3014] = 0xfe39; // LEFT TORTOISE SHELL BRACKET + t[0x3015] = 0xfe3a; // RIGHT TORTOISE SHELL BRACKET + t[0x3016] = 0xfe17; // LEFT WHITE LENTICULAR BRACKET + t[0x3017] = 0xfe18; // RIGHT WHITE LENTICULAR BRACKET + t[0xfe4f] = 0xfe34; // WAVY LOW LINE + t[0xff01] = 0xfe15; // FULLWIDTH EXCLAMATION MARK + t[0xff08] = 0xfe35; // FULLWIDTH LEFT PARENTHESIS + t[0xff09] = 0xfe36; // FULLWIDTH RIGHT PARENTHESIS + t[0xff0c] = 0xfe10; // FULLWIDTH COMMA + t[0xff1a] = 0xfe13; // FULLWIDTH COLON + t[0xff1b] = 0xfe14; // FULLWIDTH SEMICOLON + t[0xff1f] = 0xfe16; // FULLWIDTH QUESTION MARK + t[0xff3b] = 0xfe47; // FULLWIDTH LEFT SQUARE BRACKET + t[0xff3d] = 0xfe48; // FULLWIDTH RIGHT SQUARE BRACKET + t[0xff3f] = 0xfe33; // FULLWIDTH LOW LINE + t[0xff5b] = 0xfe37; // FULLWIDTH LEFT CURLY BRACKET + t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET +}); + export { FontFlags, + getVerticalPresentationForm, MacStandardGlyphOrdering, normalizeFontName, recoverGlyphName, diff --git a/test/pdfs/bug1905623.pdf.link b/test/pdfs/bug1905623.pdf.link new file mode 100644 index 0000000000000..0c249d74b4846 --- /dev/null +++ b/test/pdfs/bug1905623.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9410429 diff --git a/test/test_manifest.json b/test/test_manifest.json index a8fedcd4b1318..78c4d8f592b83 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10113,5 +10113,15 @@ "rounds": 1, "link": true, "type": "eq" + }, + { + "id": "bug1905623", + "file": "pdfs/bug1905623.pdf", + "md5": "6c180a4353bda6b3cb0c693c5e5b32c4", + "rounds": 1, + "link": true, + "firstPage": 2, + "lastPage": 2, + "type": "eq" } ] From fd6d0be50fe6e858315d9dea56bfc82af646c7bb Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 3 Jul 2024 09:57:12 +0200 Subject: [PATCH 0013/1133] Make sure the editor is visible before getting its rect --- test/integration/test_utils.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index c49cb1daa9d9f..4e7d28e00a62a 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -135,7 +135,7 @@ function getSelector(id) { async function getRect(page, selector) { // In Chrome something is wrong when serializing a `DomRect`, // so we extract the values and return them ourselves. - await page.waitForSelector(selector); + await page.waitForSelector(selector, { visible: true }); return page.$eval(selector, el => { const { x, y, width, height } = el.getBoundingClientRect(); return { x, y, width, height }; From 53ddfd139ff9ea60a4b2e1a856ea93f46a014aff Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 4 Jul 2024 10:33:53 +0200 Subject: [PATCH 0014/1133] Fix the integration tests related to printing --- test/integration/scripting_spec.mjs | 67 ++++++++++++++--------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index ed71e1580bbbd..a69ba3f317e6a 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -17,6 +17,7 @@ import { awaitPromise, clearInput, closePages, + closeSinglePage, getAnnotationStorage, getComputedStyleSelector, getFirstSerialized, @@ -418,41 +419,37 @@ describe("Interaction", () => { pages = await loadAndWait("doc_actions.pdf", getSelector("47R")); }); - afterAll(async () => { - await closePages(pages); - }); - it("must execute WillPrint and DidPrint actions", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - if (process.platform === "win32" && browserName === "firefox") { - pending("Disabled in Firefox on Windows, because of bug 1662471."); - } - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); - - await clearInput(page, getSelector("47R")); - await page.evaluate(_ => { - window.document.activeElement.blur(); - }); - await page.waitForFunction(`${getQuerySelector("47R")}.value === ""`); + // Run the tests sequentially to avoid to use the same printer at the same + // time. + // And to make sure that a printer isn't locked by a process we close the + // page before running the next test. + for (const [browserName, page] of pages) { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); - let text = await actAndWaitForInput( - page, - getSelector("47R"), - async () => { - await page.click("#print"); - } - ); - expect(text).withContext(`In ${browserName}`).toEqual("WillPrint"); + await clearInput(page, getSelector("47R")); + await page.evaluate(_ => { + window.document.activeElement.blur(); + }); + await page.waitForFunction(`${getQuerySelector("47R")}.value === ""`); - await page.waitForFunction(`${getQuerySelector("50R")}.value !== ""`); + const text = await actAndWaitForInput( + page, + getSelector("47R"), + async () => { + await page.click("#print"); + } + ); + expect(text).withContext(`In ${browserName}`).toEqual("WillPrint"); + await page.keyboard.press("Escape"); - text = await page.$eval(getSelector("50R"), el => el.value); - expect(text).withContext(`In ${browserName}`).toEqual("DidPrint"); - }) - ); + await page.waitForFunction( + `${getQuerySelector("50R")}.value === "DidPrint"` + ); + await closeSinglePage(page); + } }); }); @@ -1789,17 +1786,19 @@ describe("Interaction", () => { pages = await loadAndWait( "autoprint.pdf", "", - null /* pageSetup = */, + null /* zoom = */, async page => { printHandles.set( page, - await page.evaluateHandle(() => [ + page.evaluateHandle(() => [ new Promise(resolve => { globalThis.printResolve = resolve; }), ]) ); await page.waitForFunction(() => { + // We don't really need to print the document. + window.print = () => {}; if (!window.PDFViewerApplication?.eventBus) { return false; } @@ -1826,7 +1825,7 @@ describe("Interaction", () => { it("must check if printing is triggered when the document is open", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await awaitPromise(printHandles.get(page)); + await awaitPromise(await printHandles.get(page)); }) ); }); From 74cbfbd09f831cbbaba4d556c056092af166d167 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 4 Jul 2024 22:01:35 +0200 Subject: [PATCH 0015/1133] [Editor] Remove the option enableStamp --- extensions/chromium/preferences_schema.json | 4 ---- web/app.js | 4 ---- web/app_options.js | 8 -------- web/viewer.html | 2 +- 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index ec8654581065e..80f2716d11c4c 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -86,10 +86,6 @@ "type": "string", "default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" }, - "enableStampEditor": { - "type": "boolean", - "default": true - }, "disableRange": { "title": "Disable range requests", "description": "Whether to disable range requests (not recommended).", diff --git a/web/app.js b/web/app.js index 45dc9fd7e221b..5dec14086101a 100644 --- a/web/app.js +++ b/web/app.js @@ -521,10 +521,6 @@ const PDFViewerApplication = { if (appConfig.annotationEditorParams) { if (annotationEditorMode !== AnnotationEditorType.DISABLE) { - if (AppOptions.get("enableStampEditor")) { - appConfig.toolbar?.editorStampButton?.classList.remove("hidden"); - } - const editorHighlightButton = appConfig.toolbar?.editorHighlightButton; if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) { editorHighlightButton.hidden = false; diff --git a/web/app_options.js b/web/app_options.js index ac24bf1505873..3df0a95c244ad 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -172,14 +172,6 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, - enableStampEditor: { - // We'll probably want to make some experiments before enabling this - // in Firefox release, but it has to be temporary. - // TODO: remove it when unnecessary. - /** @type {boolean} */ - value: true, - kind: OptionKind.VIEWER + OptionKind.PREFERENCE, - }, externalLinkRel: { /** @type {string} */ value: "noopener noreferrer nofollow", diff --git a/web/viewer.html b/web/viewer.html index 09223821964a8..4175bcf5dbe80 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -366,7 +366,7 @@ - From a4ffc1066c9f6e1226b02c11a7c568a519a0654c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 4 Jul 2024 21:21:57 +0200 Subject: [PATCH 0016/1133] Move the internal API/Worker `isEditing`-state into `RenderingIntentFlag` In *hindsight* this seems like a better idea, since it avoids the need to manually pass `isEditing` around as a boolean value. Note that `RenderingIntentFlag` is *internal* functionality, not exposed in the official API, which means that it can be extended and modified as necessary. --- src/core/document.js | 2 +- src/core/worker.js | 1 - src/display/api.js | 7 +++---- src/shared/util.js | 3 +++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index ae07d8a8bec3b..163565b328542 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -411,7 +411,6 @@ class Page { intent, cacheKey, annotationStorage = null, - isEditing = false, modifiedIds = null, }) { const contentStreamPromise = this.getContentStream(); @@ -570,6 +569,7 @@ class Page { return { length: pageOpList.totalLength }; } const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS), + isEditing = !!(intent & RenderingIntentFlag.IS_EDITING), intentAny = !!(intent & RenderingIntentFlag.ANY), intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY), intentPrint = !!(intent & RenderingIntentFlag.PRINT); diff --git a/src/core/worker.js b/src/core/worker.js index 8d223c2738aca..0eff8f9520ed1 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -752,7 +752,6 @@ class WorkerMessageHandler { intent: data.intent, cacheKey: data.cacheKey, annotationStorage: data.annotationStorage, - isEditing: data.isEditing, modifiedIds: data.modifiedIds, }) .then( diff --git a/src/display/api.js b/src/display/api.js index 6244a0611eff4..8cc5bfbd6be77 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1818,7 +1818,6 @@ class PDFPageProxy { renderingIntent, cacheKey, annotationStorageSerializable, - isEditing, modifiedIds, }) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { @@ -1836,7 +1835,6 @@ class PDFPageProxy { intent: renderingIntent, cacheKey, annotationStorage: map, - isEditing, modifiedIds, }, transfer @@ -2473,6 +2471,9 @@ class WorkerTransport { warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`); } + if (isEditing) { + renderingIntent += RenderingIntentFlag.IS_EDITING; + } if (isOpList) { renderingIntent += RenderingIntentFlag.OPLIST; } @@ -2483,7 +2484,6 @@ class WorkerTransport { const cacheKeyBuf = [ renderingIntent, annotationStorageSerializable.hash, - isEditing ? 1 : 0, modifiedIdsHash, ]; @@ -2491,7 +2491,6 @@ class WorkerTransport { renderingIntent, cacheKey: cacheKeyBuf.join("_"), annotationStorageSerializable, - isEditing, modifiedIds, }; } diff --git a/src/shared/util.js b/src/shared/util.js index 68d12cd4cd21c..292a66f52913e 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -41,10 +41,12 @@ const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR; * how these flags are being used: * - ANY, DISPLAY, and PRINT are the normal rendering intents, note the * `PDFPageProxy.{render, getOperatorList, getAnnotations}`-methods. + * - SAVE is used, on the worker-thread, when saving modified annotations. * - ANNOTATIONS_FORMS, ANNOTATIONS_STORAGE, ANNOTATIONS_DISABLE control which * annotations are rendered onto the canvas (i.e. by being included in the * operatorList), note the `PDFPageProxy.{render, getOperatorList}`-methods * and their `annotationMode`-option. + * - IS_EDITING is used when editing is active in the viewer. * - OPLIST is used with the `PDFPageProxy.getOperatorList`-method, note the * `OperatorList`-constructor (on the worker-thread). */ @@ -56,6 +58,7 @@ const RenderingIntentFlag = { ANNOTATIONS_FORMS: 0x10, ANNOTATIONS_STORAGE: 0x20, ANNOTATIONS_DISABLE: 0x40, + IS_EDITING: 0x80, OPLIST: 0x100, }; From 5f744904ac249e2af38e84fbe9ea96a93d096c82 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 5 Jul 2024 11:34:46 +0200 Subject: [PATCH 0017/1133] Check the relevant parameters inside of the `mustBeViewedWhenEditing` method Similar to the `mustBeViewed` method, we can check the relevant parameters within the `mustBeViewedWhenEditing` method itself since that (in my opinion) slightly helps readability of the code in the `src/core/document.js` file. --- src/core/annotation.js | 4 ++-- src/core/document.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 632112d967ef7..d95a00c7b40f4 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -777,8 +777,8 @@ class Annotation { return this.printable; } - mustBeViewedWhenEditing() { - return !this.data.isEditable; + mustBeViewedWhenEditing(isEditing, modifiedIds = null) { + return isEditing ? !this.data.isEditable : !modifiedIds?.has(this.data.id); } /** diff --git a/src/core/document.js b/src/core/document.js index 163565b328542..ee0aaaa07c432 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -582,8 +582,7 @@ class Page { intentAny || (intentDisplay && annotation.mustBeViewed(annotationStorage, renderForms) && - ((isEditing && annotation.mustBeViewedWhenEditing()) || - (!isEditing && !modifiedIds?.has(annotation.data.id)))) || + annotation.mustBeViewedWhenEditing(isEditing, modifiedIds)) || (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( From 38528d1116edf81090423b9cb5b78b5b820f7d8a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 5 Jul 2024 12:14:36 +0200 Subject: [PATCH 0018/1133] Remove the `renderForms` parameter from the Annotation `getOperatorList` methods The `renderForms` parameter pre-dates the introduction of the general `intent` parameter, which means that we're now effectively passing the same state twice to these `getOperatorList` methods. --- src/core/annotation.js | 51 +++++------------------------------- src/core/document.js | 1 - test/unit/annotation_spec.js | 13 --------- 3 files changed, 7 insertions(+), 58 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 632112d967ef7..6f5ce184bb272 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1105,13 +1105,7 @@ class Annotation { }); } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { const { hasOwnCanvas, id, rect } = this.data; let appearance = this.appearance; const isUsingOwnCanvas = !!( @@ -1959,17 +1953,11 @@ class WidgetAnnotation extends Annotation { return str; } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { // Do not render form elements on the canvas when interactive forms are // enabled. The display layer is responsible for rendering them instead. if ( - renderForms && + intent & RenderingIntentFlag.ANNOTATIONS_FORMS && !(this instanceof SignatureWidgetAnnotation) && !this.data.noHTML && !this.data.hasOwnCanvas @@ -1982,13 +1970,7 @@ class WidgetAnnotation extends Annotation { } if (!this._hasText) { - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } const content = await this._getAppearance( @@ -1998,13 +1980,7 @@ class WidgetAnnotation extends Annotation { annotationStorage ); if (this.appearance && content === null) { - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } const opList = new OperatorList(); @@ -2934,13 +2910,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { if (this.data.pushButton) { return super.getOperatorList( evaluator, @@ -2962,13 +2932,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { if (value === null && this.appearance) { // Nothing in the annotationStorage. // But we've a default appearance so use it. - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } if (value === null || value === undefined) { @@ -3001,7 +2965,6 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { evaluator, task, intent, - renderForms, annotationStorage ); this.appearance = savedAppearance; diff --git a/src/core/document.js b/src/core/document.js index 163565b328542..b0cb1a86afba5 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -592,7 +592,6 @@ class Page { partialEvaluator, task, intent, - renderForms, annotationStorage ) .catch(function (reason) { diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index f4828f3d91096..4b3949ceeda97 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1768,7 +1768,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2523,7 +2522,6 @@ describe("annotation", function () { checkboxEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(5); @@ -2584,7 +2582,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList1.argsArray.length).toEqual(3); @@ -2608,7 +2605,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList2.argsArray.length).toEqual(3); @@ -2670,7 +2666,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2732,7 +2727,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2986,7 +2980,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList1.argsArray.length).toEqual(3); @@ -3010,7 +3003,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList2.argsArray.length).toEqual(3); @@ -3070,7 +3062,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -4242,7 +4233,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4503,7 +4493,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4672,7 +4661,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4791,7 +4779,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); From 5274bab9f3f1359a576cbab14fa3c20fae468e2a Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 3 Jul 2024 19:25:43 +0200 Subject: [PATCH 0019/1133] [Editor] Avoid to query ML engine several times for the same image --- src/display/editor/stamp.js | 71 +++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index 6b150471c6fbf..ed17593392988 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -36,6 +36,8 @@ class StampEditor extends AnnotationEditor { #canvas = null; + #hasMLBeenQueried = false; + #observer = null; #resizeTimeoutId = null; @@ -423,6 +425,42 @@ class StampEditor extends AnnotationEditor { return bitmap; } + async #mlGuessAltText(bitmap, width, height) { + if (this.#hasMLBeenQueried) { + return; + } + this.#hasMLBeenQueried = true; + if (!this._uiManager.hasMLManager || this.hasAltText()) { + return; + } + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext("2d", { willReadFrequently: true }); + ctx.drawImage( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + 0, + 0, + width, + height + ); + const response = await this._uiManager.mlGuess({ + service: "image-to-text", + request: { + data: ctx.getImageData(0, 0, width, height).data, + width, + height, + channels: 4, + }, + }); + const altText = response?.output || ""; + if (this.parent && altText && !this.hasAltText()) { + this.altTextData = { altText, decorative: false }; + } + } + #drawBitmap(width, height) { width = Math.ceil(width); height = Math.ceil(height); @@ -436,37 +474,8 @@ class StampEditor extends AnnotationEditor { ? this.#bitmap : this.#scaleBitmap(width, height); - if (this._uiManager.hasMLManager && !this.hasAltText()) { - const offscreen = new OffscreenCanvas(width, height); - const ctx = offscreen.getContext("2d"); - ctx.drawImage( - bitmap, - 0, - 0, - bitmap.width, - bitmap.height, - 0, - 0, - width, - height - ); - this._uiManager - .mlGuess({ - service: "image-to-text", - request: { - data: ctx.getImageData(0, 0, width, height).data, - width, - height, - channels: 4, - }, - }) - .then(response => { - const altText = response?.output || ""; - if (this.parent && altText && !this.hasAltText()) { - this.altTextData = { altText, decorative: false }; - } - }); - } + this.#mlGuessAltText(bitmap, width, height); + const ctx = canvas.getContext("2d"); ctx.filter = this._uiManager.hcmFilter; ctx.drawImage( From 0910f17a58b7a909f39977ed5ee690d8798e668b Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 5 Jul 2024 17:14:51 +0200 Subject: [PATCH 0020/1133] Allow to change the toolbar height when changing the pref toolbar.density in Firefox (bug 1171799) It's a first step to just dispatch the pref change to the toolbar. The second one, to effectively use it, will come after PR #18385 is merged. --- web/app.js | 8 ++++++-- web/app_options.js | 5 +++++ web/preferences.js | 10 ++++++++++ web/toolbar.js | 12 +++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/web/app.js b/web/app.js index 5dec14086101a..20976ae651c44 100644 --- a/web/app.js +++ b/web/app.js @@ -393,7 +393,7 @@ const PDFViewerApplication = { const { appConfig, externalServices, l10n } = this; let eventBus; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - eventBus = new FirefoxEventBus( + eventBus = this.preferences.eventBus = new FirefoxEventBus( await this._allowedGlobalEventsPromise, externalServices, AppOptions.get("isInAutomation") @@ -569,7 +569,11 @@ const PDFViewerApplication = { await this._nimbusDataPromise ); } else { - this.toolbar = new Toolbar(appConfig.toolbar, eventBus); + this.toolbar = new Toolbar( + appConfig.toolbar, + eventBus, + AppOptions.get("toolbarDensity") + ); } } diff --git a/web/app_options.js b/web/app_options.js index 3df0a95c244ad..e111ae44b7b15 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -95,6 +95,11 @@ const defaultOptions = { value: true, kind: OptionKind.BROWSER, }, + toolbarDensity: { + /** @type {number} */ + value: 0, // 0 = "normal", 1 = "compact", 2 = "touch" + kind: OptionKind.BROWSER, + }, annotationEditorMode: { /** @type {number} */ diff --git a/web/preferences.js b/web/preferences.js index 15da2cc584440..d181d1add0960 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -37,6 +37,8 @@ class BasePreferences { #initializedPromise = null; + static #eventToDispatch = new Set(["toolbarDensity"]); + constructor() { if (this.constructor === BasePreferences) { throw new Error("Cannot initialize BasePreferences."); @@ -73,6 +75,10 @@ class BasePreferences { } } ); + + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + this.eventBus = null; + } } /** @@ -113,6 +119,10 @@ class BasePreferences { return; // Invalid preference. } AppOptions.set(name, value); + + if (BasePreferences.#eventToDispatch.has(name)) { + this.eventBus?.dispatch(name.toLowerCase(), { source: this, value }); + } } /** diff --git a/web/toolbar.js b/web/toolbar.js index e92b546c4aad4..f37b5d4046d7e 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -50,8 +50,13 @@ class Toolbar { /** * @param {ToolbarOptions} options * @param {EventBus} eventBus + * @param {number} toolbarDensity - The toolbar density value. + * The possible values are: + * - 0 (default) - The regular toolbar size. + * - 1 (compact) - The small toolbar size. + * - 2 (touch) - The large toolbar size. */ - constructor(options, eventBus) { + constructor(options, eventBus, toolbarDensity = 0) { this.#opts = options; this.eventBus = eventBus; const buttons = [ @@ -136,9 +141,14 @@ class Toolbar { } }); + eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this)); + this.#updateToolbarDensity({ value: toolbarDensity }); + this.reset(); } + #updateToolbarDensity() {} + #setAnnotationEditorUIManager(uiManager, parentContainer) { const colorPicker = new ColorPicker({ uiManager }); uiManager.setMainHighlightColorPicker(colorPicker); From 0fba6e570e5eb31140143ac060240ffa362b90b3 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 5 Jul 2024 17:56:41 +0200 Subject: [PATCH 0021/1133] [Editor] Change the enableML pref for enableAltText (bug 1905923) We want to use this pref to make a Nimbus experiment in the next weeks. --- extensions/chromium/preferences_schema.json | 2 +- src/display/editor/stamp.js | 2 +- src/display/editor/tools.js | 4 ++-- web/app.js | 3 ++- web/app_options.js | 10 +++++----- web/chromecom.js | 4 ++++ web/firefoxcom.js | 10 ++++++++++ web/genericcom.js | 4 ++++ 8 files changed, 29 insertions(+), 10 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 80f2716d11c4c..fc08e693313dd 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -51,7 +51,7 @@ "type": "boolean", "default": true }, - "enableML": { + "enableAltText": { "type": "boolean", "default": false }, diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index ed17593392988..f4f1387a62469 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -430,7 +430,7 @@ class StampEditor extends AnnotationEditor { return; } this.#hasMLBeenQueried = true; - if (!this._uiManager.hasMLManager || this.hasAltText()) { + if (!this._uiManager.isMLEnabledFor("altText") || this.hasAltText()) { return; } const offscreen = new OffscreenCanvas(width, height); diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 2fb916c72969d..4adebd9868ce9 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -851,8 +851,8 @@ class AnnotationEditorUIManager { return this.#mlManager?.guess(data) || null; } - get hasMLManager() { - return !!this.#mlManager; + isMLEnabledFor(name) { + return !!this.#mlManager?.isEnabledFor(name); } get hcmFilter() { diff --git a/web/app.js b/web/app.js index 5dec14086101a..e25cb8c74707e 100644 --- a/web/app.js +++ b/web/app.js @@ -752,10 +752,11 @@ const PDFViewerApplication = { }, get mlManager() { + const enableAltText = AppOptions.get("enableAltText"); return shadow( this, "mlManager", - AppOptions.get("enableML") === true ? new MLManager() : null + enableAltText === true ? new MLManager({ enableAltText }) : null ); }, diff --git a/web/app_options.js b/web/app_options.js index 3df0a95c244ad..769ca021f16ff 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -136,6 +136,11 @@ const defaultOptions = { value: false, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableAltText: { + /** @type {boolean} */ + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableHighlightEditor: { // We'll probably want to make some experiments before enabling this // in Firefox release, but it has to be temporary. @@ -152,11 +157,6 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, - enableML: { - /** @type {boolean} */ - value: false, - kind: OptionKind.VIEWER + OptionKind.PREFERENCE, - }, enablePermissions: { /** @type {boolean} */ value: false, diff --git a/web/chromecom.js b/web/chromecom.js index 7f26b70dbccc3..b33e445377cf0 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -436,6 +436,10 @@ class ExternalServices extends BaseExternalServices { } class MLManager { + isEnabledFor(_name) { + return false; + } + async guess() { return null; } diff --git a/web/firefoxcom.js b/web/firefoxcom.js index e431f4a081853..a7cd0068bb46e 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -303,6 +303,16 @@ class FirefoxScripting { } class MLManager { + #enabled = new Map(); + + constructor({ enableAltText }) { + this.#enabled.set("altText", enableAltText); + } + + isEnabledFor(name) { + return this.#enabled.get(name); + } + guess(data) { return FirefoxCom.requestAsync("mlGuess", data); } diff --git a/web/genericcom.js b/web/genericcom.js index 996051018f6f8..6ab2766efe47b 100644 --- a/web/genericcom.js +++ b/web/genericcom.js @@ -48,6 +48,10 @@ class ExternalServices extends BaseExternalServices { } class MLManager { + isEnabledFor(_name) { + return false; + } + async guess() { return null; } From 70b44251ed9650a53bc1e899f5127c1bb2584141 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Fri, 5 Jul 2024 16:56:31 +0200 Subject: [PATCH 0022/1133] Fix the "must check that charLimit is correctly set" scripting integration test This integration test fails intermittently because we're not (correctly) awaiting the character limit increase sandbox action. For the first increase an attempt was made to handle this, but it doesn't work correctly because the text in the field is `abcdefghijklmnopq` and it's not be truncated until the sandbox action is completed, so because we didn't await that we would could pass the `value !== "abcdefgh"` check because `abcdefghijklmnopq !== abcdefgh` is true. For the second increase we didn't have a check in place. This commit fixes the issues by using the `waitForSandboxTrip` and `waitForSelector` helper functions to make sure that we can only proceed if the sandbox action is completed and the DOM element is updated. --- test/integration/scripting_spec.mjs | 42 ++++++++++------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index a69ba3f317e6a..b1e08197b730b 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -1535,49 +1535,35 @@ describe("Interaction", () => { it("must check that charLimit is correctly set", async () => { await Promise.all( - pages.map(async ([browserName, page]) => { + pages.map(async ([, page]) => { await page.waitForFunction( "window.PDFViewerApplication.scriptingReady === true" ); - await clearInput(page, getSelector("7R")); - // By default the charLimit is 0 which means that the input - // length is unlimited. - await page.type(getSelector("7R"), "abcdefghijklmnopq", { - delay: 10, - }); - - let value = await page.$eval(getSelector("7R"), el => el.value); - expect(value) - .withContext(`In ${browserName}`) - .toEqual("abcdefghijklmnopq"); - - // charLimit is set to 1 - await page.click(getSelector("9R")); - + // The default charLimit is 0, which indicates unlimited text length. + await page.type(getSelector("7R"), "abcdefghij", { delay: 10 }); await page.waitForFunction( - `document.querySelector('${getSelector( - "7R" - )}').value !== "abcdefgh"` + `${getQuerySelector("7R")}.value === "abcdefghij"` ); - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("a"); + // Increase the charLimit to 1 (this truncates the existing text). + await page.click(getSelector("9R")); + await waitForSandboxTrip(page); + await page.waitForFunction(`${getQuerySelector("7R")}.value === "a"`); await clearInput(page, getSelector("7R")); await page.type(getSelector("7R"), "xyz", { delay: 10 }); + await page.waitForFunction(`${getQuerySelector("7R")}.value === "x"`); - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("x"); - - // charLimit is set to 2 + // Increase the charLimit to 2. await page.click(getSelector("9R")); + await waitForSandboxTrip(page); await clearInput(page, getSelector("7R")); await page.type(getSelector("7R"), "xyz", { delay: 10 }); - - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("xy"); + await page.waitForFunction( + `${getQuerySelector("7R")}.value === "xy"` + ); }) ); }); From 3afe2d30480fc7c15a459dca67ecee9246e98c36 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 6 Jul 2024 14:08:42 +0200 Subject: [PATCH 0023/1133] Fix orphaned browser processes due to uncaught exceptions in the tests If uncaught exceptions occur in the tests (which happened in #17962 and can be triggered manually by throwing an error in `integration-boot.js`) the teardown logic of the tests doesn't get to run and thus spawned browser processes are not closed properly. Given that `test.mjs` is the only process that has a reference to them they will become orphaned and keep running if `test.mjs` exits without explicitly closing them. This commit fixes the issue by always closing the browsers if uncaught exceptions occur, and we make sure to log them for debugging purposes. --- test/test.mjs | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/test/test.mjs b/test/test.mjs index 69a7959b52096..b3315f30c2a68 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -970,8 +970,6 @@ async function startBrowsers({ baseUrl, initializeSession }) { await puppeteer.trimCache(); const browserNames = options.noChrome ? ["firefox"] : ["firefox", "chrome"]; - - sessions = []; for (const browserName of browserNames) { // The session must be pushed first and augmented with the browser once // it's initialized. The reason for this is that browser initialization @@ -1078,25 +1076,33 @@ async function main() { stats = []; } - if (options.downloadOnly) { - await ensurePDFsDownloaded(); - } else if (options.unitTest) { - // Allows linked PDF files in unit-tests as well. - await ensurePDFsDownloaded(); - startUnitTest("/test/unit/unit_test.html", "unit"); - } else if (options.fontTest) { - startUnitTest("/test/font/font_test.html", "font"); - } else if (options.integration) { - // Allows linked PDF files in integration-tests as well. - await ensurePDFsDownloaded(); - startIntegrationTest(); - } else { - startRefTest(options.masterMode, options.reftest); + try { + if (options.downloadOnly) { + await ensurePDFsDownloaded(); + } else if (options.unitTest) { + // Allows linked PDF files in unit-tests as well. + await ensurePDFsDownloaded(); + await startUnitTest("/test/unit/unit_test.html", "unit"); + } else if (options.fontTest) { + await startUnitTest("/test/font/font_test.html", "font"); + } else if (options.integration) { + // Allows linked PDF files in integration-tests as well. + await ensurePDFsDownloaded(); + await startIntegrationTest(); + } else { + await startRefTest(options.masterMode, options.reftest); + } + } catch (e) { + // Close the browsers if uncaught exceptions occur, otherwise the spawned + // processes can become orphaned and keep running after `test.mjs` exits + // because the teardown logic of the tests did not get a chance to run. + console.error(e); + await Promise.all(sessions.map(session => closeSession(session.name))); } } var server; -var sessions; +var sessions = []; var onAllSessionsClosed; var host = "127.0.0.1"; var options = parseOptions(); From 9e352a8bad79b106d017ef7ae55a8ccce448b80f Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 6 Jul 2024 15:14:39 +0200 Subject: [PATCH 0024/1133] Move the "updatedPreference" event listener registration This patch fixes a situation that'll probably never happen, but nonetheless seems like something that we should address. Currently the "updatedPreference" listener isn't registered *until after* the preferences have been read and initialized, which leaves a very short window of time where a preference change could theoretically be skipped. --- web/preferences.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/preferences.js b/web/preferences.js index d181d1add0960..944c0d84ba37f 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -67,16 +67,13 @@ class BasePreferences { typeof prefVal === typeof val ? prefVal : val; } AppOptions.setAll(options, /* init = */ true); - - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - window.addEventListener("updatedPreference", evt => { - this.#updatePref(evt.detail); - }); - } } ); if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + window.addEventListener("updatedPreference", evt => { + this.#updatePref(evt.detail); + }); this.eventBus = null; } } @@ -101,10 +98,11 @@ class BasePreferences { throw new Error("Not implemented: _readFromStorage"); } - #updatePref({ name, value }) { + async #updatePref({ name, value }) { if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { throw new Error("Not implemented: #updatePref"); } + await this.#initializedPromise; if (name in this.#browserDefaults) { if (typeof value !== typeof this.#browserDefaults[name]) { From 2a44203d96cc8c09f597e6f7ddf34dc8a13e58e7 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 6 Jul 2024 16:17:36 +0200 Subject: [PATCH 0025/1133] Fix the "caches image resources at the document/page level as expected (issue 11878)" unit test This unit test fails occasionally (albeit much less than before thanks to PR #17663), so we change the parsing time check's divisor to prevent it from happening again. If the last page's rendering time is less than or equal to 50% of the first page's rendering time that should be enough proof that no worker thread re-parsing occurred while also providing a wide enough range to avoid intermittents. Note that the assertion is now equal to the one we already have in the "caches image resources at the document/page level, with main-thread copying of complex images (issue 11518)" unit test which seems to work reliably so far. --- test/unit/api_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 44f9de06b9373..7a383498c3a9c 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -4155,7 +4155,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) checkedCopyLocalImage = true; // Ensure that the image was copied in the main-thread, rather // than being re-parsed in the worker-thread (which is slower). - expect(statsOverall).toBeLessThan(firstStatsOverall / 4); + expect(statsOverall).toBeLessThan(firstStatsOverall / 2); } } } From 1c364422a612f02bc9f50212c7c40dd473e544ff Mon Sep 17 00:00:00 2001 From: alexcat3 Date: Fri, 5 Jul 2024 13:04:11 -0400 Subject: [PATCH 0026/1133] Handle toUnicode cmaps that omit leading zeros in hex encoded UTF-16 (issue 18099) Add unit test to check compatability with such cmaps In the PDF in issue 18099. the toUnicode cmap had a line to map the glyph char codes from 00 to 7F to the corresponding code points. The syntax to map a range of char codes to a range of unicode code points is As the unicode code points are supposed to be given in UTF-16 BE, the PDF's line SHOULD have probably read <00> <7F> <0000> Instead it omitted two leading zeros from the UTF-16 like this <00> <7F> <00> This confused PDF.js into mapping these character codes to the UTF-16 characters with the corresponding HIGH bytes (01 became \u0100, 02 became \u0200, et cetera), which ended up turning latin text in the PDF into chinese when it was copied I'm not sure if the PDF spec actually allows PDFs to do this, but since there's at least one PDF in the wild that does and other PDF readers read it correctly, PDF.js should probably support this --- src/core/evaluator.js | 5 +++++ test/pdfs/.gitignore | 1 + test/pdfs/issue18099_reduced.pdf | Bin 0 -> 1483 bytes test/unit/api_spec.js | 15 +++++++++++++++ 4 files changed, 21 insertions(+) create mode 100644 test/pdfs/issue18099_reduced.pdf diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 11c83d04f4dc0..3896ed47e406c 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -3852,6 +3852,11 @@ class PartialEvaluator { map[charCode] = String.fromCodePoint(token); return; } + // Add back omitted leading zeros on odd length tokens + // (fixes issue #18099) + if (token.length % 2 !== 0) { + token = "\u0000" + token; + } const str = []; for (let k = 0; k < token.length; k += 2) { const w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 98c3f4ace1aa7..13450e37cd32e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -653,3 +653,4 @@ !bug1539074.1.pdf !issue18305.pdf !issue18360.pdf +!issue18099_reduced.pdf diff --git a/test/pdfs/issue18099_reduced.pdf b/test/pdfs/issue18099_reduced.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8fa6fd6a8d7e2f782f3f0f6323590f724a2cdbd7 GIT binary patch literal 1483 zcmY!laBiE*l&DfW-9FVg)0hNRWblNM%8)zH?$p zVorX#ogFV%YF-Lh9Y`9c!8yM)uSCI6-#as=bcMkWU42BwBa#wNz5n)=Ql>HH!Mpj$Ibb2aq?it!Fc(fPrZMN8!Uu<;KAVCP5sYk1u`jID7JBxW;+?Q=WlWLO4!7D&l2iI9J9s z0UYKq?XchnISgj8Q(|!{$jSO4i5dC1iTZAtMa3n~8Hq&-#zy)+iBPtYsi}T&X;KL& zK0}I1Q^64%k{_Cv3394}IoPKVUxExn_9aLTW*Ht|8k-_}Gj@u7|6>J#qt$k-PxCf@ zN;qDo6q&clcggSNr(;WRF&!3&yp?deu&d^`@SdG1-@8)&B+M|k7ZCY<__bo+YP~w0 z$=07%9hUfeX!^@t7T1{PA6wzG^6!hVz@PW$zFpJx_uju@<5cloHlUGMbL zoiCT~?G3I8JNo_TXQN}2>wfLEuhX0#TDa%jXY2Wv>t==A&pz)c`g?uZzqIpfy7zo| z`oMDCo2`a zZ>y9qyk1`tKB=^pD|Ke`M$OZ2TnwK{u{arME-jH*qoJQ+5dL=4s?+YiMr-$Le?6Gl zy}{W*{8)X~GN&6U&hvwNL~?m}F6PJ98l|-4uD>6Wp1N2_kFkm8>8zBE3CRmSEm&!j z6~(iS<2JkcOesEIu`M%adHp}2dYO-NdWOJZtLHpP5l)?otFAJeiM1X6cF=PfUw~=W z;;jek50&1$aK)iGp~yf*!piW7VPNw?u~UwTdh+}er57Z;uwmlqOw4fDu~Oi|zV!kL z5yB!owmjL2DLjuY!oOkL7PF9pzpyJAL z^uhxHvlV*}USpA9Q*<$AY;ojxc;&$>R>dhk0&I#?k`~r`i~qWR)wkT**71A#;n?Y| zk7wOq`6pnBdQwkK{i$b}eycyd*Z(%favj#RTv3#o#>-`(U;!lHK*7w^)Yw!3Bmo76 zz+?yl3V8@IGhjYI6Eidd7ENekMh2D`V#Y=m7-A+SnCeVTk<^tGC1&QN7J-UMUM~Hh z{Cr@s2g*1=j%Qw4z5=Lp195^gt5Ov#^gUgoZ4BHj&0Wo1T`b+4Tr5lt9G%^qO`I$Y oja^Nh%q*Nuo$Ls!hy`WF;*!Lo5=c-P8k(5`OKw$He>Yw(0G5UZqyPW_ literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 44f9de06b9373..fa446fbefba29 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -3419,6 +3419,21 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) await loadingTask.destroy(); }); + it("gets text content, correctly handling documents with toUnicode cmaps that omit leading zeros on hex-encoded UTF-16", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("issue18099_reduced.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfPage = await pdfDoc.getPage(1); + const { items } = await pdfPage.getTextContent({ + disableNormalization: true, + }); + const text = mergeText(items); + expect(text).toEqual("Hello world!"); + + await loadingTask.destroy(); + }); + it("gets text content, and check that out-of-page text is not present (bug 1755201)", async function () { if (isNodeJS) { pending("Linked test-cases are not supported in Node.js."); From b540b6333f377e790eca727b684080c2efe838e3 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 6 Jul 2024 19:07:03 +0200 Subject: [PATCH 0027/1133] Introduce a `waitForScripting` helper function and use it in all scripting integration tests Code inspection uncovered that quite a few integration tests don't wait for scripting to be ready before proceeding, which is a source of intermittent failures, especially on slower machines where e.g. creating the sandbox takes longer. This commit fixes the issues by introducing a `waitForScripting` helper function and calling it consistently in all scripting integration tests. --- test/integration/scripting_spec.mjs | 201 ++++++++++++---------------- 1 file changed, 86 insertions(+), 115 deletions(-) diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index b1e08197b730b..f41c271b882ea 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -32,6 +32,12 @@ import { waitForTimeout, } from "./test_utils.mjs"; +async function waitForScripting(page) { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); +} + describe("Interaction", () => { async function actAndWaitForInput(page, selector, action, clear = true) { await page.waitForSelector(selector, { @@ -61,9 +67,8 @@ describe("Interaction", () => { it("must check that first text field has focus", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); + await page.waitForFunction(`window.document.activeElement !== null`); // The document has an open action in order to give the focus to 401R. @@ -79,6 +84,8 @@ describe("Interaction", () => { it("must show a text field and then make in invisible when content is removed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let visibility = await page.$eval( getSelector("427R"), el => getComputedStyle(el).visibility @@ -121,6 +128,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and leave field with a click", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await page.type(getSelector("416R"), "3.14159", { delay: 200 }); await page.click(getSelector("419R")); @@ -139,6 +148,8 @@ describe("Interaction", () => { it("must format the field with 2 digits, leave field with a click and again", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await page.type(getSelector("448R"), "61803", { delay: 200 }); await page.click(getSelector("419R")); @@ -178,6 +189,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and leave field with a TAB", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const prevSum = await page.$eval(getSelector("427R"), el => el.value); await page.type(getSelector("422R"), "2.7182818", { delay: 200 }); @@ -203,6 +216,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and hit ESC", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let sum = await page.$eval(getSelector("471R"), el => el.value); expect(sum).withContext(`In ${browserName}`).toEqual("4,24"); @@ -225,6 +240,8 @@ describe("Interaction", () => { it("must format the field with 2 digits on key ENTER", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const prevSum = await page.$eval(getSelector("427R"), el => el.value); await page.type(getSelector("419R"), "0.577215", { delay: 200 }); @@ -245,6 +262,8 @@ describe("Interaction", () => { it("must reset all", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + // click on a radio button await page.click("[data-annotation-id='449R']"); @@ -303,9 +322,7 @@ describe("Interaction", () => { it("must show values in a text input when clicking on radio buttons", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); const expected = [ ["81R", "Group1=Choice1::1"], @@ -331,6 +348,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["85R", "Check1=Yes::5"], ["87R", "Check2=Yes::6"], @@ -359,6 +378,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes in a group", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["90R", "Check5=Yes1::9"], ["91R", "Check5=Yes2::10"], @@ -384,6 +405,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes or radio with no actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["", "Off;Off"], ["94R", "Yes;Off"], @@ -425,9 +448,7 @@ describe("Interaction", () => { // And to make sure that a printer isn't locked by a process we close the // page before running the next test. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("47R")); await page.evaluate(_ => { @@ -467,9 +488,7 @@ describe("Interaction", () => { it("must execute WillSave and DidSave actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); try { // Disable download in chrome @@ -516,9 +535,7 @@ describe("Interaction", () => { it("must execute PageOpen and PageClose actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.waitForFunction(`${getQuerySelector("47R")}.value !== ""`); @@ -575,6 +592,8 @@ describe("Interaction", () => { it("must print authors in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const text = await actAndWaitForInput( page, getSelector("25R"), @@ -604,6 +623,8 @@ describe("Interaction", () => { it("must print selected value in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const num of [7, 6, 4, 3, 2, 1]) { await clearInput(page, getSelector("33R")); await page.click(`option[value=Export${num}]`); @@ -622,6 +643,8 @@ describe("Interaction", () => { it("must clear and restore list elements", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + // Click on ClearItems button. await page.click("[data-annotation-id='34R']"); await page.waitForFunction( @@ -652,6 +675,8 @@ describe("Interaction", () => { it("must insert new elements", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let len = 6; for (const num of [1, 3, 5, 6, 431, -1, 0]) { ++len; @@ -690,6 +715,8 @@ describe("Interaction", () => { it("must delete some element", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let len = 6; // Click on Restore button. await clearInput(page, getSelector("33R")); @@ -741,6 +768,8 @@ describe("Interaction", () => { it("must change colors", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const [name, ref] of [ ["Text1", "34R"], ["Check1", "35R"], @@ -814,9 +843,7 @@ describe("Interaction", () => { it("must compute sum of fields", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await scrollIntoView(page, getSelector("138R")); @@ -877,9 +904,7 @@ describe("Interaction", () => { it("must check page index", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("55R")); await page.type( @@ -903,6 +928,8 @@ describe("Interaction", () => { it("must check display", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const [type, vis] of [ ["hidden", "hidden"], ["noPrint", "visible"], @@ -952,9 +979,7 @@ describe("Interaction", () => { it("must update fields with the same name from JS", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "hello"); await page.keyboard.press("Enter"); @@ -988,6 +1013,8 @@ describe("Interaction", () => { it("must print securityHandler value in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const text = await actAndWaitForInput( page, getSelector("25R"), @@ -1021,9 +1048,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1078,9 +1103,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1135,9 +1158,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1191,9 +1212,7 @@ describe("Interaction", () => { it("must convert input to uppercase", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "Hello", { delay: 200 }); await page.waitForFunction( @@ -1257,9 +1276,7 @@ describe("Interaction", () => { it("must check that an infinite loop is not triggered", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("28R")); await page.$eval(getSelector("28R"), el => @@ -1310,9 +1327,7 @@ describe("Interaction", () => { it("must check that field value is correctly updated", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("29R"), "Hello World", { delay: 200 }); await page.click(getSelector("27R")); @@ -1351,9 +1366,7 @@ describe("Interaction", () => { it("must check that field value is correctly formatted", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("75R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("150.32 €"); @@ -1385,9 +1398,7 @@ describe("Interaction", () => { it("must check that a button and text field with a border are hidden", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let visibility = await page.$eval( "[data-annotation-id='35R']", @@ -1440,9 +1451,7 @@ describe("Interaction", () => { it("must check that data-main-rotation is correct", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let base = 0; @@ -1488,9 +1497,7 @@ describe("Interaction", () => { it("must check that a values is correctly updated on a field and its siblings", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("39R")); await page.type(getSelector("39R"), "123", { delay: 10 }); @@ -1536,9 +1543,7 @@ describe("Interaction", () => { it("must check that charLimit is correctly set", async () => { await Promise.all( pages.map(async ([, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); // The default charLimit is 0, which indicates unlimited text length. await page.type(getSelector("7R"), "abcdefghij", { delay: 10 }); @@ -1583,9 +1588,7 @@ describe("Interaction", () => { it("must check field value is treated by default as a number", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("30R"), "123", { delay: 10, @@ -1619,9 +1622,7 @@ describe("Interaction", () => { it("must check field value is correctly updated when committed with ENTER key", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "abc", { delay: 10, @@ -1670,9 +1671,7 @@ describe("Interaction", () => { it("must check field value is correctly updated when committed with ENTER key", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let value = "A"; for (const [displayValue, exportValue] of [ @@ -1719,9 +1718,7 @@ describe("Interaction", () => { it("must check the field value set when the document is open", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.waitForFunction(`${getQuerySelector("27R")}.value !== ""`); @@ -1734,9 +1731,7 @@ describe("Interaction", () => { it("must check the format action is called when setFocus is used", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("30R"), "abc", { delay: 200 }); await page.waitForFunction( @@ -1811,6 +1806,8 @@ describe("Interaction", () => { it("must check if printing is triggered when the document is open", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await awaitPromise(await printHandles.get(page)); }) ); @@ -1831,9 +1828,7 @@ describe("Interaction", () => { it("must check that a field value with a number isn't changed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("25R")); await page.type(getSelector("25R"), "00000000123", { delay: 10 }); @@ -1865,9 +1860,7 @@ describe("Interaction", () => { it("must check that a field value with a number with a comma has the correct value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("22R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("5,25"); @@ -1907,9 +1900,7 @@ describe("Interaction", () => { it("must check that a field has the correct value when a choice is changed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("44R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual(""); @@ -1944,9 +1935,7 @@ describe("Interaction", () => { it("must check that a field has the correct formatted value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("23R"), el => el.value); expect(text) @@ -1967,9 +1956,7 @@ describe("Interaction", () => { it("must check that a field is empty", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("26R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual(""); @@ -2002,6 +1989,8 @@ describe("Interaction", () => { it("must check that a field has the correct formatted value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const hasVisibleCanvas = await page.$eval( `[data-annotation-id="9R"] > canvas`, elem => elem && !elem.hasAttribute("hidden") @@ -2058,9 +2047,7 @@ describe("Interaction", () => { it("must check that invisible fields are made visible", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let visibility = await page.$eval( getSelector("7R"), @@ -2116,9 +2103,7 @@ describe("Interaction", () => { it("must check that checkboxes are correctly resetted", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let readonly = await page.$eval( getSelector("353R"), @@ -2188,9 +2173,7 @@ describe("Interaction", () => { it("must check that focus/blur callbacks aren't called", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("55R")); await page.type(getSelector("55R"), "Hello", { delay: 10 }); @@ -2234,9 +2217,7 @@ describe("Interaction", () => { it("must check that blur callback is called", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("25R")); await page.click(getSelector("26R")); @@ -2275,11 +2256,9 @@ describe("Interaction", () => { it("must check that only one radio is selected", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); - await scrollIntoView(page, getSelector("22R")); + await waitForScripting(page); + await scrollIntoView(page, getSelector("22R")); await page.click(getSelector("25R")); await waitForEntryInStorage(page, "25R", { value: true }); @@ -2341,9 +2320,7 @@ describe("Interaction", () => { it("must check the number has the correct number of decimals", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("15R")); await page.type(getSelector("15R"), "3"); @@ -2383,9 +2360,7 @@ describe("Interaction", () => { it("must check the zip code is correctly formatted", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("24R")); await page.type(getSelector("24R"), "01234", { delay: 10 }); @@ -2420,9 +2395,7 @@ describe("Interaction", () => { it("must check the properties of the event", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); for (const [value, expected] of [ ["b", "change=B,changeEx=b,value=A"], @@ -2463,9 +2436,7 @@ describe("Interaction", () => { it("must check that PageOpen/PageClose actions are correctly executed", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); const buttonSelector = `[data-annotation-id="25R"`; await page.waitForSelector(buttonSelector, { From 5bfe759574b0ffa604759ceb62457c202ad4ab2d Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 17 Apr 2024 21:10:23 +0200 Subject: [PATCH 0028/1133] Use BiDi protocol for Chrome tests --- test/integration/freetext_editor_spec.mjs | 8 +++++--- test/test.mjs | 10 +++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 3150eb0b99ad0..d03df9fa69feb 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -2273,6 +2273,7 @@ describe("FreeText Editor", () => { rect = await getRect(page, getEditorSelector(0)); + // Create a new editor. await page.mouse.click( rect.x + 5 * rect.width, rect.y + 5 * rect.height @@ -2288,11 +2289,12 @@ describe("FreeText Editor", () => { `${getEditorSelector(1)} .overlay.enabled` ); - rect = await getRect(page, getEditorSelector(0)); + // Select the second editor. + rect = await getRect(page, getEditorSelector(1)); await page.mouse.click( - rect.x + 5 * rect.width, - rect.y + 5 * rect.height + rect.x + 0.5 * rect.width, + rect.y + 0.5 * rect.height ); await waitForSelectedEditor(page, getEditorSelector(1)); diff --git a/test/test.mjs b/test/test.mjs index b3315f30c2a68..b2404cb42e312 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -883,12 +883,12 @@ async function startBrowser({ }) { const options = { product: browserName, - protocol: "cdp", - dumpio: true, + protocol: "webDriverBiDi", headless, + dumpio: true, defaultViewport: null, ignoreDefaultArgs: ["--disable-extensions"], - // The timeout for individual protocol (CDP) calls should always be lower + // The timeout for individual protocol (BiDi) calls should always be lower // than the Jasmine timeout. This way protocol errors are always raised in // the context of the tests that actually triggered them and don't leak // through to other tests (causing unrelated failures or tracebacks). The @@ -911,10 +911,6 @@ async function startBrowser({ } if (browserName === "firefox") { - // Run tests with the WebDriver BiDi protocol enabled only for Firefox for - // now given that for Chrome further fixes are needed first. - options.protocol = "webDriverBiDi"; - options.extraPrefsFirefox = { // Disable system addon updates. "extensions.systemAddon.update.enabled": false, From 7ffa9a283d52019b4d857318c96533cc9dc1904a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 9 Jul 2024 09:12:09 +0200 Subject: [PATCH 0029/1133] Extend test-coverage for non-embedded Type1 fonts where .notdef glyphs are replaced with spaces --- test/test_manifest.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_manifest.json b/test/test_manifest.json index 78c4d8f592b83..b7d1190f629bc 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2983,6 +2983,13 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue11403-text", + "file": "pdfs/issue11403_reduced.pdf", + "md5": "08287b64f442cb7c329b97c4774aa1cd", + "rounds": 1, + "type": "text" + }, { "id": "issue11139", "file": "pdfs/issue11139.pdf", @@ -6479,6 +6486,14 @@ "type": "eq", "about": "Has a 4 bit per component image with mask and decode." }, + { + "id": "issue2770-text", + "file": "pdfs/issue2770.pdf", + "md5": "36070d756d06eaa35c2227efb069fb1b", + "rounds": 1, + "link": true, + "type": "text" + }, { "id": "issue2984", "file": "pdfs/issue2984.pdf", From f9d63201eb25d4b804082ebd60bba6bc85346fd0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 9 Jul 2024 09:28:10 +0200 Subject: [PATCH 0030/1133] Revert "Remove the unused `Font.prototype.spaceWidth` getter (PR 13424 follow-up)" This reverts commit 4aee67227e3c5b28f1bb4d8fa6b2ad882bc23b5a. --- src/core/fonts.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/core/fonts.js b/src/core/fonts.js index 0301d49ad35e5..c562ce12ab976 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -3293,6 +3293,44 @@ class Font { return builder.toArray(); } + get spaceWidth() { + // trying to estimate space character width + const possibleSpaceReplacements = ["space", "minus", "one", "i", "I"]; + let width; + for (const glyphName of possibleSpaceReplacements) { + // if possible, getting width by glyph name + if (glyphName in this.widths) { + width = this.widths[glyphName]; + break; + } + const glyphsUnicodeMap = getGlyphsUnicode(); + const glyphUnicode = glyphsUnicodeMap[glyphName]; + // finding the charcode via unicodeToCID map + let charcode = 0; + if (this.composite && this.cMap.contains(glyphUnicode)) { + charcode = this.cMap.lookup(glyphUnicode); + + if (typeof charcode === "string") { + charcode = convertCidString(glyphUnicode, charcode); + } + } + // ... via toUnicode map + if (!charcode && this.toUnicode) { + charcode = this.toUnicode.charCodeOf(glyphUnicode); + } + // setting it to unicode if negative or undefined + if (charcode <= 0) { + charcode = glyphUnicode; + } + // trying to get width via charcode + width = this.widths[charcode]; + if (width) { + break; // the non-zero width found + } + } + return shadow(this, "spaceWidth", width || this.defaultWidth); + } + /** * @private */ From 56653e57709719ca0fd6a44d25be790a0d36a5b4 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 9 Jul 2024 09:34:38 +0200 Subject: [PATCH 0031/1133] Also update the width/unicode data when replacing missing glyphs in non-embedded Type1 fonts (issue 18059) *Please note:* This causes a little bit of movement in the `issue2770` test-case, however this matches the rendering in both Adobe Reader and PDFium. --- src/core/fonts.js | 14 ++++++++++++-- test/pdfs/.gitignore | 1 + test/pdfs/issue18059.pdf | Bin 0 -> 15002 bytes test/test_manifest.json | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 test/pdfs/issue18059.pdf diff --git a/src/core/fonts.js b/src/core/fonts.js index c562ce12ab976..29f186fa361b7 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -3293,7 +3293,10 @@ class Font { return builder.toArray(); } - get spaceWidth() { + /** + * @private + */ + get _spaceWidth() { // trying to estimate space character width const possibleSpaceReplacements = ["space", "minus", "one", "i", "I"]; let width; @@ -3328,7 +3331,7 @@ class Font { break; // the non-zero width found } } - return shadow(this, "spaceWidth", width || this.defaultWidth); + return shadow(this, "_spaceWidth", width || this.defaultWidth); } /** @@ -3376,6 +3379,13 @@ class Font { // .notdef glyphs should be invisible in non-embedded Type1 fonts, so // replace them with spaces. fontCharCode = 0x20; + + if (glyphName === "") { + // Ensure that other relevant glyph properties are also updated + // (fixes issue18059.pdf). + width ||= this._spaceWidth; + unicode = String.fromCharCode(fontCharCode); + } } fontCharCode = mapSpecialUnicodeValues(fontCharCode); } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 13450e37cd32e..7eedf8be6af5d 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -56,6 +56,7 @@ !issue17679_2.pdf !issue18030.pdf !issue18042.pdf +!issue18059.pdf !issue14953.pdf !issue15367.pdf !issue15372.pdf diff --git a/test/pdfs/issue18059.pdf b/test/pdfs/issue18059.pdf new file mode 100644 index 0000000000000000000000000000000000000000..82416e26666310cd87f4fb87ea3858d5b825aad1 GIT binary patch literal 15002 zcma)@b8u%(x9?*c6HRQ}ww+9D+qP}nHYRo^wr$(VFTV4fbML8h>bzC=-T!>LS9h(w zx@)hluHOA6RS*%QWu#+=Ar1KZz`(E*Fc8=oTEg(~z|f0XSUa0I63~lT8#tSYm>Ag^ zo50Y^nAn;*n-efHa>DTO!8kcPni$x?xM$-|bi#lL!inwi$tSE2PjdBQ#DlNsG}KSR zhF2Gw6YoEG{UXy#nmn5yD0}Hy`{3Z~Jvi~@^n#wLVqJPLKJuflO()Z9i77`PK%uT~ z=tsbPK4siR>R(Y13<5tLQhF@J=sp#t_nfP;5h#VgUI%OQ_w#KQ+-2jRG7@(@RoI z%aT`9$F62eRqIBpYWb;p#d68B?y2%g+pqTN`uaT8?#u2tOQF2uou4m{pCRM5=O!zI z{ifUF;dlxu7+DC}92O;iczk^MO`3{so=C-uTHv5^oSI4zh6{LxdyliAfON3s53*Je z(sQ4a*Nx>(+w&VaA^}OYn1ubi)Xibr3?yyAI_)i=`_brIK;eBPadjS@ zg@emrCn5nSBrpLTdb6;XqdOaXZLa1^|MefQ-B(@noAZPQ-J8on19@R(e&EaBOWQ67 zs}Z%qjtILKV%=5m_#7YWa6+VINGLjNd~BRYfZ^YG1?SR0C)^?V@J+X;d{u<@|k*Oli z+hV-!i;t)Af}e2CB0_k_@|sIu$;nQ=IB$=w%=r-jHo;wR z=Ov6-p!h&5Fx~=zO8n8|RcMFeE)nM)dmq zf@ZU%XG|_09gf}o1fT48Q-Px3R{pjEZo)Yzo6%nR)0_$?9*!#H3qz;-&RoC9SM(s? zFZ$x_Lip`O!o9qDfVsC_yRO}z9X`b`;&do~nk28!vJlSD-oeb3R)HmHuOcgCj)KH_g6C# z5Hl5sBnMAYU@d2;Bty7T<7y~EjEiUp#NnC{Lpl;+j0?6?ARZC3QwfBWLseTtQ-RB+ z27%X#sOs}tDbQ*LpZ8(Yi@}#8vlf^x25KteSc*%>L%QV4Erz5Pnv~--_dt5g!7N6- z%_3n7uonulfGVwph|RK|{&1#S5BmzY6?4m7|2dB)K^pUpa@@WlB}shaa@ zhLO#ZXu{s3P0^Bs*9tjN>+XitdJ76AaBvuN(C`p3QE3vTjG;@ZMr)$tVC3a5oxzJK z8zypclCn-E+D?s5Hm)_{%BjiO>h^Tz*O#{yJ7af+R-R|Bi@H2T;b3QFYH4n)r@3=+ zb*9o?=x!f~zHGkq1&ri+U*A&GcYmBGtmyh(#IW!1y{))y``iq^@N>9+EX46=tgdXZ zx4BNKwr;kYJ-~IG8peZRx1}n!ULc;%Q%`F+z-va#j)umZZoebyI38(W)N1Pe@C1GP z2=Q(9bhh?(Jim0wy7s-T4a4IDjOO9{d_X@gGSBe&t)-a^(Qs*UErwuqdJeH76}deu-7N+zvjvm~&zS&S`z zFWQcJV~UK5jcO5H5q%Mn7rBY;VSWoaJxG)qBO3!xtRiid)=m1EWIwN9kvCXtu)U2L1?CZR3~Uj>Xh61^spZwNf{9epHaY~1n4|6b|{b(}qU zr^30?rBX{(ugW)~B31+0NoUfGac3x739{(*=91ir0TI{G^=wKJlQ-`?Iy`4WvBLI cupiDE?It#v8 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 literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index b7d1190f629bc..90ad056ec4be1 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2990,6 +2990,20 @@ "rounds": 1, "type": "text" }, + { + "id": "issue18059", + "file": "pdfs/issue18059.pdf", + "md5": "b70373894edfcd571a41caa1a0776b6f", + "rounds": 1, + "type": "eq" + }, + { + "id": "issue18059-text", + "file": "pdfs/issue18059.pdf", + "md5": "b70373894edfcd571a41caa1a0776b6f", + "rounds": 1, + "type": "text" + }, { "id": "issue11139", "file": "pdfs/issue11139.pdf", From d9f0ec0b8720d207623f1979e2a78fb31753a2d7 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 9 Jul 2024 13:22:14 +0200 Subject: [PATCH 0032/1133] Re-factor `BasePreferences` to essentially be a wrapper around `AppOptions` In the MOZCENTRAL build the `BasePreferences` class is almost unused, since it's only being used to initialize and update the `AppOptions` which means that theoretically we could move the relevant code there. However for the other build targets (e.g. GENERIC and CHROME) we still need to keep the `BasePreferences` class, although we can re-factor things to move the necessary validation inside of `AppOptions` and thus simplify the code and reduce duplication. The patch also moves the event dispatching, for changed preference values, into `AppOptions` instead. --- gulpfile.mjs | 24 ---------- web/app.js | 3 +- web/app_options.js | 70 ++++++++++++++++++++------- web/preferences.js | 117 ++++++++------------------------------------- 4 files changed, 74 insertions(+), 140 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 2fb8bca5ee0bc..77808b483d2ae 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -284,9 +284,6 @@ function createWebpackConfig( BUNDLE_VERSION: versionInfo.version, BUNDLE_BUILD: versionInfo.commit, TESTING: defines.TESTING ?? process.env.TESTING === "true", - BROWSER_PREFERENCES: defaultPreferencesDir - ? getBrowserPreferences(defaultPreferencesDir) - : {}, DEFAULT_PREFERENCES: defaultPreferencesDir ? getDefaultPreferences(defaultPreferencesDir) : {}, @@ -856,13 +853,6 @@ async function parseDefaultPreferences(dir) { "./" + DEFAULT_PREFERENCES_DIR + dir + "app_options.mjs" ); - const browserPrefs = AppOptions.getAll( - OptionKind.BROWSER, - /* defaultOnly = */ true - ); - if (Object.keys(browserPrefs).length === 0) { - throw new Error("No browser preferences found."); - } const prefs = AppOptions.getAll( OptionKind.PREFERENCE, /* defaultOnly = */ true @@ -871,23 +861,12 @@ async function parseDefaultPreferences(dir) { throw new Error("No default preferences found."); } - fs.writeFileSync( - DEFAULT_PREFERENCES_DIR + dir + "browser_preferences.json", - JSON.stringify(browserPrefs) - ); fs.writeFileSync( DEFAULT_PREFERENCES_DIR + dir + "default_preferences.json", JSON.stringify(prefs) ); } -function getBrowserPreferences(dir) { - const str = fs - .readFileSync(DEFAULT_PREFERENCES_DIR + dir + "browser_preferences.json") - .toString(); - return JSON.parse(str); -} - function getDefaultPreferences(dir) { const str = fs .readFileSync(DEFAULT_PREFERENCES_DIR + dir + "default_preferences.json") @@ -1581,9 +1560,6 @@ function buildLib(defines, dir) { BUNDLE_VERSION: versionInfo.version, BUNDLE_BUILD: versionInfo.commit, TESTING: defines.TESTING ?? process.env.TESTING === "true", - BROWSER_PREFERENCES: getBrowserPreferences( - defines.SKIP_BABEL ? "lib/" : "lib-legacy/" - ), DEFAULT_PREFERENCES: getDefaultPreferences( defines.SKIP_BABEL ? "lib/" : "lib-legacy/" ), diff --git a/web/app.js b/web/app.js index 82b98b8bfb6ed..d8a6a3d9b7eee 100644 --- a/web/app.js +++ b/web/app.js @@ -391,9 +391,10 @@ const PDFViewerApplication = { */ async _initializeViewerComponents() { const { appConfig, externalServices, l10n } = this; + let eventBus; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - eventBus = this.preferences.eventBus = new FirefoxEventBus( + eventBus = AppOptions.eventBus = new FirefoxEventBus( await this._allowedGlobalEventsPromise, externalServices, AppOptions.get("isInAutomation") diff --git a/web/app_options.js b/web/app_options.js index d1b5ca3e2f253..c1a3062374c35 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -46,6 +46,7 @@ const OptionKind = { VIEWER: 0x02, API: 0x04, WORKER: 0x08, + EVENT_DISPATCH: 0x10, PREFERENCE: 0x80, }; @@ -98,7 +99,7 @@ const defaultOptions = { toolbarDensity: { /** @type {number} */ value: 0, // 0 = "normal", 1 = "compact", 2 = "touch" - kind: OptionKind.BROWSER, + kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH, }, annotationEditorMode: { @@ -461,6 +462,8 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { } class AppOptions { + static eventBus; + constructor() { throw new Error("Cannot initialize AppOptions."); } @@ -488,28 +491,37 @@ class AppOptions { userOptions[name] = value; } - static setAll(options, init = false) { - if ((typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && init) { - if (this.get("disablePreferences")) { - // Give custom implementations of the default viewer a simpler way to - // opt-out of having the `Preferences` override existing `AppOptions`. - return; - } - for (const name in userOptions) { - // Ignore any compatibility-values in the user-options. - if (compatibilityParams[name] !== undefined) { + static setAll(options, prefs = false) { + let events; + + for (const name in options) { + const userOption = options[name]; + + if (prefs) { + const defaultOption = defaultOptions[name]; + + if (!defaultOption) { continue; } - console.warn( - "setAll: The Preferences may override manually set AppOptions; " + - 'please use the "disablePreferences"-option in order to prevent that.' - ); - break; + const { kind, value } = defaultOption; + + if (!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { + continue; + } + if (typeof userOption !== typeof value) { + continue; + } + if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { + (events ||= new Map()).set(name, userOption); + } } + userOptions[name] = userOption; } - for (const name in options) { - userOptions[name] = options[name]; + if (events) { + for (const [name, value] of events) { + this.eventBus.dispatch(name.toLowerCase(), { source: this, value }); + } } } @@ -526,4 +538,26 @@ class AppOptions { } } +if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + AppOptions._checkDisablePreferences = () => { + if (AppOptions.get("disablePreferences")) { + // Give custom implementations of the default viewer a simpler way to + // opt-out of having the `Preferences` override existing `AppOptions`. + return true; + } + for (const name in userOptions) { + // Ignore any compatibility-values in the user-options. + if (compatibilityParams[name] !== undefined) { + continue; + } + console.warn( + "The Preferences may override manually set AppOptions; " + + 'please use the "disablePreferences"-option to prevent that.' + ); + break; + } + return false; + }; +} + export { AppOptions, OptionKind }; diff --git a/web/preferences.js b/web/preferences.js index 944c0d84ba37f..61a01ea880666 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -21,24 +21,14 @@ import { AppOptions, OptionKind } from "./app_options.js"; * or every time the viewer is loaded. */ class BasePreferences { - #browserDefaults = Object.freeze( - typeof PDFJSDev === "undefined" - ? AppOptions.getAll(OptionKind.BROWSER, /* defaultOnly = */ true) - : PDFJSDev.eval("BROWSER_PREFERENCES") - ); - #defaults = Object.freeze( typeof PDFJSDev === "undefined" ? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true) : PDFJSDev.eval("DEFAULT_PREFERENCES") ); - #prefs = Object.create(null); - #initializedPromise = null; - static #eventToDispatch = new Set(["toolbarDensity"]); - constructor() { if (this.constructor === BasePreferences) { throw new Error("Cannot initialize BasePreferences."); @@ -54,27 +44,24 @@ class BasePreferences { this.#initializedPromise = this._readFromStorage(this.#defaults).then( ({ browserPrefs, prefs }) => { - const options = Object.create(null); - - for (const [name, val] of Object.entries(this.#browserDefaults)) { - const prefVal = browserPrefs?.[name]; - options[name] = typeof prefVal === typeof val ? prefVal : val; - } - for (const [name, val] of Object.entries(this.#defaults)) { - const prefVal = prefs?.[name]; - // Ignore preferences whose types don't match the default values. - options[name] = this.#prefs[name] = - typeof prefVal === typeof val ? prefVal : val; + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && + AppOptions._checkDisablePreferences() + ) { + return; } - AppOptions.setAll(options, /* init = */ true); + AppOptions.setAll({ ...browserPrefs, ...prefs }, /* prefs = */ true); } ); if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - window.addEventListener("updatedPreference", evt => { - this.#updatePref(evt.detail); - }); - this.eventBus = null; + window.addEventListener( + "updatedPreference", + async ({ detail: { name, value } }) => { + await this.#initializedPromise; + AppOptions.setAll({ [name]: value }, /* prefs = */ true); + } + ); } } @@ -98,31 +85,6 @@ class BasePreferences { throw new Error("Not implemented: _readFromStorage"); } - async #updatePref({ name, value }) { - if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Not implemented: #updatePref"); - } - await this.#initializedPromise; - - if (name in this.#browserDefaults) { - if (typeof value !== typeof this.#browserDefaults[name]) { - return; // Invalid preference value. - } - } else if (name in this.#defaults) { - if (typeof value !== typeof this.#defaults[name]) { - return; // Invalid preference value. - } - this.#prefs[name] = value; - } else { - return; // Invalid preference. - } - AppOptions.set(name, value); - - if (BasePreferences.#eventToDispatch.has(name)) { - this.eventBus?.dispatch(name.toLowerCase(), { source: this, value }); - } - } - /** * Reset the preferences to their default values and update storage. * @returns {Promise} A promise that is resolved when the preference values @@ -133,16 +95,9 @@ class BasePreferences { throw new Error("Please use `about:config` to change preferences."); } await this.#initializedPromise; - const oldPrefs = structuredClone(this.#prefs); - - this.#prefs = Object.create(null); - try { - await this._writeToStorage(this.#defaults); - } catch (reason) { - // Revert all preference values, since writing to storage failed. - this.#prefs = oldPrefs; - throw reason; - } + AppOptions.setAll(this.#defaults, /* prefs = */ true); + + await this._writeToStorage(this.#defaults); } /** @@ -157,37 +112,10 @@ class BasePreferences { throw new Error("Please use `about:config` to change preferences."); } await this.#initializedPromise; - const defaultValue = this.#defaults[name], - oldPrefs = structuredClone(this.#prefs); + AppOptions.setAll({ [name]: value }, /* prefs = */ true); - if (defaultValue === undefined) { - throw new Error(`Set preference: "${name}" is undefined.`); - } else if (value === undefined) { - throw new Error("Set preference: no value is specified."); - } - const valueType = typeof value, - defaultType = typeof defaultValue; - - if (valueType !== defaultType) { - if (valueType === "number" && defaultType === "string") { - value = value.toString(); - } else { - throw new Error( - `Set preference: "${value}" is a ${valueType}, expected a ${defaultType}.` - ); - } - } else if (valueType === "number" && !Number.isInteger(value)) { - throw new Error(`Set preference: "${value}" must be an integer.`); - } - - this.#prefs[name] = value; - try { - await this._writeToStorage(this.#prefs); - } catch (reason) { - // Revert all preference values, since writing to storage failed. - this.#prefs = oldPrefs; - throw reason; - } + const prefs = AppOptions.getAll(OptionKind.PREFERENCE); + await this._writeToStorage(prefs); } /** @@ -201,12 +129,7 @@ class BasePreferences { throw new Error("Not implemented: get"); } await this.#initializedPromise; - const defaultValue = this.#defaults[name]; - - if (defaultValue === undefined) { - throw new Error(`Get preference: "${name}" is undefined.`); - } - return this.#prefs[name] ?? defaultValue; + return AppOptions.get(name); } get initializedPromise() { From d27efb43cdf2904f56ae8de9b653f6208b674702 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 10 Jul 2024 11:29:32 +0200 Subject: [PATCH 0033/1133] [Editor] Wait for 'pagerendered' to switch to editing mode The focus can potentially be stolen when the DOM is modified when adding a new canvas element for the page being redrawn. --- web/pdf_viewer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 256ad0e3a37f7..d56328014724a 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -2328,9 +2328,9 @@ class PDFViewer { // We must call #switchToEditAnnotationMode unconditionally to ensure that // page is rendered if it's useful or not. const idsToRefresh = this.#switchToEditAnnotationMode(); - if (isEditing && editId && idsToRefresh) { - // We're editing an existing annotation so we must switch to editing - // mode when the rendering is done. + if (isEditing && idsToRefresh) { + // We're editing so we must switch to editing mode when the rendering is + // done. this.#cleanupSwitchAnnotationEditorMode(); this.#onPageRenderedCallback = ({ pageNumber }) => { idsToRefresh.delete(pageNumber); From 665fff020e32343fca5f685b384f1ba455b8c4cf Mon Sep 17 00:00:00 2001 From: razh Date: Wed, 10 Jul 2024 02:16:24 -0400 Subject: [PATCH 0034/1133] Fix `ensureMinFontSizeComputed` calculation if `` is a flex container Given: ```css html, body { height: 100%; } body { display: flex; } ``` The `
` appended to the `` will take up the full height of the viewport due to the implicit `align-items: stretch` of flex containers. This results in an incorrect computed `minFontSize` value. --- src/display/text_layer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index e6a20ef655106..bace7a87ea999 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -480,6 +480,7 @@ class TextLayer { div.style.opacity = 0; div.style.lineHeight = 1; div.style.fontSize = "1px"; + div.style.position = "absolute"; div.textContent = "X"; document.body.append(div); // In `display:block` elements contain a single line of text, From 403d02361726a18ff4a504ca9437b9308c2673e3 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 11 Jul 2024 09:30:42 +0200 Subject: [PATCH 0035/1133] Allow e.g. /FitH destinations without additional parameter (bug 1907000) According to the PDF specification these destinations should have a coordinate parameter, which may however be `null`, but it shouldn't be omitted; please see https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#G11.2095870 Hence we try to work-around bad PDF generators by making the coordinate parameter optional when validating explicit destinations in both the worker and the viewer. --- src/core/catalog.js | 2 +- test/pdfs/.gitignore | 1 + test/pdfs/bug1907000_reduced.pdf | Bin 0 -> 15392 bytes test/unit/api_spec.js | 43 +++++++++++++++++++++++++++++++ web/pdf_link_service.js | 2 +- 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/pdfs/bug1907000_reduced.pdf diff --git a/src/core/catalog.js b/src/core/catalog.js index 47d2812de73bb..68db4b847baf2 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -78,7 +78,7 @@ function isValidExplicitDest(dest) { case "FitBH": case "FitV": case "FitBV": - if (args.length !== 1) { + if (args.length > 1) { return false; } break; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 13450e37cd32e..8629cd5bc873f 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -13,6 +13,7 @@ !issue1155r.pdf !issue2017r.pdf !bug1727053.pdf +!bug1907000_reduced.pdf !issue11913.pdf !issue2391-1.pdf !issue2391-2.pdf diff --git a/test/pdfs/bug1907000_reduced.pdf b/test/pdfs/bug1907000_reduced.pdf new file mode 100644 index 0000000000000000000000000000000000000000..60e63076570ba837ee81bc7de8e3f74c9975645c GIT binary patch literal 15392 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 PifK2jSaJ1+-iH4KgWcLw literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 567d02a87de40..5159c166aa296 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1901,6 +1901,49 @@ describe("api", function () { await loadingTask.destroy(); }); + it("gets outline, with /FitH destinations that lack coordinate parameter (bug 1907000)", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("bug1907000_reduced.pdf") + ); + const pdfDoc = await loadingTask.promise; + const outline = await pdfDoc.getOutline(); + + expect(outline).toEqual([ + { + action: null, + attachment: undefined, + dest: [{ num: 14, gen: 0 }, { name: "FitH" }], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 1", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + { + action: null, + attachment: undefined, + dest: [{ num: 13, gen: 0 }, { name: "FitH" }], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 2", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + ]); + + await loadingTask.destroy(); + }); + it("gets non-existent permissions", async function () { const permissions = await pdfDocument.getPermissions(); expect(permissions).toEqual(null); diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 7dd8398053ff3..73a29f64b07ef 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -519,7 +519,7 @@ class PDFLinkService { case "FitBH": case "FitV": case "FitBV": - if (args.length !== 1) { + if (args.length > 1) { return false; } break; From 6711123f68651401af64ea9be9011ade6ffaae57 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 10 Jul 2024 17:28:26 +0200 Subject: [PATCH 0036/1133] [Editor] Update the freetext annotation dictionary instead of creating a new one when updating an existing freetext --- src/core/annotation.js | 29 ++++++++++++++++++++------ src/core/primitives.js | 4 ++++ test/unit/annotation_spec.js | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 35b13776d2b63..7ac9a3eed0722 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1711,18 +1711,28 @@ class MarkupAnnotation extends Annotation { } static async createNewAnnotation(xref, annotation, dependencies, params) { - const annotationRef = (annotation.ref ||= xref.getNewTemporaryRef()); + let oldAnnotation; + if (annotation.ref) { + oldAnnotation = (await xref.fetchIfRefAsync(annotation.ref)).clone(); + } else { + annotation.ref = xref.getNewTemporaryRef(); + } + + const annotationRef = annotation.ref; const ap = await this.createNewAppearanceStream(annotation, xref, params); const buffer = []; let annotationDict; if (ap) { const apRef = xref.getNewTemporaryRef(); - annotationDict = this.createNewDict(annotation, xref, { apRef }); + annotationDict = this.createNewDict(annotation, xref, { + apRef, + oldAnnotation, + }); await writeObject(apRef, ap, buffer, xref); dependencies.push({ ref: apRef, data: buffer.join("") }); } else { - annotationDict = this.createNewDict(annotation, xref, {}); + annotationDict = this.createNewDict(annotation, xref, { oldAnnotation }); } if (Number.isInteger(annotation.parentTreeId)) { annotationDict.set("StructParent", annotation.parentTreeId); @@ -3826,12 +3836,19 @@ class FreeTextAnnotation extends MarkupAnnotation { return this._hasAppearance; } - static createNewDict(annotation, xref, { apRef, ap }) { + static createNewDict(annotation, xref, { apRef, ap, oldAnnotation }) { const { color, fontSize, rect, rotation, user, value } = annotation; - const freetext = new Dict(xref); + const freetext = oldAnnotation || new Dict(xref); freetext.set("Type", Name.get("Annot")); freetext.set("Subtype", Name.get("FreeText")); - freetext.set("CreationDate", `D:${getModificationDate()}`); + if (oldAnnotation) { + freetext.set("M", `D:${getModificationDate()}`); + // TODO: We should try to generate a new RC from the content we've. + // For now we can just remove it to avoid any issues. + freetext.delete("RC"); + } else { + freetext.set("CreationDate", `D:${getModificationDate()}`); + } freetext.set("Rect", rect); const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`; freetext.set("DA", da); diff --git a/src/core/primitives.js b/src/core/primitives.js index e1bc4798b65de..a6801935de2fe 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -270,6 +270,10 @@ class Dict { } return dict; } + + delete(key) { + delete this._map[key]; + } } class Ref { diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 4b3949ceeda97..8f91754741270 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4257,6 +4257,46 @@ describe("annotation", function () { ]); }); + it("should update an existing FreeText annotation", async function () { + const freeTextDict = new Dict(); + freeTextDict.set("Type", Name.get("Annot")); + freeTextDict.set("Subtype", Name.get("FreeText")); + freeTextDict.set("CreationDate", "D:20190423"); + freeTextDict.set("Foo", Name.get("Bar")); + + const freeTextRef = Ref.get(143, 0); + partialEvaluator.xref = new XRefMock([ + { ref: freeTextRef, data: freeTextDict }, + ]); + + const task = new WorkerTask("test FreeText update"); + const data = await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.FREETEXT, + rect: [12, 34, 56, 78], + rotation: 0, + fontSize: 10, + color: [0, 0, 0], + value: "Hello PDF.js World !", + id: "143R", + ref: freeTextRef, + }, + ] + ); + + const base = data.annotations[0].data.replaceAll(/\(D:\d+\)/g, "(date)"); + expect(base).toEqual( + "143 0 obj\n" + + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) /Foo /Bar /M (date) " + + "/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World !) " + + "/F 4 /Border [0 0 0] /Rotate 0 /AP << /N 2 0 R>>>>\n" + + "endobj\n" + ); + }); + it("should extract the text from a FreeText annotation", async function () { partialEvaluator.xref = new XRefMock(); const task = new WorkerTask("test FreeText text extraction"); From 50a5a150885c03d38a535a606723905ae938d65d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 8 Jul 2024 18:00:27 +0200 Subject: [PATCH 0037/1133] Create absolute filter-URLs when needed in `DOMFilterFactory` (issue 18406) This functionality is purposely limited to development mode and GENERIC builds, since it's unnecessary in e.g. the *built-in* Firefox PDF Viewer, and will only be used when a ``-element is actually present. *Please note:* We also have tests in mozilla-central that will *indirectly* ensure that relative filter-URLs work as intended in the Firefox PDF Viewer, see https://searchfox.org/mozilla-central/source/toolkit/components/pdfjs/test/browser_pdfjs_filters.js --- To test that the issue is fixed, the following code can be used: ```html base href (issue 18406) ``` --- src/display/display_utils.js | 31 ++++++++++++++++++++++++++----- test/pdfs/.gitignore | 1 + test/pdfs/issue16287.pdf | Bin 0 -> 76570 bytes test/test_manifest.json | 8 ++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/pdfs/issue16287.pdf diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 459df1f7c6b55..30964c9ab78c3 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -49,6 +49,8 @@ class PixelsPerInch { * does the magic for us. */ class DOMFilterFactory extends BaseFilterFactory { + #baseUrl; + #_cache; #_defs; @@ -121,6 +123,25 @@ class DOMFilterFactory extends BaseFilterFactory { return [bufferR.join(","), bufferG.join(","), bufferB.join(",")]; } + #createUrl(id) { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + if (this.#baseUrl === undefined) { + const url = this.#document.URL; + if (url === this.#document.baseURI) { + // No ``-element present, hence a relative URL should work. + this.#baseUrl = ""; + } else if (isDataScheme(url)) { + warn('#createUrl: ignore "data:"-URL for performance reasons.'); + this.#baseUrl = ""; + } else { + this.#baseUrl = url.split("#", 1)[0]; + } + } + return `url(${this.#baseUrl}#${id})`; + } + return `url(${id})`; + } + addFilter(maps) { if (!maps) { return "none"; @@ -146,7 +167,7 @@ class DOMFilterFactory extends BaseFilterFactory { // https://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement const id = `g_${this.#docId}_transfer_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(maps, url); this.#cache.set(key, url); @@ -232,7 +253,7 @@ class DOMFilterFactory extends BaseFilterFactory { filter ); - info.url = `url(#${id})`; + info.url = this.#createUrl(id); return info.url; } @@ -254,7 +275,7 @@ class DOMFilterFactory extends BaseFilterFactory { } const id = `g_${this.#docId}_alpha_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(map, url); this.#cache.set(key, url); @@ -287,7 +308,7 @@ class DOMFilterFactory extends BaseFilterFactory { } const id = `g_${this.#docId}_luminosity_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(map, url); this.#cache.set(key, url); @@ -389,7 +410,7 @@ class DOMFilterFactory extends BaseFilterFactory { filter ); - info.url = `url(#${id})`; + info.url = this.#createUrl(id); return info.url; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index df69d2255547d..01b3ee127c628 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -127,6 +127,7 @@ !issue7891_bc0.pdf !issue11242_reduced.pdf !issue16176.pdf +!issue16287.pdf !issue17064_readonly.pdf !issue11279.pdf !issue11362.pdf diff --git a/test/pdfs/issue16287.pdf b/test/pdfs/issue16287.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a910231a8643ffb2a24266966fb3ddf2e4060ba GIT binary patch literal 76570 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 Date: Thu, 11 Jul 2024 12:17:41 +0200 Subject: [PATCH 0038/1133] Remove the remaining `zoomDisabledTimeout` usage (issue 17727) With https://bugzilla.mozilla.org/show_bug.cgi?id=1882168 now marked as fixed by the landing of https://bugzilla.mozilla.org/show_bug.cgi?id=1902017 we should *hopefully* be able to remove this work-around. --- web/app.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/web/app.js b/web/app.js index d8a6a3d9b7eee..136c9b0caf784 100644 --- a/web/app.js +++ b/web/app.js @@ -87,7 +87,6 @@ import { Toolbar } from "web-toolbar"; import { ViewHistory } from "./view_history.js"; const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms -const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000; // ms const ViewOnLoad = { UNKNOWN: -1, @@ -1996,9 +1995,6 @@ const PDFViewerApplication = { } addWindowResolutionChange(); - window.addEventListener("visibilitychange", webViewerVisibilityChange, { - signal, - }); window.addEventListener("wheel", webViewerWheel, { passive: false, signal, @@ -2585,23 +2581,6 @@ function webViewerResolutionChange(evt) { PDFViewerApplication.pdfViewer.refresh(); } -function webViewerVisibilityChange(evt) { - if (document.visibilityState === "visible") { - // Ignore mouse wheel zooming during tab switches (bug 1503412). - setZoomDisabledTimeout(); - } -} - -let zoomDisabledTimeout = null; -function setZoomDisabledTimeout() { - if (zoomDisabledTimeout) { - clearTimeout(zoomDisabledTimeout); - } - zoomDisabledTimeout = setTimeout(function () { - zoomDisabledTimeout = null; - }, WHEEL_ZOOM_DISABLED_TIMEOUT); -} - function webViewerWheel(evt) { const { pdfViewer, @@ -2654,7 +2633,6 @@ function webViewerWheel(evt) { // NOTE: this check must be placed *after* preventDefault. if ( PDFViewerApplication._isScrolling || - zoomDisabledTimeout || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active ) { From 9a64749dbe241113456cca5908a04472de229349 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 11 Jul 2024 18:00:36 +0200 Subject: [PATCH 0039/1133] [Editor] Make editor toolbars usable whatever their z-index (bug 1879104) Because of editor z-index, the toolbar belonging to an highlight created before a second adjacent one, can be overlapped by this new one. So when the user select an editor we just show it on front of all the other ones to make sure that it can be used normally. --- test/integration/highlight_editor_spec.mjs | 116 ++++++++++++++++----- test/integration/test_utils.mjs | 19 ++++ web/annotation_editor_layer_builder.css | 4 + 3 files changed, 113 insertions(+), 26 deletions(-) diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 6c47bbe7e0af1..03ff854cafaa5 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -30,6 +30,7 @@ import { kbUndo, loadAndWait, scrollIntoView, + setCaretAt, switchToEditor, waitForSerialized, } from "./test_utils.mjs"; @@ -1043,19 +1044,12 @@ describe("Highlight Editor", () => { `${getEditorSelector(0)}:not(.selectedEditor)` ); - await page.evaluate(() => { - const text = - "Dynamic languages such as JavaScript are more difficult to com-"; - for (const el of document.querySelectorAll( - `.page[data-page-number="${1}"] > .textLayer > span` - )) { - if (el.textContent === text) { - window.getSelection().setPosition(el.firstChild, 1); - break; - } - } - }); - + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 1 + ); await page.keyboard.press("ArrowUp"); const [text, offset] = await page.evaluate(() => { const selection = window.getSelection(); @@ -1073,19 +1067,12 @@ describe("Highlight Editor", () => { pages.map(async ([browserName, page]) => { await switchToHighlight(page); - await page.evaluate(() => { - const text = - "Dynamic languages such as JavaScript are more difficult to com-"; - for (const el of document.querySelectorAll( - `.page[data-page-number="${1}"] > .textLayer > span` - )) { - if (el.textContent === text) { - window.getSelection().setPosition(el.firstChild, 15); - break; - } - } - }); - + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 15 + ); await page.keyboard.down("Shift"); await page.keyboard.press("ArrowDown"); await page.keyboard.up("Shift"); @@ -1702,4 +1689,81 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Use a toolbar overlapping an other highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000", + } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the toolbar is usable", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 0 + ); + await page.keyboard.down("Shift"); + for (let i = 0; i < 3; i++) { + await page.keyboard.press("ArrowDown"); + } + await page.keyboard.up("Shift"); + + const editorSelector = getEditorSelector(0); + await page.waitForSelector(editorSelector); + + await setCaretAt( + page, + 1, + "handle all possible type combinations at runtime. We present an al-", + 0 + ); + await page.keyboard.down("Shift"); + for (let i = 0; i < 3; i++) { + await page.keyboard.press("ArrowDown"); + } + await page.keyboard.up("Shift"); + await page.waitForSelector(getEditorSelector(1)); + + const rect = await getRect(page, editorSelector); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y); + + await page.waitForSelector( + `${editorSelector} .editToolbar button.colorPicker` + ); + + await page.click(`${editorSelector} .editToolbar button.colorPicker`); + await page.waitForSelector( + `${editorSelector} .editToolbar button[title = "Green"]` + ); + await page.click( + `${editorSelector} .editToolbar button[title = "Green"]` + ); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#00FF00"]` + ); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 4e7d28e00a62a..207007fa41ef4 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -511,6 +511,24 @@ async function hover(page, selector) { await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2); } +async function setCaretAt(page, pageNumber, text, position) { + await page.evaluate( + (pageN, string, pos) => { + for (const el of document.querySelectorAll( + `.page[data-page-number="${pageN}"] > .textLayer > span` + )) { + if (el.textContent === string) { + window.getSelection().setPosition(el.firstChild, pos); + break; + } + } + }, + pageNumber, + text, + position + ); +} + const modifier = isMac ? "Meta" : "Control"; async function kbCopy(page) { await page.keyboard.down(modifier); @@ -713,6 +731,7 @@ export { pasteFromClipboard, scrollIntoView, serializeBitmapDimensions, + setCaretAt, switchToEditor, waitForAnnotationEditorLayer, waitForAnnotationModeChanged, diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 0fa433e82771f..139039bbba98c 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -112,6 +112,10 @@ font-size: calc(100px * var(--scale-factor)); transform-origin: 0 0; cursor: auto; + + .selectedEditor { + z-index: 100000 !important; + } } .annotationEditorLayer.waiting { From 4e7c30da9a09ad3b46eaf1a6c4e93934a741db89 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 11 Jul 2024 16:18:50 +0200 Subject: [PATCH 0040/1133] [Editor] Disable existing highlights when drawing a new one (bug 1879035) When the mouse was hovering an existing highlight, all the text in the page was selected. So when the user is selecting some text or drawing a free highlight, the mouse is disabled for the existing editors. --- src/display/editor/annotation_editor_layer.js | 13 +- src/display/editor/tools.js | 45 +++++-- test/integration/highlight_editor_spec.mjs | 124 ++++++++++++++++++ web/annotation_editor_layer_builder.css | 4 + 4 files changed, 172 insertions(+), 14 deletions(-) diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 5c4e9445a613f..52d76d3560946 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -230,6 +230,10 @@ class AnnotationEditorLayer { this.#uiManager.addCommands(params); } + toggleDrawing(enabled = false) { + this.div.classList.toggle("drawing", !enabled); + } + togglePointerEvents(enabled = false) { this.div.classList.toggle("disabled", !enabled); } @@ -388,7 +392,12 @@ class AnnotationEditorLayer { // Unselect all the editors in order to let the user select some text // without being annoyed by an editor toolbar. this.#uiManager.unselectAll(); - if (event.target === this.#textLayer.div) { + const { target } = event; + if ( + target === this.#textLayer.div || + (target.classList.contains("endOfContent") && + this.#textLayer.div.contains(target)) + ) { const { isMac } = FeatureTest.platform; if (event.button !== 0 || (event.ctrlKey && isMac)) { // Do nothing on right click. @@ -400,6 +409,7 @@ class AnnotationEditorLayer { /* updateButton = */ true ); this.#textLayer.div.classList.add("free"); + this.toggleDrawing(); HighlightEditor.startHighlighting( this, this.#uiManager.direction === "ltr", @@ -409,6 +419,7 @@ class AnnotationEditorLayer { "pointerup", () => { this.#textLayer.div.classList.remove("free"); + this.toggleDrawing(true); }, { once: true, signal: this.#uiManager._signal } ); diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 4adebd9868ce9..17d7e497ad927 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -967,6 +967,19 @@ class AnnotationEditorUIManager { : anchorNode; } + #getLayerForTextLayer(textLayer) { + const { currentLayer } = this; + if (currentLayer.hasTextLayer(textLayer)) { + return currentLayer; + } + for (const layer of this.#allLayers.values()) { + if (layer.hasTextLayer(textLayer)) { + return layer; + } + } + return null; + } + highlightSelection(methodOfCreation = "") { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { @@ -988,19 +1001,17 @@ class AnnotationEditorUIManager { }); this.showAllEditors("highlight", true, /* updateButton = */ true); } - for (const layer of this.#allLayers.values()) { - if (layer.hasTextLayer(textLayer)) { - layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { - methodOfCreation, - boxes, - anchorNode, - anchorOffset, - focusNode, - focusOffset, - text, - }); - break; - } + const layer = this.#getLayerForTextLayer(textLayer); + if (layer) { + layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { + methodOfCreation, + boxes, + anchorNode, + anchorOffset, + focusNode, + focusOffset, + text, + }); } } @@ -1062,6 +1073,7 @@ class AnnotationEditorUIManager { } return; } + this.#highlightToolbar?.hide(); this.#selectedTextNode = anchorNode; this.#dispatchUpdateStates({ @@ -1081,12 +1093,19 @@ class AnnotationEditorUIManager { this.#highlightWhenShiftUp = this.isShiftKeyDown; if (!this.isShiftKeyDown) { + const activeLayer = + this.#mode === AnnotationEditorType.HIGHLIGHT + ? this.#getLayerForTextLayer(textLayer) + : null; + activeLayer?.toggleDrawing(); + const signal = this._signal; const pointerup = e => { if (e.type === "pointerup" && e.button !== 0) { // Do nothing on right click. return; } + activeLayer?.toggleDrawing(true); window.removeEventListener("pointerup", pointerup); window.removeEventListener("blur", pointerup); if (e.type === "pointerup") { diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 03ff854cafaa5..b8b55ed89ea48 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1766,4 +1766,128 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Draw a free highlight with the pointer hovering an existing highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that an existing highlight is ignored on hovering", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + const x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector}:not(.selectedEditor)`); + + const counterHandle = await page.evaluateHandle(sel => { + const el = document.querySelector(sel); + const counter = { count: 0 }; + el.addEventListener( + "pointerover", + () => { + counter.count += 1; + }, + { capture: true } + ); + return counter; + }, editorSelector); + + const clickHandle = await waitForPointerUp(page); + y = rect.y - rect.height; + await page.mouse.move(x, y); + await page.mouse.down(); + for ( + const endY = rect.y + 2 * rect.height; + y <= endY; + y += rect.height / 10 + ) { + await page.mouse.move(x, y); + } + await page.mouse.up(); + await awaitPromise(clickHandle); + + const { count } = await counterHandle.jsonValue(); + expect(count).withContext(`In ${browserName}`).toEqual(0); + }) + ); + }); + }); + + describe("Select text with the pointer hovering an existing highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that an existing highlight is ignored on hovering", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText( + page, + 1, + "ternative compilation technique for dynamically-typed languages" + ); + const editorSelector = getEditorSelector(0); + const x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 3, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector}:not(.selectedEditor)`); + + const counterHandle = await page.evaluateHandle(sel => { + const el = document.querySelector(sel); + const counter = { count: 0 }; + el.addEventListener( + "pointerover", + () => { + counter.count += 1; + }, + { capture: true } + ); + return counter; + }, editorSelector); + + const clickHandle = await waitForPointerUp(page); + y = rect.y - 3 * rect.height; + await page.mouse.move(x, y); + await page.mouse.down(); + for ( + const endY = rect.y + 3 * rect.height; + y <= endY; + y += rect.height / 10 + ) { + await page.mouse.move(x, y); + } + await page.mouse.up(); + await awaitPromise(clickHandle); + + const { count } = await counterHandle.jsonValue(); + expect(count).withContext(`In ${browserName}`).toEqual(0); + }) + ); + }); + }); }); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 139039bbba98c..eb13c7e46c05e 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -116,6 +116,10 @@ .selectedEditor { z-index: 100000 !important; } + + &.drawing * { + pointer-events: none !important; + } } .annotationEditorLayer.waiting { From dfccc8ffd989070645bceb58220d4340b63e979d Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 12 Jul 2024 10:49:36 +0200 Subject: [PATCH 0041/1133] [Editor] Add an option to use the new 'add an image' flow (bug 1907207) UX team designed in a new flow we'll implement soon and we want to be able to make an experiment to be able to compare current flow vs the new one. --- extensions/chromium/preferences_schema.json | 4 ++++ src/display/editor/tools.js | 8 ++++++++ web/app.js | 1 + web/app_options.js | 8 ++++++++ web/pdf_viewer.js | 4 ++++ 5 files changed, 25 insertions(+) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index fc08e693313dd..3035f6500fda0 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -55,6 +55,10 @@ "type": "boolean", "default": false }, + "enableUpdatedAddImage": { + "type": "boolean", + "default": false + }, "cursorToolOnLoad": { "title": "Cursor tool on load", "description": "The cursor tool that is enabled upon load.\n 0 = Text selection tool.\n 1 = Hand tool.", diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 17d7e497ad927..53d494175eaeb 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -562,6 +562,8 @@ class AnnotationEditorUIManager { #enableHighlightFloatingButton = false; + #enableUpdatedAddImage = false; + #filterFactory = null; #focusMainContainerTimeoutId = null; @@ -779,6 +781,7 @@ class AnnotationEditorUIManager { pageColors, highlightColors, enableHighlightFloatingButton, + enableUpdatedAddImage, mlManager ) { this._signal = this.#abortController.signal; @@ -798,6 +801,7 @@ class AnnotationEditorUIManager { this.#pageColors = pageColors; this.#highlightColors = highlightColors || null; this.#enableHighlightFloatingButton = enableHighlightFloatingButton; + this.#enableUpdatedAddImage = enableUpdatedAddImage; this.#mlManager = mlManager || null; this.viewParameters = { realScale: PixelsPerInch.PDF_TO_CSS_UNITS, @@ -855,6 +859,10 @@ class AnnotationEditorUIManager { return !!this.#mlManager?.isEnabledFor(name); } + get useNewAltTextFlow() { + return this.#enableUpdatedAddImage; + } + get hcmFilter() { return shadow( this, diff --git a/web/app.js b/web/app.js index 136c9b0caf784..3eab6e6ae441f 100644 --- a/web/app.js +++ b/web/app.js @@ -477,6 +477,7 @@ const PDFViewerApplication = { enableHighlightFloatingButton: AppOptions.get( "enableHighlightFloatingButton" ), + enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"), imageResourcesPath: AppOptions.get("imageResourcesPath"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), diff --git a/web/app_options.js b/web/app_options.js index c1a3062374c35..70884696aa7a9 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -178,6 +178,14 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableUpdatedAddImage: { + // We'll probably want to make some experiments before enabling this + // in Firefox release, but it has to be temporary. + // TODO: remove it when unnecessary. + /** @type {boolean} */ + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, externalLinkRel: { /** @type {string} */ value: "noopener noreferrer nofollow", diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index d56328014724a..6adf76345da81 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -219,6 +219,8 @@ class PDFViewer { #enablePermissions = false; + #enableUpdatedAddImage = false; + #eventAbortController = null; #mlManager = null; @@ -291,6 +293,7 @@ class PDFViewer { options.annotationEditorHighlightColors || null; this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true; + this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true; this.imageResourcesPath = options.imageResourcesPath || ""; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { @@ -890,6 +893,7 @@ class PDFViewer { pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, + this.#enableUpdatedAddImage, this.#mlManager ); eventBus.dispatch("annotationeditoruimanager", { From 49eba2f8923f4c8fcd353c2304f108c7160fcc1a Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 13 Jul 2024 16:06:32 +0200 Subject: [PATCH 0042/1133] Update dependencies to the most recent versions --- package-lock.json | 358 ++++++++++++++++++++++++---------------------- package.json | 18 +-- 2 files changed, 199 insertions(+), 177 deletions(-) diff --git a/package-lock.json b/package-lock.json index 632b03cb3c601..60019134a03ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "@babel/runtime": "^7.24.7", + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/runtime": "^7.24.8", "@fluent/bundle": "^0.18.0", "@fluent/dom": "^0.10.0", "@jazzer.js/core": "^2.1.0", @@ -18,7 +18,7 @@ "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001639", + "caniuse-lite": "^1.0.30001641", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -41,7 +41,7 @@ "gulp-rename": "^2.0.0", "gulp-replace": "^1.1.4", "gulp-zip": "^6.0.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jasmine": "^5.1.0", "jsdoc": "^4.0.3", "jstransformer-nunjucks": "^1.2.0", @@ -55,17 +55,17 @@ "postcss-dir-pseudo-class": "^8.0.1", "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", - "prettier": "^3.3.2", - "puppeteer": "^22.12.1", + "prettier": "^3.3.3", + "puppeteer": "^22.13.0", "streamqueue": "^1.1.2", - "stylelint": "^16.6.1", + "stylelint": "^16.7.0", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", "typescript": "^5.5.3", "vinyl": "^3.0.0", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-stream": "^7.0.0", "yargs": "^17.7.2" }, @@ -123,9 +123,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", + "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", "dev": true, "license": "MIT", "engines": { @@ -133,22 +133,22 @@ } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz", + "integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -173,13 +173,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", + "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.24.8", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -216,15 +216,15 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -396,9 +396,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", + "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", "dev": true, "license": "MIT", "dependencies": { @@ -429,9 +429,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "license": "MIT", "engines": { @@ -516,9 +516,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "license": "MIT", "engines": { @@ -536,9 +536,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "license": "MIT", "engines": { @@ -562,14 +562,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -592,9 +592,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", "dev": true, "license": "MIT", "bin": { @@ -1054,17 +1054,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", + "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" @@ -1104,13 +1104,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1322,14 +1322,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1496,13 +1496,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1680,13 +1680,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1763,16 +1763,16 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", @@ -1803,9 +1803,9 @@ "@babel/plugin-transform-block-scoping": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", "@babel/plugin-transform-dynamic-import": "^7.24.7", @@ -1818,7 +1818,7 @@ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-modules-systemjs": "^7.24.7", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", @@ -1828,7 +1828,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", @@ -1839,7 +1839,7 @@ "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", @@ -1848,7 +1848,7 @@ "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.4", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -1888,9 +1888,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dev": true, "license": "MIT", "dependencies": { @@ -1923,20 +1923,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", + "@babel/generator": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", "@babel/helper-hoist-variables": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1973,13 +1973,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz", + "integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -1997,9 +1997,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz", - "integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, "funding": [ { @@ -2016,13 +2016,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz", - "integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", "dev": true, "funding": [ { @@ -2040,9 +2040,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz", - "integrity": "sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", "dev": true, "funding": [ { @@ -2059,8 +2059,8 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/selector-resolve-nested": { @@ -3767,9 +3767,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -3785,11 +3785,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3869,9 +3870,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001639", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", - "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "version": "1.0.30001641", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz", + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==", "dev": true, "funding": [ { @@ -4025,9 +4026,9 @@ } }, "node_modules/chromium-bidi": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.24.tgz", - "integrity": "sha512-5xQNN2SVBdZv4TxeMLaI+PelrnZsHDhn8h2JtyriLr+0qHcZS8BMuo93qN6J1VmtmrgYP+rmcLHcbpnA8QJh+w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.0.tgz", + "integrity": "sha512-VnxVrpGojAjkiGFN2I+KtsDILFAjiGWVEDizOEnKzEDkT93eQT1cqTfUkqmOyLq33i1q4a1KDYbH+52CUe4Ufw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4951,10 +4952,11 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.708", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.708.tgz", - "integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==", - "dev": true + "version": "1.4.827", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", + "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -5129,10 +5131,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -7925,9 +7928,9 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -9329,9 +9332,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.31.0.tgz", - "integrity": "sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", + "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true, "license": "MIT" }, @@ -11170,9 +11173,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -11344,9 +11347,9 @@ } }, "node_modules/puppeteer": { - "version": "22.12.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.12.1.tgz", - "integrity": "sha512-1GxY8dnEnHr1SLzdSDr0FCjM6JQfAh2E2I/EqzeF8a58DbGVk9oVjj4lFdqNoVbpgFSpAbz7VER9St7S1wDpNg==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", + "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -11354,7 +11357,7 @@ "@puppeteer/browsers": "2.2.3", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.12.1" + "puppeteer-core": "22.13.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" @@ -11364,17 +11367,17 @@ } }, "node_modules/puppeteer-core": { - "version": "22.12.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.12.1.tgz", - "integrity": "sha512-XmqeDPVdC5/3nGJys1jbgeoZ02wP0WV1GBlPtr/ULRbGXJFuqgXMcKQ3eeNtFpBzGRbpeoCGWHge1ZWKWl0Exw==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", + "integrity": "sha512-ZkpRX8nm/S39BnpcCverMzIc6oGWBPOUeOeaWRLKHqiKVCZ1l28HxPTYLitJlDiB16xZATSKpjul+sl+ZEm0HQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.2.3", - "chromium-bidi": "0.5.24", + "chromium-bidi": "0.6.0", "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", - "ws": "^8.17.1" + "ws": "^8.18.0" }, "engines": { "node": ">=18" @@ -12631,9 +12634,9 @@ } }, "node_modules/stylelint": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.6.1.tgz", - "integrity": "sha512-yNgz2PqWLkhH2hw6X9AweV9YvoafbAD5ZsFdKN9BvSDVwGvPh+AUIrn7lYwy1S7IHmtFin75LLfX1m0D2tHu8Q==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.7.0.tgz", + "integrity": "sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA==", "dev": true, "funding": [ { @@ -12647,9 +12650,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1", - "@csstools/media-query-list-parser": "^2.1.11", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13", "@csstools/selector-specificity": "^3.1.1", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", @@ -12657,7 +12660,7 @@ "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.2", "css-tree": "^2.3.1", - "debug": "^4.3.4", + "debug": "^4.3.5", "fast-glob": "^3.3.2", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^9.0.0", @@ -12668,13 +12671,13 @@ "ignore": "^5.3.1", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.31.0", + "known-css-properties": "^0.34.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.7", "normalize-path": "^3.0.0", "picocolors": "^1.0.1", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^7.0.0", "postcss-selector-parser": "^6.1.0", @@ -12741,6 +12744,24 @@ "node": ">=8" } }, + "node_modules/stylelint/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/stylelint/node_modules/file-entry-cache": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.0.0.tgz", @@ -13845,9 +13866,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -13863,9 +13884,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14155,9 +14177,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "license": "MIT", "dependencies": { @@ -14586,9 +14608,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index f121039c2b3a8..ee056e60c2071 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "pdf.js", "type": "module", "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "@babel/runtime": "^7.24.7", + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/runtime": "^7.24.8", "@fluent/bundle": "^0.18.0", "@fluent/dom": "^0.10.0", "@jazzer.js/core": "^2.1.0", @@ -12,7 +12,7 @@ "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001639", + "caniuse-lite": "^1.0.30001641", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -35,7 +35,7 @@ "gulp-rename": "^2.0.0", "gulp-replace": "^1.1.4", "gulp-zip": "^6.0.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jasmine": "^5.1.0", "jsdoc": "^4.0.3", "jstransformer-nunjucks": "^1.2.0", @@ -49,17 +49,17 @@ "postcss-dir-pseudo-class": "^8.0.1", "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", - "prettier": "^3.3.2", - "puppeteer": "^22.12.1", + "prettier": "^3.3.3", + "puppeteer": "^22.13.0", "streamqueue": "^1.1.2", - "stylelint": "^16.6.1", + "stylelint": "^16.7.0", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", "typescript": "^5.5.3", "vinyl": "^3.0.0", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-stream": "^7.0.0", "yargs": "^17.7.2" }, From c77b97daffda1a7b96fc726bd91d66ec567c9b99 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 13 Jul 2024 16:20:22 +0200 Subject: [PATCH 0043/1133] Update the JS/CSS files for the new Prettier/Stylelint versions --- src/core/worker.js | 2 +- src/display/api.js | 2 +- web/app_options.js | 2 +- web/l10n.js | 2 +- web/text_layer_builder.css | 3 ++- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/worker.js b/src/core/worker.js index 0eff8f9520ed1..3764b12edcda6 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -709,7 +709,7 @@ class WorkerMessageHandler { fileIds: xref.trailer.get("ID") || null, startXRef: linearization ? startXRef - : xref.lastXRefStreamPos ?? startXRef, + : (xref.lastXRefStreamPos ?? startXRef), filename, }; } diff --git a/src/display/api.js b/src/display/api.js index 8cc5bfbd6be77..02e2bad9995ed 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -294,7 +294,7 @@ function getDocument(src = {}) { const enableHWA = src.enableHWA === true; // Parameters whose default values depend on other parameters. - const length = rangeTransport ? rangeTransport.length : src.length ?? NaN; + const length = rangeTransport ? rangeTransport.length : (src.length ?? NaN); const useSystemFonts = typeof src.useSystemFonts === "boolean" ? src.useSystemFonts diff --git a/web/app_options.js b/web/app_options.js index c1a3062374c35..0ee11eeae1ca4 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -482,7 +482,7 @@ class AppOptions { } options[name] = defaultOnly ? defaultOption.value - : userOptions[name] ?? defaultOption.value; + : (userOptions[name] ?? defaultOption.value); } return options; } diff --git a/web/l10n.js b/web/l10n.js index 9b3e23bf86e8c..f86cb0fcc0182 100644 --- a/web/l10n.js +++ b/web/l10n.js @@ -32,7 +32,7 @@ class L10n { constructor({ lang, isRTL }, l10n = null) { this.#lang = L10n.#fixupLangCode(lang); this.#l10n = l10n; - this.#dir = isRTL ?? L10n.#isRTL(this.#lang) ? "rtl" : "ltr"; + this.#dir = (isRTL ?? L10n.#isRTL(this.#lang)) ? "rtl" : "ltr"; } _setL10n(l10n) { diff --git a/web/text_layer_builder.css b/web/text_layer_builder.css index 9e9f5b6fa6bed..0937130c9a974 100644 --- a/web/text_layer_builder.css +++ b/web/text_layer_builder.css @@ -96,10 +96,11 @@ } ::selection { + /* stylelint-disable declaration-block-no-duplicate-properties */ /*#if !MOZCENTRAL*/ background: rgba(0 0 255 / 0.25); /*#endif*/ - /* stylelint-disable-next-line declaration-block-no-duplicate-properties */ + /* stylelint-enable declaration-block-no-duplicate-properties */ background: color-mix(in srgb, AccentColor, transparent 75%); } From aea409d8cb147d32e865d86e2820a7b7ff4dc2d5 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 13 Jul 2024 18:16:25 +0200 Subject: [PATCH 0044/1133] Disable network connections to Contile for the tests In PR #18356 the new tab page logic was disabled to prevent Firefox from logging failed network connections to Contile, the Mozilla Tiles service that is used for the new tab page [1]. However, recently this log reappeared locally and on the bots: ``` console.warn: TopSitesFeed: Failed to fetch data from Contile server: NetworkError when attempting to fetch resource. ``` It looks like Contile communication is also triggered from other places in Firefox such as the URL bar [2], so this commit fixes the issue by disabling network connections to Contile [3] altogether regardless of their origin within Firefox. Note that we don't revert the change from PR #18356 because as noted in [4] it can't hurt to keep that disabled too to avoid overhead for a feature we don't use in the tests. [1] https://github.com/mozilla-services/contile [2] https://github.com/mozilla/gecko-dev/blob/196ef8360e4d3b5c334f1f8f91b3b1fdb434eb63/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs#L38 [3] https://github.com/mozilla/gecko-dev/blob/196ef8360e4d3b5c334f1f8f91b3b1fdb434eb63/browser/components/newtab/lib/TopSitesFeed.sys.mjs#L111 [4] https://github.com/mozilla/pdf.js/pull/18356#issuecomment-2200354730 --- test/test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test.mjs b/test/test.mjs index b2404cb42e312..8aac63e1a82c4 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -944,6 +944,8 @@ async function startBrowser({ "accessibility.browsewithcaret": true, // Disable the newtabpage stuff. "browser.newtabpage.enabled": false, + // Disable network connections to Contile. + "browser.topsites.contile.enabled": false, ...extraPrefsFirefox, }; } From 1c5c5da12e14959df260c978ea2c9822a3b25a03 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 13 Jul 2024 19:27:58 +0200 Subject: [PATCH 0045/1133] Remove obsolete `pdfjs-dist` code from the Gulpfile The Git logic for pushing to the `pdfjs-dist` repository was already removed in PR #18350, but it was forgotten to also remove the logic to create a Git repository in the first place. This commit fixes the open action for that from https://github.com/mozilla/pdf.js/pull/18358#discussion_r1661367441. Moreover, we implement the suggestion from https://github.com/mozilla/pdf.js/pull/18358#discussion_r1661364026 about moving the Git URL to a separate variable for consistency and we update the homepage URL to the HTTPS scheme to avoid an HTTP -> HTTPS redirect and enforce TLS-encrypted connections. --- gulpfile.mjs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 77808b483d2ae..211d4ae95a7a7 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -77,8 +77,6 @@ const COMMON_WEB_FILES = [ ]; const MOZCENTRAL_DIFF_FILE = "mozcentral.diff"; -const DIST_REPO_URL = "https://github.com/mozilla/pdfjs-dist"; - const CONFIG_FILE = "pdfjs.config"; const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); @@ -2173,8 +2171,9 @@ function packageJson() { const DIST_NAME = "pdfjs-dist"; const DIST_DESCRIPTION = "Generic build of Mozilla's PDF.js library."; const DIST_KEYWORDS = ["Mozilla", "pdf", "pdf.js"]; - const DIST_HOMEPAGE = "http://mozilla.github.io/pdf.js/"; + const DIST_HOMEPAGE = "https://mozilla.github.io/pdf.js/"; const DIST_BUGS_URL = "https://github.com/mozilla/pdf.js/issues"; + const DIST_GIT_URL = "https://github.com/mozilla/pdf.js.git"; const DIST_LICENSE = "Apache-2.0"; const npmManifest = { @@ -2200,7 +2199,7 @@ function packageJson() { }, repository: { type: "git", - url: `git+https://github.com/mozilla/pdf.js.git`, + url: `git+${DIST_GIT_URL}`, }, engines: { node: ">=18", @@ -2227,23 +2226,8 @@ gulp.task( "minified-legacy", "types", function createDist() { - console.log(); - console.log("### Cloning baseline distribution"); - fs.rmSync(DIST_DIR, { recursive: true, force: true }); fs.mkdirSync(DIST_DIR, { recursive: true }); - safeSpawnSync("git", ["clone", "--depth", "1", DIST_REPO_URL, DIST_DIR]); - - console.log(); - console.log("### Overwriting all files"); - - // Remove all files/folders, except for `.git` because it needs to be a - // valid Git repository for the Git commands in the `dist` target to work. - for (const entry of fs.readdirSync(DIST_DIR)) { - if (entry !== ".git") { - fs.rmSync(DIST_DIR + entry, { recursive: true, force: true }); - } - } return ordered([ packageJson().pipe(gulp.dest(DIST_DIR)), From 4c45948bc49b0cd321537c6d8491137024baf58c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 13 Jul 2024 10:59:38 +0200 Subject: [PATCH 0046/1133] Fix `DOMFilterFactory.#createUrl` in MOZCENTRAL builds (18417 PR follow-up) Somehow I managed to mess up the URL creation relevant to e.g. MOZCENTRAL builds, which is breaking the pending PDF.js update in mozilla-central; sorry about that! To avoid future issues, we'll now always check if absolute filter-URLs are necessary regardless of the build-target. --- src/display/display_utils.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 30964c9ab78c3..31fd390234be9 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -124,22 +124,20 @@ class DOMFilterFactory extends BaseFilterFactory { } #createUrl(id) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - if (this.#baseUrl === undefined) { - const url = this.#document.URL; - if (url === this.#document.baseURI) { - // No ``-element present, hence a relative URL should work. - this.#baseUrl = ""; - } else if (isDataScheme(url)) { + if (this.#baseUrl === undefined) { + // Unless a ``-element is present a relative URL should work. + this.#baseUrl = ""; + + const url = this.#document.URL; + if (url !== this.#document.baseURI) { + if (isDataScheme(url)) { warn('#createUrl: ignore "data:"-URL for performance reasons.'); - this.#baseUrl = ""; } else { this.#baseUrl = url.split("#", 1)[0]; } } - return `url(${this.#baseUrl}#${id})`; } - return `url(${id})`; + return `url(${this.#baseUrl}#${id})`; } addFilter(maps) { From 6dd75c0e625a3063f76c6fd6c253aa23a703680e Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 15 Jul 2024 21:04:47 +0200 Subject: [PATCH 0047/1133] [Editor] When in non-editing mode, add a new editor only once the editing mode has switched Switching to an editing mode can be asynchronous (e.g. if an editable annotation exists on a visible page), so we must add a new editor only when the page rendering is done. --- src/display/editor/tools.js | 33 +++++++++++----- test/integration/highlight_editor_spec.mjs | 42 +++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/file_pdfjs_test.pdf | Bin 0 -> 150611 bytes web/pdf_viewer.js | 2 +- 5 files changed, 68 insertions(+), 10 deletions(-) create mode 100755 test/pdfs/file_pdfjs_test.pdf diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 53d494175eaeb..93b6c49259ef6 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -916,6 +916,18 @@ class AnnotationEditorUIManager { this.#altTextManager?.editAltText(this, editor); } + switchToMode(mode, callback) { + // Switching to a mode can be asynchronous. + this._eventBus.on("annotationeditormodechanged", callback, { + once: true, + signal: this._signal, + }); + this._eventBus.dispatch("showannotationeditorui", { + source: this, + mode, + }); + } + onPageChanging({ pageNumber }) { this.#currentPageIndex = pageNumber - 1; } @@ -1002,16 +1014,11 @@ class AnnotationEditorUIManager { return; } selection.empty(); - if (this.#mode === AnnotationEditorType.NONE) { - this._eventBus.dispatch("showannotationeditorui", { - source: this, - mode: AnnotationEditorType.HIGHLIGHT, - }); - this.showAllEditors("highlight", true, /* updateButton = */ true); - } + const layer = this.#getLayerForTextLayer(textLayer); - if (layer) { - layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { + const isNoneMode = this.#mode === AnnotationEditorType.NONE; + const callback = () => { + layer?.createAndAddNewEditor({ x: 0, y: 0 }, false, { methodOfCreation, boxes, anchorNode, @@ -1020,7 +1027,15 @@ class AnnotationEditorUIManager { focusOffset, text, }); + if (isNoneMode) { + this.showAllEditors("highlight", true, /* updateButton = */ true); + } + }; + if (isNoneMode) { + this.switchToMode(AnnotationEditorType.HIGHLIGHT, callback); + return; } + callback(); } #displayHighlightToolbar() { diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index b8b55ed89ea48..c61663e0330f4 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1890,4 +1890,46 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Highlight with the floating button in a pdf containing a FreeText", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "file_pdfjs_test.pdf", + ".annotationEditorLayer", + null, + null, + { highlightEditorColors: "red=#AB0000" } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the highlight is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const rect = await getSpanRectFromText(page, 1, "In production"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 3, delay: 100 }); + + await page.waitForSelector(".textLayer .highlightButton"); + await page.click(".textLayer .highlightButton"); + + await page.waitForSelector(getEditorSelector(0)); + const usedColor = await page.evaluate(() => { + const highlight = document.querySelector( + `.page[data-page-number = "1"] .canvasWrapper > svg.highlight` + ); + return highlight.getAttribute("fill"); + }); + + expect(usedColor).withContext(`In ${browserName}`).toEqual("#AB0000"); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 01b3ee127c628..808cf43a4231f 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -657,3 +657,4 @@ !issue18305.pdf !issue18360.pdf !issue18099_reduced.pdf +!file_pdfjs_test.pdf diff --git a/test/pdfs/file_pdfjs_test.pdf b/test/pdfs/file_pdfjs_test.pdf new file mode 100755 index 0000000000000000000000000000000000000000..7ad87e3c2e328e5e25902f1e83fbe9d7d10695b7 GIT binary patch literal 150611 zcmeFWXIN8R+byc1pi~j1NL3V6O6U-(ih@d0QL5AsB2pr~gn)=3ND~l{7E}aOdY2k% zLJu``A~p07N@Be#U$x7y0cjmn3821=!UNhGp-_wzjl~!WA zJ~xjUVq;d27m*Qx+P`8`R|n`jxq1LMoCs# z4qzyv2zYjUOGQap0q{V?QbZA`EFz}@w9?dM1G_o=)sx8oJlL3(mH&H;dteaM0sPp` z!`1V6L~W>xx2v1yai5Ppzz$9zFQ~_H-myQi(Ka!&!M2vm{Rk=Is{J(kyR#jiRw+cNKFPYbT)xx&wa>TI2=}9WLq%4*R`iB}%FFOw}KM$}Y8?y`>^Y!cZjda+U z|0iAl=Pfp7S=oPM_&-r6BB!A8U+~Jw$N-FwL6!xWh{(w){SVk|ZEYQGZ6`XKx|*8m zqV9Y=dGd6Ky$h=(s{r6l21uTFM)0-w>&B+05WoS7&c>cv^n=- zRb^WtR$>{MEon>tQk zH0ynR@hry)B4^RfqiV5uyGo1`N4drhC($e1zeinvkhMq# zO_Y_H@jX2l&B`ii@mickUM9%=6esvM(=qNYpX46>2i*Uzo_|z!2jl_OfqJ-#D69Nk zQU+izI|n;2JCVOZd2Q$*!o@!C)~FR+`Jr=`+gAx}k=l>YVc7gZJAQI`Ez>u(7hOX6Rxzay;x z+8)r?ccFfke}yW^${lAVX9X}gR;u0q=jySfkH`A2RG)%9q23-K@LwXo@8_j!>~$<; z5#YaOtaF@~2=Jc?dO$(OU@uF+u`dChf&ILWb#$z_zr$tzrLw;;__yi)mD)ekz590- z(#P2yzhzbaIa&QD_xMNKazF*Zf6VPTPhDe~Z8EH3#oN-4 zL3tF7<_8ZX&+D~?i!k>0oHBb2w;trad{X>&{Hx%ioeN0PCG6-V=Ld+G@YZkpz$-R$ zBtznxsMeUd{k~rwe9UiO4nzm&BrMn&e!HI`yFq++Gm7f?ba1$%jD9t7nPn9tW@@v*ql~T#tlPqg+-2`0tb-m)HLm zR|3&`?5AW|wTtIi^e^TaOdw92i{uhfV!a?N%g-(1X0&`Y)}9(BaXEw9$AGTi=Cbx~kc`qafJ`fH^ zz?sthpk{XV**%Sn+?Tpb#1{AMf6gsMmMf~4E`(bDxiGO)KO3EO3{6Dkjm7^2&0mW8 zk81e8(EMdE{|=3;5>Q%6L`gwK8mK6LY*fcBSrH{=C22WXh5srb%YR>T9A7+E^Ixv} z*Lvum#euRcz`)MS!^zK5_8;T<+kB3{OZ{sAseeYW`uD0k?#a#T*j;pFL{$DZ!Q(P~ z{GY7vf2%eNKK3%V$ee#ipkAb&cHb^{Vwn5-&G*V5hJEAn7bo;=aKlrzZ#6nvgu+vy zN+*pRKynFB7kDia+gR#OFmxBmN^kpR^RPMafma*VP2crhXWZ0#{Y`pk>8#J6z^sQT z*omSqC|c+(0Zy)`e*$x)M0#U)Lf5BElKO9gkA7h9YN9v7TVM?ECCLRIK7(&ys&=`43Bl|MF>N#eX->7X!Vo-pQP2 z+JYhMW9!~uU#-nQ!)Y;jhU4gqoAB+#0mfIXvO7&-QRn0C;60UZW}{IAY&ak% zwfk?1HGI4<80N*^75hE7x z$p51e+2duMtepJ+AaLEp(nY|OkA|5d$g8PUXI75ZHLkl!&qOKr*@K5NzZ+cq@ImUA z!o$h1dyzxWBKap}P40eNRsY0v`oqZkx5Mwhzj=3q!yvR{y8g< zz{q;%!<$EvOIelLSFar(*0w%TGK+)g|3qngC7DZjNW0hl@UrFgFXpeIBWc9chT6~LW7Pp>Er9z{5dYW4jY zR?M)e<6yYxvU9THM(iY+Em}ZGWi*WZ3M^vNE2#1t#;)w|wyRVG#upEW2J8tV2E++R#2 zs&nf`3#5FDA68d-6tz|=qH?73Qq2s1I=lLIGK2VH94^m$RO@`m9ZBGdiO_k%iY^Hn zE_)dcHhcKz%%czA9qudSm}JFv861cv1KY0eE^c_jT6 z!~N{rqm=X6XYHO`xjlT_mDrhK#H>k_ibXyc-U+||x_Y~EHlY1;AsZV&zx9HG4nvxK z2ujC|M)7?DF1zjhWeou zvKIxzSsuSJJ#~HMVn)-7r{=1yJkLXytEaHn?}9cj6xoQqR>BS5OMEDB?e#`aSm3v` zWrO}m0iHrbYl6E7vY}rznp=$c?QB`Ge#wQJ2-91PdZ&`3 z%k#`W^nwt+5;xxY&tLYzulYJZE*zeI_U;iP5(NXxv5dGu%glBlCs_mV#u zjG?QU+Bma9xUVJX{-c&&wg>04fti&f?3m-jCzWdl>?`?=bc0Dw#Lsn(Qq}dD>|M z&x;$65}X+I&T16=_z)Q9erEElNiD0iz_~}dh1%?jUD3?C-?b;6gf`r9>)u%{zKV>K z{yi1+N~>IVOte9_sLOOXODBl0Vd)Wxk&}3VQ%4Y^U{V|DU3BI(-;YzfUrmR;ybGJ@ zj=!d>a{62B*>?-3{N|%)0}$T0rTyzE&I`bV*CiQN#ZGmFy5FjbfJtV*Mm#4bt+cqw zhkoh~<0xHLTlVXpS(OgM4uOV|@8V!>vGUJ<-h!4Y!z^p7CXM_i>ok`SMS}BpOss+3 z24Z}8e;Dr+}D4{-nL+d_G20uNM;i(OfBK@Xn+34BL4Z; zZ5(#1Vg?V-y6C9VShW-U;_dH+ zKRN#-J7l?gnU=+N0o}{B+C35Uj0tWm2l0K~`Oq;}C zd-&Cix!20*?1xxf)woNY*iYu1R7WrQJpQ}kEEJy7F0Hs*(((AvBkQ!lLENm(bNpdG zpHh)3-z&koSW8G@xpKR5QWdH?xe?*7v12&;emu)}`kR|S(n;=hXJh`*qQjNyc4m*s z!bSmdL8Y>9+vVQg6f?!5PYNO{2}=-=`}M?__=+&x*vl*UB)<&X*3DF}T29sMDdCjI zJdm<+Gf8!4i<<&W+Pr?+sU3YcjK|FvJxIP?dj(tI#jtg~anmHIgSpb0<4>2!8>~1= zt&jAP_6Bt&e*!aDU1^gS2FO7s2R)~nq;V=H#G)0%(pINjzCXBC84iuSprOi>^KG_w z_IpXKB8GW)_3@y3P|jnBrAwk4+xsUEzS;qRkNqOPTzn`5{s5Ss9dH5&aE!d_7N6Pw zth^dC-vA%m_cKGY<@4AKXe;vmil4n@J}Cp>Z~ygv@JMZEWKeYUNm$;}z|eHHjVEa$C0*vJwyKa)1=>fyz*%{?z^LpSLj zZpZiCp0Z5UEMYQ}n_-`^>0y-ZDckeREG(9uEam0C;iYdcwM@llhJ}r#YW;4sWeDp9 zl~d{RRF1T;cB4M}{45SJ3^+pfxl?J|lg=dlGz!%Q`Xa`3GPr#p7;TduRMk{Mmt>YV z(we3VCTdU{y(r^NXiN!zyw0bq$t2-Vpy8{es~38%^GeuTInirK#GXg@6u-rms-pP6 z$Yw;V#K;cv8vyNAqoq-TqvuqEIqNVMK4l>5@{h!;#367jC-sQKW}$5BEV_LDq}}P84-FgmyD4{y>HJ&%=SYpLi*(tUFq~Ab?<~36E@hT8kanx2w#mb-^nfOD7%T$M9QDou z&$JfPgIgJG{hfdsr*+<5E{XcgE9lv2Pu6J@mL*BF&6596)he{w117nGndjm3D=%Gw zE!-xpD>R(E|0o_3Y-e7YhU|PnzHcN}A30FRRtJSF=I!>@Zr@`lE2wP1g%v#CDm!|8 z!ByCk>2~lbIrdiN!jBt;+=oi1Fq+uHH;o*uvYIEP?PJC(8P#G#&}Vj`&B>#G5;eBU zR5C(0-@g&;C^2)rPm-=vc_>{V_lyS6@^S&1EO>QkRE{w=F3YNVve?JYolAoj4 zV+nhB=v0x+h5O*g6fh86MSIY(?RJS|9oCD!0y!7p{l*}qvymy?_sgVtTl$7U&!o8* zPnn-|eTX~Cna4RbojX4}g2?S<9WFHaMNO1gU4F=pvXt}Xt2Z92XMn*&y}yX{P?YM^j!Ey5R7_8WzXoD<~Mg_V0qBn&ywLp%9D08 zhBe)NWx;jY9%vVmfdJHk;)7V=Gl~8_{ZNKzbNc&|kxf2a)cze_{O3v&@%7DD6es^3 z-nD(h5`mzIWO@}q=)_Vv{}<%>GPQZ9<<SSAHc!U(tgCb^2RS7o_i z#?(CS&s0VE0Z_y|qeUxARJtc4wByvwSEB3w?$DSI(J+HYGiq6R3y~3iVgF_=JGel~ zusNgk6iV;Q^T&>YRaHdf4e01;dIela3g0!C&NuKiJm?aQ_fBjK=H0rGsjl=k@`IVw z6`xBBFe|KQ<6EU>3tU3=k#d-+wx2tcX!VStaO9`*0Paq#z-cM+*UqX52&|EbmRz@X z1%`fh-)Lz?s>AJ@K~wgJ(tBQ(EaTKqw3P_Kh1B1QN{)MYRjg@ABRlJrArh3uLw63( z=#X+-?IOs za$>M*sia1an27lVJVKvtjm$T*$z`_QfI|x#433}hG zGugunjkB5~Zom^-^Cin37Q+#uT{bJfM{fbQNaA*bhOJi}hFiaDC1KaT)9+XJ;hsy_ z4Q6$eFr~Pf5QE%7AW2LOMCgu2IHM%k72PS^K+QB zdC#gjOtKX8rZ4{BjV&_3lhNUOeHlz1-U=p{4LE zFwhuvhv!PS_rU

JS#|t6)Pg!#Ft$dQ>8Yb1;5GLN@4*>RVFqngxfo)cLh!WE6i^ zk!7o2vl%vd8huu*2X+d$#pZ21)l=RPqBiS#)RxLui+9tEUI{y&m<;$_F*+eV z*RIi7kAd-jtlC&MMw@!hWVr{>yJ6)&F;011hOG+?`d}x+?=4yQ4-=-Bg4FqsU@?^L zvDITxEo(vF6 z^ZlnBF>3JV@xa(M3To6{lQGph`)Y9K6XycLo%cybp6T1>0yEUHPIi!A5 za}|DrAfKG+4fs;2K4+B-d*h3fMt!c+arRFQm!z8+xfD**C*Bl}GirF7y%nnG88j7K z9!90bwyA~9Om)|bKBkAXHAX4!YEYiI@i<#v&3;Rfo{;KUfg2E@yI>=iUm;?gRP)I( zXfs4$?OP#>&Fa^RaJvqVkUiIjH2$*W%ExOq00#osOtYVL86!#CgvH!6|4O%PLw40& zlbx6HN1bCnBHG>?UA*2#g3!GyVE^%h?@s`(_FuYu@O1AHK5YaX!6Rdbd7n6&6Vi!Q0&`<88et zmwrW!{2Wl&u0F}9jFrH$(d2{EuS{lVWUFIukeoR7zQ@LV94RX+Aaq?G917kwHb%X= zV7ZxCyXE{r>vM6%Lrm6lV&*}($BJIg#=G^Hj-4FQiXOGi>x1rE&)c|z!!Klz6}8Di zt`5?2Dds%>CLRQj#*2vR94SyOy<#cXuFO%6BUR6zg1OwBfzLFd3@Iy`V^nis6=Ldq zMw4-2GBrlDZogEi(HP4o6##H4Y!l&^ooc}1#@%UFUbraM8f4a z<=^ak3*I%FQ=abu^7dGJqiQsFcobh9XeNV$z9!FNEK^GfhxS^k097|G3?#EAIcGzP zP}u|Tep11)-T8$2=QFQej5pcWN%SCSb>}*%bH((C!yN+;C&M;MH^&4)7>zd=^$42- zp0ov(Q7}okjbUQ*Nz;v>*RihwpMdS~twzb68y;6N(%8(pAEu*a!@YYGP$@ju7N)Mx zn`nM$#ZqIWvBe)_J;&|a`D9o(pv2W2YlgRTIkevC8NlCS04BprqK(MlOyg)iCsVndo(VD{F<2mF6|X^v3dt6JY9 z=WNpoi{Sm^n4~y=5dghE5Ic=W`TaUjZ=ZO-8VVWb%ou=f#Tgx$%x#nmE!z_wFPSZ^ zEA>&Ns)r6CFxwLr)|e^Tmsu*4kGOFl*Bg3#l6qb7geNcKY9T>&Hqr2rNLch8SRM-K z!fJFlmXs5G$gv^q=W9XcBb29TmLnQdb8kdv1{m3V{_un8P=y!5m%5qO(pOpKY{M2LG!I7n?#HP7Y82;O6)L(>gw-oxwh`7 zH$KRL%~=G1GxkZ*R$!gqJ6aW;nyc8zUP5J4tZ6VDfCMdit20#*Dl#+mWwo zC~~EB6C_OS=h#%tLm=)kqlRScdV8+)vH}ILUFvl>|j1Xs$LJ;8#MGAO)D)Wm;dfMtmfj7nHA74a%VSm88CKm zdldY=uv}~X(Ije9bdWI>Z$Wz$Ty?;V&zJ&fEb0gFu{0tjpt7;_`q6eRg=SaMqo3WF z)t;Y{r5~$05ZPh=)SR#stod$Nb%%6c^{x!tT;`!}Uxjob?Z@Iya@hW*Zrv~z|Np8r5F)RyqKON-`sx2EH*>>E(q}iL7IT|<{ z3sGpQL7!@T%b?Bmo~fQ+;N>?x3%_SMjfZ${Qkjm(wew?I3j0}$JTrlt!e}`%Q-&|J zU9)y~Pv0~ddZv%`%?SFTX!*8Qx0-jFOXnD7y zV9x`{R5{RxnSlo2zK0fAOKo!|h0oHI;%n*1YBUcsN4YFL1f9;{Z{?pR@;8@G|Jun@ z?N^?!AGHdnN;_PB8;4_xeL_|%O#1|f4ThNJbJva@m>j8(&w1{J;vW4ua}*=ErPZr7 zLPvH*^U2T(cCyy20RjD*5lcU3G)(z^)(kbo70;7Dw#sGjR}}AQ`u@RtG7*7M$Wa}l z`m596tVVoEJ(_sS^Fk;k!f^TWSgC)ABqFlx{zt=4kBrfAxOo}9UGMlA4 zJh3#;5&d!?=0Nx@z(-FDo-db>(ofD0w0-d!u-M|e@c89MCHf$*>{AlOP{F0uFt7+* zB%b|p(sbd>0t0^m2()ZSB)?d)Xf}U!re@398Afac6ghR!&Ied%p(Xbwhzg(h0M*+{ zj4s;C(^0ev`qQQTAREV?(8Z|bjIO~vs@&*l^jZ{+sZ&8|Q%xx1{v=b~w1e^Ci9*A` zshxgt)^EB~ww5oay6GLeW=ms!x8#fD=$pTU>9=;$&-r%{hUjr}qaRo3degzoH9t#- zCN4s7tJApdGB;kx5q{s_rDNk;#FC5m#3R#8f*X){nywduOD=23obmDF@UzvDkZ5mmqqbUJRMGe zgx8uNODxF^OEtLj1@Up!;Gp!!qEqiHTyzO72w0i57jQHxmzrUX46B&M4{tS{rsWN?I4=XwfxaU$Q+EvOZYA}bCW+fIe7BVbW*h{p4x$H zlypsUreGJ~EArU<(afIfLB)HQgZOJ`ck)N4NJM{`wciYSnb;|&r&&sNAawQG$=WyP zwmQ_%+ckhO#iji}hF%%PO5l|`>w4wqaR%$-SgdI290J>@I~x3{CR}yqMcvm8>oB!c z%w}6_O=CW}-<^MrR0n>8)HO}6Y0MebDLX$|SId~Z%utd8yG`Z7;3CtmG)Mtly$=wyvbAAUDe|^`RTgjhVZ-) z%x-8s?qi7Omg+LX{n2J?vadH=^yTW^VbhJ~ZhBiX9Go?sL=l`9z{t3*r~86Ye5H>v z;+f)V(aUX6OT6y^ZWQJ_2T_~FguZk<#GNsE*{?hymQC79gs&XLY^f6D3YK0&=+$_e@_d|FHNe1Ri&(LR`LH)js*VrT6U%r8XIH|FVg~)y6ryPOo zUH2mYJT;U9>DE-u(AY~iwW$@uL0uF39;o5=Sfix(#OdIjy%Vi7Xz1`8449%Mik-^W zhd{xU@TrkX7$ut4MPBzrip$%)y|UnQ^e~H}F&p5?6^!my!j|E$u?Kwu8!i?e@5}T0 zmO{Puo__GDEa^ zZ(EncyOau#A0fzHP&dcT{39fXrWAa%(Q3;V@H-ig-K};l)3V+PrBrXv1%Z*^L=!j9 zO|x?RWuL(+=P~Q-hNU`_nJR(K@-Qkl98XrvQX^NaC)HFh7TYYoL(Y@4+Pl-(QFrIQ z<*@7+1lHh^z^?mhv>AmyZ{+8^LQ>3Z19oyy!^l4SQuR$eiQmDoW|{>xILg!Zk|#{gwL+y%+qiv7nP4^ zGj+a9Q1Ew!-QYio!xcm+;s@|7Bm1B^nz-U&bk%)57ZR#vB41hfz>;*rmH zy)=d$ABWDO5U472aqgQHrD80 zC&-US0p9w~$mc-98eRA~C=pYO_WOkwTBYB^-)P8#{aO4{KB_9c$AU6U zz&|F_`zIcc*8LJD)phDDD`WRu7u`R`^zEq)1J|-QFBpw}iIkGQ!UHLxz^&A}{UoLMxQH)D&%h350{@V$`}B)Uv7kfiHlh2% zP{}bmd<->yv`jG9pwQ?!BM6y$4Slj=i4W7o)JESTN5k|{r4}-piC;OTiLw8sId8U9=EURP zb+oPu_BE6)zJmgGV}L?%Le2Vq!s6Tc%E=qkk#@!|&K3}XIB{y9N6QA8mSe5j26{mX zNF61s=N=_p9TdUzT_~mdwBXT5FAu7Au%^-I&qaEd>Rjyn< zLe4O<+;F~7D{0idW9uVCd(W=C?nxRAD5mms9RGLzax)&|HyXF+NLw>7VVO81H=c(d zL;aSYU?3$S%QKVo4=T#xtK#tC6tS`Twt1^cG=()3_>(m!zGrBQZ8LBrIGlQnNBv%VEQ;yBJ~=OgJrOm6GUs(~01 zj_5dS-$^k#Y`6Yp(Xo9O&q1Q$(Fdf`pfJg-5O}UVeX_juXqNPo8}C<9*S0(|cQu{4 zPr#1ADJ#52zFy^>W*fMY`ab1A%6#D`baByTJGo;IpfLJq zw2j;OMYxyxZFb65dH~mu4onW0Yqpk57D3-b;va3u*qUJO`A&5Qq-}5>W|@N$+{`yIiB>{}jciJ++@9hGIXgRR5(C9J>}w~D zrZuX}5kq;KlhReR?tRJ&bm~K*-HXW9g0IaB>|$}iR?k@DsLXE1`*gJWLO};DsAW8w z2bUVW&2jXw<`>uYSJ&N2`+@OsM#w!q+yy|Van)1t3CZd5_L_+LpP``g(d|$51UpaY zp;^9_`jQl-OlW~;DgM7x5jE`aJj}m{fYUtnQjKVdUSKzO?d5XaEoKGkPG3ukgT~+4I|)9I$Syu3 zRtcWeHyRlEm03R#tF}qJ<*K_(&?nIuo_e*VNc1@v)4UN?Zpz+m2p=s=4$p*lN|0#W z5{4xNfZ27M;39n7o_|~=6k)#+B+u@GQch zQq>nT4mSfl#6%g@HkrwAw-r#k3OZrtB~XiVUPXd4YZ*iYqCfC}Mn>@PIjpt7jY-hh z^|G2^Bfl-TrLpdyRyYbv@G;L&vo!Bxi=2uUTZ=R={Dqn->akJv#Kj-!6EG&+dI!@m zO;?a4`PC47b`RfpC5RG;ZoK2KJKu~b=gI0p!y29EpXg4gptFxK_U>`sLcfo})G&pTe9y-) z9>j=j_Y(#=>m@Uo3p>I$?IP#P&}rL_^UbMQM?Q=j?fd?MuNxQmCNT9o0UnbLd0^h8 zSu++|!G=bb<9_{^KYlpejXCi!_#fH`{@3CRIP#1VxQ8&{2ElMigDiyPSB!X za!Qv(ukGjEcpDW5z(v;ZRY+2A6EDYX^R0#>1&#r7n9<=f6AxLc9U)Ti^ujL zR>32C8=xa%X;2|O?s?Yequ^ilv>$}_WWb&ag$_E>dam_w(;2RXg;Tb;B^nKrFh<@s z>cKx$QwZc+#sNpx{t{5bFG;~8dB-|!3&>MxhJLFv(JbyKn(WmK!v~THj0Zwr^#+~; z2F3>Ky>|5_ijQQnb ziZoezVH&}O_^_L`T2os_ygQYfdK|NBqg`aZqoAluxM7BOPTzK5Y|a%7G4xTbzT^y! zz?qSPN738X?fx;aRb&F%9QIayuLp=t)g>Bpe2&@7y3DXA-wrLH!3t^XYjxPdK_}_` zuiHU!l{PP4lZ2cBo9>1X9B89>wbW*nYMDcMy4Q%F0L`o+9HhP^8q9q?911>43`*%D z$9kIXx9l~%eSerOL}x~HpQL5`iD&uTZvn?8~xx-9wc4?O4-M-O4N_^0 zy!)+I$+^zcwPICd^-(L&kNA}N5BO#44d>j;rK*!_KKpV!FXJ$A2pjFY`HS*jc0ify zos(3&szc3J&Beglcc$DB^hZd3s5*`u)H(st@~q?I#{dV84oe&11valTf?Ksn=Dnvi zNZ!F|_Pz0}#bufacHwITpVBza_Z`l$uIx4R3PU6NR*kNX^@*tG*f>%n`Ji~KA1<4! zKT0o86LH+pe1NFs7xFor(~#-bz$y?zVyT9SizU>X*~EY^t`*e_5spK0u*Uf#Gt*A@ z-nKNm`+2Fq1Cb+Su0F%Po0@1d0zg%H1Oh$mFs^()qX@}*z4?NS`RH3jzqWQ^ZC&GX z;ewDmjn{c&&GNTbm;jjz_Q?@OiU;E@Yt%u(k(C_QlDR^l%xtF3y?FojV^7?xmCe-n#ZrfHBe`o2=yX@Nmij-lV26v2Bq9B{f>iuMc_WVu8 z`YTZQx;z`~I_3OqxXS9??FYhwn?H3?A^Xro-vCh@--U_7BY-=$Jb7z|m18Yr?VV~4 zB3EVYO=C6z*r%B!)zu8_Upeo1$||$cNBNp>=)f$sXe+2n`Q3BFKbf!feR6pTJ?s{G z8GBt*hU$mn09s?EFDS#mclWNEgX**3S&|4QC`f86tuubS-|mMoQ5j{-DFH#?49A5YLlht)>Z~0YB!<^sw+u@n=Je!3g7v^OY-g&$y-OmFeNLu>F6-%#OIye> zGX5Q;lWdO{r0vnD8&pL9`rM6BB)pkFb0}{)PO9&fB`sMc?X-!vSm+ajj%}U|-Sh$a z4IaCRj-zj7J1HtuDLd&mIRNYz(J2PonG1(yt?vlj(2S}zq-@&rx5mF2>%Lx$_OWYk>SnwOgCM9ubz%8y0qClHM1I^4s`K8=A^99fC@j!a%MK1$G-@6n_=)pkE z(o+gE$m2xD+O#JU!(vWfFAu{cGMfWpsnl#QU)QqJ# zR9(e{4uCk8%YRLNRv&i%Gyq{HN230i8wnWYav{4H#5a_O_Ohov?R4e`0u(?s07qO; zO|157u1z^X2(99pmt2s%pw?i+WZ&976UZa~*;K$M@o>;TfN)1fQ~6mh?2Ea6sY$TyNv)@U zS3{JeA6xNmWG*~y?7!?%$tVi{Mfh=2+X^OhRhnQU7qC8EG0Hf39ePvir~{T>fMq+U z?{q{erA0L83fvAYPxGj9H0XP`ezOhzR-Uv25^!BApE>c{Mf$r@UuQZyIdpdUf+|fEtW7DaH>z z|Ct+2a+Fgc2-jnyY~oVA8`4-cUixFM(Ri|bBO45j;&<*{F2K(i$r$yc^?+Z!5R1Hh zC39+PPGfbBa8dsT_21pEjWOf=7}K=)m%fDJVU73@m7XE3jSn`QJ}}_ zQ_{AE<;-M49BXja`b_l!tHml9PMJz2TFiyWoAKxhE!1fy-okGSx;qsTq<#8oH=!?l zrL`FZq!zGR1l6e##?fmsY$j|0QjI)AaRDK~*z)w90si9RNlhre>!ERm|26pbTF{Nl z3tvk$hOD5w>N8`Lj`d#Q7*h`3xgWi52vJJ$eH)MTI^k!3(s$Kh^M}>$RUrZPq|@er zYgYEu=c>WU2gxD(FT3~E_FwMR$93Th1iaUSbP$kGvcOVlMGF#@ z%5iYP|FuwN@|KZ%>w)HY2eGXk2xzu?gzFwRjdG zVe}D7=GaJBaob)l2%DY~&!{%qM_l;a?o#yGSor3l#FVciv;o;}P5#XF(sCAg(lHhk z9oN)3fw`=o>F)wAU9OzKOjkRr9dAJ|SmpYXtZ;K`4YRPeJbZ;D{nOlte-J7T|6n3R z4yNH_5#6l%6<=0ck8plLfM+Z=wxwd4mji76>^}0jF)mr|o13jay6#%98?dCsH$B@7 zH;I~VL`?p)*;eN38*o`y^%4dPkD$zb<{G#cFoCrf=dWvd!X0H3C{E#`brS8KK6y1E zmHpJJ&SXE5PzcboiE>bU5WhFM-m3ElA|=}UDl}g|7Z~eLks_(yIJop0Gl@Jjg|0v) z*5OnWOzS>pkzFE40im8!5(`!m*l=~M>q#Ci#IvA=pEbG1HKa70EGAFnEK(L;(OZb* z7OJFhm5;d1S8IVPKt_tg$6owB#sku)KVCBn3=1)YSD~Y|W605+!8(0$+^am@eK}yN zWmW?}4Wrhzt&hTPmRTS=e+IA2dfv^C1Oa~X{^H7rmcl1(RZ2}%o?!*arE68#gyf&p zKu?dEgMF>;TE0ezzM!uni(yA#gwnhvNq>fik{~d%=Ae;8W!dN;_ek~lCS`I~uNzRG7I|l# zEH6?tbo=7Z6jXe!Yvog>e>zdWZdqdk9+eICanJDQb>6*$dca-0i8dz~1(QE-S1x@4 z2(O{z_FBczu9lY^Y-YJTfKZtsAM1AOs2Syke7Z}OJT}wF@jd4Mq3*4s<64$&K{12H z3>H|D#aJb_*kapaMvIx5EoNpWiH1hT;wNLR zxgv68=8BlJN~yiQ03B>l*%-Tzwrb2NQw!%=8tEB`38p`&OCznA%($Nf+xL=qO0}7+ z{jQoA$a4AarPjH=l}yjHl52GY#k7i=#*{L%)cB1iR>|#+&WWsJK-&OY<+`5~*a{rT z&<}XfadVGq-=lZ>i9BWyTI+sX-~Xb#A1lIR%;0Xl?I`Say{2;n<+4|abpL0@Z@c}@ zVA|7ieTR1>qQKr;C0FfGBt?+TBa^b&+cd)iC!n?Pj4L7ekiHt!g7~H$I;HBK`pteQ#^u&S7T>&Wd^@L0ma}Twf%E9g8uP)4iOV9h3+mQs z1LYV)BHZK^(8+T~ez0;u^|9(XvBRGIwID4C_*nAPx}dTreM=e+nBLH@xb1LY;jzjS z@cn**!~Y0>BJ6PuJ1>7I^`N{z3;V?WzSzGOscgUg%^-2n7MjQSRpCimpKLDW$8!x= z?45+POCfMib|2(;g>sg+>p;5tf|E*3(uf#7vrXVmJx*(RS+Rpo&7BOAY8gMAl1|?> zhytngoF(%HE_(Vq#(3Bad>g-9OSiq(880=Ih|LdfLH=woG00zAV*ck4E-*%1I2C;p z4%2UhoCCAeX9NNd$lBR$I!?UT=Ucf2jSy9*a(qKap&(o`j-x_>)qw83KOdtMGbARHa-UTB_z9)4k0StZ55uzp8B~OjOy7=kojSAs^I`Xa4F{*u{kcd(+4F>8!(~z9FTWYikU4 z$Zum${6W1F$XrhVqh|~5zgJWb@EeN(wYOEJ?9RdkhBRiDlBk&bAL>tny#9Fp^2)IP z#(4R`JRMMhc=x42JC)RhDABe=r^~)W&IW7Wt2VcFvk`G$e5Z$V%J#UME1hN$^j@Pg z(f}t*WBz=)iSO%elkr~sv4w3d-GW!nEghTl;OqGuX;DqH%sJ&Vp}be_!D($Ph@95F zpuX%JJh85^=ua5oT$EJF8&g`>Wt*|Lyg2ZXaj5az-te4A{$_d&QUDxG>~(k1+)fCi z)zna1aom||;OhCOtQu9(dv|wk>a#y|u}p#S)8=z@ZK(a5`ZRD8nOldsL4sj++)E&8 zUcM4nC$$r%`EmTVY1Uq|CQHTNK00r+Bj?G-lZ>L*AMAcr&o-+(D9dNG;(mr!==tY( zt>sQ`+Ur%s*W5W0&b-)MbDh6cd&@B-(cXxe!x2u0r!ASrtYCg>VF&kYTxmVYqOzeq zUQn%3`oZ_4k6(<@pj_5MfjyRD>XuZbkb2D38 zeOlnJ;(nkZv|VSjgV!?=#@h~kBQlp^z><_^?+81Is73wE0dFRAs4l^=CU7>|jDNir zqJ3N?J6D+;wRTppIM@2alTYX7i|2`mtBLHz)-X}$r-_~8jsB=4^Y(k?6z+B%p4uRl z41vN5Kuyw%?|L+MGtc;3T>84r@!sj1$Z>qDLFL|sJEcnJ^1Wu1Cjayk6JqK;`0nAT zU!O_E_T}>O*WO^d_x@$rqiqG6tFcPk@Ql4n3%elm#Qu13!}k=K5$h(Y+XIsNakta- zV}Er#_05c05vD9plqxTEXgg0|iL(c3jQz*bv4qBbbZ&xBf9>CyOn*-At@PF#q1@p? z=U;WYczZgrcnZfZTqM_|Ik=0pRlNt(Wj9}O&Z7xBaqZj%s?R7l*N6m`9>=)14 zGjKI_NevXXV`yqm`0-8Rc2W z?gmZny(gyVXYMR~GsKo&buV4Mf_uENw)yo~kk?HZeDi1xcV9Tr3yH?PEsqxZ$S!XK z;nB+=ow4?vnhS6Er^rk?@7h16LSOht(sT>K?dYAD826?`3PXdncgQ3s4B$Xql1=j+ zrkR(Sx1SIf2GxgS>e=^rLN%9`lK<4L<;YzJSW9~SeDo!|J_rHa1@iV5-Ed~;8E?bT zr#6Z&EN0?d@Z(REZ|XK?9w#0*=h#R(qDk$UpXXYOi)geB4I9`y$GiXf zvra3$6ZK1w`}S+l$r_PEz>HVOQ@e6@F0+Yr&m7yW8{x^^209S*dE6cAM82&9>7vK@ za0^?3Ufo!=znMu}XYA4L*-?0c$DP0M_O6Q6Bg7t}6OmiRglyb7o)>z>i`t_rkIFr# zbER+SY}TsTi}OTn#l`aJ>=&9>iIYL9!1#@XUH$(lO89 z^q!=W_OXrQ&O5tWdnXx3)Ehi)_73M}!OK*DiKR+UUo?(g*cS9(QPuMM8;eQx@Sc+6 zQCnBf#rwA&JjN0eL3SNVF~?vn?bu2?Tc>f>!^X;Hrpdl`vVT(n8;{o zl9;%r2D5dS@XE@C0+H>xb=ma0H*nH#!3*Br(hFirdNt->f$Idp(2*gq<<(gWPGrY- z$eZ53@4oRw*zWyycmhUEfLkRXFV$2!%nPBt=N5xjaQGvc&B@}M|jMv6xJX- zop+9N`1rCG*!NGRJ^eewke`@_O4#exz}q8y-%9o5Pg>14hnyDs_T3Wu0GQ~$V*EQ3z+!)_0Uin;`^CDeC zblsQfH?AHQF4~%BpDVB0Hzep}TXJWa{#-__UHrf(B*})EUm!2h-Qqc-Q}JsNMUH~CXy!yV7~OEyKyL0lpgvO{zu`EuZauEg=L z^18b-$-+J0wPfmo?fw(+4y%Lx^d#qsOU?_qwd!p1#XeNQ8cw^3C<3z0escX%t1<+B zr6`*fG^KqC$zdMb{cJ1HfW8%JId~VMDzqMAZ#60~0(^iwwU9PA_(h6;NtD-{a$TYK zM8*E_@lnMKD;He{UslY05oGhGmHuQd@1A`YVgcErl)mtFb|-KzT|!SGous8!dyqc5 z`!(<(^L(+9>UH_9i!|H+ly?iUBYPk08aiPwi&LZ>e1`8C&sgA@!#b;Ei_~<@(ujc0@;eqG;v*!PmswFHOiB_Zh`!Nz=#Wj!3tLX=?-a zf{>bI`!}>hn)oNIT)LTb33@Wmsu3+>5B3)FR{maCnj;lN6tj6# znYm7BO+`d3vk+?&a{Be!r+g|W$(;C$CGGZ($RIV?+twQ~>kBgi|Bnhe{tLYzp?Ct#2MxAqSnO>AjAzJaIic_j!}N;&u>U zNIYg7CpjGlt-o}gV0R#mYywPx--vtOdUSfNgO-vuoj0>K(Kl5#jW%$pSLa9P zPv`sRGv{IFAV@FFSIk$Aa4+s>Z&s6~Up!^-a-}wauf;u1le~J3y-ft-C+AvdS91`n zKBnR_oZ+Rm*4&dxlmcVZhj!_6y>8rgW+*zp^pg`P9a&Gy26TF6DH}*Z{VY=EC&6^$ zq56Fs+vwYU)n2)!vihvT7kw|!(%6jU@sy4?0o@&!lJi?35)YzD9mrb>f%JK)PDxyX z=3WiMUio}kb*PO_)3q-)C1W#9MBdW1PJm1sZq;Uj@`xdc?h9Si-6j_hGEdYu3%@)s&qE-xo^W^45tI zYiAN7U+6`l=sWL@ul*F%V~ye#QorE8QD$@CNKNJiCklMmq6xw-+v%2fS5BaOI)~YHv8D>aScYl4n>~ z)~C~Jf`^M}(joO|!~EH%GdDizT$tHIh<|(=B5?fIG$i0I6#OdrvSNZ`4pvNFuW>UX ztuJQ>Etbo;w#mo8))9xxM=j3Xqb-hf-}c*lv~PsJRG_3|ixbXp?b^QFt)7R51oTOb z^z;QYSPFf5&5tITBsz1K69p4~}t0-n=G=Q_^MLEO`#f5f^l@H!@*NrZSntt!E z`_RbdWo19e@$tG`8l}PSoqoB2cJ`_Y3%I{3uMOYd3i$x5 zZY&;=J*1KwyQMC~_1O+aU&~KIV?C@vGltWbYhCXK;vok7nNXiv1o|h2FD0bcd2q}( z^B+|9$mtj%0qt}mNQUfo%0e|k7r9>?NKQU2?!;Kfn#M-O+8-5p(8uxrS}=?wv>X#F zci4jlGSxbT|I}~W8V}9X1s4qQeJAS!lnZ#|%*ELPj59F_q97l3QXZUyU!mRk7L~Vt zYzM(&l-=HxF@=U(0p)Y&{M#J`MTx>ho4@LGY|pDoVu4`Mv9=k%{m|7?&Yz`~44cO^ z=A>j8Mr|$BBfPS{k-(Tu8MPIaiwPHsbR70pXLb(eNIxM-YQ-VWn_JST6w51M8ny*C zu2ExNGT8NVmML>FvOzN*c6Ctk#P$D{n!lF@Q^x|nh9f7L#5m;LnmZ09GMAj46>BBU z_0&=fNk)FMsL8g5Ww$J*!LY+J2&hM5w~Eb-e%W#J940x3lkcde@MqVa8LcL9dCZ~F zt0bD};s#tu)he&=?Ab7}`-$(_8%M2CytK=;{thf{cav2n>bUc;dbKp7TDwkG=%ks^ zRLk>M;Z>Ns|6@y>#y-p)>IfGBhz$IxB8BQ%iS`Og0=)sgJL zaw6zw2Mwn5NwDsBG&QmdaB@8?u_S&XTX_UO@9WP7(QzXNrf%;QieTm=(JRr!=-|Q- z+)+cc>BeOg<2F(E;X1aD!q`Lg0D1nyc9~HX`rlk`$Oqv5G#p<0@1<7C+}EFG&DA!( z4p^4jr)jsk%}6Ca)T7$R_K&D;t}PrUu|Jr}4rTQme@={oo#MDe_QX|f+3gXSW0D|p z6AjK4-E%jlPicNug>r{Y@ax|c=XP6nOM34n=L`$i6iOT|!3;*j%;2Br6iFSc5L;9= zZtk&VFH2|Ot&NcK!X5cp|J*`b6mcy}#jcpeJ1~mRyGoORH~O`cX%m(Cu)u^dq?eJ>D5wX~+W^>^~C zOxq#a7MeKyO%|M%E z3wEkh=cm&_E()!M38BuuREm@m5B0MmN}995e0NEdJE`9mj^Y(w_7TAqDI`bQB;}MY z3HtL*3vX7NCmPLbWMc~&{VunW@a?49O&~*M^52WT7DZxT;2yvBNtSUMUF)Yyu4CCc zL!D7yI?Ya#xZ>fq9~mx}uGH6OD7)Hvxp$PrBP1SlKRccG_;|;(>q%IWs-GS(a@j@8 zHn=(VNWi9KZ{sP$j`Ncj+>~wj?^6J#VYt$|r1EkkKW0U!Y9t$^IIXzW+`jmc!Y@_Y zjL9fZmmbdG+xX67lwyANC0AK_BgeX|nvxTUJ(9#^f6f23tw`TUz1S!^qbZs2XRHGQ ztxXMAdKR{-BC~e2wH?jeiL|US(zPRoY!txr`#2+S^x4u_ThfuJ_H`h1ZeW~xBgL#d}Hzuk9Ih8weq;p(yypg<7 z2b^r87kO*p^ZksQmI*c4S*S73x~nl+jxZ+-@)uU~U)4%3Z!+=>!Y7;*_8L1RiOiE! zZM|14!JXSr%>E?nY0BMRX9NcBy~8x$kA~cZ^Y5;yN{29<_K&qFzP<46Wu^bDh*)N} zwV6H44B<(^SH54b`lMF}!41g%{?S?Li$+jA0d3=UxvQrL@kqo0#DLN0dNL~FHY`Hg z2IUamT+VJsRT~Un5<}=DxxqxhT4V%)8ZxiJ48V^@$H|arq4>38dNmqhicZW znx8T`8hee#(xvJ!BD4o1Sq{q8oKXpQ-V0i#Pswh*nJE)jM~|Fu@e;f6PZAGPDm`7h z0yrceyz6QZo`|bWL@Q$wko-Q?G^(jI8+Y6*SoZVUd1HDP>75BWClZ-c9!bCM{AxBP zv9S|fo_D(cG_~CWSs{keaKy2_tYfL?V&-{aw7a&oMPu%gRzRuFxyrUCgOtm}29F8M zkr;p2Z1dYycO5X}7hf-a|8B0|9_2Jnb^md!6^=ME8G2; zDH^FfLMA2|#R@`s{4OeqGX@?(Pv*?@j=wRcXq4d%C^$>NYhQ3OYDFtk_P}m7URSDC z@;n(EUlwc-E%h3hb0(cTVJwn=uk38iHRcNb#l|txR`0pJJd&GRYcVB%X<3wb$Xt#) z1RQ)$4EA1LuQj?JjY?b7bCZFs-EzSr$XlHK^5d2(V(A(H=QS>S5#;_^p*LW%Y( z?_+sZHRv{+pESzaJhjGjI(7D$Buj?xPh-;$EP)iqZ6|KMJAws3m)Vxc6b zr2=p-pZ`pUS_Rs9jrTSf>y_UXHWL&k|Xt-NTCLY^XCImF2U9a0# zn(0`Q7@;ZEz8xqGuudIaLsjY390_8znzTYUc=idn6Pt+wH=hj|$$s9XxBJqwm?TY1 zz1>>cG`oC{inAP-G?ajF7P8oLAFe~PE|RXYRl4r6d91g-cGlCgaNFV**gkrt!hE!6 zB=@^La47+MzDR(q!m$>7_(S`(D5;?jv%kQ;4|ubimcV!sUNO;Re>Z>lINMr48G(VYVVUM=q%mCS4s=IQ-0w5u(s*QJ5cwMq`z`o=z^!uT)E=( zzWKv6g4WAayw6lx*N=gv)A@wC5N*$;TtEq^HJs*X1A&eq`_b zy8V{ErAS^4W!DOL`aJd16;;J8@IsT(NIt&`PyIW=S8)Asg3b4p@gIT5P4Oz@s_jvF zMuf}D(QB;a+P>nhV?D<_Yt_KQKHH+rq|+2B`U&ObnaFz%8tv*tNB4-JV}S`>CI8N|bW$ys|lSP!cH857>PcSO--CvI1*XCuMaM~_be6{gR>qu3+J z9)U^RYBRBt<&)r4kL_*I!W}%ayP@po!)-6Ex3nm8wge83bW@KBP_xA+o#ZC%+6(kE zo+vrF^-RY*^l{#m(j#+v4SC*?AotdNV;az^*6MwBEl{wts-~g2qmHL_ihtR~-ot+H zVQD<9JujnnAtNpcKx|b}Tesjj$G?{3JRKupM^SXCJf2VgWJkW=Vx^({Q){J_LgP~L zYQ&D#y{2?!yzaU>5q#q=Fu9vMYOX#xZC>;HiSf@`ZB0%=!bqIaSDnYjZkuUkOc z$V(dI#7!{orSOD}VbrU-xdR3Bn;!ihhx&B6mTN&|eZ$+7Etwp3ucSuhlFYOm+)wc& z5w9gFxSbFtcn_~{S6Egr^72L&#vc7FREgGa7i34IbIv&SW}NoLF-* z@yjL;<`FUtm7}*Q7M(-?xJWfC$#Ai%u`cbLZX3?OykhG@*tRo={2c@Ezet4qgMOWr z{hd0S?VU=Si=Bz(T`_=_lY@!nFB`!5m;FyJ?SG}x{u?H30Ly<*r2U@`82{75f2Gp? zSKjQu)B^r3%51i8wEJuv4YTa*Y@7=3vL^HF?{})q!ZF4EorU8Et<>)D_U9coMHUEQ zgp~9!9Z%r5AYWmgZ}QwK zI`{e2lZdC`E@qWcl_L z{*Z1TX$J~{1;U>kv*+(%`9B3L*x%FUoj&^=2TmryJ3Bl(@IAf$vN-{N+5aXm|2OIM zPX~EVA4nK^bg*7uMBu+FhJRmAY=FO}8XyyZ3qTG8 zyw5!EQw|r9B{rJ027ZY{+~|2BPfu7hu%+jv5108=WQQifHZPEQ?U1VJTURjzER4{588Qdv07 z{|ANG}OH>sfBuUXc-$zGL z@8c$$58Y3Y8?Xlx_#;q65WXN_h>m*r2Q(Bkc!&%g)f_!ldhpO}!(#kS5<0yQsh|A%~VMC zId+^sC6Tk4X`c)tpxn^=bmskh>!t2C?S&Lxm(Czi@)X8f{NXO%t22T4gH>FfTx+)G zdIK4mC85-BeNBlU1UQn`LEm{4)eM8QU5e}yh!9C;LV`GDKUEdNbRnim$qg@omY`C0 z9K7wHKofDS4?FufWyk$+7Dn5kj1#?-^$#Y=%FJ99hi>!Iu}4Qo_v(GS&un_*%{y00 z`~=n<8E#n;rbUzoImQWE50a@qguA90pIAJgCc>xJPKP#Iou*eUw9#f-K-mO)=qGcd z#>`wm#dFk}T7F7bR_|)oihUy9l{totwLkeK)&sBo0|*;YC0oqv1}wv6UfNF+j`5LU zBc|5J5;s?6#7S3cohJ8pd=8zjFT6UM+H>uu+*$eCg>2!C#Y>CL3&|^p6?(?JDc$JZ z%S%#np2}Iggc{|*0Bk&>b7`9|W#nM|S?1Vo&}LSef6Jf%IC{J0Rzp=`ebmDaZbWwc z+j2&aqVKFPinWoKl=z>f%OS)v1NFGT^|A0&TdEwk>J7qYJkE5~y0DdM|2Y+S>pvEx zIad&EoLBsQx8+@3&2KxP-O-6{$p;lyQb6JD%)try&`6n7Wa^>dbd)1EWX-ZNEC@aTAN9rtn>Fd`>tRl4`V4{8hwSo=(a?GL3Xj`OWc27amo62zu9>Ab zp+*ew0Cpq~(B44q;v1(BU^Mqt(TY-eRlJ$ywZ}5i*d1(nOlf6*YiH9Cc`kEs6QDxU zV=R)#F`@-?<4RRN8FTYF3Rus*Fnu)|F^{SJCURd%#ixEMRKf<*A0+>^RlO6<+cR2B z7> z%r+=Hio^Ys!eL4Rs{23)mlkXh#bC#?hHv6~3tm`KI%}$iP^%|Yd#`8tBKb97ir>)A zC6RcvT>{%SYvC&Q_DU%WLpliIF_^cL_-H#G80b}=;kPhr z4@vKi#8;oZrh+&1cP6xV3ha2f&?~zn+}ZhK8rAFcKfc3*0Hr?lC}wM!0HVc`cM6B z&x(fq^CjEI1qq-usmsIrB!GKm7AGy0r_N8W)S?+eBKo%HkLJr7a(2o7(l|G=*N@}- zo9MFR!;LdgJg&K^yz2~c_%kpzNUBRPyT5ow{g9Hiu;I+Y&Lpd)gBJ3yyM5M z_GKT>F1Eh*4mK^ivX+i;Ym4_fnpJ;t!RFl{ykC`yz1!c?{O*=xNsD5QB(|Z9g1MJ& zV~!9zV2&jf`AZHDvL0{+o`4J*bK*B?8G$mfuWe3C>ivLZTw4u7M$C~778Ye)Yp#;j z!Kz=>;c+KH5Uoo<-Z95y{T=IX>!yZ5qTJr7@?J}4VS?Ze%3%_o5z;+3g5FYAPzvX_ z$nk}y?=f8xBPxYQor(J-@T=r*Oy8$%NDq}X@kEXplsRb#5$Gxr-yw2|_OO%GPnsV) zf}s|&8w_UH-?X7`Qb%i)aqeN{^U`VR7l8&_X_+X!>6x7Dty2!L(J7D-WEIAYD&V5B zf28F30S8td{*n%zrzpHf5*|vKLs&@?Fg(cXu*ZhjT$}bYf<$c zc4e>{13Y7DND?U#Y~57gyBAu!wVFZujDkelL-zJiPJD7B^+0^Oct4L{14xeM-`|W> zOMjN_8mJ=QG_r@#j1gMUm=6X^>H`A=e~8nmQ<3GWI#rxpS*8Tc> zK0wm4D;7Z9r1_R7bt^~tS-#P9C6ZVv&^TH}MCztvH#1VuEU^sZlbCHP^ebFZFMbX@ zMx9iu?pCSB-9kDX&VC)+;2N*+^(7c1UK6KO<6bo{Li$1lzS?L^S*6f`)tD+M-->WxEVN#KJg+96b?9UX7 z?waHg5RSIRd)uly%*JtIL?D=fqJ7rx8hBPWav_CgtEh-WC3hCq*5L=glHYUr6dP(4 zxJ+`Jovd>6s)QslXNDO}hG`=tSk%aG6{qM!DdUh-WHvvh5LeZ3NQ_`VnLyctJ6kf>j zJ0g`fh3m5u^rB`CJ2n?;q#V^?@zNd3?9lHEM4h3yzEqUrs6ve6F$x!=5|Dct8j1o3 zrK?CDkqmu%lM##QpbI(C4s_2EPz(GFl!PkEF7jw#<9LJbrZJ#9=!v~(`pCzs{i&2U zlD<||53IZor62JtB_Tj4+MWfHO7}xuS%#m$n?Z)hcnmw zeD!6aleFWI=Mg=kzmq8)c+<4BAX_v4Txjl9J^m=ZX8p49hKowu8r_4NNDgc1=};K9 zIC|k;^9LBCX`9=w6ZLqe-0+l4&j@Rx*Vz+Cnr^_DZMB=MaU_2T>1}5j-#3VYQ6Axk zPtDpA@N|0;{O`4> zOS)E!kX0}F2eCT=Sit$l;^fyUi&$CQ@{$I;E*4P+KMY&kc29npRWUsDKiy=A0xu74 z&3knvukB2)amqLfXbGevwQfJySAU#dXA55*Ro2g~YyEk=Dy7$1|22!7P5ZicI_n*- zurkHAo%AJYi%%)ZdNu>%9GlW9zPFQ-f2fc6u2>v$z$Xoj)+eM49=nTY9s`y|VjdBe zC37wtLcCV}{d8m=54%gQOai+rvJ4HD<%EzONmqR_52ElFG?FfQSpXEiegv&gb-CY5 zIT&uCrLp+AR(aU`sKaq2qfo|sT_LeY!7T3yGCj6katnaR3aK7~ytr{u52Aqe4JVusjiiOHN^ zZgA34^lrns6Ra*-Jqw#Ir2A@ZaaDel{U=bZxDz?$(o9FgUXk>U5Y3e2#C$$-QlhDC zyJ0V~-#xQL^pOHWgK`aKrk{u$`HAZGgCUd&Nc=G*Sb~>w4N1zdz=;Zgp&|&F&KrYY zvSHup)^vd#?-7bczhNnENA|Lr;m!W=aMydL`#}JXmjm(_N)4pp$$|~pPS>YJ!~)^K zd2~RRDILZSJ*4;;CzdW46JO9EMiXgzlLYr@p!+5lF&_CF(GNdNsA?N5b#DaHU;gJ3 z_w@POkqUaj_(Oo@vm7|hgI+g$pQwH_TjcN{ogVfocnBRtY5AmKAH|Eyn-XJAc z780W#HWN`72vUi5;|+90J$W;NWjD*lTU;68v%AgTI)MvQ^V}41{eALTMp`l6rls|s z_(3k$1nm;VJu|c2>Mlr}K^ssZLW^I2s!P7aP+3yoe?&H{%JvS+Mm_Z-T?{R9fpO~c z0?K}tq<0PSki#+o@Kenb9fdox1_cUE8kz4;it{B;tx((0m-Qx~cllSLs=RlZXo~=m zdksI)(ctX3)kK5kPd~}Wc>h4=mtn55Wr)FHLBFa&JqGeJhxA@iu27zt-7NEjQ+FqW z^go%Or=!|XI&={>L38QW-+f^g{*c#*F4XTrD(r$I;3xkujgVA5Qg1Hs7=wmn_* z#fc?>TO{|ec8~z}mt^|;0Gyta9udFK9^~mb9hejp!AL!dQ#7ZTnx>vhB0e)wMA-5V_ zr(WD5mIYHAW5p*XW%<-DYKx$-t5IZ10gZ4y_}wi^_!2Q#Xy z&VD$T0Vw#n1w9Z$g|64p7iyS+NO`^qHEeVkzqO9B9A>DgW97R+k=s}HC|Tvdu1(vp znhvgWQIwTEf6AZKn6k$KNgCC5u3ifpLMQCaB6_3QJ~w z1f8ziLDx-M;`|^zj5R{9m!SvDtaje>LNydTQz!v>rZx#4i-y4A(4(^5p9R#l6&bd& z7uTj@3dqO|53YhkV?&v_nwE*R6{&o5?3DbL`iBAW7ekWo-(3(dY^zygr^u1QF==gR z>$TO!E=q(W*^K2r7U7Vwc_gv6m4ufqSM!t{&&};rr*h3|di^S7EHNdTqx!UjG~WZY zlrwn9>m-I2SAYx-wyviqRpZawzH-nUoC<-V8a2}V$UCimQX|z?BT{~1feLJM0yAUd zY2?jvfN*iY&~l+o0sCe{q$Gl6%E)IM$^GpE{kHK*y(#M&qVLXskQBj-iGg0^K z1*cV_qy$UqkOLj=ZbaU2x(%pQnClhA?K|LC1g3Jq_(!{Pv&-?2$V}DR2Y4nVbK{54 zo{vsT!D(hGKN++H0!g>EJ$!O{h0z?j7*JvKL4)Uzm1uPN#zKSQZkp1o(4U^|j!kXu zz6+bT&1Ftv$2Y^D`_C2H|DjD%U@Pj9tkQid&JSaQw}4n}QJf^;IYGyobe*Oqh4fAq zcm0$RRQ|yQt)S^EY-AYa6O&wm+#j{}Hpf5dLLM_=JhI&|V)qHK=goy$ZUn+?mvs7Y zFgA|$oDG#d){keFIe|MR*r{RBBKFxC`CD9#2iqQ=WwRg(f$7|i+_Ey3jOE5Sd05Z5 zE1OjPFy6!%PJRIQNb1O+kcLYoS2c;dLSvHN4(3K+DM&rN9?>1+RxZC;`*f|X>=u06 zaND|ahG8UR_W7+hhH9y2CvG3FpfA5~6e)1%qsDL6`&d?&E_@4CxU}$Y>H*zyR^SYN zx*5uQGq8n$O(?6FRHDo@kWM=^fog~x5i{Y76r#C=B`#?kwtt8`oNtt0WEw5mZ&J2w z(cF8w%1C3mLkA_SBAx?y))hU>FeU2GbE2~EgR*&SA+^m1pp6FK!<%c;U zK|P&x)2MB)99G`_=HWB}F&i`R&Fc)`{!W_>0P&E2A?S<}C*JW>piy<D@&Nw;0=ZfHXw;H!OE)m3CB ziiv++D>HZL;#T!}Q6uaZlWvI=z42j76@qZD=V44(L4Q^M1LJU`K73d~UkAEMs<9u& zkHb9Hi|WQa#u>(h4Z;PEi^EUXrVCUlaW!L-d$^EH!xrbcvH{5C$o_7oy-(LH?zmfB zd=TM$#zc4q0pTzrZXP{&Mjhn^rvZPc6vuhlJqaaQX;t<$c@6eB`+r}bPu>Ov1(L1_ zh?eGFMSCe>zeV}(ccSFpd0mf1W413y!a21a>8;+aUkW~c>Y96`J-<(A zRLA#|jFUoBaeqh4N!orL&?<@WhSqk&`S5p3|9?-nmFvH5nB-Z}{8>=IS4eNv7ptPRlvd-nnO+Hp2|eB54pm$I7C9w%FqhJ^{00?{!qoDT*(~yHcYv+ZuUoM!traSGuP%p{_mubR57j`aN z34MLTxfU5wE^jtH_j2yNu~uK|sA60YMA~otQMzn(Sk;F!L239wchXyArKG>vRRj~M zI?wYawfj|uXML)^DetgW=O7z>JJ9V^JD_Q35Pn0CRMaY)WUIl6RboubU~g6 zRCGoIHtx}6Vv}DTP|f_8(x!qI!=>@*h{htv1)%aKvMVLi+s4nU-V3RM_ww8)v3w%# zrqr&{%eryygIolO&$c>k$hy`DC)Mj9?+=4~+Kne69Z-@sF!gDi-p}}U^VC=xQBQt7 z-Q_7XK?1hp?s1HmlSXHa7}d;g<(C*ff03=~T|K~Q0>P#SaN59s>MGM~KPIej*id%q z5j(JTKx#*Rx`*C)Av@5}gaFa(z#A$!$vNrVDlCR!K1|whppJ>1n2@w!ydV@metsl) zBzny(*4^bjHV4C<;Mi_awNf)t*F3(D=<6pF0;6k+K zJ997c{PFvT7>8IvcDg;;J%fhW?V33oYNE>?Pk$3MbvW#MNZltdmo?7=&TF`aEd(Y% z8aJ}>x4>|6gH4y%Hj&pZ3!{c=yk=f@V>TV5j)_|e(k2VO4LwFTw5oFz3xEEd?V}5` z=k=eS zxl`(LOuJ~e;qyb^P&qfhM}0PXzof7n?d4y+mF@NiDya0dc{&Lkt#zwn6teLhH<4S#1bbNg5 zw&;zIlcT?Aihg{FvC;kR4?f)mraf{^xHIAIk@HhBbfohJxsAxjZ{@gAAgs+~zoKqd zaxWq3)%+UbG)Lw5qQh`gdjqL zi9dahC09DnHyCtbOOOdNpfe51^LL1G{E}n*RghbJ&3IHMnNEK>S#A$w4>Vdj>OShs zpohE!qtQEVlvLALdscN;LsqqJFHn)GF>tb_t<;3G8>$w1gu{yikCR2ubcxgvqtmtC z(+M(8(3?~i4B8h6uXv`$KHHc}|J5Fn4%ckMLZXSD35%}~oiZx5r@f~F#`MU%*p9Mc zu1!~q@r)&q==iyOVPOXak58sD#vtUJ$FL?Z@DXn?OO-xTvZIUjR=^{^j@=;GZ!O@7 z)BLCPzJNsVGp`F6T;NS28ZW50L@OwjTm((PkjG;CHZ+G_ z=pPX~gT13>5Vk!>*S4n%BYzaGgZ`T|8t^?lOxCR^4kLOKSTALa2?c#pcIx*5Hx)n_787ir2NUX*c1 zRuQ%4f{s}BKu$3<5*pn~NU{cNHTGm6?8})vk-SrB$=MNA``^j;BaUGv+jwgh zda;<`i8u)?wunqzZ}m9f-cMuP-EWyV1fN8mNYz7)4DWnjwUSOm5{iS&Kh-t^HPCo& zQJ>kXBszPYeb+L51fV>z2hgsKfnF$2Wc}^IZcFQar)6Vn884_cPy$dsIk3876S+zU z%s!2Xqmqh=&4I+jkp?_n!WrT-H4}!Osu77lRs8lm5l?oxC;3+gM^0eZ+QYPjW2@FZ z40{prGe9TeUkUlm3l(|I499{GOE!By(+`hGNymn4<@UG;c`*Y(OlJ-J3~)=skfuJ?!=)0M6dZR)iYy8s zjiBmRRtktl|A0@EYW6cYQW)P*ZSlv(s<76GH%p&a6r5)a9TWRS$7iHxBZLGiQG{aa zibzPg{;2>zZfketK53>%$oT;sC-br+0UMccW>$McH^0LYZv@q075{$zoiO}4le7xz7+`L>P?~P>%~5oOFEW4 zgIx9g-kD87F;(jhHn)xS9I~L^>FfKQlHYmtOi|$>!I(qqA-!R+q19d4SbS+tZpq7O zt6};jykfee;6z}Dlz*WgcG47tXBtk6#lunW!r`zCe$5!pN-Y9w^t@Dv*cOic^|>rJ2AL7akX^U*0+aL=E$ z6!7o7OIN0yCM~@d8&=5tzbMTsfq~@_>>rmBS|{YnX0<5(zdR|OJx|Y)tIMYiK2xeX zeszDuRsE~Br|o56!)$~!%$(cXtG;S(94M>a-f=X)*WK8Sm=0hQv zYdYo!{Vb0XI!Xs=$4@(8^FS*4h*DnfJ|-d5Ezh#@fZSi0#z-Dh4vk^7Fimn}!q6Cj zF*Y|Q$z$|KHN}{jO;_@OELhD&vQq$2tC*=ra|HDLntz=Z*U=H(9P~9<)gOZI zO^^Wb*=&ez!w`>f(5L5^sQU*k1u`HsbNQ;ptNqt3zR^FueO22H^ePPczy6M)nao%H zOo6Yq&dQuct;`o_H}fev4ZtD+Mg(Zp0(3w-3&3ShGN1I<*Oy9ZsN=XoCXUmpVc2L zETcN&-c|fk8g4zZ3k6;{PG3E*A z(|Y${|1qfP{G)^8k3ioB_0GZiVHkHqy+h6(_?U+V`|Fss@_B0p>pjfsKCe%x^TNCz zAPK-g>p^He2&j9Ias-qZyyTVgoen`=3iYa74d~j~CkAC2ZS1!@OZNauHv;N60_HXX z<~AZ0rfwNpb4zYb5pxTyxdqnT0&8votR$J$aKF`*JE0T+`~VE#eKp{nj;EkJ32;gt zzXQtc0DEYUxe@S~54+mRT;CV-0h2B5uaZh@Uxc6l!)2HBJM&7nUGAyWlo6rU57q{{ zcCkFWxL>8Eql^1pdAS-^t*Fy83(?I01X-XA0h9xb1z?$leW8T!ICBkJr9_h6w~1_G zHnE#HHaQlXPB5jYQHg*hO-vE0fd%=#`89Z0tExlQtzra~UzJozszw#p4)NtShVd~8 zrk0t?%xAcC_GBMlT?UmjUQu1PU9(4%(wx+s(r_uoNyRC}8O3=8=T{^ZlA=-3s_0O3 zE4C~4C{)`O+j-Ke>Ckj*7(wILBsG$zQN#Ioyr*t6vw&&}Cl9E4uolaxX%!hI}z-a&m`JhYy)B>~vu;d_=KJq+_J`bbM zqp1M%0XR70d8%HZ^bO9@@!j$qZP9sh)jR_aexR?qY--&kP`UE~b^|bQ?E^4>Kwh0| z9hApYP@a*;rw*>)Bai!_96E;q1vgh#b8|ub=7RE{i{=A#0C4CXW)2vKIrPd<`T#lr z4g#>uTsY=1bI9{>93TgnLP=L*^&y)L?2O5%6zT+`1s>4heR8=|F1N~Mtz3pA{UqHt zlXNdn(%m^p7lj6iK_IEa56Yz|Y3g(@)ajGJDza5Xy;k&am9Qb9}nrCeSkmxYo= z)O{!F{x0hNChC4D>N-W;NRgiB0om0Ni(G1G36VZ6C&^_*()e_*`*d@Bx?w(DoeuB9 zaCbCXF1>Q;rX~LR1;bbaQoV@3Mq}YhxUVMP2d;r!;cQ<`9aNdVn(R){| z5BOff-(lIL;cxmvXMJ^6d;w2nY5(to_22MBh&g@uJk(2}ej3%_Fw{@=)!a(gKMChQ z3jH6UfRe6%0-|YJ@5U45@gEMJ_jq670=VmAeTD1cu8*QZ`K}N56`qCB2l@)PLUn&% z;Yz5s^@ZsMukWiV@YNadQWPR|?LrhLbSIMs?>GUjyb|j1xs&Vr3h7y6=?&6&Okc1B zswmy%OE`!c<=grCg7QP=p`d&T4+_egcB8Od>#;%JybcBATGN&zcwM zgQI_7L*Fjnf4>CJHygTt#1s4W``$f9(beaBr!bAfM|}Ske9`w>D2-?L`A!z5l`wIl zFir4b-yuM93f2;Q#CNc8sqguqJi9jtvq0+IHATM1f^&V3grVQ(yS4Box(&1no_ID) zHy4ibO|IGR8y8Mvn3ihbW|G=h9qjT|!SL`jp4h+NR}xCott8tQ zj*|++Mv7DS z&Z3Y#Lj_AqR&EP~mO1Ya5v(L2J1H~MKpJL@#wk++YM41XH7wqc=CjjN!{ZGpW#hF? zhcLdk8G0$Q1w7iBO~9aZXtSHf!N(BBiOu)8X?@G)dzzbZL+a#0)Ud#x`ep_^hZ_8O zE;!nOZ0l+rwWd+Vs&QlgL(w`|#xMUI@hg9hywr|{8BM9@@|shnw2{qgZb*%%5%e+A zN!shj9wQyJYHm7)H;~Ty>2w@#7~6bl848dNScYn7MVIxX09}RxxL;m2SzZQg8mJ$8 zC=kdkdKFKki-3Jzl@~3|T`L5)fNM3<3YHNs3dz?B5ieZ^{E)kt;mV6?5jMydGiZ@~ zF%MmIC>(|*g<-nn(6BHpI20ZxPw&4x9SrBT(~QFMeZsg|z7NKi*TiybfC~oK5GAaM z|Nr@0Jo^8`V%*>M@uGz^kI@>eUkuQix@+AsN2+^)-+yS)$AbxuNb7=y%V^!UIQ4OG z@z~U&;8_2mwuS#;dLf-|3ywX67S_*fInjcx9KdefMOtM0Y+(!IuP z`VSY|L@zdm-s|ax|1jM^r=OGmEls>Rr45cw=>X_dQC(3TodW5kQ+k@I8k}wK048x`qfp-FAexNQ%*VL6dpzf3*XcX)LbK6sF^ZQgKD8IT!f+|DH=s%7z*rR(M3OV@}k6{?~$K93Fn{9p2e?o zE07jB(6SfFbI6HQ5M8J$hnFKssw-F0Si|BK%hj%L^-C*}R8Ud=3_1$?O*0dZ>Uc&c znKjU^kaP&CSwXUut0neZr|`|W3+Ifcs>C@|d#+aatR#s$WxT}kxUo3ML@LTF%Svrl z3(wF}5e(1~ye!hB7?ViUu{RXg)fEHarA$GEt9J6_21op2vaW~@7u5m2&}Jq`UV@$L zAZ~s9XxjM;=RdWKHl6*pKNv2T>!M^iPG>*whb^OY_7_qfG@K47T*IOGrxx>CyOyXu zo8iH9D2-?K^NdRmb)SVn@JKJ{bZS;lpUviS*^TN|>`(2hkP#aG?ra%UfL)l0Uh%Zzm5)xON*fto9IrB0WflyxRTLFgyDFI=4y|`O zYpbhEW-ZM84#zeWO4TDuqW5M#q|Y7Bo@IUq&kfN#zD}B_Vvzu6x-mWkjctLOX z>KqEAmKPj4pW3Z3YIwn|=2S>wRH_g!ICTzUH8U2w-e%VmYaU~9=v>6=R(srP#)6TG zSXst`RBE?7%|s;?QdtmE>FsvfgKJtG4xQCzb2?ovj0qjIGZwQsFVEv)S>`BjG?~0! zpO51dbgqaO3?g!{drwatE0m~9BHDH?CKu3 zs>e+u6kY-0Ai-pwzv(H;-kmiyLXB|Y+*#r5H!WueE?l5p%IdVbvXqa3%gqb_X|#tc zj&Brx)>G`DmEqrhz_2ZLbFiY!ELMo-GKNB#H3%RwL9@u1MYFiNW&exyS%l5k&bSs& zYnw6uxntw9ne#2vuFX6@{{}o`LT0}Q|9m>0(};hbsiM%Bd3JiPMVxpJ$a+Ke6YhJU zW(gXJCgNr(f*Q^=5TOAp^eVT;Z_~T|&N{{Tf_68=oMr8!H@FpSDQ=+Qqs3NEYpGCg zD$ke7qRECa(FJ^~r!~K|bn(cJ(lsML@@uuaxLGl(v@YMH)sX^)B8?|Yfl(gIs8Jq< zEi5idCU{)tDNq!~N15wXs*;_E>;%6%%}je?__!dWN|Q~JMi~E&%_e9{sMOnm*GvC?n z+~nNm-0eK*JmGxDdD{6`r`qY91TM_&{o>5IAW+v_Xqj^ELJR%ZB@A3szj*9tQ-I?1 z-!}t;#6BmSt37u?u1)qT4A7&ONr^!S;hzk`8W1dK$Je)7 z!r`Y@O@DULEkEzv`|6L+CjviMu<^zn_oX-Wro6WN%x$;a+&p?tbKvbemW;eTgciD`FiSB#uY`*@6S*Z={|90!L%_H~E9e2;t>-Wt6<4eEVUL10Bnh`tZ zO}K7-b;o_2iXW9yuYDS52$!t0r%7(LzT3OBQbX06ma4V1<`f0uU7)VaSE{F7mG<(Yq7kx6 zirpLsEtCx3N3{@y0GUbT7VchTM%7Yqhw&LBxl_BZhuO{xEnw--wx9&njV-d0l>ZH!ZONfJYy>rCEHjE$(MaqW zJ^s28U&v_h!GE8Q|CTXK&*&&sZ^-P&JJBzY9nFxU&7|4>GaI9_w>nQb85Kr6%Nmp> zbi^cSHEgxP>a%uR8EYCBK%8rsZy*Mz<8eSTcpxoP23n}$KWnN2nY33?Oyf>-MWrlS z!2mxfc{!20VQHs|=QUxIrKGx{a`e(|nf--jjEXFmHoai@F#%h{>powGn^G-TSZE+;h}Bn3VmT zIm#_V0t%tyeQipA8Y}uZ&Ppqt&XvXn$)s|jh!i1Gq&2c9awft?jC5E(AGM=RXdBuC z4!JXY9D4z^gVY2?(bDbHotVL>x}L zlPE;ixB&SSt_9elhn7tXtq}Gvz-}cJCc(M@In_|eeiAJTWGE=O)m(0(3fXQmS_oh` z8W|?oWz}72q`FyVAu{RiHFH}ZzvZzlzh3a_t*d@kU)5Q;##@{WRpnQYt(Z_wc72Yg zPOsbjTIS$anIjK<`pQ2upC5Xtt?K};`uwrgNpa+i%;P|#^B`~vz?cm^ELkK+t7DJj zjDtlE$wAhkI}y>>nelR52VV9b6aaslww2Hh0)f9p2D}{EUB|SD6h>BoNrPLAz z9moHKFkCPBSJBZwP%@hHownnC|kqS%|`p3D8_#g4O zEV`X7=5U$OVzb$;V#O#@K@mWo|2&=~nrr4|Nb7K$nh(3equHN7@qJI%a4!jmNnXhY z^6`TOey@-6VIe%@et1SNUMAhfJ2X{xhi7EDLxRFd3xn5Y%jawOiTty?Lh@hB&Q)G( zpX*qmTw`2gdR+5E{UgQ$ngjYbxHs&-aC~V0&~e899s8Z#YQ=f1lXF|0Hm5z$!K>^V zhbFJwIo`R|zRk}&oP^k2POVd+W1NIjIPBD_m|1WWmPsm=WYN}ktFS7~lu25Fb8T~C z>KG8`ai$CqcTYbiT5lTP1CfQ|k5kR_&F$t*X4ag>ykw^DQC-L{`Mdp0tACH5_?<7} z??BFUSduLBNjup@wviL$9derdl_-hRcO2h)nV-+r#eII`479e6zl$N8mR8O#d?);&inD#d2W| zBUT`k^C0+O<)BGb@PrpjD=UXF`{!RggWIrw*A0tyN5al`9^3o-2Ez3oev{^)S#{BWU!M$rpE%GYy~k%+z0n5S55YW6PoZsIm6 zHs#&T-jl~ykP2}YGs{0mT;W;At@rehURST@NoJpFPw-68fP&Z{7)@rY&91b7CSuaq zBN_c7s36uay4)Uycd#6c?(X;dMe}jMtb;L2TAdF65s^QNB3P;8cocHu@rS$lJ@8Cv z{0~Vj1+f%t4U(WOjlVl0kUb(6=|v=!UlR5RL~sU<K)Qh8pICu7v zXLpW9QXmBs>pjZiI0xzu(M}HWlCBGPkuLvj_%?EzUjY=LhzGonnb#y~R#xZyYsRX_HqW_Z+UoTi+KYp(Xrf`t+C#hU zTJ;iUxyjERiSF8xUU8&5I&4O%CoYKPhc?~(o9ZH-7^uc=0$Lmb*>a#5x+oQ_RjpIs zsJ~71Vfc$MgfDm_vw_`U+iYiRlraUz1f4Obg7MD>)1Jm-j`+bnR+;iVUJp?i!@5XV65J$LtW^ZIe#5j-K& zG%&VjM(Zb|6Ho2j|1Uqa`e3!UGbymzb~O4B45^}2!HFX6la|nblb2iLZwS+qt=_oQy_U) zBJ=fpzGO;GsAs{PMK#4@LH(;w|5w|Dx@+&NvkqbxOF!8AK< z{`jV*6%!v__SDu3bqU$Kk~2U8LR<>)?Lj``I7DRSV8T8x@#Z1mVwC5@z{{4GnLi>s z01u#M{wUd%#N%ZQN{=lM`8senCKxLXM2Ql$R7T!D^>!jb@gST#_m{75BKH$E_WbN; z0zgU9ExDC?gFz>#y((X$sIVH$g3IV~yFHFPg-Bn~hbw5+pKL0Z>v*wT_vPn?{gK?T z%bOdv%fo$Exso0h%;h?R1}S(Ya6^@gRwGVeN}o9)r}8hV5t z(^l^m-(!Zyghz~zd5;;62|sZi^S){LrSOZqUwVIM_(1s5@VW50_dCP4!gqP!c?(sB z1~>76mH;H7Jg?WI(yQGno5$|9DGBdZT8$RB^(L=D@C#m#CtwsT#ttK+EihRblylafOb4Pl-g`1`!Thy|Hhvz%q4@b828 zgQ}Jp17A$5irw(pSNQe!r$&7VJ{&eUk`J)+zTdfJ8W#?;#bV)ntRta;k7>R(fDro` zSYt1Wqv2$ul$?v^dbgr2-mPVixPBNt;5rcf!u99qpAy>PXhU>;*`uY8lP%b(4_J?xXC#;x`P-6BvjABb3?7w9o5~{d#aVL zq^qt3 zY}_}zu!K$s^%W+G-NO7ZI?EJrN%vls%WccpNl!fYn93bo*tL#fk2830~3 z2xOMQPrHU)Rn;%ycaezNumj2Ir#SfI=YV&>AB?wj()SD{Owku^xjNSz7iwxKhjyKl z8LUfoS}4ehbx@lcb9$8e@1RNR66L{|!;5*B+vz3>MI;m^;j&1~5h=q7zN8EXy^%7e z9G8?aQFneBPIAR%D4Z84L*7!Rq6{p$P!q3-U-5lK5aQ~ zCn#2wl@6iuf1)`jr(lhx4WObY3ltmS&l{^ld_+12GG?ZOr+5&ZvxY7Ny4|o@QC+G!;_{X zhNC8p(hIu>hv{bPO}2ZOUfU0u9j*h+i;POk=vk6CfoW#AgiExhlv~G<0#EE z9PvNO#XJm7lhcQd@sxmtG*fqYn{Ky`=+aC=N?26n0L0z6R5)JdpMyAL_HaxLFIp(a2=SIKIqLnUs}o$PPHHndFSK0vtoGbj7eM8l1AF?0OkTg$ zt!1ay>n!1 z=DpMD%-7pH_gN0_``7V{Z{nFRPOzEXNopk_PUUEXEnX5+DtvAv26&%aCkE}J(-+h0b;KF7*^tsx zGu4EVNig|MNt0yaOzG?&j!+z$CI+c2j;gJIc)%aT9YGp%FhS8SOQT(uM!Sq|J4EqX z0i;nO4=P-AP_Zk}Hb?_1ivw~Au54HQ8<{AVMXqW{#5^vq!779;5wF2B8@sGfbe3H$7=G4d6?lah1*{{`l^_?vve_Y3qDmF3Ql}Ek&iL{8>+uhnU&p^-zfgbAey>(Rw4&I=Z&G%1-HL7eHl>nRs|p}a z)N0c>BI%TFKF{a2ivfiQ{v?d#b8dxR*0f%qJ0b?-g)y~K%W{O^AmG(rgn|(i6JjKm zCcl=#(P)G~pcjqD4xoID@{{>een&o=zg?m5@pvkaPw<$hhP)W*WfJLSYUpJW=>y(8 znM8T=V4h5(yj{irgG3jALNzoMIdE3aWC(w00V@t#v1XtqmzJmDGY~^K_Z1QbhU$`} zoS^TFVyv+F(NnwC-GNGm93RGDLWh;&YKFhJGMJhj+9eVlvtQR?FO}Ia$d}Py$^Xe-D@_)q>@+w(6I)748Rrp+Oo`@@S#m;Rq;Vg^WHH zP~o`GtEHIt%1HJqD3ZMaqp{Jn-9!wgsU|`tZIdZ$VofZih6%*&>OgmghD1iyX+QbiR-SQFM44aS7jkCPL5vcikYnSN@h zsM-T6`l&;f(|bm0d@4jMP3KvSV6|i=2+HqLls=680?0md{mYqwp3WVA?{2ue&Q~{` z=$zN&SyrFfnz`|)VH$&29!1;>Dne5G1u8-_GZ>983GWZDH}c?qPmpG&g91muMQSDgTHM;<^SKmwo`X`xgB)B@B% zM4}mqLZRZ3q}Zb-s5T+TA#X$Y@zqyyffH1LoqRh^>)*#umL%hDsk3AJp7E!~pBc}Z z$M5n;m5tCOz;PNe5b(J@VxZjTE*1mzKKCdwKzwes7&QCbZZR14xr@YLh0i@w3<3s% zp^$sj$dMY2niLlmc|2~V$s8b30G|$Ee;^s?2%crr=GCX;c$9TY)fGCA;4az?~C&?`@s47@m~B4CEp0}{dP{}$h| zET{gypGFc@~DjSup zN@l+Dr1F%KQL=QIQszKqI{WPl^jv6Uq&#|SWt+c4>=w_6Oj2wVTSexicuFLcDAxcb zWHk*!-`O>&t7Qe9qXI3%{|jD+C_!%eH`D=AWt4`m(xch^M zPL=0LXa6NXH#9E1K=DBvl1Opl38+*_rKH@Z+^syQOBNYTHW`1e z@M-W7&xFEhQqnJ;`4RQ|&$bBP$VdWTV-QE@&b4&b));eX-$AV?xZG;3OY0edHEOrU zIRb$ltC95_&HQ$nFJo@-}sHiX@`WCS|KJhkS!l}@qHUlz58+zA<0PL&bQd~+_vqg( zRca7@p~h)_DzB~pg>gbvtD3618@*osA^N)9pwc)sNnAn7q|s7i{nPkKvR67nj;K?k z-$oy!Q}{jdKJz8|5}(KaP@mU+t#jDQY~|&p$?}=_5%i#LN9jZ5s=+j_m@ovqHQxHM zR%A^QeG)NIht27BC>+ZCh&wtWN~ro6cug*7Jk==^A~8_omZ}sw-mRckRtzM3?wA;; zsT=98;aJwq8Dy*L^SPs9a71~vdjv)(px0Tzb*)2)rZHwF%Pq-dIl{W~I<7v6>dM(_ z9VYY-YI$DYp+BJ~`UubRyv^o3;HVifA{L8|tgg<_j~s|P>~@8MixN&*^8l+)CKGHo zhdVgTrO9wfE9n|_q+5qmI;=~R@1(+nLH4K&vMn&k7Qzq!V^6tp(Adi|?cFuD{v})^ z2cpiQKy=8p0;j5rzMhajTLFt*`#(Q$8R`IrS`vV(tG|or(dWK#8BCyng}$6S8SKy` zIylFuvujL~q)L<()g?w3jbU4wTjDgYuC8?0bhT=~rK&WYJ#$2;k_3Gf{kW=6Uxi?# zPwt)U6KL<`A*cp3F!6XZ4SK-G8_c`p67Tr`QKcIzhoNC|pi0N|guXgigfHCu<2eI2 zSC^S9GKI3p6c4<7g=!jIoGA1;ENgN8D0e}r4}V=aVcBHcVRAlWxVc#o4o4l1NI8Bd z({PnH7;xk?yR<0NW?qSfYhzw}5WGuU?KsOZkh*;E=Z}Ka`B6%83rN6zRpGgVZe+rLODz(fiYI#&Gvx<6`*w%8{`Oayp3m4>{x!@B z8tjB|fwcbOff+ulrFd;l7b(65tR(Rp(&q*T)+41|@6^Q%`Gx{E$(u&vk%?wUJ6`5k zm00h17(beL)A5nxbNr=4r*lAPtVoVaGL??X$V#Bk1Aev*_vMp3+q5TlO~`CrdxJf9N<>dd~i5 z$Df^NO20w>vi~y~o`5IX$0g?CX8Y{K_4b>buRC5#zVCQH`Kja6q&}Ck^84H_F;FaP z5%80gVlbD*5@iiS-E@R4$l*lT>2Of>F)Ep`Bpvo-!jZrU*t^~3blQnZsYEE5jK-A7 zYr!{gCW-@ozqm(CiPRRK5f$PtsT7xDLNB2c41R-==ATMrBMr2qo)d(*-?Y#MSe--$ z2s$X-IR}cS=P$hwgW4fC9GCqmu<)HA!SahRcS5jeYjLg+svHhul|wL9A*G|rp3a^+ zY_GB>EmgT!l`=GAFkqrAmsd+AbF|$SAFavkvukcyi zotOhLbht4wa56b3XdCzkyY}L`jlP0#xZK~xteYFli-y1d9V>S)_FkIk{r)bnbJcUQ*iZjV>`Xq%H(Tf>tfH6<;FAG}24wGF4#5I67*oq}fiu?j zqamryc^Q|o8ZZFup#o1^u)b3KFaJt1tal1a?dPghV2RnIY%$!W^RwW!K86-_eD zM{`Z>=z7xz(_{Eq{31SVdK3Q(oBm2Ljl7x>#MYf-=-(@l?7n`lsg}?LD`;~FAwNF? zOeJ}$XtQsya?8~bXBGIUwE2N#FjbjsrYa&>0i0D3`h3u*se;R#%2nSUwp0*Vu-QE z6e>pz{mV*%7=fXm-bf6`Ow6P7b=6k*^Q1hIXRM)rjY$kDpNgm^dInR2Gzm3D=!}1p z%SH`-5mSoY^vf4l#2nFUjYY_f9Yxr}L(9fNOHk{Iu#-SxQd*0UowXLBE0Gny6$BV8 zi%~!#Er(Uo?3hvh?8Rghq7iZqEAj1_4Vf=9pJ#6W_{2Yt+_3fDRsAQv+j;{8z3rLz zGH+&<;d^ln9`n|ri9P!=FJ)fn-+~Kp9iF#;3(W-5_Y`qCW+}us9Ye+Ns1HVV5W0 z)8P57^LK7;Ukv{;^r8Ku(1+0vVqb<79x0d?Q|hTI4Pc*}7lWW^tzr=6`3np3Vzt4k zK@tq|)`FPLW+h5qX+kc+m2^q24j1Q|SUi|LM8#MtP8E~g#V3nT6*I+!ST>MYHicL= zkT_t_%hEQOv6AhhepgX(8sFG2(iGKIe$=46wM?N|{YWmWAE8%C~zQ;aDW!9x1~i4-`>nK^YFagJoA_7AMYx(7+4oCpdx)c>N=Qgg%7jJP3-* zS>Mhs+(pGP{vR}~oS(N1z9Ws=E@$O={FEm$rF`H;u+J7Z*k}B=Bfr}IyI+)a)m2Q- zTlVmTJ7$(OlAAMYyM17vhx^trD`~5tZ^P54^yAg)C%T&+ZZK1+$h33oK`N{$f(E4g zdfdc6gc*e%&wc4cL zZp5PLN~Mm82Wouggqd9a5`i;Mj15UpOp1}1t47fIb;O|a>4~rv zmVUD+qg_%({9#W-qg88(!VwMy!$G3(Sp!8lPvrvPF+dS9LRYkSi{Po;TBuY?r`}eC zgC;0w5{mvC{Hm-VK1hWYj(VYyP$)Y1Fcxrci*uV?}@#Ge)qjM9DZqa&!noK7*bDb{oLaI#np9f*!>Dl;`=kJGH-vE*~0$$ zwkI>G%+bEBTYrRW{{D3L`k`F>GLXO+Dknth3pps;hGpjM31HLa$ZNsZikOL^XNri! zXD?n7VpLcaj)cdfCfrWiLpS4_$!gzf|GL0U;a=S1-&wRD?++i1yi}A8Srz^}@ZF(1 zqK}65;-|>dp@T&yiatpGwJ2MpGa(yx5mOALtfacQI=LjYJfSX75|0O4eQtvoK;f7h zK^(4!2%Kh*f)XhVhe82@Erj6E0pcfoLH=Hz64y?*%L{xX-^w%F<9QV5JoCZ@{8@J+>7n{sTD{(D3Yn*6cO>TP>H3=j>=P&Ol7%JmTaYr2c<08 z%7D!(OSV-Ww91lg-BrRV`hKIE|8= zF5%ozM1)OMF2`U96qmo_DoHx1+!vL2gT8Q4Fj0m}yigPe3d>M1l=PRDVKl^95chZG z-ss5oH4Jf(mc~Kn`z%#48Yfv~g9**^hlQ%7U;q<|bC$80ew!|eSQh#Jn@Zqm+=NRn zeLur<%Q8DM6=i;%SMWq8SIEjicHZ${K6rcEll!rwwYUA^NV7-v>TA1iuU<$t5R5bH zu2vDX&#v8=j%04Wvq?)H#QSdBwA)Pc3*FgISPt~Wa56`7njR{|25cZ21{v5G%ID%! z@l>KRR;Tee=~U(LN*Cj1=R4**=ey>+6`YRKqk@yw>>AA)-5UKmLx;D+*OBN*ZdKl? z>CyG*?=bYl_p$rR1e2~zSFWqbE6Xd-tDr9ei&($c@5|3GDjS7Ikyc@CTsp68UirL=d6jdA=^2eC->h-x z2Q~ic5&6mLE>o9zYiK9GGx11rU*cr!m4esfC#%m_Tdq+KccXUVK8WAJ1aHEF?-3A~h^!*~1yn%9z z>EAx2aZnNx#uG+z+KBzegT~WF#+W8OM|}spae;pE2n&3>i}8u#zZPe~v`OPDq~dp= z!=Pe+aS{w3Tl^9phpO;6`L#4vnJw{7P?lX6=!c4316@^#c+U5b?bhIzJ2c$a=i>aK zY`)xT!9wS`LDgCrO7iAdL{q3LL-_`3)XY%uN$9GJv}H)6EsRG6FmVQbenHp-W=@$< zP@d1F_2mK&z0L&QZ=v~ug_v8XfmIi6PIFjTQgLsePQAiIanSbo7n zzZ8P_-%Dhh7tOta4jRk6Waz`ZP-N&EBKi`5*&jBw9GJIkOMKK9FW=Sh*Ox|=`+n+j z=JDaMtLgB{jrR?!j%J>EVDg#gSFRs!cZq87h~hnauG=(iR9VBuC958s_ULI9SL;pS zckjQi^^UovOA5U|U32%$`+r^G^d%_gj{@H)CHqEyld9+9xnyqMT<;3Jf~?3};Z-KY zT5+nllY7{`k9*q96P)L@(U?AuXI0xLhMT2=+2f zVvMp5YDwR*UiN$50Z&j)FvaQvL=iB-heRo2NXcs8&o?tT(XXU^rSD zEREI&>!W+4e15blN*a@EG&dU_jh={p8&TBgbE#sV&+QZg1#-gJOp|~?2+hGxk#J0> zDu5X6Z!b`+LE|$yfRh^(gZVU46I3dtq^(j);G`&%N}{A$WTVAGZI5h+6mqVZ27S4t zyS&q}wG~1VcO>^DQ^_+)HtF-r#PrL=^vlHb2TZ0-X54PZX4!U`^>ohcrE_Ly;=<+l zuVpB2L%-3KBfxmeW)3$)!TH5*yo8^|VDoo$Zl~HeTUZ@;rD$7Vri*g@Q70h0RX?9KdG)HwzJ7q59sA?SD$zB-?h| zc^hl{JZY7zjn#T_F3_70Rsi}OOu*%ss&%s9w@S3+$E z)DF4vzkhC*`{4TWPxfdavgx8KU!``Kstl5;iltcup-QPVSApC3!4Y$nI%cM$9~?4N z4Mn}p^c!uff(P^jF;zm*3i^k7U>z_X{6f2e`|*TuMSdn4&ahFzIdK#zxNdkcZpKog zx}MW=lfyc(WbyYuVDFo2@ddeXSXCS?iz2;J_-zo8!GB zDm*+tVle{u(F6yH5|1kBW;G_FQ6l;Z)?QUBGvXyybgC3OUaM5Ac(ppIsNzj}hq+1% z;HHeIRF%8w7i>^_pq4&|My0AEF-g_THmRObDI$uvvQQJ##>_ESzPlh6EvZygxyqB{ z6=V4Z%>?&MMHAnoY*sgEn_NxFnI+2=i};nAWv*rJ6=mz#b&7TTI`vJOo3%H)ZgOw* z+~i-I*v#Io?DcF(Y)Nh{xu1VT^MLsQ$0M$t?gwKJB_2%fQ$DA9PV<~=pZi(Qb9v7s z`uTq4QFYq&Lh|Ry@08zZF6MpbpR_EoIJvB3tBM`&Ug>T3-B83X<`*lMsh9@UWZ#5X zLxOE~&q+*6GL3wra;}D9d87t6%VSFvc=CNEe3fSCbxs~Kji`1fRUTGj%wg7TQt}$C zQC3AwlW#Rg zRVBmSRq1R;zgwgBr?c&nC8^~7T8$>)hShGD$KzG0)p8=v{eRkf^YA#XDpCC2s_w4p z>gw+5>aD8RYW1#OrS6v0vf?hq@+RA{60fqOSR{rJ2v}0cN{j_r923A|3nT$#mV|9) z>^P32B!mbsAuI;ZOvnJg2?;YMBqRdEGx;$jlHWbIx@|dc=6(G8zRxOE-CMVARo8O% zbMCnz(hw?zdA9g`hX9qjs!9UY?`V|3@mj#C|c$IJZhte4q>j|6OwGa3ruzv(tz91>56 ztoYU5B{R&s9ySmSMmDI@Nky7JO_h|~{7Yx`l11Z?_d0ePn0Kzi?_Pw9|1&Z!-g1(o zk0%Q!^$k;lu<=LeKbR^A&H;jYG8vz}5rK_XCYz`t+Zw3psu>x^-nJ$>HZjs*4-_Tc zX;7P~qvl*=6HntcZg=hCh$pvjXJ+BY3&#@+?eq96Kidz0u|FEG-hgg$ z-O@IeyQOen+r9Az@q^OWV_$Fkdi=q{*Go^tpGZEDdc5ao;~DAu!S8F&)MxvDF8^Hn zFWag9kX({9xlK#tGR0EqBB?BwwI#8xbUC*Y5jDME>o518=r=uG!0#*ETDl{*uVh-5 z8y1IStTnF2eM=ktYXZyC7THt8iQ?O155yiQnwl8cW(xG{Sy!saxKONU3MS!I1Ot`; z53eGa>Pa;{s^9;9vw)Y-;z4VQS_v0oT1jeiNo{fi;ey<-NL##X%;NvKWdG#Uw`)rT#ATS-gc50K;H#CsOob@@8N#5&6U8%eEdZ;cQ!J zDNA%6kg%_b?ab{ejrLPtDJHrP#WLqR(%1Ab?cgE8Jx_OOSI*u7K~GH)*`vg*Ce^b+35OQz{< zvTq=CtJV!uOOPH_GxXVKxP8A;opKSDo}l<%x-{-dnm+IR`?-L=T&0Dn^5VD?rG`(P zIc@rYip5|YK?Fz9Cc_;si9UIZroo^e(dxKKtj!{Pd`Y|Nuxe6cML;8eAUS!xb)7!` zGXCKFnR=g^4ti|7pBt)AYvrc#;^@Ufdy+=Tjyhm21tX?df+-SWiK#fyFF{=V_E%*3 z8S(Da9e=KnE}O^;i|m-eq>VXue}QTkHeqI4jIr&=2qgqHs ze9^UHh2(c&N=Pf=rj=Gl*2<#rdHoZ>lX|+?ENzxw2c$^Ru+8Gn8Kw{Lu@2#!W|u{= z+?thTL_i7oXTM`N8h0J1;~(2h&~Z=YZVO{VtY_f>U?BND+~&x1|0raUKCC^~Iz^x$ z(KwiVpg+D6Rjp!4{t^=HF#AGw&m1M+I6J~B$tZ8B{(Uo(AL#fn#q^W2vjHI;ckz+Z zfjOuwh*Z_RkP!8A7&jNhT_tVrHjj@mKXXdz4g!mfjV$z}iSOPA5y5P4((0(f9%xm~ zm9tazpKF0-D7r&6wlNF~*sK$k`GcwV;O#~Ut?PXuFx8TzYOq9S}9&P1QRB@^ag z{5cr>n!zvCOI%uxU>A3DRYyriV4^bX-tE*EYL1GE2%(0T#Gym!EVCC#XQUA&e9&s< zl|o*WO3VY#$FgOtt7#++17SL)NP(l$k9SZ#5bKzxY(sqty+R)G zUQUFV$aAG5t$tbbg6^zZQpDBh_aAID&1iIvF!FkN;8)`nHT~Kv1y?F-$@N0U2?)rH zK9tnYC;kxL7Y)OPK2*!2?&&%j=hShvEV{h`b7a-40*&efj34rMkQCWu3PJ`VsR0J= z3rZ|mIr7p3ay}30aT#(4r2x5ciZqijVLf7SvCADS?`C|clncD9n=-2{M2_)#!=OkHoBf6 z9fW-Ro{D{~W%AG>MAxQx+1g@;pfVp^SXbG|-(Ebh_(W-vjHt2lgnPDd=?}rUAb+nV zAUqa`>xjeA#qpkPr-60A8C5~U$HdwaO)Edgu|doc*1}B9D&5TTP$j717Nn#uHDm2B z?b3M78rYCAFry6@THOCeio<>Q=waT9E zH0i8!Cd3$Kw(IW(H&fg>z4T}KKy!ykd@OyNdMqLOzgXwWb8JzHYKkOK?MF*!9+rBq$-lUIqIP zs}^FvBCxSkjcs(Kj7hNJA*CS8BV=Lk0SuNA78=c9!j#rfmeWWRvOF6Q0un$uBL(uX z2!~FENl?~UH7m{y2afME?V7ai<9M$=Px?L1uNA5dLOoDy?-9uD zcj7Q6Kxuo57AK-dMvP!F8_oATXk{qBky=C7MW_Y1p>fH!`jL_4$J}g-9IRtNCLAzh zKjLJ>w{%wZ$_hBW`FedK)1lfqGT75NG}s-r1GR;>mQQe|63v-igD6QUnlR?=rdMpc zknPmigI$NjljrB)CPdB#A4a#7Yp9L|ztBz$z6`(czCdpvE@-~S=Wd%9HR>4yAJE@# z&bBuIUn*Hn7H`^SdLr1Gg+T^+j36L74}9a1q>-c3aYT*Hq49(o&qM*i;NU)o9ep;y zrj&n5K{*)%MRoa2`p1Yhqml$nCmUK!VwngywkQ}QtN=#dzTQVioM4qYHB1~Fa+0r- z?cqiPA$O|#mjhQ6gP0Heh+Ajp=ly)F?Le>wc^{8Rq*Mm?TrNwx9MVcOSBl!GFJb*v zY%%>K6H66k3%K7|a%?E63PNJ$I6P@z!#25IScfEDKjkF7qz4Vdhth6(&gU4fx*n2l zX!_q{uXV@4`Ua!lF6BxSUc#)*?#CFt4Rs=r4)^$BhM zjpNsXk`Mb29w}0|zuTn%!?I9^9>n@nN_#>6(9>>S=`u4y8PO*DOLH2Sm;3XKC@~vj zCf58?gWlwX8b|Pa3YO%hiPl)0AQ6C+mwW`XbwGVmL{bIbEY=WQi1^VT9bN%u$1Y$J zCMbWBwtGzK>~tUZ;|WVBg`)$VdTy9Qx7GD4cx4u5Uf1i{eXF0NRqo8KbVEZfeP%s0Kj z-7wvpyJ|aQhLs#!NDN_+@7*~P5t z?Dbd;Fg^yFBnG~Ay{+DEcjYLI{ho=@1v8t%pj|_kL!#^*!6TRkaNf@k}t); zAKjxoQ74s*)+!R%eM(p%WEn*l)073NvNQUy9pN!_nGVh&$+(;#+>k~tDP2i{81v29 zc@~Ld@mLV{G;V8lDa_viCB_APiY!9?8FhyX_v=nmoLAW*5RhC(+DP3`43f5wUiL6!&$j- z=f~b@dELH#+26+-laRE_dDb4u#VV6DcL`%DiqDswa;;QZ5)qdKU-0v(#4Z3YOoCT> zqiTpG1!$B=WpXrOR0>8~ZeR5Llk)b;uH)}9S~c6N>9KmLYrilN)L)?q&~77rGe626 zBfU0GF=Y29$g%uHx=a>sQ3{CV*ycX-Y_ZHJ1R4r80ZY#OAyWq-n4nh zYnX*;y-2xyfWO7RZF&W(9lwP8ripsycM4!cyyL)S$A$f%Zil~pdms#982B6XY7*d5 zig5dIeSiI0-?NB6OA1?`evr=Go9pLqMy7Wwbg`j-=N6Uu1?0YI+zj64oK(B0b9GXfK!R)t3 z2#ER6eqKEWYYf}#FJpPemnvs@H;qd@$?D3CO50WQG4h#giedx=gig9|CktdODH!h^ z0IkW}xnHo`%~t1ijL_0&;Vl2%MQ+T_>(iIs7h7YI-g5IkyXm<6&TCJl?Zee4+(@rC znv=k{*zJl)?6)8>LbZ5U_wW}|lJOh4D+NrFKAN)u4_Jjdgp-D&2IK9RQI6Q#I6P)I z31|Q}AmT%;Ece*K7&e>!E*vlv8^>~))!#qZ0QTTMH7(T`edx51)t_W4n8i=d2RF&Y zeNrAi7E46I2NAFQY`>B~sD99xMoioS1a3sFzPR}oY+JY{u{r)-TjqUjkNQA|{={oN zHcKF2D-sO;TUk?$4|?^dO{zPvTbOm%Y@m1`2UBL8X$YLoarOWHG*!SY{|$;={oFSzoLTr!{1+QtzCtt&5!XJ3ZjdKLI zF$M)bQg~WmMoXgx>yHu8aYwrv`!c^$7ptRVz((B1#|gy;_M=E69UBS7F<|3RnykCL zhyg{&LY_Ei7nwygs7Uk#&z@)03atR7g?SW_Uj&T2;N62p@5_L)-FfiqaeZp|Vt-JZ z0IS#3%8u=z7FMjVya3-bT9@Z%51`E-KL)EaZmqrCdb_!bfa*!yyymra6H}Sn>5c}t zXfrni`}{#l_lDAn4q2Z)BS`QO&XwHW94-iG%5Km98v+Mw*@{Y+x?P=)y?xnwd(p$s z*5cD^(fiK+b3O6B5t_Qw;{B7sv+B3;V{yFm%u+HsCH^IzM4;x^X6pJGe$8Mj(9DN9 z*M(MoavovJ;g?KsW%(;U_BsL%Jig;nm8i<%HaL@B6#iHniu#`O4s`923VXM_AfXD(Y!Dxm=JJvV9rcWq~yf zVhO7V^qjG_)9v0JL7ti((ydJMMmHpNx(=_S3?7;c4WT@dx{!bT3#I|_NGtSXjHrL0 zJoP~O(c`qkuF+)RmKdNde!13wpFH8N?&0UrF_nV0S8IZ#P@q$Zu|6K{PQsg+Z9r50 zbryD%pRBK}JtaoMm_xWQ0$>fam@AtlD7%^6drq&PvOWL~aN$odhLWr4u z*?fI`{qX{Nl%3f+SWc9wv3;}VH~Wu=(sEF9Hs`;3ihlPM3;R2jE^tYYElLt#uvhqc zmT8!?wEfC%UsyN=Uq6KyNraeXxUX8VPfa(js`=)Dn3(@S$xGrj-Etj!m>H%=!P|s@ zi^S>~XpV;R*uls9>WNB9|fiKU&IbiH;$1|WKv*KX_H}DKHg%&(qS^4vU41+MR}!O$CZ3_RpzY1 zqap2Q=~IzrEpH=d3%fs;AOTrjUcERs+?JmoITekFOztOw>f=RUVS^nHpSHk!>X!%EobOCKB4e z$7(bF3lElEPBW7pl?!z9bSjYr+ zhAfQ^Gu7o&7NnBct02z53kb4uN{_! zlyU+|B%|6-QnD5qEo*iBF(y|8un(WH8G(V-C<2XwG$l-2E&)`{_c6=6N_eQ0OIB$N z3WA?uf|e%$vI%U$E36elb0X0j^_IGn*U+c32E)|JzcGr8SD+Nwp9QI#q(*aYl46dd zQVvA~d0QknwzQIg$*i2EeVKI0FoLB!7C=mTK}^~~OeTdE7eq5NS{RB;aYoIZqS*g4 z@J>@n*+?AEithoHcfC{M(x7^FWa?xxUz`5rgw#Evg1yAtLj$MrC>OV@h2kp|v2`WS zE+5Y+$6w6PS9NOEt4AHCfgGRafLP>Xu4zRCk2X^LRX+wj6(m{geAB> zz@mXs)c_uiabF{NeIPuboN?pt9)5=0B&IV0^4UTEs}o~L`_2^Q24E)V_t=znLchvb zRS4rKL>pt>X-RPub_fv##2S;1?g-?`Nt$4w`XZ$XiDhC%2xCoSXXUa4t5}!qYUl?8 z7EA}8kfy%I#WD{VJGa7(`)Xcmk5T?>L7TXP80by{=;TT4w7$pW!*uH9uEyI6x-68S zb&oR?e%on;lFMJAvj`*BczYPW%=JjvYb0J4AHX1zcYQv4$$7d5EEyyT6_l>-TgU%5To=YO~_bu${OJY&T$lWLgC5e!g1;Rpj z)+j)?kQ1>`yKN?(%_<}L+qbZwa(g>PmOHHjUIX(@F{nuc=QL#0OBbO(UCoNvw)D-w z4Cv|_m0)ii)>{GSp6WX;MH~)QY6xThPrh-2m*2PhVcUL{c|iboa$jM1{gBGR?*o;A zDzXsdDEQ1@j8ii+)O22+(G3dh6YdEqo{DfPPyuZan^)^$GIPY$FIMquf65|ms2(U? zf;L2E=wOu9tgAs0_`9mWgKw;t*4!csbekzq`P9|RcFQVTxr_IRzy_~?l_!9YlZ@2h zM-lwiB_iO$Gb~>Gl(3Dc_q&efiyF^e92V*LZPS7=DE{!mtm^8TDc>Ns&9pFRF6`{I z9)Vhl{|q#k4Cf_B{u081cu5lghxpjoOr;$pAw zoc@GbGQF=S&u%r@Qh{<5-~Y_!2l-wF4|YM}kclxKbiF7$kf)Nh#S{OJ5Wm* zCxl(cyl&$uJjz4qX^5k8do5QFI)7ZFIlzp0RI2D$BnIv-y=f&k1^}+&PC7~M*0)gu z-dr{IZdxbYDh(Nq`)+c|Dr@7>yt8WSZR%qZO?UG34fNgvzNEBBz)Q7GUNBDu$HNBe z;VW-ZRib#gH#5d7`o~7gBxfk2jRuW2g!x4BVCHXDcjQ{B96(14W5J7fQx{i zf5*wJOtXedGx#iaTS^d=wR8KZNv)3ZnH1gS)m*8`v}+W>Vb)W((lx zC8@u`C1>j*sYt=hph5T`q`#z{h|*#!FQ%|IFum`WA(TXYsnNJzZ{b6EEN z&k1I@W?7#?(O2&o=pJ>;QIL_e93==@FbpaY7!=-xR#F{}r4-cnZ)RaXNRbzcu%wcgsV8$Hdm3_WY>0A z(Oa{sTCtOYsiNn=n9}vq>{`ZJ&Ql$~r1y}@eAWU!t-_uGE+U=VBWan=BC=+c@|{30 z(is|?fmBHxHf5cSfL2cZgm2<>^-eX>UaP+~cew%UcF&UY_N*OQ?Q0hG?OC>7zhQYu zV#-WFJI9;)d1-B&ZJf1L%s6qwZ4I>IwTtP42Ut@yh;&JA&(x42ZzHE#m?R9HEy{mRu$^OkJ1l=fU{lRkX#Gx60*`9#%)`R#6Xdn zB}9^)Gy611KqRQo-PJpm^dp_&@{k*lIuhp!@ZngrlEy+zrra4#3893RqGc^cs3a1I zok@;z`)kjw(C#VNGYyF1YAYfPXd)Adouq~+hA>1;WwO9>V|E3q4ag9y54n+S6be*? zM9ba7;Gb9#(Cz^a6=SI4UKcEHC85R$J*fpRZ@)yQQO+NbEkMM^1Ho(n-ik3q68PJt z10;b;B0%C7mxaIvF)QvKq4AO(HLP5H1&k@(_E(sr^SqcShkFm{=HLni4uGiK6vz$t zupZ=T#OcT?!)zeE5y`+d_{|G;N*$L{MU7|>5L|r6<@*9cT9T;+Fm4zTxdld=7$@cI zl7$JG2yLugp3YfG?cWkBz08yk^{C%#Cb#E)&&xEM6HqSa0}U#SjdwiFZMOMs#5c`c zcv^j%HvbR8Cq9Oy1$Wn*H1a{-$3W9x8)&3Eh3ai zzvwQYbGaTb#mn5bzDSASd5?}oZO8nN`7bd-6@g|?xo0zOVbhN=HSW+Y;4CRG4SpUJA}`9-IvCmoLH`hXDIjDs2+qrx-J@*6~*woK~&x#rCLQ48!c zTa50INWDQYV7d{uCdb8}cils5EuQD-nXLC@+rS#^Mc!|6f5p4hk0qtk2US7^3mOjF zg?xDn6un-H)e^qUHQ7u$8lEJ;&dBrev=~4figRNO5~=FMY`=DRZbnMyR5ddX{w2v+ zDJDf;nQp&oUh9@5l|sGM!PNG!WIAi1enFIIr&tl9BBfBuKBnckmOc83oV)4})yqygZ+|~7Jk(;cVBP2wh zlf!6mL=%wi&?_r;TOa6Z&PQe~xfNv1Ni#iP(!4Tg#hvKh{XF~%M~aZ2Gh+&RM=~Q5xLAu& zRCQ`TNPf0tS}{BmaH(DXE()YLKt@8>$_~9 zt?Xl&;_PdBC1G_v!c!;YL|bsddA>^)o^=kTd4zL2;yn4`nHW?{$T+JR+nrz8K+Rjo zk0?Vip%1{DWs)p_EC}X*O-R#c1Y69sy49Ox+}S~c!Lzz#ccSg>scgy5bFI?U zSU7`K;NrL*%E5iykTa|BIgQC&sajw1x*UMZWFt_#`>V9Y79_x{wq#Xl-5$)Qa!H|c z&b+XaANsfrvty&%FGAb~KbGlIU?E5V9sOFkc^0r#5MjAK=g>i=N_*6*C!7agnUX%`MTOAdR0gdd@sPk`@u=EPi^W!mqYIdaR_It-E_<73gQOIe)|4 zrixva1w%%o^jWgOt<025g-6jCe)*2lBCWdKa3?jI6K+tO9Y+$j<$gWPm=S+Q>>y;n znq+%NKgJn0NTI7yD3=o{mM0s&|5n;wnU{aam_lGg{!!pimKSp;U|ouH9)B9%M4U+f z2H;BVMLAhem1JU$O-Lj7c4fZI*mL}^YFuVjS=Dfoil!fACS3&AoRBd|`70z`c1cU? z&-?@Ucqay14hjgh`!t3?{fXA}X`PEsSKUyx`Y+|54jj8WE+8a68t$xlbo?0~x9O7} zZ4BETWYzNN>300xHm@&Xn0+MuG=q(aZ1+RZN{}T-!LS#}7p4F+IaJdj@{-^%O!HqH zN)9t~=cJG!=%D0%c$|=E+i=n38`DSf5A0OouruaCby#YlPW@>Uves0|(z z!XCqrllMred(z8rfm9R!3DLyjewl~@=>%cqr{K7@^PUguKjS_75d6$}(tb%-ihgJR zQP)iUY;nKsBdEV|`eq&zeV)k(2N#zSzhB%T0LSEuP=BoMjJJq*$BqA4MNwegN2$YO zj?Ng7Q7#cCk345fvn|SxTof7R!odJ?=r>54uI5TwuREEWE8fIMCaENd$WYU7vAIk{ zgo>5{MCYV~p|@FIwkrI&79l?}J;?T4 zCSMAFjoCXH;aN#IE(Zdh0}4k>IW7ADD>5AN+l;!pzIQ90M z-JcXVmbX3bAHsH@eQY%Af%eD`{7uk5_{SIp3TefGRc1(>1hTYs7v z$N1$8=%O!Vv{6pme*wD z*tux^N28$xopF!^A|%CE>w*hkm}m-AFbzSng9ebtZDNqO9tRxFic6sk>9R4lUobCt zA5Tbt!m>g7NZ}Mwg2=j$ZYZ}psG^;%z!CCRDh1B|C81apsgE2;kW_JxO|R|Q?tz;} z)c_R4yxBiaMOeNlAQb)r8$$iI($%7~NquRTaXYzB@HQUL_cw)Wu;0LSD?HIx-ChZ)Rf z#NA4);N<2h7)a`if=(O|^heWM#UTRyV>4qziInHa}jE#FVbwx(X8hjUwt*w98kR~8@!7G&4F zZxI5Abu;K7>r?TL^hP!fG#Fq^D9Ib#`)zsr{Y=o~{Z&(M2kUCW{r8y-CSxPp)vMTD zMDn0w`924ZCsoh0y{B16s_r@b)Yf;ixmAG2?FYZMvF>gHEg6zDyvgMXP>V}X6T=6y zyM}hV?K#ea|4)k}jf(Y!3JMcQE@HqH<@mNvP79%wq*BtwidCIjwh&)Je+`r!BxcIa zqN&I=1T1DiJ^H(Z4g1T;cS|ALB^*XO3yCX7g?8O=eS{0Pjfikjql$M{DU9upDf=2c zDeD@%J*G7TCM+jt{4{bTjLOkU(vyP!JWOJ6EdU->=J+?)iuvt$b?Va}eC^#H>egj0 zRLmII;g;sxvFn5ySRJg*W#39x{52G;*z56aq&M>$rx`Dh+{i7IPwP(%NBu`W%s10B z0;kyb9@(2c_4ZXqyQ7Wf0hjSh=Fjb12fq;(?LGE32b^0%Z0C-0K?pQ6(uomJP1sm8 z`4F_2M$Q01)M##?D=Er|Qbv{tihIE+qnwM++7fQk+qhJZiBFbWg6^tRF%b9EW9|CH zH~|xW4rW8rA_he;uNo$VJIdCFPwMcf&D^_E)+C2S*#(SM`wdW{B)Uz8A&^-4rz1jK zPB%HUTFmcS(Oh$6o18lFb>Gzso^LF#9(4lQ=M`J&Tk9yi zw+9z#dNYKc`6o^@qUE8(P*%=@5of)uLkC~ zc(p13PjP%o*=};lfQ450K?!xQ#$$BL(T2G+aWn{90TQxeMVfdyBu+6l!*v*$+I_jf zl&V6+pKN0vPjyfE3|TWyQ2Wcj&O^Fs)YaC%G7381^r!o(Klj^N zlYhH;E3w4%hfsR-bfe`9@HxbMSz)D8zYG|B`w{ zc`YhWATU3jkQeufbvywD3-u+N?*l8G#;5-W3flO+H2Xnv9Bzj>yozs z17=RF4fQqsCSf@`|K^C4xH!L(>re;&p$}fiF$}fF{;%#8ui(#S`4NHix^XTqa)C5` z{dL85Dlg@4xzW`F2pWb)hTtLET_izyW+dEUGHxjK8%ECt&Op$PO5N}>d2kYcb0BHp z=q@hKGXC?xK%0iFuuU6svkdeT$ZufklnoxlfK=?aKNZxBe+Ca}`XfzziH&K6DHjNc z7NVZdT9_#CU1GROv#OlH-Oeq!^Uw=jf;^|#f%B`R88W*%w(#Wg+U^D*rntma9a%~-t^^An$GjA^atCzM9lu7W6aR7Z_{6rp?VH(fsq3%?>@NQiI2&2klhT?tM-0x8DGXvjv`OOcN2Zcv;sG+oM~95J#XD6Etd94 zj_Sv$mDk(Wjk9SiO=uLD^+Y}@YD#Lf*qEd`4g5hfFqVj?-QTyq2lUn;P3%o?wfjPO zr_`I}Vv?EzZ{!oiy}NCvM|jKdC(@{eQje^dZ{hAJUYMdD#X&nz$lTpwc;K4 ztavV`glsc`-~^d=DAgX;W}*k#`mGX^41E6*-XhTq>=Q{6a$%Uc7~phIf+O^{aXQ^3 z6}iPaBCdHQK0K)fqea{ZvMjeX&af%JUn=r?zl%?u5czDw?rNKbSfa2Da%SmL3R~<) z;=occtTSXCNE3V=RSy$dN@Qp;`57Sa1)kDZIv-TlPk9oA%niYKmo=$;%WN~l5L04dw)IjEq6|RF~*N%cub<7ivV+Fv< zALbgOln^;KU`vcW#Cc6j%lwn)aabJ{vGv6O>0Wet&7>~=i|ahb`W7?fqDrWnTqZ-dk`TM%v2Iagp1jD~B`Y&Ov_Yy^>i@B1`aK{3y5V zFCSbE-xXe|w*SfcK-{T&2I{l7TI9zxfCF8D;Yn{Les-AF7R<&Th)55XO zZ}qUkUq%Vj-V92UPDF_)vGmuo(mhKiiubcKh|{;hLrkHWTweCKdYip})e<0RW8mC= zad~3<2EI&PVgTzw|6>0MdoHtA@lf&My%Vu-xjvn^9Jrjo@gHbHYn{hzoTsL9!V|+9 z$3C7&7E71IG2yjMqQ{_f(S=jZwrM3!_oc+)yPEP^frK6IGu&N?yvj0o^Hv`Dy zgmm14Fc)50prcSaPv)I)2Ixh{>+L^QgERr#PVyJ(Q?!m>45M zttfFZBw`9}+hv#75NZQ^AweBeM_F?B;5GZg#87k-Gb<=AX{zIXV7~Lp=i}*DS9NuS zld3##)18RxK9fHA#RWhhS;pf2At1Q^H3(0wlrEjMotS3l3)Jd{T2c+)`H+QTS{$*m+J)suxo$+NQ76c!<&9-ryZqS5HgWkx~IZ%NOH&R?6 zDG{vvuEN(m{-gS%o?KhvuHC>hpRE_Wa^SLM>1$#n>Zxn0`$BK4l;P~d>Y;v8a|O8P zKUbdPdJlTic&MLJ9yUvu1^A|anVg6)MNE;;9I(AtOVZm+Yc#O^Fk7Tsjd^3WYPQpM z&bH`xNUxXPMD8Yc(XeSg-8S!>?`9fZuNZn4Iu}lg*avt|1C;u3`=Q5gBP6tV(dQM| z2?XW(ksQR9C4$PH-S}bs16j;mFFO)s%y~z(+eO$9$4yy&VJDn3+dbcPB}-6La1fF5 z1;j;BAe)FjQ$}RS4yzVap?sl?xw(V%ip5a|=zDQSWYe!D#s!X%2f;`oEAFuk{U#Cf zSO_L8fB5B#Aw`TYP{zZ;-M%hDY`nPR+@nj#y(}zlI=|WIfXQ~a99T?*Mxbz?crm+# z0lEAsQXn$N^Hz2uV-)WFbYdQVgrXrtbwq|qe<5Tc4t-fJnU;vq>pIlIqkBL=VLpT6 zAT_4|E^jK=j~gBInyAbI!D*?0H*p}DR;)N-smbR>$&<+!f80z=gSxAi4i z@D#=2kjV-r0m@aC1&gQ%y|R)t0ZhUSEmEbXgj~pPcBW~@A|O zWl;i}A8Ifl8FU8E7gu6LgQ6s7G#H0-CWd8+&!Xd1$l|`LrO3Zq=nm~VW!A(>Dol@y zAkSwR6N`*veRarUI}5{;M`riF`cx&mz8n-+6XpF1zja@5!QD*IeY1FJh4GW5|DxVn z6h8~6LXJ2&{R#SUu*=;APA~mZ+B(esVeu`Lb`nnmEci}m)-qR~5}2efqw!3Wha-OS z2pM{=32>N*F_BR&CzbyhE0&7$tJzCQ!-sSb2@abiWGzmJcd+dV{}fwk?|yIuzHPmA zyK=jhy{_gr^P2goe5$2ZGh-qnwSjUFYTJ2HqQJPI<8F0HkAQO$wrulMik9ity3w`K zW~g?n4y%%@o~|mt=+0c89?##oZ0Bp?Tfkq%-^KqaZMU%4cvd|QR~a&Nm6A(kAxJK$ zG3TcN{4LfKyx0b8udpp`Mu^J9efG3Ny!HidDGU@FRa8Kds|1;ap$_hQ)~{*XAvGzL zr%NJGZdBC=n|&xn?3L`EN~>40^^-@Sft9Q%DKBd^D@jj$`jBC`E{ADcpbT~{=R?LM zC3GHm9msh5;f^i?>NRRnzq6)GXoQb!Zfo{4)~Co+W2Bc7ayxf`kL7JDhT2AecIGyP(1G#EA}Q(rrZ?po|!N4WAJv( zNIiMs6e~&E@7ouv&!@z-w$Q<^q*Zo`-e1a$NPajxF6xL^-BOcQixNrhV&oSv;G8_8 zD+S2M6a;1eDt}{;;~Aiv}?Ue@T@tz0A1jq1}Z* zX)0}y_jE8iIYDGfxa0c%jMW=&>Plrul1}B^MVsw(wcrpNp{v!ELYZ9fUhT zauhpo%dmo5o<0K^I?hv*_VG>3QG+haXl44?;B-9d>M5?$@U)s{N zxg97Hpgh8ML3;O}+^Hc|Y0QFiPQ_!8y5knW2SxlOv>F{Q6Di$KY9;ubpur&XF^Wtf z*Ep&fiRa=!ol_jtBYU!SThnjr7}Y8@PgMC0raeHYtc2u#DYfHok*x;M9J^`lQt1@- zkl3r>Wd>(1?G|{Tqz4g@>~6t*~{5NITLwqP2r4}bZqzb>ikn)R1@Hy zdutu>N~Na^>hIXOM}+s#j*E8XbbXZc@@JX!&O@m}WMS`67Ad!6(XD7T~2f)USOKlc1J~{k5 zSUj6a^+6pHwsi3P7$*D8ZavDnc|E+YW(YT$W7lg`7xeulVz%AcJ;Q@jD=IR!(fp@aQ(+;N+)+fb(0SM4B*>ObC5J8% z4$h!HF+w@CI|#vO`R8w0t?h(+-bZ9%PKSj|$i`u>*6``B}Kke}3xd~Fgr=R6CT?t;TpLfnb8+wJQsQut|aJ{LYYVN#9Aeuin zZo+@=zR2F4FOH)Gc+znu3FzsQZqMJ8MQf_BDxSAiy~YzZKYe?bb}66zz7Fi*I8y}b z9@_igG7ze>yXtf_=xH%hU?zi)2OJIC|BvNYZx12nc6h%{FmK#90TJ$R#y>#$J(Dfa zgOzJ8nXT5FO)m@n%QQAt>{Y#6q#fMq)&x5IN!Fc1ybGCbu69*gC=JEB)Xv4Y($vRV zoS83A&XhNvHAkwEXFux#C?60VZ2g~H%?DFh4a0S0pX3eK!m0Vy62Aw{(L9_k=S-Wx zY=krCw4AB1e^<^+0fepQz?<-O-C#j46%guS~@GoZy_1^8C3WWFTDEqH}2ik1doQua5@M4On)-kxBl% zCVYS^`DT@qq&(q~UQ#ujU{tFoxLh72Kk+@SB>$!Cz*T!hhy(0IZH#eCTsUaRuI6J> zsS9R*=>@|Tx+>YR{H2(1G+cOvF!O~NmF#8Tuw&sfOTg8ada(f{x5pL`Qe^{NesU3qug>?q7Yv)M67{N&jGd(~Q=b zLEXbQSlRg0{=nVtN5sKFo7e)JoE=RJZQ%aR?2Iho*jWe}3I8+UyXc=jZHcwsE?OWlQteZT6A~ie)uXv(;RdNgsVjt+|4PSB8xg2p3o&1#~PX>CQv5pMHf^wSI{L@ z1542*x$3M}zVLSMz5y(>a-%(X55Hn@FWZ?1NcYWUDs7ezvVt(zK9!ISGs&X1{v9@5Y`GVgeL&XDuu&}v5KpU zHwo4%#qFpAYlYS>AJVMGfWFiJn+kJk1f+UT^-3&+ZIY)Vh;1^{gVNnUfa8*?N^_@* zZ!qI{+8Ul;nK7-?QA2k}rf2Zt0Ua4vE!FKvCv^Fkj@t0~4WtnVzA`k`#_gX$eCcgn zN0%r}|HZz^1LzaSS+Eu2S}Xc0vYFSi$pZ(PyPZ9-r#8Eg<44!aP2JWc zTcREQ)9Rnjv6@sGGq|xE1mAuJ>i-Rdtp6VnGBGnTv;H?CnFyI#nOT_sCn&GIVVske zoA3Bfy&osKdeUVQ#;tlhDALCn4VgWVne}si2?;6m{R#_GhYm=%Q@96vW9Q#nX@d-=24d()dXxbw;T zvVPh;%kSk!`5UMYa?@XMwhLwuNx@h8l~}9kVc}Rf{UHG!Xrc4rVR|M%_NNPy#uqg2 z83EqsdVy``SRd%@7tr#kzILw*Lo)}9f4sVAHo0H%EvDD6AM z}mlA}r|hiVc52z7`-*Y{ZAh91~sN>t;#%lf^zlI8jYT;w|+E|Xn8 zS^fScKe{q>MVH-}D#h;Cn61(_Sb}8@i0BnQSt{>;8+tc?A5AK`JgZ28=Qupm4`W`osq}<-dzWcolel2WTFO zGX(y)r*9|7dL)4K1${-_xNtiN_r|UcJ*#(H8U{m5m+Dj$JK%LD&Kqh7Fy;5Tk8c13 zso4-l`VA+s2GZL1cqb_~XyC@zj8^>A2WqGO!L9~n8#inu-T`4UXl#eW1rXZ@ZI`tr zF#@9O0Z^d_YDI*ZAT6=}d|sNT;wF@2)-3!>6xP?{X(z!VXq|(TNPw&7o{aWQYPsv z7tr%fSwhU9I9DKT7-lm$p6L+@u7bdXy)#<8jsqwLzH8#> z_~cRR5gWU(KV})0qMynlyAyeLj*pmZJ=KZ6^p%7QuV7y$MdGM&(H*O6Ar{TzT%2kNgYLio=Sg z51Z%0i=+>3>68+U#X|MGcIij!Y5ZwhCUa}Z3q`b<9+;l5ZRr#U{f@{YbEwsC?~e3u ztnN^*M-(`@=sg&feufMK+BsXT#>np&?@%51zfBa=jPnuQaJzx4%DiX}O1J%#O#nOs zo^J4(GQF~RMJ@`{CJ?NUwS~*;5szL`xW$+q@a#W9rS&kV30`k%Y9OR`zwnhJcCwr2 zoJSZR4B3GhULd%op?mym(3Rq_Gd3t((7A)Xae-6faCtk{f%b0*G2ZN&_+B09BTFtG z?H#zXXMfXkeS%Ype}h_oT8~KFGQDFudc&L&>9_!I56XW7gu;oWpn~tfGy zIX4rtpXCGW%QqRXa-u9_5~sq|oqVV2lWHGiafV;`MuO+Z;YF^wetGA4cW}88<@#iQ z#p>6gA7D_X*eywxBZ9PVw&G%I+yZX z))QQ7i=Z2Vok6Hoq!IS`!`x%U8yJ1A^AHDS>$lZq=Z|ffwN6*qQ`1sHp@EUEgP4O# z0S`7IA^F8qJ9_OP5pz&55a8vhV-rKYX@NLRQplfMylAmR;V%z#Y@PE0Xu8FVfcBLW z6N5nOnPNEnP^$zpl&!YxLnFJzo>#{{dz=*Sc!$S!3{&crk{C7vW(JYWxyd#2CacLk zgRIWgi}vW+;_;Dn4@taReB?^*PI#hCbG2vHSZnS)GVa$%z2ozq5#Qpkg3MYmYjNLH zH3OAAXqJN8=rJ|?$R$`vR2xr*k+G`6E5UC$Bt8tyRc(FmB}eA z1Iu*s^9?=aZ_FmGKfExfUw z?u8{Dy7_4SZJ+d2*IanveCCJ8;SmY;!no>yYO?ySg)CMrM%xg^s(S{vr3ej1Um-bq z_#mjJc6b2LyWR&naud5K>XV1oy;^>$t-IxX8LSA+TEsYe!JE|7(v>A|3*Ws2-_nfF0yU!#`nOa*33?)Ye6zuVtFWue6Y z(+Ec^r3vNyhe<`JSW_`4g8u73+^x-H?`QojpVUaayK8a`6+QeOZy(!|t00tM$hg7& z9yXjkYC}^E2h@j@Yg3IG7reVXl3Q@x{-dGhpGMy)0~?pkj1RW)Z`ur1(+y7AC#Mw9vmo~P!_DSTx!`>h6yr&s+?~ce+0!Xl#lB_~vxNY|=)j!U_RA>K92<(gk|K*FboKQEVY~ zF#hCeepG5mswxxYL|BYR4KVFQf~ZOpyMiO^a_P88RpVTqL8_#UF!9$}7S3H(;#4(B znN9(qsUz=HcKRVC9u&zZG&I9NjSS7>D=kvBCsNLb$d2B{OGm>ENc zdd8oJU|L8Q!lw`XiQIxOXSACNA$$bXC?xcQv?sI{)2BykP>^H{GUH2-o;^AI zORwKT_8mh)QIIaB&?M@SVRgmImbIt)sXQ-JEli53569BPLy5@YQXy2ksER}BWlS~FHl+m!3c!e2kjD#hUs$g)r-XSF9 zfNK!z=xu~Y;_SiG3y)+Va|HUrqZzs?U@#g_Da+8?M-xZ~q$^HQ&M4x-jG+dVnm%A1 zZx;jdLZ)V7OCpC5^J?9Kv&|ZgW5PKnow~Mo^&u3)dFTSG%W3n=6L%cIn6V$CF&-Hw?FR{B>IRrb3mx1 zp@+1_;LoN10tZvys*g5+I|WVVUUI=GoX2u*qMZ<(elrV zRq!>YPro5;U8^20(C|3kKC8nZzDsIDlD4=f{_Wi@WHw;y&Bc_u-e~0;Mps`;<7IO8 zTK*N^*Qc7$w<#-G_M&(oaRaQZFCZ%G-<@!}7&{<-z82875YT^rGQOwmf>BHweEqP* z={lrh@e|NxU$HA$=W_g*J*8s~b28#oxPa)i0J@Z!w1l(Wg+v42 zLJC^Eoa|-tvWi3Il*Qef#hsbOU5*k(D57|T%F>TARY4UQ5HN%%;sFFujy=daq1y-| z%re15jMAY)C4O|@oz=f322chj3yuK@@o1z9+F}57gPWYt4#$d;Y%EzL zS3o0I!PEo93s*suH^{L8Tz_Drx8%Mj^!tv&+l~NF&Zns}a9C(kR#cnlxI2RC9kw^+ zemOns@%%QKJ;nj|zcE1%LL5b4AtMfVoZAjF{*Y@Z!p8IZkQ<0)`4i45h>f986ldha zZ={}>&J#p{ClC*xkD!x)V9=F>TL@30!G0SUo{piQ>)uojrYp~r46cs{6zm@MO4d$y zke0AndY3kvau^m%-i|-yIs2knazr#_`oZvlPy$YGTB53dt=y0!U_zolAxiPsB*nFRsX9-1aK+5FhZX-0z9(6&paSP^Z4)8Rh$m?t4Ygb574x}t3ZSJ*f?lok7<%oHiJto65^1393t8(`Z zRpUWpkyJ2pPsH7OwvKarx16x z5k|y0@7%gnRA|BwF%uyqZKO0=j*qzJKm7qSPXdHSC{GOJtPmkZ0)$JXO9U1f@0Ek@ z^!J^L9$ol>oOGAa*PlPaU@R9Ya98O52$<~HU>vVsHkUs`!f80}vwiB!h?nUPo{C}E zs40I>BE;U&m|{2|c>*aCAZ-FE!%@8upTL8dfgNUoEeY7I$V2qdo;?vfvBavQJG?+S z!71P!rITVoB|>H>lD5`>Tmfa2kzhe3)Od%`p=^AWG!yQrcCUyCqy-B`kRQY1ad@?ZJ$)EUetf?5XEaK#ZeXdgCaww$b1G-<~W|DCsK#D)V$$3I+2_LDwV33^@^(vjZeM6f-mGvE0 ztrtr8>Y0>0>Yc>N@yKK~u-f=E0-@hAU(~tZn9beyY|Qf{((i};b0yGJ$Su#Xl~kBI z<;~B~yZ9_dG4uqZSodH|@^nP%E}JR@y<+DS&BcF|G}IosBWmf^W9sB{O5FJ!vrU3c z{7W!)Gyg=(9xkvku~aKlT1aaIvPf1gS2PEtSnxwHrAXi<269a`mM25VQ`=FIxyL(X zg#-Ejp&sU=%$epeKMkI~&(JfKNbLEmgLM^U>6!8r^T$CI?>s8DI%o!E=FJq%LxBjI z&>^`reW*Hn?w>O6Ob4zKA*AWLqV8XVeF>diC^XBR@aBI@=C9Qd0 zVRh$2cCHWE#uC=&y%9X{G%4cn)mdi^%eSMA7I#10$zIpAE6Nv&Q*3onazg`t!8z;c z8JEcN?4t$Rjaz+jBh-o!nZ>N=G7h$bji03h8Iv7rh4vc?3uE|VWqxD?W|50wBK>Y* zN-+)9G)<#tV#tP+8cGqmQ(OPT#H0^k1{gevou6gd1A+_~Oz9GZh0_%4`BQ!&FSjvD zyg_e>~(k+!!g z)pr2)ZJ3I!Bh15j@@wE#ieQk=T~SAj;1OlcA5K2HY3#C zkrmG}EtYv|^dhA*xt@d8j3k@8yHLaI#!@{aU%X;=V3R@a?80&eB@{Ecl5x3n&HA2c(8P0dL*=Ux z(l?XUwjb~2zC=2`yTuff1$;Bf#b_<6eR=|(oqkq7QGs*02cyO79U6?WtB!nafpcSd z*+h3!BTU9C;d8scM;Zaf1PlfYaCAtSGRCLo;Oug_J-PSn!+W(<+e+&GZWJ+w9QOy^ z%_1zqV3!-z{1a5_u<<=UfiO^(Lqok{15^({Dc#7mH-?`{v;~b) zqWgnLXj$vX6I_9#)^#3Av-L7}v+O`+-UEtqBIRC00cC`3h-QQhxli3&`2lg`Zsgpo-QNfP%Kx5Rn4FvT~AKZ*3dIx3OQw zhCsa^GLs3;3f{7!H$Sy1j&?j@g%pJ3rm?n+P16c~a!Bbt3$$&fixnnLCJRrr%w%o3 zbUdi|>eS>lxmSEZ|BdViE7<#pb7X&X|Lh0-=5CyA{Yvn4Tk_W^eLs|?PWDTRIsbC= zL2TSc@>Dvjxd-U^?d}>c%r_l2(NOZCz2y?7bIfo`rlxxOVMRsMlG3V@s(M(cnw7iS z_@zT-a!1+j?Y&a_8XHDI@iFXDcLOXlqY42mzxXcsE~{o zY#?K`u`O~qWS;YGw_)Ft~qca3_t=!i2+7+(s}IzC;4NYtq;X;wdV38LX~(y}1i_ zKjPC;#ANK1*ePa^ z7t+ct1)OE5?OL=JYnc|-WiAerRj}9N%+M=boh8Q8FxmM9?A19qw3xlyyJlC_h<7R-DqL-<_G77}6 z71h}-KAT{h8ClCO)hyApm*O7`-qR0Y-hnvuxa058pPBTQnsD}$>+JKwR&myLy8SKf zwtB3YJaj#&h0tb;?Lw7{pzWA!%~G4S4ktk7v(oVk-@!jt(NKhYMf|ms%3WAo+gj|q zIMdqDYCL`LSa|w9vL1>_it9Gz0ReTKCpsrQ`>0zp42Z8=VXj1GMBl`lIr?JkVh64IqJKlt^WiXXtr==e#mQt05?`{gt{`ji<+_ z^8X?M4t)O9tNhq00^sA5{7LhXe0JdH@DGBxBs9ln6^yNEczGzZvPjRVMm64Wm%Q04 z)Ha$sbpyJN2%kk$7=guXx@^|1Kie zTQ>u7WmkCe0z76zsN5Sop5?!|xpEcruS!XU`%ZQK zo!m%Y)#)CbRt-lHV4oxb5h|dJ&FDCX4c=|uUoL2p@?D!>_xyhqlLCuvJV15ay~qQ$ z@FtXXKx&w6dlA4Xa(DNAm(JvS|*H5FcM+N@+7$`2xW zHH#`Hpii5QG_l*ia3KFa=uOpx61Zv3I9s(I4Nsxg3B(&ARvW}~%u=eqFYdE=5JS(p zUFnpc#XUf(PIu3nIXmTMlo|gGA!uGsC_WTb4{FA+6)-Ud3tT6|D5uw(O_cY--97rC zkV%k{Rmxv?|954QA7RE7do@KEM|v{fRcS058z>P2II1JMg&%}NKPod7#U8`HG`wwC z_UDY!Yo)df6cg0RX+FEJG8!I|hj;?xu{01w{Alu&6ASN#>q(_lYdL}9z*eJ6CH0`g4wpSL)r%Cp*@5?a%6C3}VblD=|c;=NR~Fbfw#I#MGE77FY$baoxz z*m1@RW+m~r31^(PRsRMpHCLO<(-~BktHy`M&jVVc#btw9vXQJ_gP46%zL42e5V%Ql z-7INyxu}C=mvUXX)7dv{y&A2c9uN_a~qwC4y*Q@qTUlRgLa8k)!Wd0yNNvKPR z*_k)ps@T~ESVkuDqQ+GW921UZfoJA{w4pRrlv!6e^YQG3*uThljm?vZTN`uLkmfgR zg!=%SbHod)Q){)&_aI%XsG_}URX((LCM2|o&Q`;n;GXZ@sL-N1>nK)^j#?FK#S+R7 z!LQRY6S%x(#4{+I0bR&4A~-n{8-pyyM_mNgq_$dGw_c@Ha+XH6;w729IEN}Eo3;il z13o+mis%^UoPAYb1+0$X(qGpvh1!hLjMD3s>BU&*GK+JrXRg6h3qWgZ8uG&*r#y*c zR7$LDWJiMwr4L*2OnkxWk2L&b9`_WDsC17aiS?@4q-n+L10~*~F0P8&z*;Yq$u~uI zj9>Boh|q78=^-@r>U}u0TFW9uWlWt{c&IOpRoLywT8HPY?M5%fvLgG2wwgH$XjE`d z!IZ9&@q&c7byONE=ith^X?nt4g@v)4E)V|60O3f_51UR~htZF+l}qJhWExW4-j-#Q zNNJO5n!9_=y>uVI_Yao!OhRWFlj^AQZk=XES8f#AaEo?jGjIfpI~|a6yMOd5&)JJh zCyPG^9ZH+(%SZ$i$r9I!jk@!?4Z6s6*v`on*$ui0g~3Y}EdI~|3F1!S{oW^N(;5nD zMvoFF$ljVQZQnZM$M&Y~#EGMdN6A^_AC~N+iU-L7xUrMxOqtT^?|~E~W>E9RvN46V zITE~-v<<59)n_o3jYK%}pbidJ4VC03g4?W{VT&$nBGwL@Rb|pPV5-UrhC=qax0|k^XBH!=F+v#cq zW3bpeyzyASj2lCR)3nZ#?J));Px_(yQFNGYN%M3lz%tw1!u#1VlUbNZYBv4ut zR2YdX=a|8wQe`MAFsm~Z=V*4CR^L>4Iz8k(D`VEG9!~0tzt~vVX>*tBYF4s~!rC*Q z>S^6!U*scd8TniJ7W*ZkhdmQikJ}REjXzpCX?WLDWrg2@tm8#xfD$0PG2CbWn^nc6 z@W(sSE#&Q7RXDf_map9lSsuPkmj_tYUW#3<+gy*wx`C4^yXL}Q@ zcXbt%Ij5*iPwp7v_wGcD!FdM@FEae50GXx8PU@2NAf&t#2G8>>G+ks^lq{%WL#z+veq5eq24(C$e=47h$t^R%4n-bWK4qp|4qYB)I#Zqmf9>!*{DL$| zv0R_YL}1))n*V&9n)HO zqh#fkkkIvr1~GN+{!DcKZag{aG^HV|DQ!hLy+d>H`C$8YFr#V= zA2tryNU`)>e{J|xJpe!06_!pvgIFn|vCPqq5OXfjsIs6yEdoC;HKQ%3;z2S)MO-}b zEtE|$U3)Y!seLIXQ_F~-2$g{0 z7@y(91pSkaZy%T)8_sbmUgDR=yv~@+!7hW(C0Kr+6M};2MCC}CJx>0Zk0$vVCh`s6 z)jgc;ejFqW?*Vk|q%O-iJlGf~MCYrC9m{m$ALQK|KMQkb^4=)qKw-Y@UBGOo?#WR- z6hjw+I)^s-$B{wy;OWiL#dJz8={Caq-R2c=WP9TD4=#$aXOd{fF6h3L-B5IPsPc)) z08SSMae5dTlFOp!)J-}oQ}Xg=*3j;*_i8~3e&|l}<)7_*+a}d1E44?xviBX%(;12; zHOK2K@I>A=OX=uVy56hgO*H4-RirslvDs+W83?7O*^Z)@faJ^9_bC(jUf!Q4-81h2 z^RZBHqeYo-8DQnO9e{Uo2Y^}8x^=Y~*rc*-kDpy3`frhKi^QA;Wcu-8if=$cetbj_ zx=JDxWzYsk$CeGNb%q-s7FBXY?m@d3wHG&*$r;)+9k&xfP#(ctGFn(3>g@wlZ6vjz zwe!R8uUon#TNGF!FWB!JeRPtP3GCT*+psUBg5HUP@`{=o{BqYF`p@AKkI^q+Z&l5P znhu*LbvEwzr-?<{8F;kXDI87qMQ^oS@H$GnDnbR0ik#$$I^jFDotdC$rzoPM!vh~{c>3J zhr&Mg=c;qaH}}c6c?jnVa=5Uss7p11b)Gj1k}Y{w@(e61yk;qV7HC5AHiP5&0<(Bb zhc=ttU2t+5ZKjh$Zb7VOnuhWTHFf1d^|#3thv#+mWlG2AyU+;9cSrVCP5E|=CQXzl z{ek_yUGMV#shY`81Xhj4GuOKx#E{}Ur|R*oU`kyM`?$DhaQs}now1irL-urz*$QjZ z06pKe?lVgF&NeW+YJ1K?YXB7ku0EFz7N!0Zb#OE(q**_E@t}!@q09KKaK9F;ItMC5 zz=9DX9iGgg+}}V-1p2JmDTqf>c}Ywu1bYy5P&(ukmI8Rv;9A|nkaJw2XG-sAahCtR zO~DPJ$_>*wr7BIuv|hsw60FcU*}m~!MBP0^$7mVzYp@ z5iA)#xu6=(sFE5pkZBI~i<0D-I7%}KeouV@+)m)3asKvNs>7#F4)Wu!p1rzkf;9I| zl19cUl51^{6hs;5_IpT_-cq|w*Xk`S)Kj8qRMoP&(^$i<&|oq8+;D5_FdqBx1n>Ou zYcW${L{2i_r1(c|Aj$irx#M^P&tCn&^1w3hEt9K>6f2Z5mjk&UhO^-Kls=`Z5qY=_ z2QlK^jHx!}_n8jzrCm!I1h!e@u)fU!f^{gl>Ub$LX{XhTziDi+fwY|K#i@=gmHN)9 zn#f?k{5#b<`#0L=`FC50;>R=GCzP~BwoRr@hPeh)wN`Z@w@LL3&X_w(JOoT|fX#y> zeTOr5ru6aLi{rm}{FIXZ`2LmohBj!K>U|Z4LRd;GZ_D$&=~7wol$RZ?68@J8U;Ra0 z`wis<#6+RG`(`(nDMdtzdH)rUZ3Ck5^GSn#%tLXlL8A%4J@ZqqgE$$)-U+<|LNWWz zEc^!E6=zH5DK)szbyr-CFJSEZMf>?R&ud4T_>l#|9a?CtgFLC{Wb`|xfop;T()G{# zFi(s0x?W>Bt?Y}?=kdn;T0{L5<#T7B_^G7Aiy3+4?w#7YD`(+#)Qj!WT%I|vv=&3z%|A_=8{AK!9A-riI3I>>{#ey>LFoOgp82-ASOw?y7rTT@+M zHit*s6q)hw=_?x&4^CgL-2_`oK6e6U_3s>e)Bv=tCv*cU#}vr}Hd9Q1*2df?^;V@Q z@KM-oCrP`xehIxO*TV8_ILusJr!4X7V5BFy4s{F( zhPTh-U9~`a{`dK5&meDHYCwBp?h8Y8TDt~{r-vR*sFLD={1BV2f>Gk7u1*ba@Et!9 ze9;3pbhpI-af58b1<@71rsZ6SRE(ab9Xexbz(7buAoXvFEhw5aIv!U`>{P%q>KC}b z=&35A0;7xFa28EFW*xo=4#9K>u(Lx-JC!550{!n zQXEie;3jMC8AG}&9Kf4EO~k(C-6ll&oFbS@Q-aMcgx&nSo!h0pq2*9@>=*vC3jLYKw5d&cZgceho>K^<>_ucW->BamPh9$WM}!W3=WX8uh47{k=+(4F}l6 z!9sbP5~e(9sMX7xmD4D`gIm1 z=%~Hp{O;iFN|iuNnv##8tGSRQ#QAM3KxUh31=(h_;jX%p(lcOp;!xCd`!KBM(B?_0 zKQt+TkqG1|e270f0IGCH4rx~Ywoq^!S#^7`XdXMI-cwSrMJ_)`bECk&q(9}n$P00V zz%WFbEgZ-!?k2yafHqeb88xt&lX21jw+B2H-PF%VGicqdU4tr1VVuIWOK%xaI%hh_ zrI)Y@B-S#8EDDG-gaWJwI3_;-379b!CmE}*<0IYyh5+ixpP$zl7Y4UmfNJea?nGng z2x3$+0d@2%Tq2`>0U|wqxPqAJFSse42SO4utdbUo89{{I!=&F?>}#u6aSGePnH>?kd7mxv9 z<-0u#bweY((1u|E1J4Pe$@2xH}VI|Cdj?&4(#*{b7){+97zv+wg}R#&+#c+ z4Wsms`nO{EUWA*pz)&}s{Lv~lJ;^V6iQaevHBKVfEk}v!BuF&+pe5w(Qd&Tj7P$KI-U;qAHq_1QAz_PF# z2r=#X%&{*q5c2$D^4a)7(E}i)!O8zmN@4x4l)}jJf0a^|A}zZ~hdx@2&vz*YO(86> zjDA3OAWlDX_*(1das}IV-8It3-PD_OyjX#fr(FqhE!S*4z zjJ=(A7UVDunWadSm~t<}`pIKJ8D!n^Hyl;xx2`4CUg)nkOEynq%b^66ZH6dal#E3Q zex;f02EsQv#L*^n37Ok{#|6ltRv>H=VCbf+;ugbfu*wD)l5OazGG~mSDRT6dbR znGWlT{#XD93|Ni!im%^hKHu*56G2vo?T2K`%o`b$MipIFGBiaTGeyQqSUC*%H&ILoc~>&}&$ z2sw&Ei&R+nZ3mf!HEJviPQ3oI5c1$XDtJFRB#)5v^LfImR&yts(rrpxA_u?~w6K7@ zaS+Ww9awO8ioPvDVYjRx(CQfp6+0Mkrcp+caQuJ^>y5gx)7;9z*whpAmuPjOG+W1g z2XcT?yDt41$AX#YLF~q%DBhVjoey669}Xnc%a+^-t?Su@+Qyf?m|Ilp^j>*X>udFJ znIVvlZ5a0W`BLGL+X=i-iks7^ioKr0qvWgf#51e*lx;E7qaV>8G2^tK$86MR848~l zP#D4`TT1MonSnoLG<0OCr-SlTJJJ|CglUs$LS?ABVPtZib>6|Qsw~@}gx_|O!1I6o zSdqlTnx9?f3+Bv+om-UpIu`(Waz|l}^@mt-$Lrvn1Lx&DXo- ztqow_3qXYwwJ?g4)DI0n!>0Utr_*tV^!g7o>XT=tN|nZ3Cbg;+u;1Ip#E$g8Q@ORw zvR|aKgzk3N;{vPBE8Y3=T*1EJc^kAoZhiNtf2%v;`m*f+t3tE&khCB< z_mR>4xz0i|hZUgPFs}Hz8T25}K(*!IZ~-a#;mra#>=5)~PZV*fPx!FKu{9f_FNcjkv3LLoENLN+|%k4_Uei|4wIBITROLIX1=u6UA*1VU+3}KL;Y_$WL~gr z9Jb_KcH~^5Q!_*T_1j`9YY=_jkjp$x(a%Ej_0b_@{Xfdj#M?71&}dHoO$M5kXfIEB zoEB^uI5nSi8CVf-o25w+7BwF61iU~p6QGh-_ssS)U`Kj$2_)pEBTh!bu3(3u^vmd?N+0&D;g>F0`|8%+E;gTO)-|5+XL;5 zjf(+bU9BmCykS07F7HH4D3VOP3E7G<0MMQ((6d@Qlu?Z??47Q=1ol;~5rznzD>TAU z=ij2;FE`cNW6LyG+HB;Xm&=1*oo;~FjhsV}1PjYfClwAja?toKPfJc6jYBq8mu0A6 zBhM>qjaGQI=i|$H5gYX)D(L*cAO89-fSfQJ9BfO?LTuC&fSzbcVSDzj0#2%z%bg*_ zJQ;?sKNv8BxHdd^RDw-t2y&|{^2V-~oTCxdhQfY-gZ+w;O9RXNdSKh3 z3mD99?Ha@67&$nu!(?y-dT@>#RRP5y+K9tO*&864G?W-*axS!1u2JXOZ(-n13J305 z%yHHkpiee)fxMWa-sTCLvhXbFXAm@6qMM_3cLwuIv%mrnnc*hC4s|0~&Mi|@U^H99T3l|~pcGZ&06dDINU{(1Zp=&F+B`q5Vj7tmaymCTa|ZsZ zY%`WWCZRPuXBo#^pjg)5Yvd0>5zmnU+>uL>u&mEM09YYuLe>jV(Xs*itC3<=2I}== zP-%)rI-e!NgM5gYnp6PDb>*xltQVZ2lDk}+*2o1m2X^yCTqT0&o$d9*D7{Y3%O7<@ zeB;lBKLf-8sQ-KjSFTX0uVAG&Squv(h5mX+zDsfbs$6QK2f*9zSOR&uPzAD3J7sN1#7K;9JEWDK+{eU-50mDzEf%j&N zdOf>^nR}O==J$k#;1S^BCDDt5aZl}k226RA@rGfZBf5ZKwXaUly}s;r{_cd{5~q0< zzQXNB;&G4}-tdR}?%>JX&rULz=bY~O|KpCi&rAr-^8aMr?}wUJOI=Zc zn10S0d|@m-@0;TPQehfgVf>`8nA#nvKGoKx)b^(UKf>ve;vJ_+l4bLqAH?Oj8;y}m z9iHP?auZ9P&LI>_3-bmP;_=hv=InCd-TK16bS=KZz1-1xeV}=ed_ceTTf&_*qdiTz z>dY~m%73s(>rM*o)_8OtTYt@XyHEA}aGA{RaGB6{ z+~CI1cY+B_se(mK;Q&jVykz5sY>ZzGxxm>?&B`4P0^8;7PFRV02itSEPa8>F3--Ed z@=k6=D9HNi@{aV*|AL{8mkQ`h>%={PGwI+R?=w|pI?!Ww4wkp5Sz8p2SFN1GT99KH zRHYyoGK8Ra(+nZF#pRbH2+nIHOH|8`qe!5TCzsm^6aXP8($|zh8;YS5SB|0nDJ>&Y z7H*akLfHv0r2ZG_73N1V1FQtl&;4sy#()u+I)N@nV5pI3r?v`#xk(CpiQgfU%ajGf zniKEevMdk`9{)D`BHoih|k;KZ*HDQMpGdc)arp7e`2SJ4(BVEYosC$Li8^Mchj zOvLVuRco5Oovtp}I#KUt$9ezm<9<_ad&Bv1^x*($C7KyhR&bPKAI?cY+kcL;%jIfj zOZ+OL0RTxzIYJ0oya*Y+N8A7eLE)*GRp*cFeVXRw%EugJfljVLkwHSrGxzfLU$ zY0<%Lf-&>P4s4}prD*3-gYEe%*q6FOK`r|U4Qpb0>T1TrDo=)78ge6%D(~%%$&+H7ygJ6Ex?kgSjz_xv{N-F^ji1aDzbT`*?h5IQ3FB zS(~}|1-yjnw@?gp%uMk;m15)wl;DFPPC7+7H4BCeY0(bR8r+1xgoL1GvY;*%8aX4P zX%d`U(SR{8DtHsPjn?PbAl7HiUhy`><9lC-A7lIXDgt!O}P9_Z_ zi0>n}jz3z{#NeMjBgjaQqP|!b%!4!Nmwlqz@PAVdMNOoe42(1>bFJ z+n-Bw7FV5ZZ=OVtRP?Fc$aQMfh)satG6pw8lnbWl)Nri;dFk!6qhDbWMryT z0j*I+DvX1U4VbSW-q7}P$zCfixuYluqXSXikuZ{N&eUnK70g}Pv0(Jwf%<2E2f>($$2Z|7if~|oQh^EJ4A#Tl-sDv}_5HWgCAhEniHNCf$dy%d%>}bGsm< z+0Ldn3rN!4=;ZL8<=R4~Uas}{t@ZW3WtqC<&M}Onu=af=nFcaU!Dybt?_$$Rkgt1- zI5Cx$P`fagk3boCji&g%L{r%oTEO^*8(hE)8q-A1%@wro!XM@%9aPL*!hfPCv5~OF zYb6;6W(~JEP<6Mtljzo-C!TF3{U&Emg>y(lZO&J2@`8rRD_cu@+-m~Z;PbV|2dG1 zI#&V!7O_noZxxTBg=jKgB1NX~p}h($$A^5V(P1+EJvd+i|FJ8T;TEUl?o7R`rv1BV9%c zT-BPe1G|V51JNYFVGSGy2I&y;l;%e&Od57l=^#8{L??UE20Z3YM>7y77HPmpkOY4C z0%yXb)vPzYSsa;n@g8!kqB5J{!lc9Y_!+rW-Vow(`?35xeLs_=)qA72%!H1K=Y^t~ zMy;uGNh9mW`j|D%l#%E2!iSl834TX+y`Dy1E38?DDX~Wwj0`UB&&f*)POL$fbDeXP z{3gOa48mUDrujg@9-2pCO><3gO*Jg^ok~D5$gkM%Pfx>KKz#?Ao8>I3z~Gl~1_^|& zBE_GMW%#&$OuWJs`qyqf+2iEF8F5R<3x~DOFBIc*$<*9D@+>Va<$G#IyG+84Rjo`F zkBB!X!Q-=JDT8k=ouLPVvRHXB>UTk@dZn`dbaF1w*5{~S=dPt?X7u2UPat&C+!nn( zlH05sb|wjM$vr8SQ}zhP75fsPZWS$vz^X4gG*Hw&pAfrnI~vQ#gkghHqc1vsV!K-F z935@h*zI-f03=703_*r09x7%m@P5<-Q$!b|>51vcMosj33Wm39#xCP(;H7p$y5IB_ z%WG#Q9cEzZUs{nkRkW}ZQ>}7&R0ox(U1F*g1GDYTIbB58LVHktRND)=XF5wH3k)F8 z*pq!v?<&Vo1P6TOi^t%)2J>(?fg7KDFp1H4a?(LBeKQa;^G<8zZ5e}RJwQnzY9ppe1|^*t>kUJQ zHWT(bSt3+~U@i57?z{QOMdV}TlHSE`V|#+0Mm{ThuLF`Bv|%|>CTOhv_}Dv6rUr+5 z_8o=7@)Q=c)G^jcqCR;#`X+(epJPW6db&T8Te1|eC?(32@&bYb1Rj0^MEeQLO(`@f zJ}K$%ge^q&3yujMrGkL;B=JHWiIeb>)Czos2^H8Ej#my+s%d4&;%;tYRa;!{pe50i z3?y`F=wV%4N%G33XaJAZhqN*QBGdTb`Ja?|1W2<0(+z8Hpy^WtmF<8a zi+eg4FVyqz>(M8y9z7IJ%QsE7Z6$b(wSP}J)LH9zqIW>g1_4g|*F9TfVbyB@h0Rxd%Txw!RNRzEY^-wi|a$o{u0XMYD|bss7sdhNdhW%&3_fKqC{ zL|z>?BfM~k{Yv7BImi0%c;_Tg?bFX5ZP2C>d`w<9mPe=kQ?*YKGsbayCu{-ICeA^L zDtamO4*4j!EqqAx_Oo|TqX4pdZ3j)6e>>#(dlz?4mvk;F2Bb5`FFE#cQ`DtO9rKK9 z<0tA0DG19O!y<@4CdBJw+is^NA;g}IAH|js;5{v;aP9DZg3*4<>nD)y8*gi?VVDk@ z;O05OhP_Bq+GjT0UH28@V=hJ^#z^BLEsR?`)b2&Wl5lFs@o#vBj8$*hx@7DaH|;w> zvN{qKMuP_rVCVk3Aor;64&vV=x=A`zq`+Z6=A7Nfym83vvVlsExkVVBE}th5BkW_r zQxx)8^fe8~W#mJiz#N(?48kWDD#mmSm4B`T>45Tkx7H9;JQuqOq}*=TU%hsNuWtH; zZOWi3n#J|$x14Zt&T@ddB*@ul{9d>yK-MFBWc`+^Bu>z{p(!-s1cZ~s&_Nj9J4@6| z!L{>!+$jKGe8rxaP2>(W&45?j$M2I52-7D}OScS{gjmPB2a}gRCx#JZ#w=a{qToDSSKhL@;y}p5Je(!HPuq8V4nw49YT-~ZT|<(1cW9yvr&&-l=M;k zW2P&2OU?TAs*Gbv@{BuZ&lIYmsk$AQvf$xLZk3xUuYw0L$oPrR6Xk5aD>{`I|^y&s7pDHb5XD?X6)B72CIKq3OO19Y3AZq09c z%&K{$SGkb5Q^u^YhL=jh&Z`9^HM@c^R)ToZ=vQ`7g_yW#rz*jVUeK=*6S0~(5{Xcg z;&DTPWN|ByNs}cIf(io9L=qI)I#NzXjz+p#PP~f?w9GKN8!3(t|Kz?{oL3;p3{uO# z+8zq=qp~i6Wnz>XZ?v1$+I~!HriE#lDW{1{8C#?IGq+}I>#lOU&(86tIE~HD-9vDM zdP91x`RlDUgX*t!x6S1D$t!lo#=PgioY*D(x_xw=EuK2FkITHSK1ENn)5k*O;so-} z35M9ygR;g=>IRc4NDZXZ#4G&lyQqaq48ILZxTGq6wd1%X8Q@iH2Sb~dDbMi$#f}3F zHyGT!P@^xE*R&5mm+-r}RUc}brq3=K-t&W`z+E`$$Kx20GiHt>8fF4f#Iu`gqMr~H zs2Yd`vFz19D#qkBh0&R3A!}d9n$wBT8KX}>+RL=b)L(iflBatxeG9`PYbP!_|as>qT> zb1M`eWSc?hxcu^0MBSar(d9xPv=FjrM#E;R#*Yd{(Cb@aCJZL({+YnO6K8F+hKaEp zDj#3cC&N?8)3S^u*S?yGz@9PL$2XfZ*_IOd6P{!s!K>D1+0`p z#d;G9CB_Yn;kNiz=dzBRJ<;0IcKB=Kb}DwIFCyKUl(dwEE0S{jvXVyr;5CCECKAoT zbOY2t0XpSjBZg?LjpCa}PvDP)luetO=|JS|Oh^3qmM9bP2%8ZBI&BDRCxe^^&zH0s znvOr*mGLpxi8*sRAY%H{ff5ohXY%xQ{u&vblOP}rNwV-s^Q3~-+YAtlDYbzBI$wWj z_$IF>vcDFNAJqRM8E@ETOx@r220yT#S;6lkSUds$yVB z&&^>Iq|faI-sxYKDLPcag)qx7N|G)~u&~-Vl~O#>2tK^eDI9CH)%J!lbtXHK>{8|{ z##Z;M4%euvkKoNXeUhcMVh2sUJE$h$`s=4lMoQfX_sS@p!4^oWQ& z7@olmGh6uh6jh403|&w^H%eao9sMYDH}dXUqZ-KxPODU4z03ilQ!=tU&epYSCf!bV zChbvSIf+@>9nCBqz1XeIyd4eM&9YQwu6?qt=_q+{txKW{J2^oe7dsiMIlw)xMluMn z)8$A!vMXf z8g$u)o_1iKy0A))xEJ|*jmdn>X>5lv6n2l(W^ueq1;59*VS2i%rlU@?W@TA;u{I=O zwQ;S!V}F;;#LZPVKYDfqjrMYhlPdDn<|&n`FW{G5V=X7W!Q%JCR@!+=LX$FKTCr3} z`y#{6@+v398ly|Q1o0tJ(kw@3`@XjFiO*4{A1fe1uqDQhooO$y`~9@HY2?C*Ix{0* z;`%QEgJ=Hrnv4pn6E<*6^-hmC5<8q4la!QF19^`T0+Rm7kQuOQ zj?I0TTCsH8(JauVvgN~#*b;*b5~zVzhswrEFRL!1i%KUkvzEGUgIBIC!e)9DNfj(7 z591&>xAPV=CCHm*u8%5Ym!5Gl)zX0z~gX z=F%BM6@}1*d_qHx7)e^0^ILF2GJmqbTiL@hQifNLBO|X8I99=r*Oh;;U*!V@)wxlY1KY+dcXl3ZByLjeVhIJfVz3$7sky)y6Ru71lu`SC@ez~GB@1#h|OhXsX zVUS*0|Mf#xGu$PnL;!jcpqfjbkkm<+HN#V0@B#b-;SLY1q?t77 zD_{>u3MVC#eo_ox1l}+4*`dtb!uMhc6?d)7trk2i0#k<+F*SHyh!>|lq?ku6kAkod z9|;xU|813?sUpF9^P-!F;Js}2oXq_57L8}d8`P_|1sANNTFMp4L0&b)K9f37teRB7 z*=(P+adCY}r5PhTgI!$?D*EG&i1t=pzssgzwul^FmDh7ZSDXyh4nxJ5rXF)`r zQ(n#u)%ZlFO4!?o4^sLxQj(de?`7{`?;~%B1rsvfpC4Pg!HZ_3ab~bM%nRo4Rrc~8 zUN5+XbP<6-uqC_VWbl!6eQ2~wGV3{9vXJdL4)x<=TMNxuz%{i}WlyT=2whqE>b(1@ zsiR|KWHUpxb<53D&`i&BR$w>XQ%C;P7`Z4rp2r#X7Ply(>zCit`@>a*&bKDNr=XXW}m^kVK0rY^f^NyoF_kPBdYjt1gd+??E-EiGC!NE(cwNO&7r zY0q7bVp@S{YU=E_P)8#fJA?b{zls43tz}B~gft)jUbJRmVY;}}+MdhudrX#sT&26~ zHf1{jWYw`?+2xV-_ja$XTD%Qp5DFuNjak1~IpuC>wx<0;jAn+%DI_OR8rFf&7gQQR zsUO=Oolh*mY-Z{SwtCR^FHj_yX5g22bn(NwlrGv1l6hh6Y9YRJY|)z4aA@gB{kyU;eX zL8Vf#NoP$JrJdu7e_-~&S5YC&eGY5H=O`#*Zq1^?P8CBX#T{yRyoV_{Y zlE_?!$5gy5db9k|t=X2ObWyFmosElhaM6gBseR8!F{p?5U{lTGx_!JTq7IlmDUYD5 z*|=`V8hrlT)mtxGiy`*U<3)D0f?}b+*)tLcZLVV=dIDWxN;x<`X#4ybr7#iojh_W? zAJ>tyWf|W`Ac~Y1Vxw-|dOz6LpBGXN#66_)&v?Ex+I&G(1J(rS!^CXSkLm9%?MOy?sU zx>|vSM6wLq&hm1zA?Ub6J5Wpy$XB)={6B)PHap2Cstzs*B%}kLH=?0Eeu{X*wr!w) zgfJy(1Yoq05kxPNg@ir*qkFn53@2T?e2OYHd}F?9B9Eb#$h({qwob#J*=VauZ4z z0x?NSXavXY7fw_f{IjP%Sx$}11Be_EUA?Pt@Ec@8H@{}8NhJKwzp#J(!}ASyX#~L` z<}rd`jlx_Nl0lBwZ8VOZq30(KPVWrByzqU}Abdwn(?|RJi*|;8`LZw3E{N z@|B7&p4p@sHj6FGA=S9Ds}-$EgHE4cU&QCi$1a@>cho==n%${84cWYd2(guTdRNk4kawasqKdLCg=APsl^B(Gws1L5`RJC06m9;c+V)ZGQ>3#Io zHpS>IZ#*r=KG;-p#5%L$xWrstk4QOkW<^yt z1|yNcR~b#dXbA2Y)i7yro5e*^YURtXT6u$d#F!$03hZ9uGB3UdIVSm#{djYu*+sk# zKlIC9cBrpX;UXT1qSWajgX>9)ap)CADwE^@m_PSxFRJ}OEnjV)Iw>GmDX=H-Y2 z1`aS98GgewC9JN`Fp;Iq4X3T;-f6vjcaE-hc3$4uZ9HoW?JQSVNKDK>b}ogeBxO-a z#mVY*_&J^&TQa+nJf_v+-<}f0RYcL{U7tL0_SASxb!(AmDJrhvI|Y=KVB_*!mr2F# zlg7Blw}LBht0C5`>jfx~?MQ3@I3@C0@M`6rRN@=qMcLVFlc{yE-$qScbbvw*i#Pq5 zm-rFocO!Y4rSoJ3HI5$Y?a!gDXC?R!(z*$ip!QBnl<_P+Nr7h)lY#Rh<7?QE@N8Dy z?b9dnel`(AWqf(}DSx=@&PgF+pxL0r!tXm~ityD8uQDW@-P&&$0j+}cf^)oJC z+_K@DvY;Le`2`(ugZ7}qWd>>l6tvIYBX*!nBrRjnu#6eGEOcqeSjcD~Sr}<}c?bRgUhpy+%ZU^1%f)u(&12?0 zbEgbo==I(6btnpegX1K04g`Slamg~kl7Pa34eB4}Ieq!SdlI>|pLZknhGTs2t|SAE z7$ZiyFd~M+Y}#f1jzt?mBMPMQnaK^@7l!Tv-|0Yjx4XqQ9y#A^LvaKwUf|b%+-;Ij zYw8TBoQkupy}6t|0@`6==?QtSNO;l_gN~7L=6P*-SK?}|^ZIC*E-y>23AlPvJ3CWb zm7Abi#=-^@qDN})xTsCyT?(FFOT#^o%Cgi3g}38z&}{A~vOB_!K)Ucp;}yMktW>Wk zURnG^N#3w#kIo{8Y7WH3zlJhaF0)QghPfmJssp^A{YH?OY+gXieB%4}u&Y;GZ+`-xJ9?`-Xtb$sC*k+i%aaKb zGppruTjIqVI7Ft8car3VVyH?xQE71$q)0#8c^^VPY|tn)DJNy{%ikd=GIm&igVZ)6SjHT=as2P*&~|#19kgi#TAP|8Ebz-Y~=(< zuHPM)MOc~n!y=mHWis}7n;*-@sA(f+Yvsx7!2*!+VtzutKpY6cc#r#1>+Y+dc2;gZ z=crvfy-Ljzo>WInPejf5x;APIu%jg_wriB@VZ3a1XFOd!z9d5ikI>D>%dxwLaWIP( z%cLo=PGBc7UDCW;8+Cf5I}~~)YGOq#S`uBR94Y|o&j)BCRQdfua?Yjn8I0l@=EAXJ z`uVZwf%}7FFtT0gar*>qD8qqPWiG*Cm0k5nyY^Pfg2R&!HGBT37)SUXfz6=5n(e<4 za+L+MF#;Uu*IC&WKCSF^Jr1~`)m*L2Q1+vFol1&-ZddMo;p?cP zCLPXOwTN0Ipep=aq5C3m%lq>r-1=*#|Dq>gwa*mi!_4?`1mH-!ZTnry7HJhaO`ZS= z4VTL#M|--HlVLcKiDQ+dFHMLRB?Q~Bl_*P+$yOIoR_w591-ej{bXk&$4zV2QP*b%i zEMl@|mmiaoPrtmte#6!a%%h$*cxG(qDY6FepC&RcLu26vsj+c`c$>Dh2cv8Cdb<%( z@neyI-Y877RcL6|91zV+&|WypSQS%sNOI~i@ikI*EVPg|a!B|&tSd_DS`=2N48tC! zh1D2HXA4_g1oPrOzq1?Yfd}1pN!&QIjEiGE> z?&z&A3IKV1*UH+0ntQ96dGnchhkZ>E${!kg`gtd+*4)9RvV>8r41Y~OTJ&s&iZDsW zm0ZuHtOK#t#b0a{zN0eaaBG9!#m-4EueG6OS2wB=fl#zC#wBzVHFuV&pAF8WhJxLo z2t6g1V>YWu)><*+YU2jy!P*o*`Q*u1|1Bia~(cVy?TU3R}*z4WTMt+0U z8Z*lFQoFa%B^#B;ujzf-R7JT)V&r7>v|1J8>{4#*(9pHk{V= zZ$6et4aWYM)lWe@5p>R*Jg(~;1FYMKQ9e-@q)0IFu&_2E!~YY9^gw8y5G?vDexI+q z2y+=Bl#lxgnD_=|Fa7}#s{>?9Ai+%jizCd3CQJ-Wo+1BhnU8{m2!c2mIvRQ1Ys`@! z&Qo-kco*XC5N=O6uLsm$aX>aVQ#APtbFC5jJ(asu2f219?Mauuk;q$o zp4v;-`yOiY{^AFaHrd;e1)eOOUfK8BT~A#4Jo?>U>8tMtBHfpmw@lZ}w^5!ZU+GN^ z{2amZnMsAv-sx|s;WO&m34=-{*>G88jhhwZMs=M^vSSRDds^!9zuEyR z=PI#-l+TwVl+}WE&ESyLf_0m$AgT^@WJ8qCXhaazhr0i>PO7RNvdgX!qH->y4P7;l zrD1^jYFbqf)f3SbsB#`d-2d+tFBf)lu%haE%AR*iR8^AtbnyTspd_4fpfWgW)qjt3 zuK7^eNb1&V?glWraAE;UeDbxRvXIfPdtjc4zMUMiK zO&W?kB)ebAzt76TxqOTt=>7Erozb+i!RdDXizlhn=-xQoF2a_{yUW!ZhpeY(i`A&B zUgHXz5HC=@S^`3OIGhhjLP*z3& ziG+~H)bEldKF7)Y$yMQ~4@pHhg#^Vm3-zcl%xIW%%GDc zq}QJss|;_;ffRfaq087>kM4QRyi9OlYtj?^!V>f7wn2q)S5X={H6Mo;KPPcbgpSrrR0n}LyC1ey+PO;obBM1)Wy^o^M zHr`M`x&GlR9cGqR%lhc31NAGWXfo}`c7z=7#%O<%LFrzYrtO;LIC(j}Qs1ry;h&%BqfJ3!xlj9g4*IOiPFZcS8B z-ql1@N_bC5_np)7k=eGRq9k*ae%ic>`2zyD_>fHeCrB{-H;`awX8+%is8*G-Tc$@q z(^?nQY*99$QbCOeMw%rU&3$-#0B(N<+r&Bn2~OqKKb!_5ZU>zw47(ayuZ9PXr8OwJQvd;sH16iK?2jQxB2VztTq!3S-d}0T zW&t@;u%Tkjfi+)ohy{7+uFQdB5LKpgtoUQI7vKbNSsVoqXZ%HaR&gpi<^pV4~im0;xDu_rbU zKhXv(U^95X0D~Ppn0K8vq%EBO0heJmtG%BiJ36V?$ihgOEfhq2JqA$n2g}U8_H~91BQV@fK}{n~B&gWrFEoM}unW)hb*N=hpc{9j_ zm8z**CE|kyLk26}rM*eOVU-a}vzs>nj6?5syRECt zv3>K~G&wgTwyu*=PhyT(CIh^)nu^bs3c$Cv;dNK9VJBLvd89MU3l2S$;;aMZ!W=v# z!o1c`5n^=xT6b^qS5v{yFcXKX)bXe55_ZTmlLU1o>I}c8!>2xB5_Cs=N)7WL4dqql z1BMhVSVTmkBNI@W$= z2UHJQuZ%E7Yg)2wtoM#nCd(0|tg$Uuy9Hvtf;Za^r~?{VpR#d6@Hjh&3r<9$qPaih;h(#e3L(3s(K0S__XXx~n-H8}K{R0V zD4R2Lr%9b}D=+0V0S=cIYfXW1U=lgbJmzOWoGzJl1jIi+yWLj1wwdrU!R2B~qvmUZ zk(l9|#2Wq*rz zmylWl1vI=^4;#g!@4zFWvAhKDbjQXJ z=KBvPo@mc{+f>8!D{c^5B2czw*0hDYp7y1loqmL|qx_wPa-)PHbHaP0Q=gp>6%?jO zvPQOZEMb)oJxf|aqAyK)A}#M|yW1J^7@fj$R?u|nw@z`4%NU={MFyh+wgi>IYW%g0 zg~Z1N6IK{Jav;#p|DqG-)1=4qOq2xAT9m@xz7+(=8hb#0DFJioQG9?}!Df<)C@62J zcTKa|9%lJ|y2oW0rjLQaxOHg{vjPZR_r&h1ALl;{h#etUYU$XyG~)8OLGs;oAUnFh zEjF;c&|irqIXVT%9U!!w6DeXJ`_)la`&`u6NVW5<~M}iVQQ@&tZ=x?z~Lraisi%2 zzA>X=HKKVIW@c0d77YzYc^;=mzt%$;SKZ)LLKON*hGTIAtQ1Mi5@CTDOqob^5I#D+ z#3lc@Whpf}^NoW{_yh@W0dC9Q0IKQj!@aeM{!u2P55$^%;`HcCht(Q@AtSk+1(8cJ zI|Jg^7buX4>D|OY=13XBen-XZng&h5U=%qgVnIcY&3=W%Ak`Rl29yo*VHi6Pj2M&< z>lr5cw8&Mv$+9%!R^g_p#j^Ugf9ZKC7;bzL{JTXWlIhLmX<1&KKwTuoQv3u z0Qh_R2T>ou%_uJ6KedMcE_0WI<9`WcnK)Sg*Z%!aAj@gtuB_6ia<;vj=)9=GnG+px5qwe-5eE&7qZuhkZ@6L;^O|fjYqOwV4Wg`&)$RDxOUuY<~ z6dS!?x%msmno9-Hd-<>5zi7T~66M2~qfB3Xy>Z}EH>JS)U zp-*qMwUo(_Ax$Fh1N>sM)#OIN<4sg@kt?lEn?><2#pCSNCYM&~E|Syb6i( zJCg7qgE#0f;5Z&quA==L)O(;QVSmLDl9!3H$6zhzhe(|@e;3o!amW&jK{HSV)}K!wn4Re%-v9rd#w4v zpz}~=^1wZTjBbQN7BK_tFOb#=Kfs)Lc)0ImjQDt?LLA$WuS3B1aHd8WbI4%*f=-q}fG}g~(gQ+|L-&C=QrY2Q`^`aCw3D>as-I;8_PWZ%9J-nA$@?Ul|9E9u3=cc%_>m%W^E*0!O;w zd_@~Pu%8A9z9Os~krM0vd_!{~Dwu2YHA8;bytHGJdV(M^E}YK&E%msp33j+4Y5O|t zfM@djJfeqE)M-6aLm^@;Xc)(^k|6wW%JD6${b1|j^!zNoF-WCFSZW7b)Y&5SCD&-H zL%c^>?s&gh_ii1MyCi+mtiOMp^@;5Jl^-uTA2-n+cW~X&{t5u-2FUHhc*~?}M`ty_ zzaH?Z!+3@EkZ8|iuVg3SCD8GcYiz+~#7pgyLdynAFsE7_2s|!V*$1yqx82i_8Zzt# zV%1~a4cN5f@b%lR%dKNw`}F4gCVswA0YT9kG?+G>wzn#*v3C2s33TL|qRZHvDQ!#R zW3?(#*A}O{r(d6ko$VvU+~C>gUL(mg$&5U(qpt1GxKr5plMPH7T|$U=w=ePF&d4dkfz_v}c}XWA%0&n2__PA&Xv6nWZY zpX+IKd?wFagt}B8s?X`sgJEWb<+{)L#qoE)>y3k{fjxDwwf=KsZX<7f?;WZ)g>U%t zJgi^|yCkP}r?_f)W`(EbcSnA7o=WHrJ`6T1Y$-G=^zIKYIos6Q z6x(>*l-$(Zc+X>gry|FQEz(Gua6H&Xv*YD~WV+0e9uO5dY1e`78^yp9bC~ZsbQs0q z4}@Rf>j7|Jl6@riX9Z_jlYPJ`N*L>Okvqk(20J>sKG|;hpv@b=ZGYH&SeQM6-VoaZ z4tL}jWbc6AJ_p)Jp#~h4@rpHJ)?jI)M|F-%s{{lenj|x4d_)FY@2`Jfba#W1=d3YJ6nR(fl>ixw^yot!#Bx9FUGvZX1D(60-WJ;z1iTOmdp(wHP;X&buRkz(n3 z>@$N4QEZO-8iJ%FZ^rBstPWhV`&RS`s}DQ$5e|(8>|Vh##pkNM70r=5C}B&?c2K6? znEu}&T;_cqL5F3z&a8ug^;2R5NWPSG0Y2shs4KL0`7QGyFYMx0<^);;yoj{+$Rg)q zUw=ksBs4$Y+jTxUrwwHzTN&3O8|0;byzy55r2-g$UGHN}EUAWv!5CD8{@X+ zZi%9f{>gSgj%_qz4K_Di7tAeNM(}>&dVg3}9>&w2lOsfBJw%)NE%rw7386aBS1=v= zhPE0|w{ORg%F&Q`Hc0=8aJFc!c;FNR`cfUgnvi;UNxU_KzAtoNdP?$>OP_HE4yZRQ z=o%nZjY;-I_|ClEa@OwfM$-XRnL)ir77XVju&Mz#c09x_WsY3S-gozfH68f+V3KM2 zUUlG-xwQeJp8u|{24@}BX785@_yRV@hq-E|> zyeM6|FuMQ^?SOt^d2UTvrnIlMD%5pq+?)9~Vd93LsbWd3N5oBVs>#0AGBYIVMd9! zpxW76!a>16pm!@JEh{M}FI}{P^GoX4S`0+JUWu(hWz9^b?Yo|vot@;5H$;l56rVDW-{G?s?JeV zn+g@ty@Qv`mbadek&}!|;mYOAvF_m(!^I;9VytO{u5vaR| zM$nEMs5E`s3I^*QYoCp3Q(T)z3NK;KvR%uZ<55KWUDWcwABv|?sgj89E>hcsVwA2f zHu2su%q+ST&6MS<64r9srnOerHN`bI3eo7Pf&lU%wNj*P<*pjhgJ#-PZ!oQrnj$X; zs&}G-EpVkZ&F_D-8NhdT;$`ujrA!-DJ&(efEmfXUILdK5=}9rip9h?BJ(_T;mhYwge#9T zWNg%r7?qk`G9*xX6?aSPGOQJ!)bU$3Wqe)PORR$nn=)ns6O;oE487t%I=g4iAB>F2 z?ei)LL>AG=%Z>u7r!RniJ8G7Q5ec3IgqT#GQjglDUi%`lvSZ-40s1*`7~J)&%m8M zp+r)G6l>;|HPNMe2vo27)G6@u6f7%_KP`!do>SQA=;HdSy*KU$J2_z0?dv}*m^!eJTbFiptdcCdw7Rx{9M`n7j2m6x=W*uOb~Khga>Rfi17;$6H@mu-BjBhD%uVDs z%!XoOd~mndnTtI7h1vFEmNLI#G*qlg+UQb@XMmAxnkmzCjwwkbC}YmMUg0^R z#mDRuZ#7Ds!OYKV^$WW`oBU@4sy6N7OKJ61U2RT_ zRcxftXNo>|<9vOV@(H8);5&kG2ge|Mf-ybf@viGC96W%8b2cbn%HmvL%L75es;USx z@q%-LdxGr{pq#t7AqWYMz%}S}#Do=b@5e|hTY!EhCljXI3-wF+h^AfyL?Gb%(SxJG z-vElitzOE^x@MJy5rU`(@_>npqaav*3Hd-|RW9QBScX{oU!SzTNeqQULSBO+XgQD! zfDOnF90X}%YVmmSzXIh+QSc-09)Deu-U#<1r0xYQUjWxD#pKf4b=B(%tj;}WPqB@8 z`8^P5e%?T9{khloa_jv^cS5L8N1?yqn#q9^!8Pjm=58PnCR8Q(HY7@7oAFyVgPfy_ z75iw)aqfJtJsOkZ?%A()#(1_lQ#n5>`-T-xJ#+U;0g}%R^3EIP9Ld1VbDK_+2G73# zTD!rZqK;8}ChSSFUaBtHhol`bQZ$Zc)Y6COuAECMoDWab+LuIVxo^5D@mnx(y6XU{k-x7?a5Q2o__5fYKG$^9W_ zLP%tXZRSeacOmBuEhs-RNs~Ie*jkm5)A{MF;@|jQ135UCuUs3JZ3;Ly#23F= zyL5rR-6FF~_V8ZT(po=KrnsFpna;xDX* zKlZ&fD}(d~S&k$tKp;TpaRwEm$JZ(8H!O%Dq*T1CfG}ZWz~ZTjQ7}?nvhB~jXFpiLnL|=Y3VX={4l9mfJ<|e|>OOAe( z8f{5*XfU=;igqG4YB8>+KLnu$B1*2XsLdp$pn$uHNyi2Z6=D~@qbIr}TFMkKHti$Q zgQ@@1SCk#To98WG9d4#6zDgv)63i1k2yucB1%*c-w*h+3y?$?5IQM~rVF-{1nTT65 z1J&esr)GJe6d&zcC?^=F=?nCz{P70iGi;|Nxq_=I9uk1dz*>TQ*yP_=$fpEc zJw})72of)emvAl6=S~Mv5T(Z)NY)Hn!wC9?`&b6)_C4)2YoMc%M zF)Cynsb!-3e^`6RAW_05O}lN|wr$(CZQHg_+s0|zwr%UQZFlc^cfOgOiG9C`nT?64 zUlmo6PgGRJQ+ef`cOEHV#i5ZO)1p}8@*1h`AUX!wwrCI``S)sMdkN7rmHV1*E}dhJ zvv6XKYiP%(r>+vq(fi=}19M%Hxp+0WUbs@bM7uz{JaNU>NU+PLl}OKPx-z~~bW7sb zz%PcMK6zE^%`x+y8Ux)s#w&Bu7&noHT7Z(GE_Fo)zI)?5iI_dU7PDv5XRbEBmrCgu zH1g#qmAyx0Kk*3mUy|5~!@~NtR)>A18=*U>2 z_-zkYQ5|j~8e8byUBSlvn@q`8sgYVlaTBz#g6x1!G+XP4(mtDHj%Uwu-Bqm5h};nkp%4ipsqbYD!PCiqfgHtm1I&ZnUl7T}Qr04pq;y zcRe@74lc?KS^b0msdepVEsGTztE!Q&uyY~Sp{v<;`KYdHRJ0t4GFS+Dck_6}cwJvOw;kciyao6M?XRMps3-Jv%46cs zm96{P)GDrOT&U3X!i)h}j9Dh^gH5$-3I&O?4-pEHX9$NbnlqhU@Q112 zVdcBTXWmL(em_zzu9hT)OBH0_mqI81InpAo{^cg+5Cect!spRn6!<^@8QfI)XQJ%2 z_La;*01)qC2$Q6{8Q`Xmo2g|VI8vJ#Z*1pq=LTlFWZi?tknu=5@iwxntpPa0UmXb=MEIOeiWw+2PcC-z@}>W+97=~rrwg1E}U0w zjM=;+2?wwz)+{A8GZCo61p#FKGJ-J+m(wNiTt1F!7V_36o{_L0#R@O&njtG@+mPM> z6~cugb6oC|bClT({w(9{`~tFd{h{B6=_K>5+l)jlt+%}I6KE4G!=)%Xh@=D0{N#k3 zvI9+A%TGyNxqqgZK2amlI(yv+5BPXy&&P8(N8yuvfcM`7SDcu_Li* zBdr9n*eeS{iWvB$Z|cEK;UM?hVQmgAb@oiHd{$TwXSItF772NV95;K2Irrdd1|0X{ zMBqdzGKMA&rYer648H&-OLvFhuM27RkSsX-naN=^2QKXzWuO{Ul0<2WM0ml#PxCQy znKXI9OrMGfKKiD=oU>idI<9`GLpSsgidL(dc33e%U*q>OsIy||;Z}`?G$F2{mRX$Q zGSofpr#k)UJ%Vrge74#zCFfm`GUgSZk7nhX?55Ybl@#a+tm*RJ=Ox-T&7AhC%Cbaz ztW3s53x-fk8A;|tu2jy^W7ee?m6K}SF62!#`3D`x?evAS!Ee}QWjz0n!87&5n?B+C z)*vH#z)OZpOmsu2=FyCUOQ1Q#@xKmT;QBRsU*lj2`YF%5Wes?~&J)F5;ck&0+YopWu?mAA( z3BR##KPwID0>$6gN?Y!<^YLCN3q0DaJI$yZW!L4bwXqTUvo?UsiupF0j4c9YOKEEI zP=6~g4;l?k-Gan+;KH}bz-@BAw(TQ7blp~l1mjL-Bb|`rlT;^cFixa z{D-h}7H?BawFZpp<%>aHXZ#wQju|It?G_epswoqUH{#D9xW0>^KBgb#R~Hm9!~dUS z|A+Z^@2v58?>OKP$WJ&VGDLH7d9L2e;m_iu|0i-^L1~at904#?Y%d9VDc4CIB6 zReXG>33<_h=qT?K($~k(nFXCfZWqp$rWidBqrJeC29f`?+|Ke}DeN(_v;LoQyGORJ z{3P4}0YcXU>L=iwRjlM-iJsRS#uGRN4Qx12!QGp=lGDkrTWe zAEA+L6;xdZtwbO4)CkdvfvL%71Je-^57D6N>*^-39DF`?ZzSm0p#vSZ8tVe3Mt1^2 zNDkmr^1YsQTwg`6vNmYa4nJmXKJH;8TUStSri`|pV5hO;rt3Nf`4+HYdnf=~Di*0( zoH@;Brq@X$6nQ>;O{6BsKPy@Mm;Bz@Isb#~W=K_59z_sAR%4h0fiFZA5!ShA)MWGg ze6}OggHVsf8CZ@gm%;Jpr~f=-JqOE;tWrlt!Xj$*Y1w|KvKGoEpV$D>^sBo~trPVe z1xe$nOAF(Q0kI-We>rFnlHvEH72t*_kjr@n$LxKE4*-EH!OX+=xrLo$@|m8R#0JL6 za-0NNw8Ud%1b$5=59)kROBXr#X2#gIp^=bHqe#G7vu)r2z??2ja#x5;J91kwT}9~f zI9Lo(&XgljANwSqAHl&5)xus*d~lSqW@gIT>8D9;L)@vhkclU!yxcfJ6x*NS?G^Rm zLedmk^aw2Qp?kURqfehe`pU)9WazGAI#T6dYS{!%2OISOF21DpV!I9jfVUG413D3e z*pC){z9^NUi(TLn=RUYwCrPru)ZmTAVLH=fvst-cZC?%sq^ad=Xoq5*S~)IhAK=k_k|4cUzqtubkJf3$&~<`VGJ`_35RIV z`J=dB3B+TB3w;O}{WXXkM8vZ>l^kr$WlW3&KU_*pP)zx>hsjMGt3D<+h-7yD|c&QBa{+v5pmqt_|Lr1{;%lI&Bo8f(sF*K3n<%>sK%(hn5`>06{g?{W_74&dV6Q=jB?>)38o z@Iop(2Yj^QQHxyL46C2>Q87CrLb`3YzD!+TYEPE4jtF9d` zj#dR$!5ta}Xzfl)TvRq1yKcL5S594X0%AgCIgkc1a-Ow27k`I5fMGvto46T0)~VM4 z7ZLTk*{xWBIX4pZL-tWfxN&G3%2FN+HU!~qkXBn#5@`o^DPWJbvttLBYPe*YA{A0D zMT2tQa6fy8^9bSx1xJfA$zr7@`3R_U*rRMn%?s{miVHPM4T&zxN&&X^&3urwWrv(3|KyGPce*iQdpj3X zI~Qkyf2dZJ=!Ko^9fa&XwErQtWMgC^VCQ7grI$4|u{8X@&MFu>{k@TZzFG(2IJwh%37ox|k9${UeeRz1ZI%1WfzTBReW6kJY2zN^=_3YP1q&x zZ27utsxyXhxtX%fsK?emmCm}Zh1&Ai%lFE^=+;%kOf__~U3pEkg@FF<3B@-Ndb{}%7m z*2XMp?ho|q>a3=&Zif2SclV!MT>f%tuJ}B@zDaZM{WAM->wKucCxd;Rr*l0BZrmjI zL{l5J2=5WD=KcQUV7JTKQ_<$4&9BC;b}B26c-jzhuI;Sd_;VIt&AWT#YGaiCDP>wa zQk(W@{`zTBW?Yh7(7)6itcKq-=piF?Sr`G(SCz}c{tJ7Rt8*-qwn(v~Z>@pj!TAub3n<@}2Ed50wS zV)Su+CmH{z^dI&0-jGnu}!8RVv zI0n)NRdbEv>EEZ(e6VRe9>G&Ls()#>_-Z2|4F>U$$PMD?c%XG=`?B*+Mll$GVrxh} zu5lLy8eC?{TlCVQRvyX1l13MO1>5|A@t%*?QM8}!l3$}C*&Ak01MBo`;dLL|XysLd z-qy+SrXZB41(!u{;BXm``{$w53LhMzzCenbS76M@)0`I(NyiZ&ScprM^ZnbZ%++); z60KAYAs8SK`NN>>oT^$3We-Zbh_84B0jg%-n}d1UVXz57&_Sqfo;INTu#KND?2ga+ z?iiFsCe{$L>dlL0zS=#6YYvkwYWb(mIpvvbA4X_rPQ{%Bn_FkbQ6r-zXIAOq%s+E! zF^fcHW^JSLyJ`zA{D|!;KNlxGgnE^rel_ONjHh7XmD<^Uyb63c*Bo+)&f_?sn%4j< zbFv8{P0ezE*iV^+l;IdU_2zatg4B=}ha68MQSf^#?b?9^!@Gg@k`NuKiKhF&h)MH-iD$zftbTo&~U;>febNe6cJuS|we= z73BXIDP&?`{3?o(--+LlC@T8{(VyIwBCz=z;J&b+mNeRn*Khw54a!U#s3T;SeeR>L zDX7wD0M-kgs4fD?m=A$zL82}1FQIHk^9Ed;=IPf{#WTd=f zq%R96TR{N8K$W8g-^`68f)h~YJkCFLmoU&>h+*w4XC-W7O^%PEaL^IngIN%yn%Wd& z-b0@Jk(a}+`JwV&DA6fyc9sR_)d{j!3W_Gx3FJxBOk36M92(s3SRlZ9%m@f1fTv;d#?e~W8?CJPpz#!ag$5+J?c}uuxbK5jPJLcdB zc|)ExE&y=Ot1J0l*Zaf>xs2qQs6IQ=ejhGV<@&=|1mhSd=;dzX*gx$=i_x+f#N#sk z>0ZM6wSS0YDPRjeU0o|~^Y@?Aq=fE*oS>-@yE2Kl2hB=9riY=Gx^WACQyXUuGz?NC zBo&S{UcC;DY`jCtx2)UE(JZ-Z-PHW|%G=LNI zS5)fg1BgvRzcW(Ze)A=%*5M6jrXat04NNY|wc~yhn)Ip7IhQQPiR)jrRq>&&xQbIO zyKoz+JlydqoJCPq>C8NJ&T+b*#)6wAsizk0oui;f$U&2I%t>0ltt0RLU#}5tdXE+dm%cb;`-jJ47AH-LMy|F`~;yu&kYHq+986bL74>+swgXcE%$N zbTG0|KyOZIHFU;78(srmE7b&n=*@KlnVzSgaBTpoxjSkD3MPuA;4$eCSFn=Ywc`3X zv4H&d7-T7i-Ka?BKg77h@@I|wrfgVIIA?JZ*6=n)i#aH5^E`o)_yi^_qa>wdLLd-y z$0jx1X@S5p{t#M~HzI+b$=mooFc5MH=DXHH92zu9a{Tt0M7aZ_<_4=adl{!(x`xfO z8v=y_)mAIzx1~-+VE)2I6x0`q_$`;^!!7(oix=E@m&F$P@5eE)_U_KAd4Uz808&&KR-6h2#B)u{2IL_eRV2Hv{ z6Ksa*J?BmuDd5PC8ape2S&Yb2P^zOS4S=B7) z+{{YE*4GAwc!f${$`#6RT0G%5^)Ii#qsIztN`#oh9FXjRnaszW$BTh*aK54+qh7oS zybJSD)*rH~sY}^#&}G{uo#rFcQm1JEXxn~U90X{>Mu4<#-Pkg;aC(il)Qq;OE(6l?L zQ}yI`#g|svkk75Zg)1mgAHi*y7A0fYJr1=uEjW`+^>9_#jhr!${8SJ}_H(imbA&Iz z=!#S%{mpKy7W9-7ef}p#iZ5QufLDpwTr#1HGVqlt!b)X}97;QHitz{@Cr3~kbs^51 zxjsCdC5J613GBt=e*1U}jJZ^sn@|FMGiOTcNC1uhMYEM$!aU0>TZcx;K>E23lQ%xm zxB&oT(5v+fq|OKEWcI{``+kH%^nvM^E@a9VQ<~hupzA9=t;|zIMLSjk9opY#A{D`V z^<<`nkbs@os>By_W{Z9^00o~mE<+tO8W5?$O%|c@A zWyqf;R8fzx33?tM3r?_DFNnLerx|f zMQva~!=@H}5b)5RBMXyTi>OWT;*2o}xesGyCh*(d{BQzZ=t2k&%wWPaR(KdN^H|6# z2+EBUO%d$ncsX9G3s|Iv;-Oan{2Q>diwkK^tj88_^H$FfuWJ(#gW!r1*$C}V61f&vaMdrrZ#vpdraKo3*c`JBsSBY zrOKN?T6?ULn}1;jR*)aQ=L3_Mg_28JFkx^3+CB*)uzh)tfB={tevtscfJ&@}bSVgO zp8(^Q&eqZ*K_%Nd2?@G%Ow@awNK#3}E~BAayMBJHe^M!l?1=RIbY(3Bbf?8U0%#JN z;d>I*I!V$;5#UJ!8U=C6DS(h_X}YlCCp5b$$uv%kyOK>|IF-eQgIF$(UObmhC&3>M zg1Rm2u87XfYUc~+!;7+p!bIJh?{(al5(*b#?Fmaxsa?n$1!hr-MD$%F7k!5!ZLyvfVfEaNYQf2 z$ioe#y67Sfsr^tq^4j?nQV)%sN8&NAlwHK~^49c2qMq6+wa&}r!*GCODXC4lZegiS zM_U`oeNJsrFKR`)>K6eagMk}$12P1p+{+V70uqG^6dDT|YMU(yJ=4t0Z{@-tyqH%j z&H5#R^IY6lKk*v1mU{T4LnFH)?YEf@c)v5+4G+n%Vb)u*DDgqR!PqgwvCE$dJ9`&g zqwb;6Ydtn1YVeWa!c|MP8plOVbxXnj5dblvvcjgSC2NYVl@i@E5#?@FEm^YAta!|l zLUn4Fkkl_{5a%xh5KW{1Cna5W9}{?AQlcWBe)-N9vTeEter6bh?&4%|rh;-bvtixk zYQU5+IR3)}J&Qd(9<&!DaCT^3vhS#NkmT+0X;5jBvtopQj@Et`D7;NPuGEZ$Y$H0B z0(qf|)b+N{yf={gu7dDP>k$h{zHX)1E!G} z!218qry^kDq?aRL{1+-AZ)BzHV*7vCzQqm`lL~`EUHv>flR5(n13*LY(__}^6z{tdeP3ljOChA#hta{dLn{DUq2KR}oN67^?hXZVk^yL7Z`al{ePMmbXB zY4Pzl$s1fF8ac);#V(8K7ugYrNcKVRkX&|ba)Ba4Op4Qg=oJ^8&(6;(a&n)zLLi2t zRvWvwd0AEUeO=Gq{D`sI5x4yZcXzuue=ck79=3aDKPt^0o;x3IW=%g=&0a6E*t6*E z46;?QU!5PW+AV}`i5`vFM%ZScnjNLs`|a#@vU@jrcCKwKs#^TK>y=|$cbYaUxAFMA z3ND{*TEit{kg05LuAdt$RUxjP<=op{cgEa4-t{eC!548X81Mw92t&qGJp=f5J@H0c z)tWc8Pv0+EEv1?^Cpk)7_0DPENprh5VIY>hULRVs`15|wjz2G)jdd5L#rkgqv-SG* zYkm<@G<$x;KiGTM;}g1A%91(wtOgH-?(BriZ*=a7Lk_U=+}m{pt8tLF$}hwgXpbWG z1L2!N0)-ti1}9e|5Muj>)g12XO}(Jj^2L0#^cVQuDcJoxL~_w??A<0f`IS z8n`#hMdf_S(Jd>LiWbwJ`<>Mrj5Li(?hh~sXLD=z$`>=~J^y|;KRWAiSx1YIU^M!O zy+{Pp=_=HAWy`{$i<{PyUZYd;c9;E+mAD{C<6xI9)vyMeHI2m1($jI$g}mhX1-tPS zMgnUZ2ks4fIo>os$D-Rcs#hTDjpduu$oA0eu0(=)wu7ycl3{@`nIdsFoFtLCcG(7}v zn|jD_#mv%gP{x8mJ}^Y+{DM2PVc40IxDvZCZGs?n3KGByhEMkm)R=Tvv5>~`mi(p@ zCbP1h3J|iIzvs~CFxfka!OZK|=z4)e$Sp#YPdp>Wfa1k;Xn@@g2BN0}qwi4{W7ld% z1O!3|yk58UZ|Qs^e|~DPEq3?vX&77(j6&-?Q^2wWhpgEp_lhbNrGe3ev5grP&cH?w zmu49?(9rj4z!+#VtrLPzy;wuNV}gvg2peis#X`C@s9J!R458GiW2>%myctX2Ukhx( z07nPMH%7U9lYIYcEm4${3@cHx=_3-p!X{Oz7>@DL?=9inO<;>c3~3wuA_*g`(1|7k z1A<6(dS#y|QDdeQOY9Hmz8ZUr#`&?}<$b|(an|*+hx_a6VBcSW?3?r9ZM~+>78+Rs zzNNbVJ@B=$#dh$r-Gyyr?LmUR-}{>}a=$SMZnR%I_bah!^^tnOd8mFocvlcYWIBPS z#${kz)uadg4_eZu#G-tI9XsI&k1@}mB^s#a4S^U`0aYTJ}k0_5Uqq1Ns^cVleVnQkgXW6V!(Jw&%-hSp%Z41nC@ZAdChFlGgldRG={hh zF9afL$MieV<-n;eW?ac)XRGLX3&c5#R8A3u?rU z>+^^c4cjXeufszr96=$M@p4BFcM+D!P4i%0*nq6)W-3HHsFI+yP=@3#G0+Ow1m;of z`P$${x=g81J8BR&xRKWTsnHq~j#V2tvQqWMiXLJ(^}ojRKe(agsX1f?!4!0nUEndv zUYiqs!B7R9Nx<%LlGqqwy}T~91!L3(GZ^z47|%-irS%!jOX?#Wd#19131|9w?IGn0 z>l(d@oSB|wu9$!DC6ZM=TwI(6hnR95juTw5VVV@;*ba1gMv+I?aYs+tKjq{oFWlmySeAn;NJUkX zMp;D!)LI%a24*3yC}h4A#iQ4T@SEC_zr!$rmAGx^ES@zzps$Sjnv3i$dT?iT9W;T7 zj4H7%PFOqLp%Rpm4H z+mDv86|*yUL6<(2S9_D+QzROe?Oon5R#({vrL#T6%vuz`xUZs+Ub)O;4~Xq>mmjE( zwl}h}yH=d3LJE!ZN1MKto2_)Jxi-pE&RMJ`8vGlW>GeEtR@9%mf$Z*As#TT-nN+NG z>L=of?KEeqYGHrkBDLibB4%>^OvIj2_AK?d0E4LpG5k(JuJ^2}puMC^z3~{%TenP-_^BXYNOH1-03p}7 zf9Z6|a}l(wqu|_sGHu^KqGs+tKuzh8=z=H7Or(=>v{Q|TTvrYN~3Cd*9IDJS=WCGWm{|g)&1HB+0B*VSxVa5Gv z0k^$%yz;?rM%W~>F~apZ$t}M61n|L=l(MqUO-0lQ7=ImOmo+5TrN(w($xGOokqoK? z>UirdlErAZ%pF)RK8DmIJHG#FwrwvrQGnwnXw?+ExH69KvaAbk=JU}P4>y(~6N07U ze0X=C^#Nwlp{kHteKuZoDk%DZ2V{QL-YJJ*w#Jckws?hvQ^#AR8X%Ex$3JdlZkt#5 z;mrdn*3A5A`XqiVpJDvs?9mt0vq-vs+>(_1Hi)G0rKmJW+dLZrb<)V=c){iem&?P) z@Msl6D(;v!+MG_q!w-quy83o~FE^=u#G;$BbEcg(FC-6dkxG<%a3D%CZN0yd6z9+W zE+38nrhi^vnq2fE=6ly_g+8hwqaMrtA6gZVa&Y2|>}YsU?=H*(zR0+WvR7tg6HQKrw9Gt#|4HmHuGnd%6|`fl+p1e2QPRux?T z&~MYkIJp4^wui%6wxdTQR@^a5=_$1RdDPsu9IA^##{o;z22Hb+r`#NiTiwOT^(Y^O z;cfmh32a!toBd<>X$jP6LHOqSH_LCr^($6Sv-W(**Hi9HQ@KB8;y3fP453(@kiHFg zvi@~{tiC=JP z7pGN;+!fzSqsQBjyIN!eHM43(7RW2*7LtD9iMtV{3wRd3&Sw!SLY8U3rok4+A0))~ zn+&zMaK#C)*7Ga1^vD#MV(9a&1Veg^AnFpGhh9hWeLus|Zf#-Ih$XnPQWTa$i#x&C z(GBOhv|@;9KZ{a0>x?ea-Ml2+L^$LGd~obChE#WB-6802+B?Sx??v^MaSL}(A&zH) zUMZ-VQlI(7UfK|>m{xEX9HdAuS_bXpP^{D?Q-~M*NFWf+PEWw{U^_?tJ1~|UyZG_e;e!jws(WNWSoQK~ z?5!d`cg#In(o$9(E{UAy3)Kfda{pt`W5%d$0I}e)YZNV~@2Qw^JI({URhl?ALr_Wm zyj$300*{av*VB2+?~G>cbqh}2AX@y)c}da6!P2hphOiT(&7GuJfvPv)gmvF7iP9!npplGQGY#LO6} z$*SS@D5;f$$%!!rySHVW=m)eedaQGaXGUW&OT^>qNE^@SKn^&pF#y6JL)v#9Vd=f2 z1qeIg)owJ(^j&%TfewOS$nH!A5i$8IK#kZJ#{;0N@*#pjHvNTI!Ej-qQ|U)jbOQlD z8SX4jjB6iOwnTdI46{rN#K7sC;s1Z{C`Zv{+}0C`~zeBtFYo9QU4!u{QuIp z?!WG7|F^JW7!^(Lb^z-QsgZ015|a~fR%E^OLJyS^y&xf+f?nhU%##QZGE#!$M}MOH z{JOlhb2j5T0~dLHA=RrwUUyb~w_W9@%@VuW@}c+phMix>_c^}q?YdX}=$$>KDa!Wz+$P&T$o`4UVv(e`{cHcc(sph8bVMX?;Cbgd z?7VaCyy_KJQ$W7gFUNXx&8oNCEILgy40fKf^Xr}eiE{$)yr~*{ zxxKe2F}`}er?pctc9}9ZbT4|f>k{1?Gya+McNFkxI0g&LCrIa;<5}p_h8_K0I|Jma z)$ULFqSBs^E79Ja;7*LEF1yn9l~t!>uciyvth979LALUUYCRBs4e{+fIK zl^B$ z=8$cVp@?&I9+B$s>!Fnqy(-J3T|W9op(u0j;Z97mQ?W`iEu{`7MSRx{v&-izyNPVK zrpxv+TNs6ot$FM53J#c!1+BiD*;DtV#Ji8(;MT`vFY~`Kk)HhzN;=C3`Cw|aF0VOWvynQxg zthA$WM^@rW*6L5s`?HCNN)(w3OCKUf_lVt{Aub6$)qZ+D)NO1uLM!1(}b) zpxn-dTI_9A&ySM8+y>eY9+i;oH9AZ7mVwAx^@Jm#1?~()#WcNpxn<6a)ZF7%4hE0y zNjx=3A%jh^6<%5FtQ!}rlqS2?`5tE?s(;t8hesF_4LA|3+nZ>3NSK%LKtPB*C`RR1 zZ!dG~{7M#WHW<$iv-z$J2D6Gz*9DYrcasv>={e{J%C-X;ii_|P^##HPTZ|PoQX;qj zfeLkg#Cua8SfrJ4>-fZ=9mZhsu@XOVfnCG`51W=Ad;fEVx)j5hwOdTB007h=BVS0I zeriegJmpV1#GvaUFh`$&XoI`J-T7k1`69E4lmPvXxhux=Ue|iJ+@IFHlM?{gG@-X5 zsh1DI=Sbz(1@=^3!TMDtW6>7!a#S0Wt%nqB$J;)n1kPf)$BUBus@nr%x&Au3 z`_!t9d9N(+`FfI##~s);hP-{4ba-(fG7*=RX40q#S3{+`dt}3dN(-NkEz+ge59T8< z+GGzDt6TsXjW0)fO#ho|jI62ODV!@1pRq~t5RcDsa?)5few1uX>@@FmfKSfz}{{-Vlq40L%7666X3A)ol4!L4b zE61Rl(2>M+xvb2I+WD}EXapDZ0C4&On0dq@Y7Jo?pCZ4%Rw+Xz2=6l zH04{ygh)cc8lIFfvY^#;kHj0hm95lDLn`8reo3EsbXG6<_CK1#)PEQX)YC#Ev25tv zG)}OXXL?@HwySbcU0`0MNV)j#ySsa8fHKV=y+uzTXm$#)?s~xGY zia`2AZA9uE*k=Xm#XaUO3|!b*x#e#=ihEqv6Kx1MLHtD(&Mvb)Nfch$ce3Imn#k-U zN`sWy;r69n<`(gN1P<(VGkmGaM2tsqxcgN|6=v9luv+_n_ThvlA{W8ar`lM_bW%Vk zQFmBF%=d8y&U<&}?-8Lfa^#F_eb=cv5)QwVIs7UX>m~-!{Cb^ZpT%N*d$RLl&5QQr zx&h0MTUZ1?`ike%f1&eT&eCtsCBvPd&t{=_|85?f{06@EXwSuScD`93MIHT+thVMB zc0m9dM*N0)oyC_*F$&7FzjoHxKg<4bhp$^UhR?~J1CZ+8}Pbj82*;~#Y5k9Hm)2l)7K#H-qfdN zNt?*EW?cz2uHn1AapGFq7tzbB5ntKB<@GTuw#?mdRjOetBY_cw{z`aaQTVlxfBPl9xVt+ zX@3bF0@wXCvl<=IRAHl>(kH4KgS=2pMgmojYCHBsH%9Y(pmZ-`z{r#=mO0@_`%1B+ zn}|)SYgQRT4B0SS{_{N=*@m6NExd|fB5l=M6JCPCGKlBvqvftd4TF$9s?(!)2a)N2Cw8t`QV8=4O2T2k?cIJ}miLBnyXQ#F7 zJcHHf5v_(Ve!5=hk=9|Mg+G+phap-5M8?3JjgpmxdMDextjfzo*7$D25d0WIMya=2 z>`@W`iJHQ)dq@-UD9o~&+-N)a2-Xc&uj57zV`L?1A_vxyW@4M1oNV<$u?DdmEdkuE z{XUHG%AbIV-Zz;@Xg%3kWLKX7w@|}E@PX(_$|8*qcw8+_dGL7vwdEde-bmN6WFV(r`$$e5K}D-Rjee4!INi$@cHYmsu7V%pTjEf_IK+V@BVxz z+Zp|$L}-Za1EmPh^vG11%X}kB&&0%-vDup@%(>`zrkh3~lj5MwpzF%v8aZwV%f2A% zLRi}bsPI|;*tQlW>3};g#3vUxM?ML*b5iG`Dzc?_`e0(OJ`_O35$8IcE`W!`;O zTrogznl~nXPEO(_g<>W+DeznGGj-yRk!Fd+bL{L0 zB9VWkv|GL>%G;jU3?B8&Bhi0f65;j8ijwBr|GHRXiB}!H9~+idrbw6_(w{N{0vi&` zef5F81ObAzS)A;S&sK9+7&AsPo5cVXLH`V8hx5yi`eHrQF5gj56qO?cs zFgT`OKQy#-2r<{_*eYM*xETDaj#ZRxK>m{uq=)3)|6ZFvbCxioTa&D$zTSjdMRPPf zojqnjdXiI@enUyISbt0|@9WXZW34L9$UvE8myc|+K`Z*|5kWSBgBb4m45B8eaa9Ovy9F#J3wDYuc6^gVPU47m%luhZfqC1;P z)xN)dnV+^?f!Y9`8hjJ&$Jl>JRtGIZslmhmijRdt>NT{>N-jm-I6CRlHEbe}V%b=c-oEYul9wt)&^5xY^AyI>Y1v%Oo-w_#O=XY<(CKdU#9 z%W-MG4oCy==|kV^MKam$xJKNLKuF0sJ*j$bIWTI|?gK+vH=$2rO0S69idkn(Bb=gf zHqa^)*DDr4Z#Q-gvtq@nGeg;V)Y!3mZ3K=-a5XiozU`5-m0Sy#K`6Gl52ESl?$+F@*jG3t$Z?FUznhYqqXoDP3N9#(v~vsp#PxHfeZCV!o*5m%x)gZ}BH*eW=$wXk=} zNcltFxg{x#u`Ql#tHe^@j!g)n9VdKVB7KNu%06e-Jn|M~EvwXFF9gb5eVh7t+=<)q zi!=v#K~qW_Xs2Cvmy5YP*Eu3_k!8Yg*tRkZWfeio{#N&QPA`M7#?pw>u|^pR?MWFY z@%Y@?;_UhUTuW*9{IlThEN5fZBp0UBP!))2iaPXEMrTC7I48)r_bZwtV0Ml{0=?2T z>m6=^wsp_i(EE#8e!ru7L&Zwbw;m1qU~mLT++$Y}M`;0YYGa4I9Pi;k4C_us;P{C0xnM!9?w5@bckW}&LS$|IK z&J9lfK#He?KI~!bEIePw4Ek8XJl0D_6K*|RmeYWL{b5;T3WV54hV21+4hC_B?#b$M zMgTTdX#i60(am^^CwBH=yT7)!KvF2@on5WvA2o!3pnSg%4QK$fJ0ET9L>y z{UEKGiR^dLQha_yz5s_rT7L>-`?H#*$4#@NXb5FL4({so(=Evt%8rCq^2K_;qCda0 zP|e2#nTy}tn$kW{p)FaCx$rosJUb11RQ_z$6&rOs%h4W#>Gn9;Znli?VPb7#nO>*j zm^rnv$Bi`j+}+&8JzVT_Ny@od9>HhwA1RdU7assydhnYLIeZ{+I=o#!yDT8$(@`eP40`4Dmy6e!&YPFr(@^>5^5QsYbtuu5fu)+}iHtHVuyh&{CE(yiZSXQZ z^waSDx`jh*TR&+HniQVSua|s#7kPVJ!Auqhmd&;V_jihH;4FpF-6-4BqJJq~0MgjS=gg!-)uZ@#w!nX=1l^e=_+E?ZiK2{4k#+ z=}Mw`TYvW^ja<0~bP2YlGbwhg*+fY`9P^>Q9Qi>)_vnU_q0u;R9yutfyxH>y%DyV< zYu_Pm%Fq}acl1yTP7WHadPpv=J*^{1ar;nPpNS-1MlJQQGgXaB=-ttI)e4^TlXbyRD#>apD~X4PkfsoGqcw!Mlm@2sWN?y z6kSKAc;nXdJ$a55(*u@sr@>&F`VVjo829Kup)dcr9sGY2`>6d>>>~vH^iKja{yT5- zf8#!k|I+*NKP~R~hm7Q3!Oy>-rT=zE&HrB6KU!=4_UedP+PIiH(TmyqZPyVoHMTb~ z{a5V^Gb78t>p1hs9@f^5#vMf@)2d$4iqx$oB{ z_%j3+45RkVU+iV~H7&E+ei>&ZnZKW%d3}CfQQK2-{UA+>r5X9y`t$SAdZO=tQHlTK zz2m3V{c-o1=Dg1P^5gr{@_qp;DXBW&n}t6kifoKfpHyCD-No8*FU3Bu%5Fc~yUA1A z+Q2wM+32ZNDF%)|cr4hB8tcd>(fvjP-SxY*0#cBo)l;OQAJm#HZ&iC6A|rwF}Wc}fqlTcj7{EZU8JOHXX{(XUe=hl8Wfanp-hOI|1Eh% z>PGWI25ByKne6X4KQ9+D3Z_&Wi;VuiGb3Tgh?J~ZGY;H@^-mF8>#{a8FO70YY2}k( zo6)2Wrr?vVn_zI+daGMo3T$tGe;8v$i!HP4I%UjkPTwR_T4r^{H9@k3K0bn%VYm>7 zN|-0233(MQ{Zq#y>oe0FN}PC9e-xcNMd`7v&(9qblJbK+3HHXClH1Ju1>-w zKk9anz3L+63pfRS$5LsXWZ4l>W2?>sVPOj&VPlRbE_eQ9|2UjzL>D?Hq2gfT;IC4# zDJ5pK?~UzyfiZ5>>Ycuwhrdk>P~3qn38W}o*t|0fAgG|0ipy>=x+FLK>i%Ety#-uU z+t)BG9U`fe2tx@-O#(wnmvpxb%+N3}3@M-@AWDOPfP|!oBAp^3N-5GH7D|Uos36vR z2o>+;-uu7L_q^Zpe81264a3>z?6dY-Yp=b}*{e2}@f=}!oVKb`T}8d#RTw_WRzaNu zLHt}jt(oHW!{@c3&B1Z(LFRmtQqq~cHSdh2WLk(K7IP@Evx)+@sU#SQPsL1$85;FM z)O6Ww>&VV!FXC>9X+TD$FgaPF%b11^&{JZx&DvYDSQz4{kiV!g4yuqRZ^Z_Wy`41f?xHX^ zi9LF9I)k;kU+4CmJ2j0(21bf(IX*mO&7uRPa(xHXnv%a5NHoKvWweM#u zsaYJmyi#xE$c>`*+s}YW*~my*(igu}>Xek2ttFKc!uRM!zj1#6n+W~MPh4g^j>@Q$ z)#TR<*w-tgdCbiw61ks!PQQH=Rm?S7CLz0cmp(d=oWGZ}G5&t~xsXgBJWty7X9{Dl z$XO(hd(P3(A5dF-6a!?KiWZo{1=ic3vwA6Wy)@LC`3$egAsi*xUeU{15j@NrxWt=M zZ3ub2zN#uAwPb|QTU}e-0PhnU2s~e z;~4soL!kGw_DO;JeA!g@-QxRG2+k{3sge+fw#1!2bQRoW#CY%ZY>KLo^B^ZBLX7gt zO2eSK1fGx^Z|~J7+ERU_>fM)#4?#GctE5@#%B0792p&b8DJ3ZctvI^Bd|U`7mS)#$ zHZ8hOIKApa$(f(1ih5vu|Gkk3i?VHNkf74Q^CRk$S`is(w_KF{xGv}>dBIcQ9K+>P zwgr{n!ir*UerXKMYvtXrwfiuod!D?eq&C@t~(-&8sf@yslx zhPa+5Y6?sW6`l{Xx}KWlaL>{2N#4t#>%mdO6nrKcU%C?2%cn&-^B}jsnI^XD=ae%% z^y640qBHb7%WTy{nBGKz@WVeG0w8oryVm`$|v`AJbxT zF?@c;aBz>Vl1C3z=x$!DK$&k%$0%D+d$$-%EZ~L7cE_ezDQ$4T)NQ zA?sW3OxM0n(;C{jN+LZ`Tq|~ogVjA4e37a35g)G;w}IV;KVxuw`$Ad%sc?UfgI89G zGil=A(gw=q->kk0X*IS8IWd$JbB8x)mR`8Fk+p6F7T(F$Hu#~6-LtDO(lwEX`AhIg zY*1GgWd&uTthFY~guMR~WT|bRk*EHrk@ZJboMx=v>yFW7^9o`S<0KuLUD);w>drq;!BC=;TF9+SCw8Q` zJ%mW#R2v0Lz_sNIeD@x32D+-dyq%Z7p#+LZeZY91cc2iFI!Ine56?Sjr_l={8N;Z( z^sQjkvL#Kw89?oP8w6V*FFLLo+1eH*7rss86Ug#QVvjK|`Cxvv_l?W(84Xlm zu*@4dZLako5f#t-4EANKDlfPw>*zy-{h3)9V^_xAbbYF$+;lGvUUk!|m!1c9OPg9y z`gn>wBak{uKEX<&VIy6#U_n~DL}~R>!NJrO1~0hqJ37??cv|+$N{jcd^_ZM&o}3fa zM}4*Sp999SRe;{9Q2FtnvN~Nt-R^}4z8zng8+xO-{Zq$Bx zLDw*l3P^c!1G-N%E9z#n^a+-!5@zCaEpp$w<6Y#krteWL1c;Omg1l1Mub(%58rUXH zbq!MK#J`en&|xLZO2nr)sT-fK|8byGGf#b;T(aZQx-+?Px81G&3767! z#g#dqk8w-^T4o1tJpva_dwi*sX0<#Y7HkB)c+}1$-Hs)|P70M@>(bh<7R3sP#`Vq)pY-AWFkJ zJ@@w}PPwqPOT@&Zdzz1IWW!^iyqesxL8I{qtrC8+660&#n&g+H(IzP*oKo^_dHq9o z^s9qtW#+^BmBVX{4FA6@8efyM|l$MIiWZ=+qh>^(4;hwxE32-LUn{J{B zC!EQJkO57F>Oxwdds48TyvdlzaVTPd=?ptrX(g_|N9R*X9NIjt=@INuJb^j?)F~N1 zqnUN?o6j7p9phirIGAZn=(e6C0e@jfusNqy9KW*`^%BHnXiMUBt!<<7ZAYj|`!XNF zQ{~Hc*xTdp>Kq_Tq}PY=mnwqYU&buFtDOvBdLE_vl>0L0gK26}G8bi8z1v43x$!2F z4VRW4t)`1MEnu(ZFGMwn7V+GCK5}s&U_}cVHFHDN8eONSJ#|M@o~6)!?DT3u&V|N4 zsCF9n$r!-hu@&A`AH@!VqZbn2O_G=Y~b{{WYB1zX~sD5&KgV^_WJJ}FRt%>pe!O}$yqYEXT*CK+#L~ko& zL!)IbuTs3cz+QGb#P0d?m}k;Cfr{K3U%ga&KD)$-{qmzDYGwR~q>?5S^6f!6Cq*1By(;z8qgmhOG?@Er{3!hcIl zdt(y$uugomq~5&laqUIMM?x_<9Tir6=2xQn-O)V(6pDsq9-axYj!U;AePSA8&EhMh zFPiEt&W`FFg^3AJ-b&M+7?gpE@~Wpfuve^wm3W7x%{OcWGM&6@;;Mvw_tDw9h{)RD z^MJZ-WCkkr{G(^4ROwSBPge)HFHSp-qLUkG9+aj_bOYJx!|jtl3cY8cnayp)!_A`e zCc)4(>XR)9>ZrIw{ndAUi>W@e@)>Am_kBKKcICmiO}-M*AuMaX)5~kr^tu|J^U5@% z#>rue}TCqjM1yL<5& z(51sN$8N}fA~|YLA}kuDFzTozq5kB&b!w9#wg?rH+{)G|rgpoM^lFt*X8yQ$(&5H6 zV_V+5NjWc$rAs>9pZYbjdR?cZUI>r0dE8PJS)hGs^|+!|&E^dg(KP2+V5O^9lLnYW zBkL1My6DTV^ua70-E!739o=WEC@&i|-%z=V@byQW70{65y1~T+8 zy_XHVCUV-jrsf+tcwevCU5>qGie2G43R{UINWtl&cp;R6$Er8;f&8xr!>v11*ScPlZ1czKGhPHqXj#eMAksZSkA=TvUKN zwkw(fK~xw!{LdgNwF{F8TH0-3s<2xtBD!{kA7xt9)P!hHw1tC6dmj**dIVYbGqMu! z$;CULE?K2-`fzA?GT3gx7jNc?qzB-OOnOE488c0 zkdLb7u*e;b>#rzcYYS-3yI*l1E>ik3)EM*x<#u4|)^HYe4P}NAI#@#3dxJqOMh`hjx!PuIUKnKZN)0x5hrU8`>I z)#bZ<+hm8A=g4kSQzj{3nw3ZC0jZeSz z?8pnx`zFA4cv+ng!M#|r%ll3f?4=%f~a!x#i;8`=f9*dfte$7Ly{{UrU| zJlp_iAU`)(cP~jl87>r%vj`&z9B-Z$ zq^PK`udj$NOazN_5QQL-NKr6U6bcmvY6yGzyW>%Q!tP$&+d#g|Gf+x@uB!yD(i#R;^XD8?1zwwd1wXb~d%g8=}2(g5y~fa)CWZHK|h zs_JTjR2{Ki_-&qmfbk*vxP$?V8UWh zKmZ{S3{)5le1yeNFqjw&36YS1qPEcc_1-_hRd@HoqulK_q z-LCcP-P>n>1-gaQFYRyF+BI~y=Y=hZz{CK|Nuoa{rC;IyK>ok^;3u>Gn*r|z_q&nZ z-1x`i-|YHF7VWO^n_auP@sG#9+4YYs+Fju{yLNNqACG^t>mOOPyTaeGi}L5P2FRr< z!{rMsTJ~Bu{e2+Nww@8tEAFhRAP6x)&j|fXB`8D}P=Z1X!7xK0HHvSK=tDt0@X%V1 zra+7sghYcxfH_2+0qjIgM=(cmafqf!NNtElO;bm>x`N30!-)r4!ZE|^Ygt!gI1@Qj zIkPxp<gXmdk7m|9+e@i^7(5HDg|0(F37v?v81~1NUEWw_!JP#k>Ja2~ z48JPi1G{+UhAoKa+6$^hgp|}7U=?jeCEl?Y>Ho@Q2m+P(+jKCH?NBja5e+S&&IgaY z)P>ZCJj$R@FkB1_hl|6(Rd7U~7+7B%tUD5VtxL3@+`|(F)jP8Se;>4J(EC1>Ih~oM z`8~>h0$41thxB#R18E?eJI}2m%Gv0DI_})s-xyAy6>{ zL<}r0kA#Usl@x)kgy2X7QUay|Q6yn(Et4|Yjo<3%o9iz_)KtKo%@NwrulGc>r8`#+bXI=b zM?ku7>?xh=9_$3xA~?Js4r70S5&{uBK*`IitfK-P@8Yt&yE_)hr3``W1pR$auKd5) z7wosvhikDrgkll)x{{>2uKhf~NLTO?Pm9V$7 zw*yo#P!viSE)D^dCMYyo7;0yWL7|`$XpHy|Wz27-$#12}?zhW-KK?m4|GDmexyJ8C z{uS$fv+Fmz{uKxQRpsB(^_yM)iUa?u^8dJW{ijNkttH-O5RC-{vZ+PTxB9+e&31Vw zzD8abSZ)JuS=&3|04r<-1E8?EWZcXN1DO8+`_raTjb90{8bV-T2t*tLhryvx;ANN( z4CWIMRg&k2ii-fd=NM{&fQ7p^4zLr04DEpn2K@HG0#g!X;N)ficfDG(mm zR7V))>g3=q>58!jPS{~xu{cR?6%_;=3V~2!I1FZU1D?%2cYJY9n_)m& zzoG%G;D4Z@t;KH(7Dw1YkWgVeC>kjYhucGiQ4qAPFba5u1Ps{XFf5l(I|{&7M4(NS1b!IpzauGX z$nWOlh{Cv{M0_x=PHs*(tdEnOlfbsBU~Ap|bIYJ@2JiHM$)+dF?rXbtKicw~_(v2s z{Z}?I1Tq2d=q->iQ7sf6=j3Mr7_zrKZMM8^fMenB(1dp=w%YAz@yqT+eh)sZ;2fUwW$Ev5^9i_Bi7p$4LDSQ&`w?+t|)&D z8svz?jq4r2&TU(^K zFa(1FzA!+}pl|_EZIl~E)W|~<7z^?^9M%_*Q25SXR9Fvh4}gu^$&~aJ`2mHgn=8l% zgYyE0r3@E9CZIBvw#B0Tw_PrNxETD=-NMfi@8$}4du+Rb*rR@_=x2}G(lL11Ibz&U zqCim?4Y;3dJ3)YeqNKd8y7Bi$K!FobE|9FW2f$LC z43{e&$0aN6<$-elSq6FG{arCKT$?k4uos|ClZ0$f3xAvyb|!^QyF3^ywN>5M@&Lxk z!4WU%zUAVzeR2!l&KWM*|Gb-}MK=+X1)L~=zfBIx3T)2^a0pBUCJsyp06D1;-dFKA=d?gM*f&f2O-)9Pl7;N_h z@ShEN$ktHCIRN8X75H-kW*7t#w16wlz$p!og%uct2JMV_2nZ@70fQkx5D{@P zV0y4~+nNg8K*CTFabTiA0EB{E0ZA8u0iwV8PXY;)ks@$}#O5y&ws{pU0tW7diy$FD zNn8X0gMx%1B7oW#C_+V$V(`shaRgAZ1?@R;{G}V>|G(L?%T;d={rVrg+r_rLxVPmF z@Rs~T%6Xx#I=X!zsKMo0w|zqvJo=-qPEl3kVWWH3^b9D~n-*=^|}P20GV z7rzF*l?5`=A3JrBe$NiurTqP8%>L2*v7HTn?Z0Hi$wtcmwG}7*t8O?UDM$AH{xc#T z#AzhZn^#KBRBYPY#vT&@`)U?B!q@jC?>{kY{mAwIkL3M7x9+5|w*D8aI|PT1mF`L2 zF5}hS<{kKdn|Vh80aH{~f`C=zC6EXOxS|3Aa5Ymv!XOX@1z_S)*)s2-F~IxK=4%K9 zctHWdfk_D@u~h`#l_9`Bl6x3{#E_dP`T!Oau|FGvfY;&eabWt>5(NH(B?$ewrL(QM zxit2|^6_?R+DJO3&vqM++Q!Jy5RG#YN8>J2MdXw7ToylDOClGd&c;K^#cY4pbghM0 z4say>VoF)F2WNL1dSD1BQv9c_2bflOUMs)9`G6p>?bnC@-a!9t-)rDidTXfwJT_E- z2Rd-l&=BA#czag|LqmYo;BCv9p&=AF-ZHKMmH%~0fL#x?U4(7h>i*v(>^m`=gl)IJ zP1qK}{}sXjQT)+mzny}AI|VxcexCmWPQh@PBt#5Yy8M6HDfrJc{I9TY!69N2NIP33 zu;4}l_ARhDM%Wetw-d%-Y>`L|6b-l^Lw-92e~(o7ZQuG=>PXQ4xIXgVq5C(re^c@| z%=z=B-|YG`PyXi2Z+86+bN+nkH@p7KlfOChn_YjyoIhXsJ9hnt_N^_4%$=p~rbc?x z1_Hz?0*fhq(eHs{o9q0|@}`2_3s9qO6?DAuu1@Yi83Nl*fTxRc07~#}lh>vhMN|<` ze!F5FcG+aMs{UA5d%NQS(10)M=3a~t*!G%G6SY;?R@`ra`QFlQIJ-<2dnmNNF8~qZ zke~jfY8NU^PR2g1raL`SsGGrMMqsAG!>g~M2X7qZ?-kGe#JJYE%tG!#=C|-RmReo? z3JZ1B*eVHWV11$*wZrt91G|XUi`L_a>XAne8&0eaG<_YY$@(@T-8?bXUo%#j(q3S) zDm}M^Dt^)@@HSu|z;0-9tlMDq$=ux{uL|67rtG^hUlpz3P*s?ZgI{KA)~SIERFbqsS!NO^U@KK;1vxipNe^dM%i7*L0J?nO+#5#-f z-4&>KlH*fFI<4nv^s$WV4B-r2v|V|^Pyw#_WBr2EH(OIDwXIvJWYms1-mxcmp#8|1 zD*Y;buZWNu??j3&>`rVtS(JCFw(slk8p%o+o)r0n9^fE0>Mh0w+`^3Tx zt4*#-UD9V^;?27;SK4*duXgc>tuxL5q_>aO>89(&G4EVfpBqoA-$a}4bMIZr(Dm2* zs+JbpQgJwu=!<8=vi@@Gsn91ScfyQG`9^eOhd!OYLH12wn)p>BWOARPv6C;YqJ<0X zxU0%c*8aC&LW4$x{d0m|Qyx=#)_C{oHPuj=0S?cy`Ijq5mtIt)op1$G2wO4 zSc&~d(iZVgoQ4`W!$>GHx$9r@7hYK_Y}!XVO~7{7OPT`b@`~vT?Hy)&CW?qgSZQHa z;$+nItgC%yE$W8n^h&&St(fd2bvNFB9nk;emTdM-bMb-Q2LZ(BIGg?%3AI>`H+;Dl z4h2|fu}DSYP%OL{v~AA=L`%aJ>Y4(g{S}IHa>7+r6=o(jxDkah9)G&Q{6}=Av$tMW zPM|-(ND(~Ek>T(To;+#vswb3fd`YdTL(IgF`tBVSov*S1t*5LjPJteB`7cKEd5Bnv zKkUlucD!<^jxRNdx9yA7jGaq2!ElUtMgLgVitT+biCeQ0@rAm0kJQf==i^{dMLlJ; z#sz(5fAgdxwF}V&zC9+;T{6w~%fNvOW-ql%B zJN0?R=Xo;stvY#LpTQ@vD0G)Q7pWchq3+FM3n0lF=w7ZPl~O#lkl#4LDW4s%Ig^x5 zVg;vlSt{PQS%ndtf8+Ej^R383Cfeq_;DtxAI??B~DPBoS_O_sy&Ai8LoZ)dz#@6i3v0J6MKT7@o9w?8 zE3sa7-1WtX;6U{iidIjScqv71MaggrCX4vN23dN_N2iV!^{0O7rzvIauFf^AK0sx0 z@~x>`C>#2r*UV8poy?q>rR0p8Y=y-U%=AP;!)&T2=E-9o_6H^6HmDQJF3#VjoE$K| zd6Ii(_LO=}+<6O9Vx#xzx0_TBWM(X@JWx2r5N|h}%CF}-aZ*aA*mGPdY#5Zi)Y;dh z)=WK_a8dag!j_hsspn%-ZhvX@WGR*2GlFhJ$LBsHd%6$)nso$RPqipYSiE1=+Nc(t ziy#{tH%q9$jyEDB#49qc1(ZrWXbBO2w*X>qi+Se1D$_cDphfjS)`l$8djz|*?s4*GdJ#9`hFW9YiN1v6g2Q2iVk^%Li66j|zPHa&`hmiy*o0qEb7K@E4zU zH~0KZ8k_VeA$01qUCTGdoStZ16e_-)`%~+}6I|9hh1+`ng@y1Z8IIb=+*5|^MqZbD z&QsTi0h(VmZ@K<<&mw6IZ768vRhVg3~7!K?nDpj=Ns8QFl1$D#=9s zoU}z~I%jAVp7s3KSF+wQXMO71sAk35hCs0U12+slqnh*{XHpobna%;sb)%(IHK^=p zvNHLiPj@Z7XS~!?T1XT5bso$UecP}ZYs_QOMRj05O$E~V{PmR>of&ki73uptQ}v~s z*Y#W^j5T~3Du?en4K?NXeXf!^mEY*Nco+NRRC3-$y;EiFk56-uiqQ2j^4(yiwI=XQnNvIoFFz5#4|QG_3Rx* zb>h|UA;W#IES?x8$JLnd*F#ORT1wvCz2Z#bl4%`nJHRkvRA~F$PrUca9fj;md`zD$ zuR2`+h#*;&Fn!})q0DR&&Ya|sJYK-77~gTt>Umbry$D@j4xXFIikwdZ?U%_1Dx_^r z2zx2GCgeQUKh307Zmu=W%msf0^SHPcZ?WOz&GQsZC^Nr!q)w+CGZf($t(J6cdZIrf zr)7}Zlee3NbzbPznCk_o+H^e~*jY@~DjgEiqWamvB+JU%d*wzrK~vDO3gK6~uhimE zj-1e#Ij~RGw`?Wz(K^Ks+=)*?wC_pfEAP>mrBL9}R@7_P((`0q02{|!C|P5sRBqZw zV^{7HWZiA3dE?~>Y?GHo=p!_fM2V@8yu>i20(sGYwfGU}NS8~pNLjofws(V;1w<7Q z>I|9hDm`de8F%98gkQ(a;sVz+Wh7mOqcZbZVF|%h^;AzkBjbR`o?r@@zQNCRi1wV;__rtR2T*G(nW^`lkuq>J*> z8)b`(_m><|_h3f-ieKQJC15)(-qgrrulLc2F@I}Vj_W=XI&+j#w_&MBRTnI|5HAxO zxrkT8&_oGH1u9GX8=3eN4C5@iM{i2<489CdJ3^sz;z#z~XVy+u z!rPwa7{Mmcrs))46`No@_112oIR&nqy3ed3uG{NmYtyYy^cHa~?nO|$gSQW#MD_U? zR@Uj_^c{*4{UfN0OB`S2JTCz~djb_^GMOD|upuKkEwjwWrrJI%Q_Vf_M(qO4GaUML z%1U0~Qrdv9^E3UbMQcXjy3K-r}{?yB5P8t=`6FcZFP@OYp&r&Ag62 z^6HrvMK$0rb_ieQH6z1t3+k@)KiqfpwYuS9I*RvZPSjSJS{f5y8IOEkfNnL}F z71(ukLGKrgb5s_)Ntsj0aA^8>tLyqHFRkbVW=E#=xOEEKXT_Hkn#!gux{|A|M)7bw zy8&z=y%MDrBbR!j6{!d6DH4Qu-5~L@LBz=mY2PZYdy9izl(xDhLu6mzP(h2m7C{OU zIz$n7_T-4m6;%?*X{O+kFM&Q?e8wR#JH2XuUL)qMp?d#auy%W`(p%YbxYt0Z`l`}M zCxY$6oBlwjfcz(T=Z%lcMJyR6CFcd5$Arod$v}~dTQof>hlK_X1#@2lLWai2@;mVCC~ z4_4u~XojvdccRJiC-sk88I+97ja_c?t=L`u!=p;9ZFJNw6|C#7#^x0t3fWHF2$7qCCJ69!#^lWiy*4a4m}HyV zh0;gA;_J{14jM@4_sZ-P;6l<}lurk7MCt15q`T&Q)d@(N1e4!wo&o6|BGJ9LQS|kj zDd9nyM99K{v!N4yV*5{I&>ob;QCQ@nDH3@N+b!%&bxa^xriTsBtoe}WGrVvzP3RVE zKlx2{MqcmOec5cu`U!);Orv3I*WxMls2;b@RFZJbqgm>0U5Zqmz3*YU#PI{i2|K4s ziU@IhLs_(DftM2BO0o_IsClk8J3a3mxlSTHR(c?F)TN1fIG!R*&gYGK*!`sV(bFIW zMS9`=bO{0aHn*4-AM2)?=w2Q$p|e)aAWR;eJb*SGbz;o9Gw*wtD>_)sgqo;D@VIq3 ztY{;SYDU?s^V4x4xKG`(bqFRoG(LL`Vj9IKsL zR;17Gp1LQ&E%%%{wMIhJ)BON(DS^)CZ*;+GXzz-m)Mrm@P^K`&_=j&w=C7}%B+9lo zEzAs@@184c`f8PR{M*_)S!rvL4cEnwEBJnAE2hNc)K$3PahFpcd<UDdjlzD&hCy!o2 zU)WvCeWKHbt^Ba8*zwSD<-_uM&!cZ$WRgi1vwWJ?Cc$>`e|DozBZ=-KFz6MzeD+tf^QJ z4nAj{CDJ7HKQ0qvAxNn06gzx7D8I_IzdsN1@KfMWu$E}hWh z>M6F%PLr=~?iSRCvAR*o<&lm@pX<{oyy@#hn;^0TU#wsoA#5PKZqiHT(O!PRvMf%f ze(I$<`V>R;`{QKRRY|oI=9lFJ#M;w|%jaIbZau&ByrFk_fH6oRAhaghncI{4Y+TpLLnc=`E#rcUq3`u}-vA%n&+VPGCNc z_DR(hGsSwblq++NDuWy5F?HWaUxmX_<49P!O${lF7dO$T@@tV_P5pXw?cdp#+(?`c zHq8k+GN|ZnQZqv*h^>LK0d5h zMI`>YNT5D4TWIES_@k85?GpEbZ+!M+xaqlOV|s_gD$gJDLL}PG{9$1A0zIL) z!>tGPnYWs0-FUtaV_C8fNQuK9*oRFJuY1KII5qSrYbF@7Jb#mG*#jK>5)auiQM7B)*aE^Bl ziuTGJ0R^V+6p{HXCze*?_4(7zP%RO<8ZzxsSdXon{Hq_l8^!Q-2W$5xbZ_#_oM?>Fa+|)>d-6MQB8cQO( zzYg-2v2N*x3k{jpt4`h`F){i5UQZhiVO`kj4OWHLi|_QI!sm3msOm&sK$X2P@` z3go>zHJ@o4M;j!{o;VyrDgxPbFq>rwH4Yhf+`wjL# zxEsnGSH#ApCzUh&#%P=@2I9^usbU6`9$@!relzx^;@a@B8=$lentt6}18WUMx%ZI_ z5??;jhdOrKoj7+dNreZk!ed)o4igwSZUxNF3JF=-r+TzNr80B!5zJL%!v3*|8gB$c z7bfODk}=fy4lz!DQ1?DS@T!LM+N%$)^dwFj&74;|QbX=$rS*(Fg2)jOj2#@mGO_CE zplZ)}SUM+OX{dX;_{8ycT3Ji&7p)hjuh?~dJH3IU+z?%tImKS+ZP@K^`R=h`Maj&< z+rq*DY~fl8TTqnDmD?g8A9|3<96O%Xj%4$?6R19?{MB62HiqhJ?}Umj)v1A(H3BWc zo;;if=hJEVn-i!mqp97G#$9uQCnr2my2EeT^Ex4yzpK^fvN7WP$>1Btg$Ty`krVOc z9p^X+@`95DzQ|2GPQAV_s`6N|;R&aKE5(auM zkuJxA>!i%_AT*Pbo4FB2p|OS8_($X`Q5y3k=De;W zx{sB3xs++RL|n{fP3P)|Wr)1URxU8N7REc*SAUrud~=ZFQ|cRi8Pd2THIjxUR#PFv zp+ve?Zn%~lp7%{XEFW*V>DV3)uj#N~a(NE(4h}p$a6pRfTv?~P*x~d_O{!ZJ6SXgf z;u_pdzMMv8ymkOF3ZBWHk{u&YvSi`wk1v0}|7EDjZ6(DuO(&0|#Rt14#m^8uXqKnX zBLH1x%{wX=l1<$#c|g8IBx&K8ZmaLxY9c-sbHtmGgfQW{$(fCC%d$+}<6QnaV+9i& zC%lV~zM>C{RXm*@>7jB{isvdSMzbm*!D0oPgK?zvtm z-Pis4^{v@Yr_}?e!ttL(Oz!eK6wVl)>rvqL9jBM<6@a}M35gpxNy>y{;@Tj=>CxBI zKlVy_Qq0gaYm&{_@mPL?_-5%`q^;7`FxJ+WqzNC|-B#$PS;*bSbFOE$3F3>V45}70 zLi^IsXOt*4)scDU##DOxAMv$4yLv?}aa~9Z%2e{qbMdg?f~WKZ@j{fH=qsk8V0s^J zygKRIV6{hi-mgj?D)8Az`TO2QU3ziQa6LJBLZzxrU7(nNF-k2|b>tP)-Cda)VqVm1 zRYMkypl%U$7NkN1Reaj0_;TomyLOATP;oiwqScK=v!R2*!OYXrkhKOq)`ecXO16NP z(b!8x26)Z8FsM3+R7O@IXqoD)L#07pkQ7LKIG3xA{YJs4Rgs-wiUoB^O4*QHx~Sw{D-VeRy9&(sp*_rsrBQAaJ)`uBr#(#U6Y&;f$^8NHu}oN z149Gv^&=88zoP4J9Cy{T4hg9qbSfoyM(roR|6;_wkHNn2X&>JVR{DCIUY;&gYUV$5 zYMkorLzTI>Cf_gnEu|jt5g5d2tq5b<@2lK9S#8m%6>TOh@aB<>Q?7QY;KJxc#T(s0 zuyG|$JUV~~~ ztK1@+m)``2=k)e_Mf{X2XwLKkfQPESrhO>v2I4tY zB=mxR*Jb)kbLlO`$P=|WIq#3g<-QUpa8X`)bWU$U+a7p)!8q9#C%%3po6tAYP%$`* zrxXd5yMvg!8CVwK)Oab{ZTeojng6gUauJ11(A;TGoTU@B|Jj2kNp-%M{MKdnVE*Jl|}l{?aBBV4=7Zz?x*93Xs0nPF8y z9&q}Sm|J{o&V6=JG0QsNg~?A^BSmxi^}%23o{GFpsd-@;o=;w>miE!@7^%!N>UZwW zj+9?Z49>(YyHh=fH-89IlyO6`wpgyCZ`gXO8yoz&vRQj?~^ z=-S1AsM0|+!L@$1cLnDb#G{h3?`2wcJBz;+XlXC6(j5~a)N>Ga=tIW$SaiZYzA0^> zZ;)8Uaxz(@gv+pDp@?`9qi8i$yXHLQ)k?ecZnIHm6PDo1SS8&$k_*T6UBf-^k6tBJ zUc8)6>6%Vi`ZAr-^^|wjw4YODt0hZe_Dk)GXjNj{z#`K&jm+|JacPC}%c?Z%5x#F& z84g`n!Jb<|reEJj+g6fnuzZS2Zi8RthY$xSTY56hPS@xQ~k;`}jel{Ybg0I7CbP>;OKk{)jVn4UKz&TLZqKOIQc@r_sD?S z)RkL7S31qt+$1W8ytt_gy?nys&u(Jz#y2l8 zt#%ERcN6CJ%SM0FO@4@C(@9%sMES_f-r|5>d2Vwjh*ah@m21FwzKh=%kr5KuWijE1 zz84=#u1^TrI2tDgoYdtgYdZ~S9IB>U_e|QoW!V01Vh2IQc8uvjloL?c2~b1ffV8SV zOzDQ3sYv;?umMi4t|$=78;=DtnF3*Tc#Id`$=yL_4#146k1VXA2V+uUl;z)_TFxnM60kCJsJ7EW-ZF=^^2ncHU$tzuq$0p}3-FG9h1!{-ZUl5TH2SOnJ z#F{ODKUuSr9%~1YZSwa$#vP1aZ zxa>G(e3wo)Ks^6|$gcPTC5vsi+wuYz--{`~3NL(Hcy|!lCVX#1c1yA$;A7$ivg9|k z6!@ioei7a6?%YNNx@Z6VD!TC9qWe9H^w&YU9nS*)MRc)tjzE017hYH&Ym2qRdH`r? z;q3l^*e?>i3o!%~vKNnjm0-lSf6We${zifkyCqob2Vz(}5SzP+7I5$@UR#iV2=p$z z#E^T9^)2{61sbs(#j%6e-v~5fw?O~EYZIeCp!17N??Oi$3EzWFTVQvgvsq>fYV=z^4TRJfYLihiApy*}h!)Y!5l|WI(y#qzXc8#h3>w%(3B^5DcC4?9ZCJ#}B!xa=@ zNF+oYs-P&ZxEVP5Gf;FhMD$0jC=k53nFAC8ys3f#U(r2dMS+Z>JHet`L798Rivl}f zZFz?NUA!p$=Pn>#)Q>>{{Pk3K$Ga@`mMe4Ln#e9{MrmG@m7^e~0kt?B;31BbdumEc zN7jFp#+LUSIc+3_@_q=vJVWH!NKDH;=*t40X3WHRIeD33t*LYbc;AKmW>YE { this.#cleanupSwitchAnnotationEditorMode(); this.#annotationEditorMode = mode; + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); eventBus.dispatch("annotationeditormodechanged", { source: this, mode, }); - this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); }; if ( From 8a979c2d0e37d3fb398fb1af90953998b1f89381 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 16 Jul 2024 13:08:26 +0200 Subject: [PATCH 0048/1133] [api-minor] Remove `Outliner` from the official API As far as I can tell `Outliner` is only exposed in the API because we need to access it when running some of the reference-tests, but is otherwise not used. Hence this seems like something that should be kept *internal* and thus only exposed in TESTING-builds. --- src/pdf.js | 7 ++++++- test/driver.js | 4 ++-- test/unit/pdf_spec.js | 2 -- web/pdfjs.js | 2 -- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pdf.js b/src/pdf.js index 9921e324b7d25..064ae6c61f802 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -80,6 +80,12 @@ const pdfjsVersion = const pdfjsBuild = typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_BUILD") : void 0; +if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + globalThis.pdfjsTestingUtils = { + Outliner, + }; +} + export { AbortException, AnnotationEditorLayer, @@ -109,7 +115,6 @@ export { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/test/driver.js b/test/driver.js index aaeb20d580dd1..8b26975f92aea 100644 --- a/test/driver.js +++ b/test/driver.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals pdfjsLib, pdfjsViewer */ +/* globals pdfjsLib, pdfjsTestingUtils, pdfjsViewer */ const { AnnotationLayer, @@ -20,12 +20,12 @@ const { DrawLayer, getDocument, GlobalWorkerOptions, - Outliner, PixelsPerInch, shadow, TextLayer, XfaLayer, } = pdfjsLib; +const { Outliner } = pdfjsTestingUtils; const { GenericL10n, parseQueryString, SimpleLinkService } = pdfjsViewer; const WAITING_TIME = 100; // ms diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index accced5554d23..7e2224c8de772 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -61,7 +61,6 @@ import { AnnotationLayer } from "../../src/display/annotation_layer.js"; import { ColorPicker } from "../../src/display/editor/color_picker.js"; import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; -import { Outliner } from "../../src/display/editor/outliner.js"; import { TextLayer } from "../../src/display/text_layer.js"; import { XfaLayer } from "../../src/display/xfa_layer.js"; @@ -94,7 +93,6 @@ const expectedAPI = Object.freeze({ noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/web/pdfjs.js b/web/pdfjs.js index 8b8cde0169d0c..ad709d93676a1 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -42,7 +42,6 @@ const { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -89,7 +88,6 @@ export { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, From 37db3a7143827427711cafc15dd9f8cab05cd38f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 17 Jul 2024 14:08:54 +0200 Subject: [PATCH 0049/1133] Remove active smask when restoring the initial canvas state Fixes #18444. --- src/display/canvas.js | 1 + test/pdfs/issue18444.pdf.link | 1 + test/test_manifest.json | 10 ++++++++++ 3 files changed, 12 insertions(+) create mode 100644 test/pdfs/issue18444.pdf.link diff --git a/src/display/canvas.js b/src/display/canvas.js index 811e625a72df6..247870f3b2d3c 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1006,6 +1006,7 @@ class CanvasGraphics { this.restore(); } + this.current.activeSMask = null; this.ctx.restore(); if (this.transparentCanvas) { diff --git a/test/pdfs/issue18444.pdf.link b/test/pdfs/issue18444.pdf.link new file mode 100644 index 0000000000000..f1e437217b264 --- /dev/null +++ b/test/pdfs/issue18444.pdf.link @@ -0,0 +1 @@ +https://github.com/user-attachments/files/16264442/9.170.1.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index f970556c0b512..5f1edf3727194 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10160,5 +10160,15 @@ "firstPage": 2, "lastPage": 2, "type": "eq" + }, + { + "id": "issue18444", + "file": "pdfs/issue18444.pdf", + "md5": "233b7b72d67133044338a728d52d58a9", + "rounds": 1, + "link": true, + "firstPage": 1, + "lastPage": 1, + "type": "eq" } ] From 99f34f4c2edbd64a6fc39fc57b3ca26995281589 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 12 Jul 2024 07:46:05 +0200 Subject: [PATCH 0050/1133] Include additional data when fetching browser preferences in the PDF Viewer (bug 1908401) To avoid having to request and await various browser data early during the PDF Viewer initialization, we can include that data in the existing preference fetching instead. With other planned changes to the PDF Viewer, the current situation would only become worse over time. *Note:* Technically this data aren't preference-values, however we're already including other non-prefs in this list (e.g. `isInAutomation`) and doing it this way simplifies the overall implementation. --- web/app.js | 35 +++++------------------------------ web/app_options.js | 15 +++++++++++++++ web/external_services.js | 6 ------ web/firefoxcom.js | 23 +++-------------------- 4 files changed, 23 insertions(+), 56 deletions(-) diff --git a/web/app.js b/web/app.js index 3eab6e6ae441f..b74ab491ab8ae 100644 --- a/web/app.js +++ b/web/app.js @@ -155,7 +155,6 @@ const PDFViewerApplication = { isViewerEmbedded: window.parent !== window, url: "", baseUrl: "", - _allowedGlobalEventsPromise: null, _downloadUrl: "", _eventBusAbortController: null, _windowAbortController: null, @@ -175,32 +174,13 @@ const PDFViewerApplication = { _printAnnotationStoragePromise: null, _touchInfo: null, _isCtrlKeyDown: false, - _nimbusDataPromise: null, _caretBrowsing: null, _isScrolling: false, // Called once when the document is loaded. async initialize(appConfig) { - let l10nPromise; - // In the (various) extension builds, where the locale is set automatically, - // initialize the `L10n`-instance as soon as possible. - if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { - l10nPromise = this.externalServices.createL10n(); - if (PDFJSDev.test("MOZCENTRAL")) { - this._allowedGlobalEventsPromise = - this.externalServices.getGlobalEventNames(); - } - } this.appConfig = appConfig; - if ( - typeof PDFJSDev === "undefined" - ? window.isGECKOVIEW - : PDFJSDev.test("GECKOVIEW") - ) { - this._nimbusDataPromise = this.externalServices.getNimbusExperimentData(); - } - // Ensure that `Preferences`, and indirectly `AppOptions`, have initialized // before creating e.g. the various viewer components. try { @@ -229,10 +209,7 @@ const PDFViewerApplication = { // Ensure that the `L10n`-instance has been initialized before creating // e.g. the various viewer components. - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - l10nPromise = this.externalServices.createL10n(); - } - this.l10n = await l10nPromise; + this.l10n = await this.externalServices.createL10n(); document.getElementsByTagName("html")[0].dir = this.l10n.getDirection(); // Connect Fluent, when necessary, and translate what we already have. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { @@ -394,11 +371,10 @@ const PDFViewerApplication = { let eventBus; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { eventBus = AppOptions.eventBus = new FirefoxEventBus( - await this._allowedGlobalEventsPromise, + AppOptions.get("allowedGlobalEvents"), externalServices, AppOptions.get("isInAutomation") ); - this._allowedGlobalEventsPromise = null; } else { eventBus = new EventBus(); } @@ -564,11 +540,10 @@ const PDFViewerApplication = { ? window.isGECKOVIEW : PDFJSDev.test("GECKOVIEW") ) { - this.toolbar = new Toolbar( - appConfig.toolbar, - eventBus, - await this._nimbusDataPromise + const nimbusData = JSON.parse( + AppOptions.get("nimbusDataStr") || "null" ); + this.toolbar = new Toolbar(appConfig.toolbar, eventBus, nimbusData); } else { this.toolbar = new Toolbar( appConfig.toolbar, diff --git a/web/app_options.js b/web/app_options.js index 914bde602c761..d61aa0bb7f437 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -56,6 +56,11 @@ const OptionKind = { * primitive types and cannot rely on any imported types. */ const defaultOptions = { + allowedGlobalEvents: { + /** @type {Object} */ + value: null, + kind: OptionKind.BROWSER, + }, canvasMaxAreaInBytes: { /** @type {number} */ value: -1, @@ -66,6 +71,16 @@ const defaultOptions = { value: false, kind: OptionKind.BROWSER, }, + localeProperties: { + /** @type {Object} */ + value: null, + kind: OptionKind.BROWSER, + }, + nimbusDataStr: { + /** @type {string} */ + value: "", + kind: OptionKind.BROWSER, + }, supportsCaretBrowsingMode: { /** @type {boolean} */ value: false, diff --git a/web/external_services.js b/web/external_services.js index 8a3043833039e..55fbb707d1d1d 100644 --- a/web/external_services.js +++ b/web/external_services.js @@ -45,12 +45,6 @@ class BaseExternalServices { throw new Error("Not implemented: updateEditorStates"); } - async getNimbusExperimentData() {} - - async getGlobalEventNames() { - return null; - } - dispatchGlobalEvent(_event) {} } diff --git a/web/firefoxcom.js b/web/firefoxcom.js index a7cd0068bb46e..63f82397e8ec3 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -14,6 +14,7 @@ */ import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib"; +import { AppOptions } from "./app_options.js"; import { BaseExternalServices } from "./external_services.js"; import { BasePreferences } from "./preferences.js"; import { DEFAULT_SCALE_VALUE } from "./ui_utils.js"; @@ -396,32 +397,14 @@ class ExternalServices extends BaseExternalServices { } async createL10n() { - const [localeProperties] = await Promise.all([ - FirefoxCom.requestAsync("getLocaleProperties", null), - document.l10n.ready, - ]); - return new L10n(localeProperties, document.l10n); + await document.l10n.ready; + return new L10n(AppOptions.get("localeProperties"), document.l10n); } createScripting() { return FirefoxScripting; } - async getNimbusExperimentData() { - if (!PDFJSDev.test("GECKOVIEW")) { - return null; - } - const nimbusData = await FirefoxCom.requestAsync( - "getNimbusExperimentData", - null - ); - return nimbusData && JSON.parse(nimbusData); - } - - async getGlobalEventNames() { - return FirefoxCom.requestAsync("getGlobalEventNames", null); - } - dispatchGlobalEvent(event) { FirefoxCom.request("dispatchGlobalEvent", event); } From 6cc32b699f79a35505659351fb04a5c8e7bac3fa Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 17 Jul 2024 19:51:53 +0200 Subject: [PATCH 0051/1133] Add the possibility to change some pdfjs preferences from the viewer (bug 1908483) --- web/firefoxcom.js | 4 ++++ web/preferences.js | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/web/firefoxcom.js b/web/firefoxcom.js index a7cd0068bb46e..3add4224069d6 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -150,6 +150,10 @@ class Preferences extends BasePreferences { async _readFromStorage(prefObj) { return FirefoxCom.requestAsync("getPreferences", prefObj); } + + async _writeToStorage(prefObj) { + return FirefoxCom.requestAsync("setPreferences", prefObj); + } } (function listenFindEvents() { diff --git a/web/preferences.js b/web/preferences.js index 61a01ea880666..d44e4a39a74f0 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -108,14 +108,14 @@ class BasePreferences { * provided that the preference exists and the types match. */ async set(name, value) { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Please use `about:config` to change preferences."); - } await this.#initializedPromise; AppOptions.setAll({ [name]: value }, /* prefs = */ true); - const prefs = AppOptions.getAll(OptionKind.PREFERENCE); - await this._writeToStorage(prefs); + await this._writeToStorage( + typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL") + ? { [name]: AppOptions.get(name) } + : AppOptions.getAll(OptionKind.PREFERENCE) + ); } /** From d24a61c648fc79ce0b87137266562c8dc04e841c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 18 Jul 2024 12:33:29 +0200 Subject: [PATCH 0052/1133] Allow /XYZ destinations without zoom parameter (issue 18408) According to the PDF specification these destinations should have a zoom parameter, which may however be `null`, but it shouldn't be omitted; please see https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#G11.2095870 Hence we try to work-around bad PDF generators by making the zoom parameter optional when validating explicit destinations in both the worker and the viewer. --- src/core/catalog.js | 9 ++++--- test/pdfs/.gitignore | 1 + test/pdfs/issue18408_reduced.pdf | Bin 0 -> 9787 bytes test/unit/api_spec.js | 43 +++++++++++++++++++++++++++++++ web/pdf_link_service.js | 9 ++++--- 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 test/pdfs/issue18408_reduced.pdf diff --git a/src/core/catalog.js b/src/core/catalog.js index 68db4b847baf2..2c4551d3ab2c7 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -64,26 +64,27 @@ function isValidExplicitDest(dest) { if (!(zoom instanceof Name)) { return false; } + const argsLen = args.length; let allowNull = true; switch (zoom.name) { case "XYZ": - if (args.length !== 3) { + if (argsLen < 2 || argsLen > 3) { return false; } break; case "Fit": case "FitB": - return args.length === 0; + return argsLen === 0; case "FitH": case "FitBH": case "FitV": case "FitBV": - if (args.length > 1) { + if (argsLen > 1) { return false; } break; case "FitR": - if (args.length !== 4) { + if (argsLen !== 4) { return false; } allowNull = false; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 808cf43a4231f..dde1c01660e87 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -13,6 +13,7 @@ !issue1155r.pdf !issue2017r.pdf !bug1727053.pdf +!issue18408_reduced.pdf !bug1907000_reduced.pdf !issue11913.pdf !issue2391-1.pdf diff --git a/test/pdfs/issue18408_reduced.pdf b/test/pdfs/issue18408_reduced.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a97f2d79d57aac68906a82ed2db93d4153702bea GIT binary patch literal 9787 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 a6L|iCLJg!4+ 3) { return false; } break; case "Fit": case "FitB": - return args.length === 0; + return argsLen === 0; case "FitH": case "FitBH": case "FitV": case "FitBV": - if (args.length > 1) { + if (argsLen > 1) { return false; } break; case "FitR": - if (args.length !== 4) { + if (argsLen !== 4) { return false; } allowNull = false; From 5946d20dc984a801648ccd9fbb334d9560b9e6c2 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 18 Jul 2024 15:12:35 +0200 Subject: [PATCH 0053/1133] [Editor] Allow to change a preference from the editor UI manager We want to be able to update a global pref (i.e. enableAltText) in using a toggle button in the new alt-text dialog. --- src/display/editor/tools.js | 8 ++++++++ web/app.js | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 93b6c49259ef6..61e7f7e8cef25 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -928,6 +928,14 @@ class AnnotationEditorUIManager { }); } + setPreference(name, value) { + this._eventBus.dispatch("setpreference", { + source: this, + name, + value, + }); + } + onPageChanging({ pageNumber }) { this.#currentPageIndex = pageNumber - 1; } diff --git a/web/app.js b/web/app.js index b74ab491ab8ae..b4f57301c221d 100644 --- a/web/app.js +++ b/web/app.js @@ -1942,6 +1942,7 @@ const PDFViewerApplication = { { signal } ); eventBus._on("reporttelemetry", webViewerReportTelemetry, { signal }); + eventBus._on("setpreference", webViewerSetPreference, { signal }); } }, @@ -3166,4 +3167,8 @@ function webViewerReportTelemetry({ details }) { PDFViewerApplication.externalServices.reportTelemetry(details); } +function webViewerSetPreference({ name, value }) { + PDFViewerApplication.preferences.set(name, value); +} + export { PDFViewerApplication }; From b71fa727e16bb24c6260510dc4117e3b7ca46590 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 19 Jul 2024 17:57:53 +0200 Subject: [PATCH 0054/1133] Load the image-to-text model when opening the pdf viewer in Firefox (bug 1908938) --- src/display/editor/stamp.js | 5 ++-- src/display/editor/tools.js | 4 ++-- web/app.js | 28 +++++++++++----------- web/firefoxcom.js | 47 +++++++++++++++++++++++++++++++++---- web/genericcom.js | 2 +- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index f4f1387a62469..c84078336e784 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -430,7 +430,8 @@ class StampEditor extends AnnotationEditor { return; } this.#hasMLBeenQueried = true; - if (!this._uiManager.isMLEnabledFor("altText") || this.hasAltText()) { + const isMLEnabled = await this._uiManager.isMLEnabledFor("altText"); + if (!isMLEnabled || this.hasAltText()) { return; } const offscreen = new OffscreenCanvas(width, height); @@ -447,7 +448,7 @@ class StampEditor extends AnnotationEditor { height ); const response = await this._uiManager.mlGuess({ - service: "image-to-text", + service: "moz-image-to-text", request: { data: ctx.getImageData(0, 0, width, height).data, width, diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 61e7f7e8cef25..fa4be1fd1ea60 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -855,8 +855,8 @@ class AnnotationEditorUIManager { return this.#mlManager?.guess(data) || null; } - isMLEnabledFor(name) { - return !!this.#mlManager?.isEnabledFor(name); + async isMLEnabledFor(name) { + return !!(await this.#mlManager?.isEnabledFor(name)); } get useNewAltTextFlow() { diff --git a/web/app.js b/web/app.js index b4f57301c221d..012cb3159db34 100644 --- a/web/app.js +++ b/web/app.js @@ -155,6 +155,7 @@ const PDFViewerApplication = { isViewerEmbedded: window.parent !== window, url: "", baseUrl: "", + mlManager: null, _downloadUrl: "", _eventBusAbortController: null, _windowAbortController: null, @@ -205,6 +206,11 @@ const PDFViewerApplication = { if (mode) { document.documentElement.classList.add(mode); } + } else { + // We want to load the image-to-text AI engine as soon as possible. + this.mlManager = new MLManager({ + enableAltText: AppOptions.get("enableAltText"), + }); } // Ensure that the `L10n`-instance has been initialized before creating @@ -370,11 +376,14 @@ const PDFViewerApplication = { let eventBus; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - eventBus = AppOptions.eventBus = new FirefoxEventBus( - AppOptions.get("allowedGlobalEvents"), - externalServices, - AppOptions.get("isInAutomation") - ); + eventBus = + AppOptions.eventBus = + this.mlManager.eventBus = + new FirefoxEventBus( + AppOptions.get("allowedGlobalEvents"), + externalServices, + AppOptions.get("isInAutomation") + ); } else { eventBus = new EventBus(); } @@ -731,15 +740,6 @@ const PDFViewerApplication = { return shadow(this, "externalServices", new ExternalServices()); }, - get mlManager() { - const enableAltText = AppOptions.get("enableAltText"); - return shadow( - this, - "mlManager", - enableAltText === true ? new MLManager({ enableAltText }) : null - ); - }, - get initialized() { return this._initializedCapability.settled; }, diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 0e8154f9f041d..af2e8ee4b41ee 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -308,19 +308,56 @@ class FirefoxScripting { } class MLManager { - #enabled = new Map(); + #enabled = null; - constructor({ enableAltText }) { - this.#enabled.set("altText", enableAltText); + eventBus = null; + + constructor(options) { + this.enable({ ...options, listenToProgress: false }); } - isEnabledFor(name) { - return this.#enabled.get(name); + async isEnabledFor(name) { + return !!(await this.#enabled?.get(name)); } guess(data) { return FirefoxCom.requestAsync("mlGuess", data); } + + enable({ enableAltText, listenToProgress }) { + if (enableAltText) { + this.#loadAltTextEngine(listenToProgress); + } + } + + async #loadAltTextEngine(listenToProgress) { + if (this.#enabled?.has("altText")) { + // We already have a promise for the "altText" service. + return; + } + const promise = FirefoxCom.requestAsync("loadAIEngine", { + service: "moz-image-to-text", + listenToProgress, + }); + (this.#enabled ||= new Map()).set("altText", promise); + if (listenToProgress) { + const callback = ({ detail }) => { + this.eventBus.dispatch("loadaiengineprogress", { + source: this, + detail, + }); + if (detail.finished) { + window.removeEventListener("loadAIEngineProgress", callback); + } + }; + window.addEventListener("loadAIEngineProgress", callback); + promise.then(ok => { + if (!ok) { + window.removeEventListener("loadAIEngineProgress", callback); + } + }); + } + } } class ExternalServices extends BaseExternalServices { diff --git a/web/genericcom.js b/web/genericcom.js index 6ab2766efe47b..9c646099ef58c 100644 --- a/web/genericcom.js +++ b/web/genericcom.js @@ -48,7 +48,7 @@ class ExternalServices extends BaseExternalServices { } class MLManager { - isEnabledFor(_name) { + async isEnabledFor(_name) { return false; } From 216d3a9fafc949ddeae1ca2115d1ce0217d8e8ce Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 18 Jul 2024 10:00:19 +0200 Subject: [PATCH 0055/1133] Add more validation when setting `AppOptions` (PR 18413 follow-up) After the changes in PR 18413 we're now relying even more on `AppOptions` and it thus seems like a good idea to ensure that no invalid values can be added. Hence the `AppOptions.{set, setAll}` methods will now *unconditionally* validate that the type of the values agree with the default-options. --- web/app_options.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/web/app_options.js b/web/app_options.js index d61aa0bb7f437..25057367409a3 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -511,6 +511,11 @@ class AppOptions { } static set(name, value) { + const defaultOption = defaultOptions[name]; + + if (!defaultOption || typeof value !== typeof defaultOption.value) { + return; + } userOptions[name] = value; } @@ -518,22 +523,18 @@ class AppOptions { let events; for (const name in options) { - const userOption = options[name]; + const defaultOption = defaultOptions[name], + userOption = options[name]; + if (!defaultOption || typeof userOption !== typeof defaultOption.value) { + continue; + } if (prefs) { - const defaultOption = defaultOptions[name]; - - if (!defaultOption) { - continue; - } - const { kind, value } = defaultOption; + const { kind } = defaultOption; if (!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { continue; } - if (typeof userOption !== typeof value) { - continue; - } if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { (events ||= new Map()).set(name, userOption); } From 006242489dfd3d1c76cfb6e7db648ca5c24420b7 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 20 Jul 2024 09:27:43 +0200 Subject: [PATCH 0056/1133] Stop using `downloadComplete` in `PDFViewerApplication.progress` This was only necessary to prevent (unlikely) visual glitches when `disableAutoFetch = true` is being used. However, it turns out that we can move this functionality into the `ProgressBar` class instead by checking if the entire PDF document has loaded. This works since the API is always reporting 100% loading progress regardless of how the document was loaded; see [this code](https://github.com/mozilla/pdf.js/blob/ed83d7c5e16798a56c493d56aaa8200dd280bb17/src/display/api.js#L2735-L2740). --- web/app.js | 7 +------ web/ui_utils.js | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/web/app.js b/web/app.js index 012cb3159db34..83393b9b2c6c4 100644 --- a/web/app.js +++ b/web/app.js @@ -1188,17 +1188,12 @@ const PDFViewerApplication = { }, progress(level) { - if (!this.loadingBar || this.downloadComplete) { - // Don't accidentally show the loading bar again when the entire file has - // already been fetched (only an issue when disableAutoFetch is enabled). - return; - } const percent = Math.round(level * 100); // When we transition from full request to range requests, it's possible // that we discard some of the loaded data. This can cause the loading // bar to move backwards. So prevent this by only updating the bar if it // increases. - if (percent <= this.loadingBar.percent) { + if (!this.loadingBar || percent <= this.loadingBar.percent) { return; } this.loadingBar.percent = percent; diff --git a/web/ui_utils.js b/web/ui_utils.js index 1ed7e5a3dd7b9..08455659cf959 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -740,7 +740,7 @@ class ProgressBar { } setDisableAutoFetch(delay = /* ms = */ 5000) { - if (isNaN(this.#percent)) { + if (this.#percent === 100 || isNaN(this.#percent)) { return; } if (this.#disableAutoFetchTimeout) { From 64a4f0dc7e60e86a7c1da1dc903497fff71abe2c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 20 Jul 2024 09:41:44 +0200 Subject: [PATCH 0057/1133] Avoid downloading the document twice in `PDFViewerApplication.download` The old implementation in `PDFViewerApplication.download` means that if the `getDocument`-call hasn't yet downloaded the *entire* PDF document it will be re-downloaded. This seems generally undesirable since: - In some (probably rare) cases a URL may not be valid an arbitrary number of times, which means that the download may fail. - It will lead to wasted resources, since we'll end up fetching the same PDF document *twice* in that case (once via the `getDocument`-call and once to allow the user to save it). Hence this patch suggests that we change this very old code to instead always call the `PDFDocumentProxy.getData` method, since that'll trigger immediate downloading of the remaining document via the existing `getDocument`-call. Finally, the patch removes the `PDFViewerApplication.downloadComplete` property since it's now unused. --- web/app.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/web/app.js b/web/app.js index 83393b9b2c6c4..7a06a9c5c9b6d 100644 --- a/web/app.js +++ b/web/app.js @@ -151,7 +151,6 @@ const PDFViewerApplication = { /** @type {AnnotationEditorParams} */ annotationEditorParams: null, isInitialViewSet: false, - downloadComplete: false, isViewerEmbedded: window.parent !== window, url: "", baseUrl: "", @@ -942,7 +941,6 @@ const PDFViewerApplication = { this.pdfLinkService.externalLinkEnabled = true; this.store = null; this.isInitialViewSet = false; - this.downloadComplete = false; this.url = ""; this.baseUrl = ""; this._downloadUrl = ""; @@ -1072,12 +1070,9 @@ const PDFViewerApplication = { async download(options = {}) { let data; try { - if (this.downloadComplete) { - data = await this.pdfDocument.getData(); - } + data = await this.pdfDocument.getData(); } catch { - // When the PDF document isn't ready, or the PDF file is still - // downloading, simply download using the URL. + // When the PDF document isn't ready, simply download using the URL. } this.downloadManager.download( data, @@ -1216,7 +1211,6 @@ const PDFViewerApplication = { pdfDocument.getDownloadInfo().then(({ length }) => { this._contentLength = length; // Ensure that the correct length is used. - this.downloadComplete = true; this.loadingBar?.hide(); firstPagePromise.then(() => { From 482d6211aa4ca8e1d8a495b7f0af353ad62cb584 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 20 Jul 2024 10:19:45 +0200 Subject: [PATCH 0058/1133] Reduce a tiny bit of duplication in `PDFViewerApplication.setTitleUsingUrl` Rather than repeating code, we can always fallback to the raw URL instead. --- web/app.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/app.js b/web/app.js index 012cb3159db34..3f024ceab82a2 100644 --- a/web/app.js +++ b/web/app.js @@ -859,14 +859,12 @@ const PDFViewerApplication = { let title = getPdfFilenameFromUrl(url, ""); if (!title) { try { - title = decodeURIComponent(getFilenameFromUrl(url)) || url; + title = decodeURIComponent(getFilenameFromUrl(url)); } catch { - // decodeURIComponent may throw URIError, - // fall back to using the unprocessed url in that case - title = url; + // decodeURIComponent may throw URIError. } } - this.setTitle(title); + this.setTitle(title || url); // Always fallback to the raw URL. }, setTitle(title = this._title) { From 57ee0355734d0f4f67e4e327faa39112464e2a3d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 09:04:18 +0200 Subject: [PATCH 0059/1133] Change the `compatibilityParams`, used with AppOptions, to a `Map` This is needed for upcoming changes, and simplifies both checking if an entry exists and iteration of the entries. --- web/app_options.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/web/app_options.js b/web/app_options.js index 25057367409a3..e000d0649450b 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -15,7 +15,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // eslint-disable-next-line no-var - var compatibilityParams = Object.create(null); + var compatParams = new Map(); if ( typeof PDFJSDev !== "undefined" && PDFJSDev.test("LIB") && @@ -34,9 +34,9 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Limit canvas size to 5 mega-pixels on mobile. // Support: Android, iOS - (function checkCanvasSizeLimitation() { + (function () { if (isIOS || isAndroid) { - compatibilityParams.maxCanvasPixels = 5242880; + compatParams.set("maxCanvasPixels", 5242880); } })(); } @@ -447,8 +447,8 @@ const userOptions = Object.create(null); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Apply any compatibility-values to the user-options, // see also `AppOptions.remove` below. - for (const name in compatibilityParams) { - userOptions[name] = compatibilityParams[name]; + for (const [name, value] of compatParams) { + userOptions[name] = value; } } @@ -464,10 +464,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { if (kind & OptionKind.BROWSER) { throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`); } - if ( - typeof compatibilityParams === "object" && - compatibilityParams[name] !== undefined - ) { + if (typeof compatParams === "object" && compatParams.has(name)) { throw new Error( `Should not have compatibility-value for "PREFERENCE" kind: ${name}` ); @@ -554,9 +551,8 @@ class AppOptions { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Re-apply a compatibility-value, if it exists, to the user-options. - const val = compatibilityParams[name]; - if (val !== undefined) { - userOptions[name] = val; + if (compatParams.has(name)) { + userOptions[name] = compatParams.get(name); } } } @@ -571,7 +567,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { } for (const name in userOptions) { // Ignore any compatibility-values in the user-options. - if (compatibilityParams[name] !== undefined) { + if (compatParams.has(name)) { continue; } console.warn( From 7bd920691fb950429d5ddea3f56da30b395b2c6c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 09:25:29 +0200 Subject: [PATCH 0060/1133] Change the `userOptions`, used with AppOptions, to a `Map` This is needed for upcoming changes, and simplifies both checking if an entry exists and iteration of the entries. --- web/app_options.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/web/app_options.js b/web/app_options.js index e000d0649450b..4e5a7e88ddd48 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -442,13 +442,13 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { }; } -const userOptions = Object.create(null); +const userOptions = new Map(); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Apply any compatibility-values to the user-options, // see also `AppOptions.remove` below. for (const [name, value] of compatParams) { - userOptions[name] = value; + userOptions.set(name, value); } } @@ -489,7 +489,9 @@ class AppOptions { } static get(name) { - return userOptions[name] ?? defaultOptions[name]?.value ?? undefined; + return userOptions.has(name) + ? userOptions.get(name) + : defaultOptions[name]?.value; } static getAll(kind = null, defaultOnly = false) { @@ -500,9 +502,10 @@ class AppOptions { if (kind && !(kind & defaultOption.kind)) { continue; } - options[name] = defaultOnly - ? defaultOption.value - : (userOptions[name] ?? defaultOption.value); + options[name] = + !defaultOnly && userOptions.has(name) + ? userOptions.get(name) + : defaultOption.value; } return options; } @@ -513,7 +516,7 @@ class AppOptions { if (!defaultOption || typeof value !== typeof defaultOption.value) { return; } - userOptions[name] = value; + userOptions.set(name, value); } static setAll(options, prefs = false) { @@ -536,7 +539,7 @@ class AppOptions { (events ||= new Map()).set(name, userOption); } } - userOptions[name] = userOption; + userOptions.set(name, userOption); } if (events) { @@ -547,12 +550,12 @@ class AppOptions { } static remove(name) { - delete userOptions[name]; + userOptions.delete(name); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Re-apply a compatibility-value, if it exists, to the user-options. if (compatParams.has(name)) { - userOptions[name] = compatParams.get(name); + userOptions.set(name, compatParams.get(name)); } } } @@ -565,7 +568,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // opt-out of having the `Preferences` override existing `AppOptions`. return true; } - for (const name in userOptions) { + for (const [name] of userOptions) { // Ignore any compatibility-values in the user-options. if (compatParams.has(name)) { continue; From 26989fdf24fa526e8e8f95995deaf6b0bd28e47e Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 09:32:36 +0200 Subject: [PATCH 0061/1133] Add basic validation of the AppOptions `BROWSER`-kind --- web/app_options.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/app_options.js b/web/app_options.js index 4e5a7e88ddd48..962cac375e8c1 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -477,6 +477,15 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { ) { throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`); } + } else if (kind & OptionKind.BROWSER) { + if (typeof compatParams === "object" && compatParams.has(name)) { + throw new Error( + `Should not have compatibility-value for "BROWSER" kind: ${name}` + ); + } + if (value === undefined) { + throw new Error(`Invalid value for "BROWSER" kind: ${name}`); + } } } } From f8aa15aae99977405936ea4461eb0222db351bf1 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 13:42:31 +0200 Subject: [PATCH 0062/1133] Move the `Preferences` initialization as early as possible Given that the entire default viewer initialization depends on the preferences being available, try to do this as early as possible. --- web/app.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/app.js b/web/app.js index 7ac0a383061ee..30cf816446802 100644 --- a/web/app.js +++ b/web/app.js @@ -139,7 +139,7 @@ const PDFViewerApplication = { /** @type {OverlayManager} */ overlayManager: null, /** @type {Preferences} */ - preferences: null, + preferences: new Preferences(), /** @type {Toolbar} */ toolbar: null, /** @type {SecondaryToolbar} */ @@ -638,7 +638,6 @@ const PDFViewerApplication = { }, async run(config) { - this.preferences = new Preferences(); await this.initialize(config); const { appConfig, eventBus } = this; From 45ce3a057ff083123de3ff37691e0704e0e44ee0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 10:10:23 +0200 Subject: [PATCH 0063/1133] Use shorter local variable names in a few AppOptions methods Besides being slightly shorter, this should help reduce the risk of confusion given the very similar global Object/Map names. --- web/app_options.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/app_options.js b/web/app_options.js index 962cac375e8c1..0450d163d16ae 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -506,23 +506,23 @@ class AppOptions { static getAll(kind = null, defaultOnly = false) { const options = Object.create(null); for (const name in defaultOptions) { - const defaultOption = defaultOptions[name]; + const defaultOpt = defaultOptions[name]; - if (kind && !(kind & defaultOption.kind)) { + if (kind && !(kind & defaultOpt.kind)) { continue; } options[name] = !defaultOnly && userOptions.has(name) ? userOptions.get(name) - : defaultOption.value; + : defaultOpt.value; } return options; } static set(name, value) { - const defaultOption = defaultOptions[name]; + const defaultOpt = defaultOptions[name]; - if (!defaultOption || typeof value !== typeof defaultOption.value) { + if (!defaultOpt || typeof value !== typeof defaultOpt.value) { return; } userOptions.set(name, value); @@ -532,23 +532,23 @@ class AppOptions { let events; for (const name in options) { - const defaultOption = defaultOptions[name], - userOption = options[name]; + const defaultOpt = defaultOptions[name], + userOpt = options[name]; - if (!defaultOption || typeof userOption !== typeof defaultOption.value) { + if (!defaultOpt || typeof userOpt !== typeof defaultOpt.value) { continue; } if (prefs) { - const { kind } = defaultOption; + const { kind } = defaultOpt; if (!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { continue; } if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { - (events ||= new Map()).set(name, userOption); + (events ||= new Map()).set(name, userOpt); } } - userOptions.set(name, userOption); + userOptions.set(name, userOpt); } if (events) { From 80d6bf6319645ec0006384980eddfe17505a7765 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Jul 2024 10:31:38 +0200 Subject: [PATCH 0064/1133] Disable system fonts on Android (issue 18210) To avoid introducing any inline "hacks" in the viewer-code this meant adding `useSystemFonts` to the AppOptions, which thus required some new functionality since the default value should be `undefined` given how the option is handled in the API; note [this code](https://github.com/mozilla/pdf.js/blob/ed83d7c5e16798a56c493d56aaa8200dd280bb17/src/display/api.js#L298-L301). Finally, also moves the definition of the development-mode `window.isGECKOVIEW` property to the HTML file such that it's guaranteed to be set regardless of how and when it's accessed. --- web/app.js | 10 -------- web/app_options.js | 50 +++++++++++++++++++++++++++++++++++++-- web/viewer-geckoview.html | 6 +++++ web/viewer-geckoview.js | 3 --- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/web/app.js b/web/app.js index 30cf816446802..0deb80e520a29 100644 --- a/web/app.js +++ b/web/app.js @@ -1003,16 +1003,6 @@ const PDFViewerApplication = { AppOptions.set("docBaseUrl", this.baseUrl); } - // On Android, there is almost no chance to have the font we want so we - // don't use the system fonts in this case. - if ( - typeof PDFJSDev === "undefined" - ? window.isGECKOVIEW - : PDFJSDev.test("GECKOVIEW") - ) { - args.useSystemFonts = false; - } - // Set the necessary API parameters, using all the available options. const apiParams = AppOptions.getAll(OptionKind.API); const loadingTask = getDocument({ diff --git a/web/app_options.js b/web/app_options.js index 0450d163d16ae..3db027b3622d7 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -39,6 +39,14 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { compatParams.set("maxCanvasPixels", 5242880); } })(); + + // Don't use system fonts on Android (issue 18210). + // Support: Android + (function () { + if (isAndroid) { + compatParams.set("useSystemFonts", false); + } + })(); } const OptionKind = { @@ -47,6 +55,7 @@ const OptionKind = { API: 0x04, WORKER: 0x08, EVENT_DISPATCH: 0x10, + UNDEF_ALLOWED: 0x20, PREFERENCE: 0x80, }; @@ -377,6 +386,19 @@ const defaultOptions = { : "../web/standard_fonts/", kind: OptionKind.API, }, + useSystemFonts: { + // On Android, there is almost no chance to have the font we want so we + // don't use the system fonts in this case (bug 1882613). + /** @type {boolean|undefined} */ + value: ( + typeof PDFJSDev === "undefined" + ? window.isGECKOVIEW + : PDFJSDev.test("GECKOVIEW") + ) + ? false + : undefined, + kind: OptionKind.API + OptionKind.UNDEF_ALLOWED, + }, verbosity: { /** @type {number} */ value: 1, @@ -464,6 +486,11 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { if (kind & OptionKind.BROWSER) { throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`); } + if (kind & OptionKind.UNDEF_ALLOWED) { + throw new Error( + `Cannot allow \`undefined\` value for "PREFERENCE" kind: ${name}` + ); + } if (typeof compatParams === "object" && compatParams.has(name)) { throw new Error( `Should not have compatibility-value for "PREFERENCE" kind: ${name}` @@ -478,6 +505,11 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`); } } else if (kind & OptionKind.BROWSER) { + if (kind & OptionKind.UNDEF_ALLOWED) { + throw new Error( + `Cannot allow \`undefined\` value for "BROWSER" kind: ${name}` + ); + } if (typeof compatParams === "object" && compatParams.has(name)) { throw new Error( `Should not have compatibility-value for "BROWSER" kind: ${name}` @@ -522,7 +554,14 @@ class AppOptions { static set(name, value) { const defaultOpt = defaultOptions[name]; - if (!defaultOpt || typeof value !== typeof defaultOpt.value) { + if ( + !defaultOpt || + !( + typeof value === typeof defaultOpt.value || + (defaultOpt.kind & OptionKind.UNDEF_ALLOWED && + (value === undefined || defaultOpt.value === undefined)) + ) + ) { return; } userOptions.set(name, value); @@ -535,7 +574,14 @@ class AppOptions { const defaultOpt = defaultOptions[name], userOpt = options[name]; - if (!defaultOpt || typeof userOpt !== typeof defaultOpt.value) { + if ( + !defaultOpt || + !( + typeof userOpt === typeof defaultOpt.value || + (defaultOpt.kind & OptionKind.UNDEF_ALLOWED && + (userOpt === undefined || defaultOpt.value === undefined)) + ) + ) { continue; } if (prefs) { diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index 7d0d936f74e18..dee94a04d6394 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -42,6 +42,12 @@ + + diff --git a/extensions/chromium/options/options.html b/extensions/chromium/options/options.html index 8d5661fcbdc3b..78385926fc98f 100644 --- a/extensions/chromium/options/options.html +++ b/extensions/chromium/options/options.html @@ -31,7 +31,7 @@

- +