From 5c1cc5dcee1579c5b2f26533adb7525f03eaf152 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 18 Sep 2026 10:37:04 +0100 Subject: [PATCH 1/3] feat(render-pdf): keep an inline icon's text in the PDF text layer An inline SVG icon is drawn from paths, so the page has no character where it sits: a line with :rocket: in it, copied out of a PDF into a messenger, loses the rocket, and search and extraction miss it too. Since the previous change every emoji icon knows its text (SvgIcon.text()). ActualText cannot carry it: it replaces the text of the glyphs it covers, and a drawing covers none; PDFBox, poppler, pdf.js and MuPDF extract nothing from a Span with ActualText around paths. PdfTextLayer writes the text as one invisible glyph over the icon, on the line's baseline, font size the icon's height and Tz scaled so its advance is the icon's width. The glyph comes from a Type 3 font of empty glyph procedures whose ToUnicode maps each one-byte code to its text, a whole ZWJ sequence included, shown in rendering mode 3 so nothing is painted. q/Q isolates it, since rendering mode, Tz and the font outlive ET, and Tc, Tw and Ts are zeroed inside because a letter-spaced word before the icon leaves its tracking in Tc. One font serves a document, one code per distinct text, a new font after 255; the dictionaries grow as codes are handed out, and /ToUnicode is attached after construction because PDFont's constructor reads it. Text over 256 UTF-16 units is not written. Handlers reach it through PdfRenderEnvironment.writeTextLayer (@Beta); the multi-section path shares one PdfTextLayer across sections. InlineSvgToken and ParagraphSvgSpan carry the text through layout, and PdfParagraphFragmentRenderHandler.renderSvg writes it after painting. The capability matrix and the rich-text recipe say what copies and where; the committed emoji-shortcodes.pdf preview is re-rendered, the only committed preview whose code path writes icon text. PdfTextLayerTest (8): the emoji line extracts codepoint for codepoint; pages with and without the text are identical pixel for pixel; an icon given withText copies its text and one without adds no font; 300 texts fill one font and start a second, ToUnicode in blocks of 100/100/55; one font across sections; the 256-unit cap; a 2:1 icon after Tc-tracked text gets a 20 pt glyph at zero rise; deterministic output stays byte-identical. Each was run against a broken implementation (no q/Q, no Tc reset, no Tz, one CMap block, no code cache, a font per section, no cap, no handler call) and went red. poppler, pdf.js and MuPDF extract the example line identically. Verified: full CI reactor gate (clean verify, 13 modules) BUILD SUCCESS, 3254 tests, 0 failures; examples module 87 tests green, including CommittedAssetDriftTest. --- CHANGELOG.md | 25 +- assets/readme/examples/emoji-shortcodes.pdf | Bin 44751 -> 45692 bytes .../document/layout/InlineLayoutToken.java | 6 +- .../document/layout/ParagraphWrapping.java | 3 +- .../layout/payloads/ParagraphSvgSpan.java | 5 +- .../architecture/backend-capability-matrix.md | 1 + docs/recipes/rich-text.md | 14 + knowledge/api/backends.json | 37 ++- knowledge/api/backends.md | 3 +- .../fixed/pdf/PdfFixedLayoutBackend.java | 8 +- .../fixed/pdf/PdfRenderEnvironment.java | 45 ++- .../backend/fixed/pdf/PdfTextLayer.java | 244 +++++++++++++++ .../PdfParagraphFragmentRenderHandler.java | 7 +- .../backend/fixed/pdf/PdfTextLayerTest.java | 291 ++++++++++++++++++ .../handlers/PptxInlineSvgRasterizerTest.java | 2 +- 15 files changed, 676 insertions(+), 15 deletions(-) create mode 100644 render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java create mode 100644 render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc651b5c..c6610ff78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,20 @@ follow semantic versioning; release dates are ISO 8601. as `โค` rather than `โค๏ธ` โ€” so the text is spelled in the fully-qualified form of UTS #51, U+FE0F restored after every character whose default presentation is text. That needs no new emoji-set release: the published `graph-compose-emoji` 1.0.0 resolves to the same text. - Nothing renders differently yet; the PDF backend reads the text in a following change. + +- **A PDF carries the text of an inline icon, so a copied line keeps its emoji.** An inline + SVG icon is drawn from paths, so the page had no character where it sat: a line with + `:rocket:` in it, copied out of a PDF into a messenger, arrived without the rocket. + `PdfParagraphFragmentRenderHandler` now writes the text an icon states into the page's text + layer, through `PdfRenderEnvironment.writeTextLayer` (`@Beta`): one invisible glyph over the + icon, on the line's baseline, whose advance spans the icon. The glyph comes from a Type 3 + font of empty glyphs whose `ToUnicode` map states each text, a whole ZWJ sequence included, + shown in rendering mode 3 so nothing is painted; one such font serves a document, and a new + one starts after 255 distinct texts. `ActualText` around the paths was tried first; PDFBox, + poppler, pdf.js and MuPDF extract nothing from it, because it replaces the text of the + glyphs it covers and a drawing has none. + The rendered page is unchanged pixel for pixel. PPTX and DOCX exports do not carry the text + yet. The committed `emoji-shortcodes.pdf` preview is re-rendered: its emoji now copy out. ### Performance @@ -71,6 +84,16 @@ follow semantic versioning; release dates are ISO 8601. `EmojiLibraryTest` checks resolved emoji carry their text and a glyph named by anything but codepoints resolves without one; `SvgIconTextTest` checks `withText` copies the icon and leaves the original as it was. +- `PdfTextLayerTest` extracts `Launch ๐Ÿš€ by ๐Ÿ‘ฉโ€๐Ÿ’ป with โค๏ธ done.` from a rendered line, codepoint + for codepoint; renders the same icon with and without text and compares the pages pixel for + pixel, which also fails if the invisible rendering mode leaks onto the words after the icon; + checks an icon given `withText("โœ“")` copies as `โœ“` and one without text adds no font; fills + one font with 255 texts and starts a second (each text used twice, one code each), its + `ToUnicode` in blocks of at most 100; keeps one font across the sections of a multi-section + document; writes a 256-unit text but not a 257-unit one; gives a 2:1 icon after a word + tracked in `Tc` a glyph exactly as wide as the icon, on the baseline, so neither the + horizontal scaling nor the spacing and rise left by the run before can go astray; and keeps + `deterministic(true)` output byte-identical. ## v2.4.0 โ€” 2026-09-14 diff --git a/assets/readme/examples/emoji-shortcodes.pdf b/assets/readme/examples/emoji-shortcodes.pdf index 4294baf029c04ecb0b3c9c40c2139e1446404bc3..e7dfd1bc6a19de7199e54c305db42948cebc04b8 100644 GIT binary patch literal 45692 zcmV(}K+wM>P((&8F)lU=CHCa}-wH7xFd%PYY6?6&3NKW7aAhDbLt%7bY;R`@FIHu8 zb7^mGATKd4HVQ9LVP|D?ATl5@AW{lGJ_==SWN%_>3N;`wAa7!73OqatFHB`_XLM*F zG&3?YH3~0AX>4?5av(28Y+-a|L}g=dWMv9IJ_>Vma%Ev{4GMUi?7iErWy^6T2H)+n zKVm=9K%_v=Uw-#bzxnjt|NZa(;_cJ-AJWIYR%vhcbC%xmv*o`X? z7-xO{`u?G=k8RG}FTmO^FUBwJd>X&*b9J?yOE}(iF7$?>Pal457{1~6=f5!gR(r#K z!}Nvc)ze;Yzxerw_V&;Ez=`8}pZqUu!?0z4@yicAz1hb<&ed(;FaPGlpZryRE9>o- zfBUqbX|9HSEVUPWx5R$_?B~CI|Ls5i{_VTZzx_|YdHd^sdHep?zy19mKK=G@zWYDl ze)*q1{JURXzty%8#GFmvDsQ>H_|_Z#4{!dDzyHl2^p>B!@du`-N6rb=Tql2$WbxR) z{rzu0{paug@NfN*_{#a-e{+53&;IVaKU{ye3IFbcZQ{pdh?s4COxZT*Eoc1K$o)Tl z_11OGvrM04*=sH>GmrcLtRG7mDWIc`)ws$$ zTKnor8dB0s`f{t=Nd7aCwr1*+(46t`$&M~zquMce1;f+*V=HqG|Mr-bGwL5_OI_rVa&i<~0#;DMqCC_Yg(FkzrpOWp+a)w_oEX&dk+tgp#=8*~~W);G+$)I~Bg!pW%nm?p@E;Y)c@lmStS# z73?+2uDIUDQ7N)o%@Bq)8Howk#wuIhI%{dwG>PkHC}5@GHGt*PDo|+1Oe-yIvC;aQ zj$z4daVpDp_YG%ouR650m9kL+U?xC|D5Kl^%Urv1WZ!8ER5}P2d}D}+ zH@2quv$GnCiPycx*qmT$B~8J0?Sk0>a2if6t?jcm`P1~}w6Kdrmj(`;-&wq>NRHlI`dX4m6JEBW*{A!%SNEY_j3~c)DZJ z__T2@4qPKw5oopDS=_#6w4@`n4I|69&rZ|SS?jBGjWkvV27xo#H>v3v_Iekt+t-a; z=G)^>Zl5s#8(p~X!`LGw?F%SBv09h(trHj{)htW_u+ofA`ecSFQj#G!(!$sYOU5ml z!?r|pGQ};9ABf8dF_Y=w)266s&4ys^pMB7FUn@5Gu7{74VMrWC1>(F0cX=_=E;<7y z8`8SKoNt?ovL8my$|Gr?Nqf8my~g8p1Bz)8kc`cLc4(*}A#2sK1(nuST|z!WC4Po4 zZh^@_7Pf6dT*K^vL$WV27m&z+3ZCaMbk!pDtmMKMPUC8(Fi{=K-A3~1iPPSs3Yw_I zNn0jp8;(Wq5boU2XBZwfVQHEMa5Fv{L8j`W^l>#eJjOaV0&rhfqnSeMuZA=jFvK$r$NwSC1-c5_6oUW$Kzj!8VGF?UR9 zw)zsLhldHp%tf0N;gNg=<$mQjymrLM+-%}z!)$4_d57|1DD8eWQia<8jG`ByPgh<5 z+A}O0SrqUB<3gzj+XFn-sDAbdwRegaJVQBSV}O+~QxIHRGb#t;wXHWAnAOiz=4l6+ zm}bB!+g2n|t!bty^Npa&oJEVZR2QdU47rF!j0wE)q*ZEg*eYY|&Qgt&I0n0GAYheC zBx=llOAi1kHa{EZ!Fq(n!F(QRv)I*{4%KyAmKmsiu)6CI!}f92%+-usX61n~i=#|p z!sfO6OW5muxE`0~$0d%<#UR1aO<#<79@>g5>jn1~_L-rtXxr z)xC5Pe3`V)Y<^H&Ux8vFPveZe(|ELLjhP20V=C1G%oOXkM|M_H6Romk%-F0?8@Q>k zWWsDmQX{O9(F4|t9dSMk<-S&ptY*dxjx;f_`DN4RsZ&0I{KGqum^vh7qb2(Si`PwQ zu=v5)-A6UJIl;qL(5oGDV@97}LUn?`GRdq;X6T(lrW-nswo_F~k_m3xS0xY) zpA<`Gt&Q{rm{WEW6~h|JC3y552vDosDjd^EB8GQhQAC;uHQ5s;*&I;5c}&-ygXf~A z;fADH7^A<0iL1RDnK!jK@MxpO<|j>T5+|+H5O(R%7mU^L+pm%P2>bMPR{pb~`}`6L zYD!D)5^#B`E>KO)MB||vCRnnQ=XQa(AUntbHyk8X0M|z-==CK`Prn*jVJwrBm(lVh zcUhgXZ$MvgCDk5j_HjaO1$*-V?!KEQ-pcB)Va{QmMj8_{!VxOWaA;z7)yPKFWJK+I zWhb=j+Z{>>&+pgj78iE|O)Na>j{0bYi>xl!X8Uk?XcW67L}t~t*g!Ki z1zV8AqV3&6qclx->ZF+^pQ=t(kAX9lwDs`lSsdS3&p!0gE~glLHM#0DeQx=nOqh@} z!8azgwSwb}5PoJ_41J~=>B#h}!#Jc=wj^Huyy&mQ)_~W6+`-tN39OL!k5FnYvjr*T zIAnIvyC~a6xbE3ZYRVhe!3>-H9NSKnvz8*u!SK-C@aXC41QPqv{jSYYvB+xf_bqLG88y zMcQMcv%_H94&Qc3mnIG(Wv3kxi8`|u9YLmir2Z17r(ca6TT1){(u&?V#-Ng9Ta$7N zWy28eLd>{Wdwacy&#)_*Y*)TPa&V#7Z5;iNB@@h^)-D9TDVmvlq}C}-ag&_IMoe!n z;$UWqZXNoXWjesUwn^^BtddlH8@JjxSlFu0Jngq7ZdKRPmyLu{A+=B@K4BF^hBmUNmkkoFBb&&+37p zRkmAKd-Sc5Jr$Q7GiO&0v#I={|C-A0F&|S*hdawmEX!ifHZ!*idvb@)G6@z&WiIju zl}-@@^RT&lS3v>7>TsW-eIkXQtr&uvL-)=cY0AKsCwR@g^u|=Fpu@*!EW3q&x9=qv zZEAbU2?lSGTv2U=%h9-F)i|?#Mz|Mir(+s{k0wvlt1Wnhefqjk4Z^gt-|4SP#RG$@ z#qav{7P)Gq}D890wF8Jn~GM6P2Oh?wRS)}yYz&*C+^0?v?F@N%R-{-f# z`~Hfcf|x{hQDf2~yFT6yQ+iFw5S;iBK(>wD)#B+3pDDs)*z4tN(NsUzif0XU7!xjf z;Y6HJPn&%bI<=8-mqs_&q4%4;)P0@Vee~1g)|fan3s>i}oN@!stfp_9u0sEV!BQDA z1|FCFl6)#6Xv=k@4F~w=mr$MIT> zfm{WC9@lD~{Wckcne{CdWi$M#huf5ZMsCOQk)>DH%Ep#n`wY|5uSVA5daY@J%U`&Q z`yHzU*!@{Np&1$pN<+IC2TN!e?fRfK$XP%6WvnBt@XJO^+=UM9t(mG>POf zDU*v+ikQ)RQ&@eP2$QBg5Dz2F%(fDN1k;CHNE?=@Y8&iub%&6)HJkufwv3A9x= zA8D8f|OIg?DUrT8dj1IG6DV#$FR&zSm? zzp6+&`sLq#sBgdg{fG4SOC;j`@}KelOa>(T@&8;>l`gqeKl|^_$vtIQ~+ zK>g{Se|F7~`qkgx5~hCh_wT>?`&(Yt&;IGVPk;FB@9$|<@Bi`-zrI0!dwU&V;`&=4*xf*tEXRM79hsI1&Z{RTbOd4A`_#|wO3_2U*%G54Ak zIP_y9Ob=Y^X(Q%PT>D`oOkhoiLY*QaO|kL`B0Ti4Ii{yk!@X~W>2c%Sh~;?*Wh8r( zzB8&2Y3yVRHcew)0n71DgZh>U`ehi`bA5W#wO-~0`cA0vrsP>Vrm1zJcOf}IvouY2 zIBYG?waASQf=8&C3{N9p!NzF!rw`0&Myn0#2XH_mCd*ll&B#0m3v8czS@N7WA|TYh zIUdNxaJlvq>`n(#ATJ7THERw21USwS2AD9mJ0(V@|wM{0x_I<}OF zY;&HzV*6*6ZzuK4lnCzDDoaizQ*`Rknvq=gChxbF(%_lhG9_&7WjP+IsrzQ?WE{L7qDF$jxZ?)}#UZm0!iY&M4;ommkP5`W%-6*ZM z!;=V*sc6eJ<}L+S$E-Q2Y-Ba+@R-)5jg_YyFyuaYD4`>x7}EqHRzLU%$nuEW`lfFij?k;E{3!~|ReE1lnE(E!!a&lz81-c(Mw-E%$5ntYXa*qD=}Gj7 z{yB;wqzx=j{B0#OBnc=y0iF(=HjdVYsDLa4tYd!(GvZ3hV>PQf!;~>Tj-*vr!_r0= z+VoB&Zl>j)Ei-8ihRo?mk}4~u&8MmD4p{|tAyNXEeQ(K$V$a$l%*026OqQ?32!j3t zKx=PRNiooTCP!}jyE_95GUxEdX$M4+E+knuK8>g`*8|sZg+9;|d~&ks8@xLMPbWls zraZ4m^V_lRl9c>ip@srt=e0=nHDR_KXK+~)Hj?ykKzkXUi8M?KftJ@tM!jer%~}^h zrq^Y`5{}`UHqoZtJ=d`Kz=RYG=|DB@^7d-4uNDgYN~SxCxG*6@c?(4zfA}n@2~#%& zy5ux+cC%;mZ6^M5kvo#0no@9xjUl%xJZj6V1i(ybuS$b48>m&nkf5n#8|CQf>9Ais zXAK~mVe@>vynE($)wbPHYcJW|H`Zu=%ZM|ZI1iGfu^G?p-D9-+5{9R<9mL^zyf!nY zalC>8X6Dq|&dWtTt$OOXKCHFO6w@&ra>-q}O;`yucjB;=?m0)Lr|E<;jgI7-wliu6 z1+cLd-o=~{WFX88Y3+d7PU|4EC6`yh5ue(>4*!-&uO>g(kFeJ?@%}>ul-=)zU7R}z z=n+cJV$wUT`&61QX$x%yktIhP^6<4gZQ>k|26pibc{?NJ6-dvTXlb4tUZIk|zl0L+ zl`#-Jo{CSgVP@LKqEuNxlR^~G_Tdk}8QV(d#ac?GlJ^XIjh`Pt=7Lz7?nPz{=JQJ^ znMOw95UH|jf0Ge~qhlk%MW1hlv2bYIW+iEr#-y6F^M-ifE0{mst6K}hwD?5{{|wbK zvkN9;VnvKdcMgR0d2lhD9nZn8J9A(cOT}U~(tlsUUL)xJsz_HU(xePVEq0`xlY1`2 z3n{LQ7)pr12k5&C#^FI5BpPcq7ud$X^mJRkz}ABV$E@yIv480XV1lb%LH7drQM!L< zRw%^h6$L@ZSZ+Jn`Ezv0D-T#ME53f8`^o5k$4{0tJ>hvNW=RQc>Y*?=kYP+098%_U}v zgF=QUSnG!8_gdvNDI*7AotWNdsT@siUnIbI$w~IgdOahPaZpqx4o$6mpYN(q`;LxrQwD?)ziRh@Y`9(^sT; z=&0T2v$~iu(&fxzfh?hw))9L7brQTWhEQ`45G+#4$&gfYL4 z=xK_G_5|Z-dJ4L6f82#NSaC^-+bfly9`Mrp{lLuJ5ItS`u^ii+rIDj~XxQwB)g87y zs%hAD(PHKz5}c(Y8U_kSV%46-HAWN66N8YAD|_9rXfz*bQwRZI;_#8LU4I0Ir(v|0 z)SX&Ok#;c=6!UwFCVDva$*Yq-(xkiIX?wP{>@`Y@<(LGF%aW!BmAC6GGvwN=Tn|g4(E)Ow<8PSl~)}j4CZ_%98CrF9fx2m;irFHCDEq@x9vLnsN%Z+mLO3RNZ8B>o)Nco^ksn0yk4IWq&v&xwT4# zrbSq4!BCR3EhH58B6>uCvpIpUu8dMUI!S0)RX6XEV_0|S6Ei7BKajrZW^yl5oCfYV zIl{UUPLQuVcc^iQg2H4$>SuUb~FBfPAQ>JHvhn`bSmg`!!5vL$tFRX`(gVg zqXiGBHF*3Y{yV}aO%IzYGP^4q$dz~Mv@#N`63B}zbmQi6nGP&eR+nJS*7t#L;cWtbTx>2+uS~~*a4I-XGB;v&P4WZ zfsK_tp*-cZrQyLPC!SvBtlFFzpEf12k5Kx0Tp%+wy=$PcTW5j>;yysdpTmPOv$}2D zDCM2w!KurybUz4v1y$ghb-SEEJ!_(IN_`%s%2qOrB!PNaGFKK<1cB(`)=4$7;z}bN ztg>1k`NS*OEtk>nT4gVZka@H5`_Kc|5oO9{x~^)cH#rJ-$&yXgjj;~H!L`y>YYFKY zqB46}S~4(|XSw(#m=FkZqWPW!!cU2DZ8Zm1VLau|@ zGyUxqc3#2mZsw;b1U=_fy9hnK9nR*~bYBppn_71#Q@EQUKCpZ_^7>kyifGcBg*Tq# zbe%U;4+kZD!p_}nLE-o5v*qDZ4AnXS%UL(Fal*{5m4kiAV8oe*Jd-b}AF8r3k3Pd~ z(?l&u?N9WYFHm@isN|C4(O%=i()h&+KjGsvuPghaw}OZ&ago9HaP4rw&frhVg%eUs zPfbBL1oAZX$JAtjTelv<|0agX7bPL`>p**&Ye-3;CdUE$R% zUjlLY<`8HJURDB)X53B9J3&L%nle)8 zLRo>h1qS-|YLY*SOlZNwuT9vgU9!ao?rW|iR@|mfW3(K)%@o>PSW7)IO}O6+`uu`D)i*DB;G}8nQoB zcxRAnQWvVpi`CdlyC`F!X3_9KFzn8EM@8|#L!T-O^;Ps@8(G5l)~UmZQce2}H(9II zx3?P?mlM;zI09<&%P(NsR`*jz{84fIORW7G$NwMBIR48*fl+EO3I^J`6oXH1MvGpx z&N%eIlu=OZgc?5`uuV5^P#sWHi;t1C5lj`k!Y)<)4(LFNa{=YQ-=LTeYUZ5ZF)rgH z&l#OCPW$}z{X-or1FyX{G}aFgGTeE&S1N&;ds?$saG7GM-cfE<845en&og* z_OoTZD(z>Ql*wEBiCA311tyid`qRTe?BOY>z|KmzajbS&rN0gc}cUHl=<_4 za1Z=jCim@s81wV-SCFOlJjDAPKc8k@4@3eDW=wO!*EIf}yxgU3-!bkA93#>*YAk*I95(cZ~LdKFRu+m{dta#s||ESZfS-yy|egCdN6o~wB5Y~Tv^uQFi-puEg9i#j%lG&i(5btJv z4}eCg4_nW>+;)<$B+_nM7NCdyeoo!IQ_q>m$*_0lgpIXyMe|P>Qs|+9<94AS$VfmI z9x5C9AhXN4{01c$7AT)w$P+P8-3ZeW#7p)Df+|`G>KojgkERM69l)xOBE4=W5YvvY z0>p_dra=VZ9IzBb;t-~?nvT%|J_U5Jmzn#pI?@yWuI0X!oI5KHR>_g zH@G_SP8^7K42WyEoeR!u3bcn%uy-?74@8*+DUJe@raZ;C#;)XATuO{5H**peZ;vj5 zj+p?F9M(OCWT!-RBZYZXN2~&N;eI3e23M7kZ<^pgC+2~8`a!2tT~3~5gX4r$YrJj^ zSVjO%$ph54D~+J$>|`I0Qk9C6-AZFo92SNoTQp$lxXZ=7S>!-yYv9s(-mO>Znd!&W zx}oQ3Up(h!_J=?JcvWA&$MKin{loij|K)bIf zOR53$-e7XTWw$*^C_@cenMa&AQXB6UZ8(VGNCf}2f3YSlpe7`{WX4ck8-pzr#+Ilp z$aBq`)m0=kOm$Y72mm+!O=#sk(hXPgbk2`ro{O-i2?<6Mw;;a7i^wPRozUHT8V<#U ztO9>_?Oh|y{4#bY7^q0XpK%KT<3p3h0|`qwZl_c%^nBlOfu}*h~-2&E6VZ*WW>A#WrN_y%X~FYZSA{xHE9 z`wOk=X@3(uwvXFedn^04y-1-DogIV|Rt>L6z(xO-aFT({R9*NcF z_0!I?@_m-?Lh$ZmU)FPdKGyvTL7#pae%lSdD{*%ZD3F<)ERo0!LcYT~JciCpPN-@! zU*CvJmWT;nza22m$c|2L4&$-RCnYOmNS>| zcOPDD)aMV6TZ$ZF-ze&YKTaejn^V3(AIT1H{2Br^BI*0>`ouMXa+e+8y2`(NPvcV&NmLX*>cvkcaSp^Kp=in5;-s2(_D^Ne&rRi?^?+7 z_6$}V*fb>;Inw6`=tMY)I7ThAMi@%jZQ&g^A^_^Us=nveU92gnX%}0YVLD%bgzdH; zpN64{EP?B1CW28!-8r2rOcVG0J)o#bI5{%orXY6B6a^GD#T1F-fN0vNkXRnZop^}X zJ!9;r*eF-i;A*P;k2g~VYVM0sU%o>2v%mb!_y753q5SIo^KZYuT|ocY-~QwK`%8PJ zmJCwb`-kk65WY`Ut1B9|tVi#qad{ZQ5AI7qM_%~?1gO@#|MChu9hevB#e;c=%D;Gi z=I}yZ=5z63UgmyrVglOg-HQnrk{52DxxvDZd4XOWnOEqs%=0@ZI$mJ&Ra}y zwbCo};J)Ot%cK92_(^vMCh^!1Tsb|M_k9Q#<`v_o4^vP?L{4)Ju)dM1ae6T|qcX4V z$xl8{V~-c;mtl{dOs;)Bx-tPpt;zU0e3?KmqH=C*K|Tod=*@hD!gu*KFnRPY(2GZt zP~@|o9!<`%++CW04I`C$BLPCw-KiLWR0<8Eu zQQne%&jb2SXr22QtLJf5Ij?^E4q!UBF(bV*pfUS*hxR#qouon3_vztC+(?Idh0fPV z{#w|}J?p4f>8{&ejO-CZx(gTMX`Ob0WZcaJ^3 z5vT2R*#oLZO81cg3#UET*B`z1zy)qzUETIKEYffP0@*)BL%J#>R}JZZx@kyYr7uSP zfQD3Wy3jxV!{7Ys58r)%SDF6lSHF4x)nC8g+QI!XNdTiqc>ME+lEE#?1@>;RftyLt zoDZns-^Zbl0&Z~}kamXZ4_G#hA-AcwIgHnjvhCF6{YXvXxMLm%L{mL}y$1ddAoTs? zwfN=uE(8DP)EM|b=YGEGSl|sWPl5l6mUF6Z0Yy* z{da#b_4WJTzW&@S8LqMjQW7OVdn|#{RP{pSRVP#l9EHUyvSRYM+DuR{sIbAR-6d6mOu%Q4)$YcS zSz%=|q*tTIhvtiVoVaVO%L1S~s+wuWql$->!fLCWol@2Yj{S!77GJH2bHhcUek2Dd zLR2zy(EU;2B)ePbvV2xo)g4P@F;~tGS|SSB0&|HmxWW!mxF&;q>&A(sqZ3wVJ{ovw zVdaCa(6>Tsd9zfgLfV~}%jkQUVsmn(>6?)RI}}GZvmYL>%q+~!owkhwhVwomgbcXl zv;~A7s9uCgbW2}3YrVyTx%kDCLZVt6wpkk(nPr1PzRchS?_{H1s30~$uU>&a`Y_|A znGtBuBEYIZgbi7l%u)ylgQF+~Qf{M|P$Nh~{age~*O(f#))7{1iVOnb1@+PqZB_&k z#9-%>LGP;E1IW(G0=9Tk(S69eg^&|I+C=1)*F6x7?aQAl$PUz8{fruHp@cPpv71XO z)%`fgX;Uy3Ag_pm0HtE3I0*weYzR{;O~%euWMbPn$Z@<=>$+Tl(D)Y6ju37V^7baY zsb#RM(jdr))FAWF|9B^pbWrGmT6$8k(}LhJFB@M$#$?%a<_1J{a&(|YUo@^BzEd1# zR$CV9L5(T7RuMaPK0r55d*fAAVPr5*qrSc1fhTMK#vs2j$Zrht8-x7DAfFiI>%7qI z9kK_tTN)E^Wc17aeg1IYXycZoB#lC}d@2nKWns$&#CNSuf6zG&<)*~9;7(J~KQx1@ z8nuloRa+2c;NZTarHRyg?5Z;hxVu0PG#;DpeDYE-$SWXsLtSI=nZ^=FL@%O%sP6Lu zq$H;YpehKFcyYOc3@SxZ7EtbSHKfPe$r=J8vxJ6;Xlu9$aseS~pdil;*x3RIAYQfu zXrtoB6@)xC<@V!F71{;@ru+hx-QZb3OD10RMhzw%Tq7iOxezxBvcZ0I-3mY{l>j0( zTV4ZESmgM~8i)@Aq!d(uy5j@|NW`HR`_S#TC*pbnTg{9hx@ zo6Z#of*0hOjXZg$8bHGrcG3b94gYLX_ z?L4ojj({3jIyP}P`~3s?iZ(29f3q%3Eq&UEe4!YaE?{Jbw5|)w50G=lnKi?~pbf-d z_a1CJ%|aLAC`lcrFQ!F8J*h@~Z#R07g5Ha3{eIgC>}jG?8jB{9Q5dts;o=-JZqbYX zDYDLXx?wHW!h#_Mbq%|QDFgFjkf0N>deLG>v%Y`=NO^O<&DkV+*)?suv;U3~!t4Qw zg=X@TLKZ(cZdVBtR9AsL3KY6^8=zNP2ieGy(AkBIVS-Iscq_(bRwr*`7Z&kdM5vIJ zb^$?Nsx>fx$oJ<#tw>-*of9!ifMoWelbJX+vI5kdnN?DPUu1UG!cClmU8KRQIM+u_ z)QtG(IL0OPQ^RGS6?QvrxRpDodS2QEh<;}%LWl}nF6&sjt@5jDkUbwDO?XO%sd+Om z4sBuPT?& zK2Y6(pdPK@I7wYs7DdaxZW2yG&;W`N>VCT)^wQrG!{+R4l$)-Vzu-zpUSvABAT z!|0kGvWu&;+c6QzLH+i@)Wb^@D}WQDF95HOED*XGW<8p9 zm^WovQ@c*rm|qWIDVnchH-w*J9~&(Q}2rIzuiZ<&F5JlRrMrgo+Vw=t<)6JExS^%n%$wi~Ux+wAnEq3t3)fnBAK z3hUwnFkNUFpg2Q9pNr!I^75P>y>JG0I_P+uQKb6*T#ABRhl>xvC{-sfXcFmcr4BV+ zJiT|B&E%?N^}+HA_ld_D0FfCOC?DK@I)6Z=qJ!J}I8^B@K2Aj=ZlLZu z*$pTe)9BRow>7lGdIgeZB_{|(MQ-jmc2R*#$GO(mKsa^=MLT@VDqf5NDq5J{Mj@7z z<^xc3H()s?BALZDCLvAR49764WBA_eD#!3nyxn*Br{Sp#p`Q1_0g`)A4%d;*zaAtA zrq5{rBuQjBrQXSl1Y6l62u(Z;nOAmPY8 zH^-wk4>{$anG|Ps`xs?BD!U`Q4cC3_Q;n!Z+XC?!IkD_O_Hc`eqbL_B2j@BX$JSv0 zNZ#MX5-Ru|u3;f)qD%snE#naKM5&koV; zrkm1N3n>9W7WIL2I#{An6)Kz~vn&{=RLCp;;B8Z+VZq_&FiYL(^%$=Dz(%(Aj(O>}{NQhwUDNra07K2`IFdtQIH9J!qETL!&5R6&^{reTCOVM z20I&7oPnGOyKtGVCm(LgOU$Ct3{@v*5rbf~o@tLfs8ov!Mjq|;CYsG*&J=QFO2 z&K@kYe3*@_*T8VW3YRZ~3Od}*J5XSMS7K>WpC^cytnZ^jP@@TRb~O1{;aEuFXqM?y zQDfM*6+IfArEFI}8w&znV=v)(=xm0Q6^k=Q8-bju;&A}dhQW3nmaluak1z|SRz}c$ zJB{Y)(IZFEl%j!FcFD0Tkocyb_{OVRVjr&T#Rgt99@KnP^XgNe9VqXrSS8;;GL5QU z4^8+1_~@9kO3yCTKh6*#3~>biI<-_T0meg);E@YksK~~0#x<6Tpb-1tJ*nWQ6aSAY zbnFqdT)e{Z|9r2AH}8M`ii&ubsu80hIr#01SLuGB67)nOGTL0woXD(ek2=G1)p^1u zdJ2#3ULbXwdj@^_@B%oXGolFl@v<=?C|f@vpFWI%E50LNMj`UD-Qa7Bo((o#FV!iy zFw~72%N=bsD93t7>kH`8dAjM}*?8y~q}e`>YnWRa*SB@FzMA9q)rocqi}Q`yO|+i5 zVe%Qb*YXjt_*g;<+c0FY%c^Tk1ONPCpoWs;L@*$;qRO{SPc%9o%wsErnB-zGc5HUE zbYOzCn6SLH3V=}4I(aKD8LcR==Ns63`yh+n`Lc=jO!OzBr4hcF8~^cxdy1}L==pMv zLdOR`aD|#bI=XQOkj9<1W{-OL#Vy>jQPzD77NxFNR6Jwsi`ach*b)|7g+>jHGgP=P1-pFLS}`jqA4C^d?$wud%P=pd*G&E@#Gsge{)% zvfUh+&AVVxcd96pWAiAeyznOD5gx<|Cj!3;OR*&1QZug|Ryw zysu8OE;7L?A+}A)a`Ns&=Lxlx&`kgoztPzSAOm!s44bZJ*sxNGAB^pZIB8Pm54a7g zgBp@rh$D0l;*;om?~iQABBW8ad^0E@cMSDO|xv{>iJ=loBR#H-_ z451x4TqwcDB3%sVm|&Q10JYR3xKe5XXf;=+Q(L5{XBZ2~)#YS7P$P|HWBQV)t94N! zMs1Xuk`QV3qcJ!X(dewId+Z(R5DpePfmE3)rMl4UrMb*9;xzI^)#oYA7VHP7jbGgk zGF_emiPdT(7R;2eS1dKC$it83$1VkYJ-grQMP5mFeNa7#J%Kmc73wg;#KG* zi%P3sW;3>5A~^;@({ASj{kAV4N{lNd)WYDW$r`iV-LX3J?*zpB|y&|USi zYHE}G^((7H$c7UE36BbT!M5B5UDm4-Thf9`ywTAr>GiTT?Md{}N=w5Ud>t5OTeW56 zv0(Q8}PItX9EP0b<(>6s%(v-4r-&W65ad|5-}ZK?I9yw}D^*(GnG0(A2P#8gaJa9nE1 zt(*mi016q-)O@QD5fIt8nU_a{*)J6b%P=BNndokKan>%y9^17bCX0nHsRSkiItn{t zxuz8O7P7$*%E!zZrZB2^u%P(zZ&X_isT@wcHYFlVH8M*IAQWU4$haCo(-*f zXY*7`+p2<5BRgbe(#(XzM5{4A)_jS*3KfM-x%qln{mKBgmsh^aY}KSuX)S*eJ)wQs z9Zi1>lxAR}6m%T|hy}!r>DwuzMGaW%9>YZ4yLw@5BziaoQ8L~pHZGt~psLZ(toa0P z7R-wzP)37MUukUjuc}Fr9xdW`jDFYx!SA*X2O=)JlNmjb?S;U55cnS>7(;^H1mpjD z6O1VK_&DUp1Y=fj)}LfoXEgFO`^YNb`w3~l_>8Bdf$;sJRmxZJ0M=* zXP}|gy&(zEnw)Cdg6>3+(TK2V43(1vWq2;RaeYcoGc1gFhb!(Wv#=~Wa+%PmPD~JT zm^b=%7m-s@#VbHp-bEB(YLhLJE3|LN$N}UeIt;gL5_=E*C4?F(V<3qNXa zO4!UkF1JZ~1QbqFK}{6p=_7j=%rrrVJ!1T2yNaLx;)@&ii=U$lAYFL>{KLD8*1##E z`lgex6frxx21XL9sM=;gYZf2r9i5Mj@xzVa9{OIC1Z^w^d(m1zjS~>oJe}5zeH5$4 z!;R&i$*WAgaKf;o?Mb{NVPK*cH7PLH&_>B%6-(KEw8tc?$ZVWEnizxW4I3j1!wy*^ zC%R1oHleI3cVd%65om^+B8;X+6jpymb7#{#Jfg%a zA$H5DXxa%mC4lJqAQck+n8D+i&6JdTV4I3nbwt4tbf&d-)w+|>#_ zw{W}dtT~UiNtNMqwnG#`WwSE{NTA0Nn;R04u)5=6YFxyj+dy#bP7vwQPF*f^T|;2iE~UHs%d};ba;J#k zafBg@?1PM^gUj?zflQ_d#q%TF7c!RTuH4*-h(+@3KlUBr)24IF;%#NCRfRs|8-zUh z%1@iN@3wK-6WtvEWAQz~mh%yGC~W>6gJOy3)||`MES0_}*z)a#qaeOrO)$!AHbZ_k zj;yVC$%qQ8i67}rMkneXAFD2&z-O$gY0)-+w{JjUI$yJAua6-Z9v(=u==pJ^y`nqJ z$da%j9t+TBm<7PzYIf+{8drBx6wlOIKg6~Z%`Z1KwZ7zWpJFq-9I`diq`3!kOzKTd zrVht~nZ&)vhj()C?P6{p)@(+mBi4*Cp3Vug*S#l`9t|PPC@ksRAcsgJM_=x-MAI3) z(S&{Qw>@?|ZBTdQX7-GIgzX+r_o+7)mPPlcP4Mf?lvSLTwe+_arrIdDUJb zd`bg?p@JYfNkVfh%ye8%V=Rt;z#QLu`t_C=xnSE)S!xT{q6jn6$?j9BN)iG`$XOg= zu;=ufK-&W?o1Jg@aF+%#*<-T#xiW))IMX#z?jvV+p4%mz;kAfi#*0cQjWtQ7!ge6& zO+|7C>sYJ@3?&CWgPcDZ=1HS~ZYM`G703qR{&3!G???_xb?#tUj&39__2ry>Htsaq zp>DUT;lc&P4p<+t&%R__;L>aw>0oMP>BYAh*VypkR?CcS788TIW|myk?SY#JFcZP9 zj4N+}ur00;Fv$aS-CVbkhy={)o)(5p0Yg%6j(w(#g1T1}kc29N>`M+bY2h&-Q|u%* z9)%7B9L6RS1pC4pC5+5qc0=5mBh*EcHaC`s1+6{$+l@H_I=1IsTt+sXgo8N(W_DG% zb4Or!Nx%hn#BP^0Z|o6t(xd}i*ki_}!O$z5Jpxv%$9A1Pl74atx0v%sOsEXk7D|CT zjJ8r0SC|O)8`g;)qrP&1X&+5HiHbw#$W}dwyc0U83%QyjVhO#93fSIj_zB5b``CPm zkZ!BX4tU(JP*naHl?izR%#EU;Qz?e!`Cd$3|M!^G;#_01#+450pmS!^lQ7k3syUFr!F5&EJLCh zAG3oERG_72G0Ze>2z*BM#cwiB#~`Y1=RVJl(^OUM+cQIZ69vMF{+aWJ(H4>7w6(sL zo9iH@Ji;2QT~%LoK$#SVrXm%^xc7GJcCf=sVan;Km%YcrkuRCXzGR)|qHK9L9C?ic z3rCu0zra3`#>^P?1l1Uc%&e5+Ho}I0M z?Rl|{FZTdEK*PWLC1JcGpDz;`qJM}30%FY6tFf`;&8vOB#>Jy|NRC`{_B;fE$QA6# zONK=@&?dAt_xATZHxgk1g()E5=q$8AAg)We*li_%wYZWyDtvH+<;Xbe>=ncy=ZdtT zsagbtj#)jWC{dwhP&0WQgK3sh6)ZCbdnBmqX|vru;Ypcdgv}HwWf&@0Yo29gpO(K2 zP$e2LooT>l#oF|2`o}D=20sfkvzEuaR$6U3bsF{c`F7@u5oRiDe}b`3-s95`P{Ky9 z>j_p~e44J}6Z$TwVW>VnUdt?xu(@-6erL^VJiilHF&>|1Ylg?~+@FqjuGya735UVc z^Y@XjVE)dsZy51+rrpcKwm}#3pv|T)f-EIA?IeW8OFd2=lj=9qDK2QOq!Y;Ad|m-H)&w=Y)H&XK~Fa&NHG+ z!bR_4E2B_Cb0{)1wI1UFNf<7XqFK%Q*0=5CdE^#lD}$FJ1wq?u(iO@QV1-!*Qo*4) z0M@FX6vLxbtrt+iw#Ny1WC%h&*xNN#kx<;!Arx&BG;hS^1lqncX;#k>G(;RbliTp* zjFMnDam`KQ5Q)jlccu<@v|?Orw(L(gQ@tGuuPH}%CeTx%L;D(SO_Av9?CTLUH5yxH z0@@K3A#94%qW*&PfW`)Et|Hn4BWTKLC2F*Yrj9qRZObBBVBW|vPl=*($dpkmD1FLZY30Ak_A^HtI2D|m2L#wylCBbV|d z#?2JsWGCM=)8%BjU;|h6Np|_-xm7!YmuwZsU5T|c=;QwI7=bxE@ly{^*3A*U%%YX7 zx`MEl>#rSEK(pTtHoBV{xK@o!-r-AuyYzl#mCzFB!4xT2s({rv-^wc{tog0vrA#lv zdh5Z|kP9K$_LDfx2J>e}3E9S!Fn{`l)|o?qn|zB@?LnU)SE%z|_mT)5JA|@XQ!@lG zw2Xivze})^A3@U(EHdB%4Jd-^?5CHlrI;4wa-^9>uBkj~EnlhbAh@~Gqq?(T_?7*j z?%<}~!Cw!3XVK9f1wLHgS@2j#)eqNq&}%Woc_=)%si#{MSALJuzac$CGBD@GIbSF| z3l5$&A7sq}N004ysf1jZ6!OOv9t6(W;qNBr(7jXf?Ni}d+~APrlAH@c`E`}hdfN6q z1g~@(}c!p`BHX&gc&F3&+jZHKff~V z^7wR~b3cFO1%3+d4?ew-)rKD*zmI$c^LOS`uQ}gY_CAmqdxrdpcGGqiJ8?b{0odz& zAQ!WhR(T++ymNadz&wI_UDsJiof&r(e!+R^pT5$*Nomhh31w%2fyU(#S9Xxc5NG62 zb|y99^`Pt^SW+Ve=}>myl-YirHIyA>a)e42+76O5Qf?30&ZIi9Z`ux`C6Un<+RntC z*}&SN?L0#TlT4%*$EV-D^NH+c7XHy5v`x}z+VbyIhc6&ul% zPhSNLfu)&S_$u$t2+xBSGzu_J14oE z>ds=_i*{3Y5S1G7?L*xGMV~56s5`g`9+zfLWra!J^-&3e`-`de4^U*DJ_84vxL>rL z#r)@a({|wdm+-l&JG{NUZ|V-j$uZFN9Y~`TF|b45VVu6qo4$j3Kv}`AU3&XeNcFiSKb|Mq41z1R+z=1@Sr?BE)?Ar9#jsfE>AY@ zta2o>*qnzMZ+BQ-k;hg_OuV*sFuUxZmXlAM4f%M4^%BAjW#Z%M6Izia^)BdSKQl^u z#tOK^(4y+zRbH*+CHX^_aa686?@A9!SL{>kN!4M)8RMz;@G7vQ%~sJx*DEjQ8S(9B z?d2=g9=PJ>Q9!Y6Ic!nvcvgAk8}%;0dKpR1dvpwr9TamoI}Av3DE@TeeNi%@J-0kq z#;(a^6+&AM+bz`2F_H%(VkCshDDCz}lsar4!a2WyN~uXM`~phMcKPS<8F(uspj(qGehppe&g!e-!I`xs$_+Nf ztw9pF_Wo-BUo=O3#U?)gH_j&Pl#KYymW!&eeLVxp#s!LVjuIs+Yisdri+P(KGv&-G zE~|0-nprVqG5ILjyJtA@?8BZo&}Vk~Zbkr}-)VDDjAXQ#Jq@%Jg;1%>#nZ4MrJ=1} zi!%Th9d+;_DbzLTO_ds}KFbs^t{e-9_frCew+|AsV9YTG(`3_X%DYr3;He>+~pOZ)!8eJ2@wu;%qZTp;Z1 z6$@^J?Zow8GhzSh?>%M3EVRqNA( z0iYv=z#*=|USanI2b6_b;7Gr`k*xvtynP$>?uL@FPH4{@MZLN6G5DFM7mX7^|qE1b|BuW}nl%6Hg(H33D%b0jTaUYOWa;9(O867d)l z_G0Wl4mn0S&+FSx(4BMwdQR|s-?@#l zu-!_IiaNNP&F!c=;$*o6?n`f8u(ed&`6q0q2*4s(tgVzxp}|&zT61siUoy zc8*LyrvG4uedKw8iDXKRC}NDneb`w%`m#|!;|fDv+9}W24eC2Sw6$}W|PHlHGH{4%IMb05yVb!jf2IoOL zI9~O}$9=wJl+9a4nNc(xN%x=MJ1M4hJLR8mP*O6v33Q_D{UDs+=TTkwaxidZXm~|B zjYE&g9*oFecY$3_#s$igQSssPh3CmQzkEFz=M>M8=Tgo$J}>%aczjuAUl)AJ?13w? zCEvsxe86emq~QU&vjPX6^7VY(DTlA?Jta2syLZ3JabU2nm2Nt~1$M`EK+ieCo2x^8 zF6DXAUnQ}xQ$8g&LZrMp8Ny zGkc`3+geW8ox%coO6>EEr^G(LeZ}}W$#ZYc#hiQiyxWg7aLQ~P({ex052rM@FPzdR z*3{lE@B0eIH2M!PruMabDYv)l`+kfsJwxvj8|7NECn%f}d&1#cZvtD_$Hz@zBci$T zz6)%YmG*-Y0(&C-;4;?*Hg;0?Tdx8edD|)*c?xWlu9kXois6A}?ecaP*n{On>TM)5 zPQ1oK2<#Jf6`faT2-)+EA&Sp0p8|XGnn%8jS|5#QNQ0efo-rJ z*1;*TQFnWL8bAna6y+a&XDkHv^EO`u_WCJT=feQKT^{(~@0SNqSNM;;JJ1*IRqXwP zrtOA%0YwXD;{D2mk_ce^gRQMS^e*qa(er@!d3aP@SzGUf=ACL}K_HbskAd13GpbZ1 zTrxx5rK~r*#NDu0t8L)APJ^ANN_gtt`MKjVY{ZS>skELbO)Si`*Zm`(G9{K;4LG~W&=I(Y%~6ZXImj30sEOI#vlX!rJgu`!sm}kUPOe9x%OgkL^7M&XGGn z@j_l4D*f_lkw&lXkI*dNKZvqgjpeXx!C*ytcQj^R$P%rV=r*$b<~Heih3A9Fd?&P) z9*k`8N+e+_Mu+X;wc8h`;$EIBwDSs8%Fj_5k&%O<=#&e9wjiCHkUU5jJ|mbJu=xw$ z`~UkCj;Ou4QK{*&qc}(nm2L>D)9+TRkeA+>$%uJ3a3R8|{llhtGm)H^=80C0z9PDj zu^uz*dn7q}*l&QP4=++%n5hRWnUqbA<2||ZXT0C+1HVu{6Dr@PbE~%37v9LXaF&(!250f=q&TiioW5k_?k8nszl@Vn>iNHh zqf+v5R4Oh&6V|hXSR?rrGH>|*vfwF5QAyryh44^$lvMROnMeDSg09C9BP+&I%nMnL0c@LyHt4 zA(yFM5F(+w_lzPh4Y3Zv#4CVeM({?Va_;pwl8S)6BF^_8;s2S9cl_ouYg~z(ja(IG zD}|L702(n69d!m_D{ZfVwb0Ti7S^M$g)M9LSXf=z>4Z)J!phOtBOy~-3%gTUENpn8 z{8$)~J%YxwF7{(nwgd&L)x)91-J4>{I15cy1Xko!$}XXc12gvdrrtcm{K$Ig-O7xX zDbgw0=jwN)Ucp{t{r$(dKki``Pu1usuI(EYP~OvA+Ntih=YR`bmInTeWqVaA!F;D` z_p<-$1N;6G%3WEir-@c%sfq**_iouTiPksjf!4KAcFeZt^$an>-c)51IWtVpBy((0 z=h{u`XCOB&)h&(B4&d;eGhX1AJ$dcZk){&B?29sz??_*MsTA1;uVJrI{{ExoqGHd! zP$A~Fr4UflYf$W&rFy|7FuZ`J>Fl^MOPJ&&(w4t(1*nak!SI@3IyV_;h0mua|2cyElkILJ*1fv=`xeYT0Q#xS@)E-qjPFSfa zD)^>@N0h1C0QeFztlyn@6d1sqs#_OEtuyX=;~-*V?8v29Ko!#i|k%| z!pxBxJlYWAfer?^^~zp{oMkObLp%`-SZrEsg+Hqof}M8%%sTmP-o@(=@18_QxMY2; z-c5>Wei?YyIqTa?%6$JRbq7up|K>dSKkm+hv0sh)QUAe2)#&8~$iCn|P+npJKV+Uy z!95|Ky@jyc_8b4ZfUAEN)VQOpjDnIFH5CNxspjQAJej;w-}i2W&;a zQWHq+bA+?b(vXBIt;%54OP<1T>YUE=c3su=AKB;Hq@#5PhPBVFr{W~X#x#J<8X=aN zv8jMA1ZZ-Ey&BC9u>!fuqsQGxsXc>F-vqx79y{4`;ynlQBtwvGJ)_lMwBH* zMMzxb1jS)AJs6G?AkN#AsB5ufo^)-E*E-fsSa#fQG)DLP>{1)n1dIkHG zR}5T|e91kefMw+@BKA>JP$s&VXlb2BA?dnx-@AsQGKBRSxveD~597kKR0k6XI?Vkj zGl6QOP7(V7LHg zGJ5+=il0K<%cU!y-8xwn)nLeu^Zgwu>y0fnZ%k^^MlyVLHtQ|XHLqz?wsJgKP?xpC z7U~6L-x5>cYV(a+Y`vi3AbsjPDXFpBVv!Ne=W*R%hp>q_VqABuP58P3(SpfHVX`2n zUx`!DZ^95REpj-fur<*eFKRxp!2m!bPrpiffQ*ym_4Mr8$wL?CSCGBX9wu|bG;2K_ zP}F@#{cV1Sy{Q;d>bQ&uSf_1!!t}I>jruq;9W4@$7e3(YtMQDejeJ!NRU91AdDiYJ zg$=eS(Yl70a>Gq2eHETak3;PfTc#JwUNlxc#G-K}K@N5lt(Ig;wR$r6k@W~@Zb{I& z(42;EOsb|_5uZS@o=LmObA1gzIlf(U^DIuoN7E#Y=6&SMtWEY}*Q(E0$u=|z= zlauLQTzeX`(`Q#n;X#@^Nu;Gw-3MR_`ZxsQ(aaMm(UM=N!xjE3UKCv{tM5t6WfTR? zA?Zf;)YG0**7El5-uM%hG^&$!osnd^Rj3Cuw<9l?s%eDqA91I{r)ip>7W265E53|D z3=|DrydZgL?n*J)JlK*3(%nB3I9K*$U>JMi^Q}xz_|L4KymTvHA(Vj&ZCoz`zNN*5 zB;W`z6N&Cw>s^c#8OP1NV5alxIH3Z0s&^MKS~aXP#JJ3QBqJTnE*5L{Zi1XpV(Cx- z3oapv!)52Am1fVvt>P7}HrH^5i&$uLr?R2H4ObXgCKBv=!8-?eyk?BsyM-t(rVCy3 zL8)D}>dhogX>Ed~9Yv^3iI(dR$5h?qKsTZVv*A6Ty5qD8W;%jLa*h7uQG4@IA!s_U ziKma)HDE2}sPpC)0mx<-Puv-_|0XnS_8?$M1n7K#brgS;`IpAhr*UuG%XgZnL2!OT zh-7b5k;pPg*JFn*lD3iwppKT6WwTe5+mxomGMTbPcsrPiUH5W}dsKxF7PBoqpLb^0 zuvB-RT%@7k_zU9xYK`9L(F6TvH`eSLeVR2z>j3z2c(}`sz+}$dS%Yc*;$5Z}DD57) zKW;J$ine6Z=%rLNgLmwBFp8VHwyx~vFN=9|^G7?XB-a$EZlikg zdrK?HDi|w;7f$}R#j@F6jT!FMnD{PHGP1GRHDNPqP%hTL1gLkfvRm>xTn5o;1ed@W z4C1G~DDFZfV?t7E4%+4L7{H_HmBc&PKbL}t|C{QN_nF^b?bcT-?K}4rzI_~(&o7~b znUHlKcGgkpP^*_;H%!|(3&Q#;VUAouxd*k((EX)hIR zcei%ST=THAwfri>Y|S;4fh#d3+yG4L1FUQh2$?i2`6bMsele1*WvCc5SsJ`snk4zW zOFDrxkfm>}B5c3hzX#Y?k_Z|BHNP{KKxCg^0STJf3p66E-3Y5=N1UM!g(v|{;o#V; zo+AS~?pCw)GtFYT4iAk-52He&Q`cNntPr#bI#gd1j>APMsS~5^!}{*R0!q-PrB7Fy zmhx1v_!wZgucWVJHH(u1m;K6aPjU*OpothUl3uB}uSSQnHY%eKQ5ckre-lBawSLv3 zpmc7=P+!1aZo~W6x-e4qP`2)3eSQUmDBIH`qH9V)M+Sn}YwV~bA>z~uJn`x*b-cP< zbwe^@5iF^KJ>n|qa1@zE5L?gOQF?{S@~WZ}9ogLVlq1I$J;wd%I#XPPb}A|04tZTh z64v6}&RR=+ySq~^AvtANUv>5rrc_wc362tP5s^JoB0QSe)iu&o52~l=VP5090E08n zAb<9GT;9vK04&~Nqx-eO7B!3)`)XB(W@gX4+P^2zR~~PeCs88bRK1?ve0hXQv01)~ z)in`_t?6ab;Y{A)Z1!ej=uTdmEIZtD@&&KbWHQ4==-HdotFqF?JN(Y187Ng&WuI*Y z28X+<`}P&Xi+Z^CbNBM@C?u|dtBy#NKqYHUr#J6SCYbw-&*n`&5(-6FZMsn_O}Tj@ zNh_ZI38Z;)HZo{+s0`7`6V3B*l6!Z>Xw{g=KSs8JxY^Yz!F7j2q z+QMhpJ)Z^_v+28Tpc16h)bI(|x%SE@s6X1>CvZZ0_Bf1fvTLh#BhW_;zn76OFn0;; z?d|Q(kv;DyQWq~gLHcq;Bm!z@v1^-~u^unN)w(-is+XTfo^(wS&!{Py;cc_Vq$!t_ zy|mT|%U-Dd{&aG3I=*wmgwN*%&b|)W_&^h*Vv%g=j%?Gz8kTbfDX6kFE7+Zn#*UW z7hbu|?ih8f;e6G@TE7xUD4Cb^9HQjS)#VwX1h4MK@B({pvvPSIau6cNx?Er$#z;qf zbswqH+vE1e19p7c!!if7^wDJt_#1)bk!f!n-#%aw1W8^r=Nx&gFkbky=&wZ9fREa{ zj+tpA*Zd8Nq;)r@hOZZGgcm{8)affsuk4j2C2k{aSLb?o;R(~H_4El{%vHR<(R9|D zAHFSESb=ps3_RCzzVLC?S9E2+ZFu}dPl-U6>B-IsGJ0kTTMAhmt^YXT8XSn%dg$!! zDT2jwx(8}_$6xteS)KxmSlja@(U8NRrk_?_Ra@X{_U;*+2$l=es9fbIp9MeY%}zD! zxEg9L5`8=pzY1NS0M+12iL-sNnH)GW?2j-R~@f3 z!Ys%s(jZs?&&eGAM?Vb4RE5FD*5G=2#_?Lnck>w~73xIbp4YD6##nJxKg%5oA4c&D zf_s->0W*cqTPazDjWcx6s=OVKtNu!KA(W(cF3}@btbL>ss&A2p41uiemAdE`Unowq zJU`Sd@_N`jeYp&+*<;#f1Z8&@nTlVXV^44OVC!k|@Fby-Aury?!xT&Lr*}ifmLlg; z#WHhhM;Q6Fc%H^+OWfAh5e;3DW;`tM6c|sEP8vPSds~_&PiWdqS{4>9eEv5!PgB z^2kQc6|z=qk=1Eain>%e{5WiRIJ_>EI*nn)xd>!-au*WN(OD0VJz98G?Vru#stn#x zq1vH;&eEndG~=k=6 z<(SYO{CU`qCZ6u`k&QjRoOT+;RqsFTMTSm_9}jr9!E4xesA1fsVM$>1i}`2KWE-&Q zUyfef1--o*cl$EV_+CzX-`;nH>C1Zhgqp@}#0MC%SBIxT!9fL>X@Jcwg*jNs7V|O5 zm|TBltR66wD4q|nqTYJ9r*R!6r+)j+6%#R^XPn(HH7>azgheHM zM!@RqMGSmay%8xQ*{`D^N2mJJhF(wSfzh2gatt*phr@;a#xSmCZ~I;Mj!&D;5RJqK zyf8o|BH~gJ#(+*QjAd6P1OpzSQ5Dx+V`N45Om|eTjy{ZlnHIYJ)9lx%r=eP1T}2w% zqs(l_1(lMZWrYu?t+quNxO3{x$pODK2e!T=0*&6rgc+;#(n5i8Kp}VgWpB)W#0TrT z?BNx5?zW)yLKT=c?t9J)@jMgRy;up-cfglvn^KfuFV^r3-Mw%lr1qC= zFxW)@hT#a>bB*{miVJN&_vb93KSCN_S98M7bzh%85EP%e;xVT43gh24>A$H> z0%u3Qa1YLo-I{CMw&wv_unxdRKdvPXbVkP9(`tSLZB-d46jtbIC@4GaVde1Pmj{?F z$1URXXk zeBU$9P6R*RV0%0hyuqdf!+$2C*HH<-XY?#}&1LT#wd|*#4l^lN6eh^!D#CdSa<&o* zK-5n)ouFqfwx$%Rb+F@aAhX+MaRfLJF)f4%iIe0<%`DoKUx7Y-_{C3IdqD|Z&z)jJ zkUg#0S-!Z!x}0>zst#SH`9Z9CcuiI$hv#j%zLP;3-J1RE~y zK~sxi1=*)!@YQQUZf2f+ehDS3=_}urxnL_-rSJdk>|J^-%Z~Ig7@l$9-;guYfXrU+ zN89i~vMj@%iBkcMax&2@DU;P^R;ye7^(9F-j0{h3ga2Vi9gq#dFq9xZqhIb{n?}u@XNSR|}7QmReAox?RE_ zUq%e79=>|(q84qq^TT1zsn6G`KtlA5u!+`(g8^!_peK{dM;yzar|6hgsk`D|YH7#X zHTQ_m>_DM~?@pM`Oy?-Je^NNYhe8gNGAk}D@&fiR37%)#d*2%(S&VLU_}+92&tUKQ zlnZT?B0myNd5`lnOKij7r8(-P9cO$%B}m3jcQ{`R)rikg#mg%z?Nxb$LgFbQhe)Sk zMuU%?%HxbYsK_GZSxU+Kye2$m_Y&)ai%usLUKP3^Xtdvhl5(LYKicn3s;@# zSs<-as4^3;qx+HV-K1;}V7Msidv!_vP~B_KoE)qA|sQ7h#g!Yq(tz zmt)p$W=~{bY)e*LOeYvE+qW4fYc@)VM!^3pND2}Dyf?pG`{EX&5Kh*1DF1Q*=Kp1U zAbk74D|b|GO@E6`v@!A2TPIa|hBcLKPR6qodZGoq0`5f$@P(=H2Zh?uK(nxY5s$)d zzNL!Fqu)agr)!l!0^Y|INo!js$wp61y*zleS8WqZxJ|ny z_(asA3yLMf1T-;Cmg>)(@)x|)z>?LK0FZb&%vN~?|Dq;{Bwfi$HO#0jVCjc)SV2<> zRSsZ=*XLNbOM36fPAl-U`dN%RX0{(#G=~k!O7EY9GVF2iYBBI>&YRE`0PB#_XB2oE z_ADJ6%&wfyIxBp6BE?YagBnAjvbwq$){5xh0!-dhRi6tdqvnIIgaJs7(! z-!1rCo!cGkL%S{RRpbrHQ_g#}?zys(x5?A+I{nDej3${lFCvIeBU1cao4MOA8MTE_8X^ED}yBRi?kwa(0{x zhPqM8LFVD!*?WaOrVs#bCnaN?#?ri^#pRz~dJSj6S&0@~vn^badFurKI2bNg1b<Z85o8iE8Rc8YJx^aWhR zI3H_8QqBf%W6XFgl^hEV%xR0x%vGYS*GjEwgX>{YXM$C=w2~vbMSd6p>Tmr|fqJ1#nnHMTS_{%0egXj>1bP`o$$fpMl4j>h-p7SsckVt0hc}oAT@$DXv!> z6n8lu03KIy^)~XsENf>ZqZd>akq9giU>xI8i7kQ_vl~gy#1o z)@a+ci`9O1rf%GbAOPy!MMuE^m+Px2LkL;oEc3M4G0sy)jKuD+7Kvdvq7mjrjf+?? z6bh9BTW%SaQO5Su-A?~a=@F5@@HXq=xBtVVDAyCj|NI@9wmkihI(C~y3l9Ia^yy$x zoX3e|wSzHnhv5LW2leZrcJ!(}Lw``u_~XfitGsZ;y`^?hxh=%Q)Z*B`(BrVelsp1z0&tvr61f0)KRZs|Nv)&?*$kDL?$_(?9>|zx?T^fBDm=?|=O1 z&p&_u=?`B%{rStE{_y3`-#`8E{nL*>fB*FL$DhCa`G>Ece*W^WKmVIg|KZ1<|IdH^ zfBxt1KmWI{Umn|j4X6)5;PR9W^E93-H|GDwm-#EdeUl}#acS%P6q-Qe=6Fu{=gN^` z9}lxqVvz6r)TWQ_$Pm1y1nX}gwuaPM5KG6+gx%m!IpP8&6gOTVp@>5!DZ1(H4t#y& z9cGj|`p+HKKb%`dLO0a&DoX3482iy4uHkiFMGJY90s^rx-Sm)@d;(JCCD{#Y5W50W z)PX}HNNP*Z?PKnpPaxw%pFnLG_&?55=bRrv)(7Ab#qMTtM@)iMWFeV0W`l2{X5mH! z&E45KgYWH?W1gsQO4>xZo85SwxsjsQ(HkwXKfuMA9ALJznjOF!bHhAHNqh57E!H5xls%dqHsq0W8%c_&lnGL2_2jKM z)+7l&-l_$y9i}Mvl`XAyzXpf43)qvxO9~v3N4R+B)$zMnsB>)2jv|vq?6!{8enZha zNz98&0`OS9kPe};kOLW))q=nq3%R@TH4eNT_@yDCJVoXNrx_VVS_ju}cFs=4%BNQA zsBEu;N`7qgHs74h`W<3vJZ8PbtBZ}3#YEoY!`&roi%=AyiLWO7>Nlj@E&uu#*y8}H z9y^80o8Rt;kPaPv95G`Q+$|jTwNtsPfe9AI0~)>)i%mxPQ&3p5?1jMB(BQR7sBCJF zR~jaEK&U7_SO{l|sRq1Iydk-fCX5qHp~B7xAZ6{>Io@Y{FNE8*_~mtD9>~@w&iS-Z zeWGnth0`XoHx&2qXggDmUBQHKOd@Yfb~V@p=WNt_q3A8B_kKgttCt`jv$62$4H4h< z(skn1%)wm(C@D1^LTNU>Ie7nv@~3BnuQ`VeVRJ*eTah=e&42t}KmFr(70Y%J_|2NV zTFN7Va?$KpXRo0f;#U$Ww=v+t89e?xU_$D(ojoVjYry$s2R8_5+WKU!>0`clX}{|C z$1c=kXm`Nc;h@DbrVCy-!+l8P{}*37m~~r3&c!m*NBAtXCZP;rJ*Oka z_N65Ta~`14yl<1G*0+@5kZL+VcWJ(jyQLNv@Z;gvHW-$=X*ZU-gf^7qM{Qn|wZw6J zsI|Re7j2eC!qFS7!A`?;6PiIUqiTFcEe6VYegaAZ(Bsq1RNwT<*>9WMp*=k~0XaSo z6bz0)baarv7W=)!U%b(aVlMr#K;LFrn%XU1+Y!;g6O#%VXL!hx0PbLAGyPuGQlqJi zUQMuuiDHsO+?!V+@zM7QL}|*eAa)BKi~v48;GGp)0=E`%Osen>Q>>SkjbqN|llJK> zz~B}KRuFXB7cVWOOX@vDXyTKNjR!Iv%xQ{;&nlrH8!ViT0t%$fuFAtO*pklXLQON2 z0-%FWT5aLvA`KR{5Zu8Ua+85A>NOFS>yS~{HD7i`7ioA&H`*x2%d1XJxEz6}mk)0j z(WtI79T%uDL}nc>;6aCeGQ8@E$@$19$-$eyOvBr_|1UAccFW8Z$V{)UT`R7LIcUr4 zg9K@`ec-os20BWn`N|-(UUHnXO`G&FO7IdYbaRT}{Q$9Qu?3TKmuj%fJHi05hFvHC zR&l6ax&N+-N%5&EJVoCLc#7YHQ-3nb0yyJSgTjbe{5udUqYy~E;?;5Z(MCTRiIWC<5P#MQ95l}KQkK#K1n&Y~Onw4k>EPlSASzS` z2medrESVbUkkn9Ai*g&)E>$Grv}{L)@$Q$1 zJaC|?Z0pA5Np8tFcw?aU-kyQz{}PGPVtS(Zz0QmkJSlqyM3(wEpO`{?e2xkqUnOH< zbhE}#vT!tJk#O>`QJYg;0ycnIlv?N~)h64n$?yWj8C{sVKqrU^X*p0pW-~I&ANabCRpZTW9cOJO9Ud zC9e@3jg8B8IKlkgd9=AmxNX!;B<>i2Ob6qV!>_MKxubujm`{jsMb$6A^@U8pU?sqTnsk)@PUVzoNx6XO7UyE24qqZ zzvZbE4X8wET@Vw@6%52Hj5K5d7l0aKa2D|wB09uzaiJ~miJ+Jd^cnl;BFXYDY{6~* z8rS|eHY^9JpdT=~LV>~lF5UfN_6;>fW#Jk7r zTt+aBHQdC&EQ*G3iB)}#mSGGeh+A2TGVqw+ZBRSIfNHWm5{Ssp-UMWI&>XoG6@izP zEi0nQO`iw=LSBOn1*n7%v2#3QnGsETnIJh2Kdx8^uNZ=ssAR!aoUMA_CrdbBwRH{# zTZR{-F++)rDhQL%3zcWJdr4=5HHs}{&N56JV25>=S9`LV4^wSJXeX%vl+S*D)cD!6 zKTYNq{QczACjaTckI?lBgQnYfle3rSen}kn9boZQ6x@*B6-Hht*#MA@``B-;<`RJg z>k5KVO=Mx{k)0m`l1!q(9M?`cmgR7Guj}fyvl8P^itQIjpMUeD69Y17R`^6h;O9xh zuG&I5KQu9nqGx?AZ2MNP_imA0Q+SGXN9ATsZbPllysiZzuH2 zZUH#J0HrJ)(Z>_A8 zh*uQ{n1aJIPy6NG59{-)}A2^&H8>-gmfZsHqNh^mOuG2r)t9yQe zXv?ADPI!@;f&%tLh1p&Q_$%ut<%vu;o~Y%HC@C_PB{g6*XcVmPN}SQv1X7B2h&UD- z4}QSH=6l3z^gqKH#HF+kRi0+8V;)^W9E8p}$KE5*ULPZ*CvV30t1DRmVf@R`^53kI70{Vvu_2XQvg33KNh#FGLXCXPElHI# z>FC`-1zvFIaRm*hfi&@e!(@mDX;QE}P`8G(!~5&g-5~FF>jfoeUXVYQ2bbfBfzds? zfkXxRu1htmvc;nv%VWlvSD#B>^u9{^AeZnaPBr|^ke6o8#>OggJ5@F(WSTDeO;S~} zf)R<04O00eyN!OmNk~Y)l8_Lw;x}RE-~RXSdxWLeNABx1Cjy5-~ZvKFW>*+Pe1+q4`2TE$L~M?{rkTJ{fB?}^OqmK{sSWF@wfi#|MXi$*uVVY z-CI9=fA>d(+yB%5^ftu)SLIcd$T;}+o653MsIc>(em&Hqum`HyJg60Ut9}wKX&|`X z(jGYw4{DpZLBH9f`z;mKc#xO&_|7@1e)2J*2aUsi{P9g*#hdrq+aGRkzOB59@+K>z zy`96AS8+}C-5R~*RU{7Rc3@lQX0|6d+|`{}=Y`SMT7w)o-uKYjW9^QS-m@biy9z2;Xm6u&}R z^2hQke*gK0uV3D?)xZ4s^B>>y*&n~W@ZCFpb;~@ILp>|LrdJ&QdsDY$I^dPfl$n2K zl~xqqIZ)`+LCkLJi7KsVm~=$!p|YpSri7Zvrq;8li=0zXOGXeLzW|$+jjGS@E(V*A zpm&8CQD;VU2hXPT47+?QoW0B-M>8A$aF_@;>Laj(MVX_xs(|`&?oZt9!N6X;n7QF9s#Yb9( zwmz@~6?4=~{e}wKVy+}tVG8?O(r3QA&`$g9a|_pc>k_H3w($*3rF;_bSbSJ* zr6=?7xe|$a1niS}GtIZ_^B1^n@|Hp8N*o+pcC6166`#Njp(z^IjGwKz4F}rUSms!; zb&99Msw$o$vSan+>I|mb(De2(Y``pQyL6#;VND^q_90EkNUDPi2T)pOF-;?f_?HvrT84Ya3yY3yi-uGdGu$(k47c>zx`N?w zqld?HKbD|1O87%XyXeK|P?taxJ_}@qL-9xLh`p*Lj*HlE1%X@FgOhewk0*GOURQ9? z%n^4U^No%0;AG;u@G~s=2b$UA%+dYB7&Nm4m{t6Wa;lOvjH$DQ8}lzL@QKB&JyWxvB5zq*!}2aPWDleqR8bq=ef20lM@p3x()?$DXFw_BRtE-3)% z`L|)}E0y~tgahbm^v4#V9yqewrg4klFiu7=!1EH_(cWEWqTij@#S3J?q*unx8*tpP zc`7*j?S{tT4hkx`l6ldRz>Qrg8@#>aahzHadS5mB$gdV$lShciNe}`{F{covUBYmq z*K!>A(PQb>r5g8T3-qG2S&7Iv6~*9?OlSf5I8o2E>qNdhc@(}yRjSpU`sUs-9;aEc z=;g{fR4eDCdbnBghuM17Ex?{>cN_5boGhP{I4=(aglAUl3&*+e zfUu+zVC5}0nV~Jv7^QIg9JJ_+=MEMt!yQ=S!;da|&$Q3(=1{#Qndxs|Z&&eMKPA+t zhd_65YjATycwOv@E8)w+>zS&vq5ksXW3ll10o!K*>qNxq&Q#$05*;Ddy09w zmMkNFu}|XHTPh)>V9{njQl9vQ8X9%TUXJxbr^=!ALgj8jn}yn(CFHGs@s*~x@18`D z(~!)P4mf2Y9M=Vs3GIv~=AN<>?^gDi_W465{f&0}c1u+wO=(u#k0xYb5oY55t%{ge z=bj=?>aLinLfhJUz*g1Jf2FzH^UISt3?#&gCZ!U}U^lHRT9+Eug11|Tor3UL)5x*L zl3ystlieTXnI`%2`XpeEq$IklR9Ab3)vxEyXm=dwTC{^GRIX-RA4L>z{0p>ilZzCy z6hio4*t3>G4UqwtiJk0WT7}4};`>CUePv7~VYB8S1A`Cl?(XjH?r?y?-R0o!Fu1!9 zI@rP8-Q67qcemy4%_cY5FWDdauR7gtce(<(HKT@n3YJh7|4bj|@85uWxCPq_smeXP^1sC8-2_i zZ#OBt;NNH_T^eEP;=gJcbCstsZ@U1d=Hb<4@M8dTdUG|K=1ixD(-c>`D3x=6*7m{ZyJdL#;G&muHG6c5ffSJRdBCD-CN9RJ?b((4qQ69xq(Wqx&QuFfcwQD)MX2n3vX%YRiUn}Pty8OiFUuq=NDT-?iA z&GrAa^?gnjQMNF6nxS^xMxGx5!HxD|GR7ynOdc<)iT>Nn^WN|CIg#Le&JcI6MOms8 zCgHj7LZd&3LX~{H#PG&VOri88hJLNcPu+gJF}ILyE`T}EQy9(%p!r6}>nn2E0>(Rd2{=`juH9xoA zr1Etlhr%0Oi|+g{r<^;(MEDKDsoqdmYB8QndwH*8C~E)43$MYo$eT6FnkU~oC|}@b zR4(%mO%7}N|3mqtzS^lodYl|iedgO65t*XVBf6OqW!I2|9*m^Np#n3k_RAjeEz9Z~ zxuB3pJuUIhb*+7n|%L5E!upnvIB5Y_Na)E2G!x5BN*_ zz|Ze(X*r;U2q9Us?Qy@QD&R-QFh`um7dpI1!ag(%vdvJfJktK#U~jp($(gB-59VDg z(?yxdW$~SyX;Bi3iyWFL&}?k#NPc@G0(j$%rlYX~_v*Z zXZgVEJHLMrjE8q;nSzDAhi_}+FvUB*7Og%jrMp_ueXnd^10Iai>P1)7;!09zMu3wmLmfvV$MnuIVb!K)Wyzt;@ir%ak$g z8b&UzROMdQiQ(775h29pY5}}ShGMMK2aNpFCVQal#ex16xF5aQyVz}|L5_E6mfH&a zEke=h0r5deI`a1%QhlV;8U5;HTLOW(w}**D(jj+_*YHF)u%QjY#mUkPVA#=QVf$XL z`6|9>-A;vwG2Jy=L4-|p>T1~zW6duuI*qfEw$T(*xq^_UfDs3cWSK|eDt-M0w9U{? z0%ANka+0U33h?qaa~pi*8Z<-*dhzZvx~XQJhZvWluewj!AszQ} z?)NBw`ct9y$lSNX6t#*QyqsWN`)VpIA-C}Y$9fQ2tl5FOI&_{6ULR*{)9goBnK|4l zA+=w)8{l5Q@PS(x(I>?@yw6KrWyb6lwv7>*2hWeG%%Z+vZ`$4Id#?A~RQ@k|zpB!8 z`%qEKs`G8JBG&v-aJUd;421*H4&pj?LRk-EftI`q286Z;yr)mNATh!@v$rUX$8qn# zEdo18@V)|`rt?;siBN!E66iBPgq{ zZoU|4$(;WpB#9JYv*aV~z?guI(mK69bN3))lEP|z*KU%PbXtKLl1#UkBQ^URD?VE* zwBl94nv^V!>q6swIGH1!HmWXLXRuRjXbwGnBSv$Bf9|^?kn$=9eeaaV%c^?3Mvo3{ zQ-pSG6WkI9wi3Oz{+v>K&5{(y`nbHUbmz5S5d}P(X-{$dDg?@2{Apf!5lZ)Ht_vI* zrjrH*PYNaBVs_aFosKI_-^mba^*+FX3P(cnEK~CXI0V7FYWX$duv-S~U(oWN%}Yjk zbM02!{yQjNXIn09NzedJFLG2Cnv*KVTGa4BuJd}y#R3GF_<)n0gItmk0ZZ$d?vtEX zCOA($VCM>r)Y5Et%Ye)kijv=z!ZciwS6r>IryDaD9!JK{iXZ_h)^8L#dJ}SwasziI z+|WSwNbGAoC-bOm@f&XObzkC zZWyPpth!=YLh5iW6pJtyP@4Yla#{(ZO5R)|g(|zJiQRTlhjee2!}YIIj@oiKCn2w* zfp%YM4wV;|YDMP#e3XT^lLl4S`&U#R9&0MxVaVL2=;>@f%2_xF3KJlHg zL2}1(F%9%WFI{&*f8HJ>(NS5a5sl+?N==dQ(C9@Ys0X9E6@B7KQFZKS9Q~3?YrN+Qol+GMO>G z@@BAm*qw0|zpDZs@S50wZCZVfF@3=UQb!&fgtCDJtw%9&@ferB0g*3ngUKy_t*#PW zWO{Ns74BsAPh4{NJx7ZOw3H^L8W7v`v`&bYQSmMBACy-f>d_A#H>U46aCEp$tGLd$ zLuHs2clR&os1d%g+7xE%Wq|#f)-H zJ@;z|O9$D37+?--$JVy@XWlKO4rb1wWsLbNQ?AppY zHKI*cFQBLVJNN00A`E!{&zm;YdzmQJ=QlDQsYe@|sn%ufrhHx2z36g##zpp|`?ggF z;{91ROPBo<`mNau;Er*b5?{;2WM8t+(sn4U4gty<6?1qm>=~rB+_Dm~{3Zc>2|FS( z?!;*kULGR4Oz!lni)d6?yTFPUd)4L6$9JoT87)K!Vl#HQpM<1>%`M`($C)xe%opFM zUTi4PzdE;3#ur2Hs7{>u{6Gpk8?^&?NJb(4ez`OJTSsT~MT;|jR!Tb_Xdp+&1y!pN z6l74Bhn+{(Fy#6xX+>2$j%>b1hV8a23IvWAUTXZ%*yYcE7jzoS_BIK^$>@LeX%gJo zer%8)q#v5ma4K@7%P&W#6g9_2M+f)b{4)rLbdWwFU2E2l%f{ha<(llYv&3LkRJ(tf zX4KHz-9gUikt;mMfUcNwui1lrF$iD~NZ1a#%^+Wmn`?I7E`t!AsH3x%DN2(B#Hvo> z^*b!U_DW;vq~d3`<=o-_n05Dn_)|SnD!K{l%KO`LX!{EP64p*FY*xCXe@$C_F@K;0 z6~Wvq&vU+Fd^XyGNmgaCMTp$gQ`z3^z2`s#Dh?y`K0S>612z6f8QoFEiydN2wvFm& zLELlcE_Ey*sNM^Xh47r9&2BCL`!7cA!jjb0p6Usd8ESp1^c+feXq+LO9YK6SE&5~f z5n0};!nMg(e-6{+3n(u8!5N*+Q2P2orcb%jrLxc{!tvTuu{Uzy2sh`*?%Gw%*m)G` zhO+bseyPRirmcMz1PxSt(2(P`;I!bTo_BSaY0hY!TEEU9kFXeLuEN)!q#1~&FOzatpjdMl zB`VAA+pqjFm<%!sLD3mb-DGyToMk@0p~{0j{61GNrxTh@U-9S^Nk|K{ZuxnFJ{*?m z%p2=4AzK{VicV!=UrA}Qfic!W4K=At3jwy0vG|EsKzALU16q?y7+pp7`eEyGRc6|U zRqHi1_o0P@rdSI}(NDdK026OJ6>|`ZKsMRP(_%?EavAY4h#^?0<4eRvjfs;t`*0RB z{1`;fya(+Kx|?6-$8wIgC2qhh?6J2P*Q}Sb>=q`Pz<( zKr$5uLyxgDzh<61ImmY}kSPLMP6139fho3bA2Nig)hG)b&kX)+vq6P=Jq30n1{!(2 z&ZBNmLx~+va->x%Lkj{LXif*xfim#+lMXB>s$9e6a31e>`9!B04F7(>P7D6Eg{Z=X zLIaFgJMI6@M#kKG~ zXnf^wPkL$)G$Zbn%&Qg;nmrjkBiyNs4=Z!%pN_eGhAC6b#ID))A$zyZrSs-dR(0X( zU4L6@X4E^T`;It*PYRED1F%n(>wwT+mb&}#>2}^_}$16=Z;{evmdEg z9U`u!db`JAcRCrjN3YpI(^c+Ij4aDCgUC4_xkQpW!v)XSGU+`}^OMxCKWX#j#@9Rt zT}iMF9$@|HB+0unIR{Jy@l=)km8l<=@pP@)Ph-Z_4Z$;BY$*X6IvemxqYdp3bjIF0 z+9Ms%k9lZ~;r7UWj*UnkvXm9en-aZuedI|%?-GPEKQ@4UJo_B_g-7;1{i_QH6=s|; zP#yQ7Z&}uhuf6Z1Py^91W^DWD&HP!);oR7UAs0jJ!eIF9 zHYeLyX(pxU@f{~rAH*!a-kOBidZ_nFjYsb~*Y@Zpy$e=O6I+MRqe&~1y<+48@qqzg z3~v*URFPfia{Bzq>%xO%x4Kx8cdeBK??4Wf>x2wlD3iy}BATETri^a} z=FA};e}l%mk5ZRRQcBu>aXclH7wHfF%~;FvMgpYIFS-5cD?O&@h3$7YocG?CVXS$q zsb|$bb+w-G7d#Z?>cmcMTu&tp*U0HO#pM2W5*V{(L*^21*%fi^`70F%3zGxl5&ww1 zC?f7$+7@os5Nv&?Bop%ryoSYIavm8c24-1_Y1P>SS0Ta~b(y=;)0ne;Nt?rev1hu5 ztlo@k%!q1FS>jz!YzJ+dHV2ws_53W!zT7adb~TxQeCZL&$>zO8gUs8_4)LCjNG+Af zNsAHTOL>!$#pz`kF%%CVM9=c(Gl10gG>l0_(~V*FhT2Ozba zn=qAVCL&PQa}h2{Yxsyk)U?%7GJwxrNW@XBx#HK#4wZ@3OIvH^XAVw#{&NzFGof6_ z>o%5StqlcOsW!`ADmm%Jq8^9XQf5a|CR`7E#_oYfagL7>76Dt*=sxub)1;1{Xz!8Z z-1W0?(x~E`;*HKbK7zd{6xko^VV4(~lX)FFjX1f2udqhcQV9@MKwLnJ+g`P0SC)s6 zgs1-UP4jnd$e$S)m{C?R_7YaTKzLBco^&zw)>mlWGbujDK${l+{Es?C1#FC$2KXm_ z0`tPp@(ki#roB6p{u_utWD!{e%fT`sfladL!`SiC;_>t$pRd$dsOF(5;d`t211)k< zd-nHC@qqZh2RfSUQ9<0xa$-@XuNZmR#ZpE(J;u}183c*TOA<31<43=485juHlH6PQ zEpKNBuHMV@#69Xj(;Ev;S5Z|8pCmSeayaGQr!C=~h8&Xi{q_{}?0`&Nq{axnR$@y8 zgB)5+#xjEP~o0&(_53 zR<=evAftvM_AiMxTIYa8tXe~i$JexHu17l51TutU=#gpLQj@Tp$2pe#mM(1L;SkP?QeHyH@?V33fRG2et|^1g!sDxb^wp#5 z{Z=fsGIt>8)%8sY94(}4o`+$j|E25a^9q5v?xSrUN!+$&S^&z*;>m&{!;ulXn`JdQ zgSAp{QgkOS^0&LO5Md_+NmH#8_|6F>=8K zN2z%hNY{Bd`9)VljK)vLmy(@-aL7dz8b+P%F5q^uoJUhdzsMUkIPDb|C3{E++U+*8 zq+*$)Z8Qdup^uvY=e6x#JX$r&@Q?1y0bav+*h!$n zT^TzdTAR2;$p~JBySjnlO?4?PsXzB6HM1mev3;nMSxm{)BO2tH0Nbtb;bCO<%v>UL_uxt>T&W7x?wUr zDdg-VI@I)?1V76i_!}#lcppLD;DT0c^&ibI@1|(NQIB2Wb}Y9_{-)w3R&{8q{v`Uq z5iLULQxux34olC0WM0d#OQSVD{~fQ$^j3yfOI5hGw`AB1W#mh#AB+kT)#viZj@_7F zGfxmF6gbeDc}?oqwSjCf!%LL0%N6`4&dL5-@U+j%f5N~x8RBZ{I(?ezszI^jd>f@r z_DyQjF?6@_I_huV`3S9Q1|QJ zT=XjWCBpx0vR^qpyT$>l1G;AE4D@rvU|KGUxm0FFsvhGQ2Fi-^7>HF5OBe~eV~Hb? zimrYt=$7V-`KxOV5f} zD?#&~ye{gc!;{zUd~WeL0IXK5Am3vQ6O!S_u%BCMaAI|2YxkVmEfVT7NSQ~q9GBC7 znZZ43Ujq(Ia%e3!M#?O4ku1SgU}!CrvS7!SSmfImZ~>v`TJGvi|_M*c2K zo#Eq!Zf#GBaaDlGZckkUZRHp`Tc)+t%fF9d(IrAr>wj4qTcye7Wja)$?_hrIs$Du3 zo+fXWv*9QRK{lc;f72%SXyxs@1kxP?Mqt`>tr)_{$8$V$*h$)I>^h-bN2hu>=l8|Ki&3^V zQ=R0LPcDXOo0o2wl(PXdq2mT_dbe|EROnVsq|H1m;n$3@S4IT$!qnx+#P zaeMk)LmPEKd2rOP82>EzW$f+3<<41`Lw88resXMfzbYZCS%EHvS5Xf|zEM`QbyPQn z(s6y4&}q?maSF8+7GVcgwvxs%0#+Tnm|(uJmik*_?1hQvg^#F_01uRZ(r^-ob~+p#R1% zG`nMz#Vkus!eM=+lY9Mxd7eJ?3CVoREvEp=g{$}1-{F@@_+Md+X2IDJ|C&35O!Ys~ zt#SoxT-}RxU@`OBn_S>VuX(cobbrurvZ8d}kH?s__EZRlVdnJ~1o{Xg7=R(5+CJcu zgJ2mCc~@ND(aKz3vWE2&qrDRbR|BV9u7 zV$8C?^x3!UUaT@PB_vcv@3lUflOrSK#v|#R24@%mglssUsXLLxkl}(v!=0vq+N)%u zW*$HZ$ZrQlR!2EvB2U@C+3jQ;i_B&uqlLLtUtK|)K{L%BraDaaj8+C1E2&M9n>tkj zE%;uf@N&|fI=3av#b|0^Fke>Z2=!Ij{N=s|2z}2sT7HF9k0yu-pRHRefq8TcF`id+ z{(wtY3~n5>-(c_BS3^{7HQ+GtD-3w(>~`u*sgvNUH1Lng4)uQPQ$`ba5VlTjUEaHP zTf07Q+BT?RxWxMX9el83%@k3Jj0sh+#_NfYDfo&_Q1K!3{~ICpomktnM?1?+=4s3Q=dRRXqW6Cy%bUmtrJ z0skvL-EVGZz3;svH2^(?#|dmAKL0)yf&%alnB+)lh)+%FS%neV@K)lMBG&Qnods>& z-MM8C1Z;4hVe63>JSmcOc-;avCd}Rl+_W0;n6$t|E4ncl>RAruJ)OX?8Yqn7u z$^&rph2ZVRvF!~sdxc!XA&y%E@S8v*8N@_(W~oXb|x z2vdVFfycIkQ&-W8lKlfUcx1wGDdme!q=G)YOsc`lQh*JGh;O_{D{%4j{M=beQh?H+DAMg5$krY$7NI~R0Dxxo$8qUp zZbjqb=cv;-a!nwV_3q`l>p@f6j`X0P^db>ZIuugjaW+wsLIkn1p)Kmj_j~@vl;b+f z*SAQMj@jjTA5Lz-tYLgtk>^dQGwz3>ma?ZxEEx)iuez$-z#DN#e zghkSp!BLQ)-oZ`S?C?_X82Web&%#QS4f6~EvEX%2lja@~c_t~q6|=z4-W{CDT-3nE z*HS1~u|-fg^FZ_0I$+}z`on&1_1jebM!3avt0#7JbCBiWzByGJaK2U~`d^CM6c6E; zPkCl>9lRTc+1q&vY>qXxJ43A)wAp&>d`c3Zoa^RoB2-t)nH#uNKai9eL{JAlqm@6Ggm+b|>f`4{xlf@E4A#OjKJwG9aS za%nBYS^W8>a>H8Nikb#lbmlIAWgH8U#n1wh4Q(Q8qs}hy80S<6LsL63aWB-Z3+4;@ z@=78>yl#7b$U)S2!hdVh54H}qvu z93^GIozF+-W9df~z#)*V?&(qmH2;NdTw zYTV?90b4j*St(2Llx@=+j4kFDetX=}2{vC!0qxNyXZ-2kpS-cLyk}?}&^zcZ{;$%)JiZJW7smB2T4a~c@QP^ zAvgS-3v~_J1`!lGMwxCqqE#Lg1skxB#fR7e`a#WTzt!q-T{)uLDaP(EwR1I;a*}Ty zC$2og!C3ZqiZb+*o#TZC;g}Z>ywg5rwM&m#{7*k}r|t^M;{2i_X(ndPrkXBV8d9GKOHCMqTNgJsIk*jG)JEs_{vKXO@ z4W~R;X-9YV(EKICm+K3$j@ex?0YUARoatmfTkD9`3HJaN!Glwkp=(^wwX)<3k_(eB?v@Qht()w-H(2O zz%xh$2t^ST|w1env5@UZDJV-FmB`<;AHc8wU{Y`^R0(i^6k(Yi#-t7 zVTxgrPOf-4PZ>p3OQ6Mddk82&ASz5pKXD04+OC>~zwI~^mhwkgxVbNhTqFUqu|ATK z_R(h9u5|nqJ_#BPWflN@AH8qRfM7r|8E#plrDCZ;rVC+PbHM8?$@uK~3 zWnL3TW%i;5b&`O8=cNS(E2AQY`_4vdc?!eKqK?i)09w7C=CK;TyPCs<-}jOyDGtTG z>k8Q1$pDmrEh#-rd$-Lo3W>9k-^#A$U?2$Y{UWsuCVVzEFQV<5aBoIi(RoD5=G7KymKC8k5-j9%`vRnM>*cZ;gl5ouLg|<1E`!z)vMjA#8 z*Yz83(4wmWzDf~?knV7`nuIl4KwS|kqEB}^+&qYG=(xoeM z-};bJYHGo!!drI^NMZr18iQ}`8e|Ts0v6YGmr4X zDS1U;W8y{hSn)UVXfx#RwS%2p_W~7I$`MaI!B#t>&nWn8(qD*1@Rowp`e%vdK)Z?% zXVeaqMHH^a~MVNfze5jpG@UYcvYeDKNmuKT+@)8s-d$1kzdiu}@;etBy$d=qi%4jK#z~wzeH@~a zChQvj+XR)%&=MctwpV_7N%fqv`FlMXq|}1q(P>-5W^~~d8hf$&64k>Sfed)4q)@AK zE7R$HN3nPDXZ0JmyRb}rON+#f0Hy~D(*UKhQCZ!mb{t^sbtLH221a*D1A`nqJ~^t9 zT9?>Vm^J~IzYcWecK(-wwf^KUtBrC7Z~8r0ER@f_x}kQ_7Q_OzFVh}oVh?;F61;sa z7ov=0{9^EWSliV#=5dXBtr%s(FY1h6SXN(bF>GTI*Op)-bFkDlkI!PZQm8@Pg(YHE zK4I`9Lv^I03J=P#=hQ4Zx~w;we%SI0c*dYg+w!-++^q0fQpM6=dvf!qBKf)3{so`ulDg%$I9OR z1MGmd!Tp)ed~Na#KFfOuE;H0XQ?n4_nMZQIZ1YEXew67H8HHuvaq_~xOM3ZSU!Me@ z=Al_00s)ls?gJaX4!~ABG77E-t?PSg^HI9nP)xQ)iE#(MLYh4piJjpADQ->B!GT5? zT$oBbsmyX!5l?GA)(s_Ct9TP{5o^x)ENyD<1(RW08&PqyONhO>j#0FFfovvSRPRq( z6t#GB$fZL$Y3`v4MEqbx`9G;1NW5bl-^kQ1x`k~;A^bA#&sTq5vm4C*W~-gd`gb5# z+VAfIojOj(hmDa5am z>`M#>E9j;+^!ahn_3`vP=J$s5`7qb@%1@UQ`4jACfYFpBXO`vL#}-(P?@Q{|w~2?t z^*{CKsFu5X2%yvi$x?W@aeCccQdwDr{-Fm+OcD=UweV+gMeA~PHe45Mk`*@op z?0oZjU%33ZMf$uC8`}T$BN+(LMSwN6GjVotGBvdQFUj7>3YLYNh>7UG1U^1kMmbYE za~BIDR%RAhMhQzB7gHx9MhP237gI4)V|x=*SOEdp|6Yv%>z<_-FK-vb^fTl!%P8fT z1ROQNKRE6OB1+UpDF~YAAx?*8kYzG(ZOIKhzV+MlT&4#>7feMchYHpY!MkA{wrRK# zc7;88)?hgO5O^tg0Mjker@82#z-f-ATa&MnhoDG~=#%BZ$Ukfjaze%@@SV29(vbGv z!!|4WCeC7CM%YR#PpLx1B@E-=q$ zw;RPtDua3GVS>qgD3XqJ9L(0|Dy*p($y&A+-epOI94tF|M?YM&w^^fC&5b+mf<&J=Kn3?{kvYDBf zn7&{#VS@b+%h_4}Zv>fHS(#xO)jS+bi5L})tW;fWVHp*O*f{?)tY26=IlBeZ|kl13%M~&D$KmU@+Wzl8|*ej zWP*K%A_GQC#mAOPl8K7IkkY}>g6@0ava@~!Rw64a-H^akBuvX_QGqZRxaekSSoe`!!MnznS2op%mx$<{NbAbbP)V z(`%S7$Ztd}FiyBqG9cXeGW_j7k@xC455cH5Ak>XqXnScbTRb;+EGt5JJ55ML%Np9y zqBoX{=g(4a!(pKE0%R}gd3CK3CD6m!ui7H{Py#;M`nd;47zfp(-$xK$~Hqm3YlmU=^&pC?Ta7A;L8qCDJt;Yc6YBkjtz* zvx_KW9S%Ka9UhPE7{^-~8&B-OOH&BKOIHcXO4BgzPPoX5UKn`K_4!D|Y|yHz_{)%y zd)lVxx@A*NxOl$7j?%aR;ufwD%s7u-`^!y7ck_aPD6g%DnX>)s;QP&xx^hRwi=HY~ z5tnMZR8ap}Ta}x6o7>zqE3M6PSji5FLZ;qQJ-$@yOT+3|N{{|y)AkM4ovy&&lJ(-9 z!>y(+d)#m3*Cn|=_U<}uC7b;0U3 zjfp#d9-f{zG@k(<8-@urqkjHNDyqj|+!!Kr6+=j4z%GmS@E7_7{wRnR-lufA=X1`by2Y)p&p1Po#B_rLG*0_#(*_i7haOyxD2_~0FW47dbErA}lM_CXYKoV~=L6sQKINYFb+Bp1+ zZm(EqAu$tEfGaK-X^6H*RifN~$2^+o?unwLNOB@NTnOv^4~B9g42fNW>Vh{lgVW?H zI%9`L0cv;{3&nVZ$mtG`V`wZOlnMCSrwA;r*svsn3t(az6dzDi{Q4Q{F{G( ztMUDV7%>=VB~NO~ob5~Ho-2wqQx^(@+dI(bw^e@9-2Y^qS^tA|W?_P5RI#^rA!7bI zsRGL=Ek>lvC&tdoCC14n#>~da#VIPz#mUCRCBZ7r&BV^a%FHbx!X+jlE+Nd!&d$m# zEH2C`#w{Vi%q%V@%*-ai#3IhbEI{=Ce+#|;<21BXOwC{!RV_VDiN3r7qqLogsXLJ_ z)7Qb*C7MLK%tS0i%wOXFGfF@omqZCvLKB3N;`wAa7!73OqatFHB`_XLM*F zG%`6hF$yn6X>4?5av(28Y+-a|L}g=dWMv9IJ_>Vma%Ev{4GMUi?7dr$X3KFM_};&w z9yP$AmG_Hb7?3E-^Qxf$KU1wEk#@Bx@=y}|?-(p=KDRk@zW{5yycoZ<^J)CL&(+m-F5!67xzHPizI^(%VfcpMU;NVWTkQ?|4bvB% zS5JGr{qh%|+S|YB11FB_ee%Dw4a1iG<==kl>CHa=d9H2)|Mu@c{rO+#x3b>;_8*@1 zGtJeokEQm4@0QrlKl{aR-+%irzkmDT>u>+tZ{Ggyf4u$p>)-zV4_|)!_don!Z-4vW zKKvPJsL2o(Ze@5>A_0vDu zfb@Cpl7aZ_M9 zJEt)WYGjf|!?xkQ!*;6iy)D;N9BMC^nb4l>u)ULH{)V$ON^YDT53>E_al|ZbK6~%R zPpr9(0>A2Iw&tl%?7AAKlRo!7DyEZ-CCoB?l4Y;Cw9Gv61F(KBWu$+O%)kZ2Ct*`kFC}4BVQnZ^AR>(e&iO;}%(^ET)-Lac9?b*YPFW`vVhUoT-=H6H0) z^|Z~7eOS3V9*fb^D;RfX6oG2*nULN<9ssRU`t*IN_Ug)Sf>6cev|jj=hw)JmFy?b-#i1K>2AT3Xv@ZStq-%V}X3v1H>(Nw~JRkV2keLgUMk z#>ulLIYupnrS8j16LM7#r@D6Y3H3ZV$vsnEKJh)O!*uWs){E_cbGruZibIbs)Dxq{tLwTtQuW}1P}keAJrXVf&5>1b#(pvKo)=1AkiPC9KbZGO#^Spc;z(gC#V zNZO&=rTXU2SsY<&t)|)7iXU*8O-LE56eZiy8ysjH&qvyh4u_ev@Y!U;`|)(gqVZ|t zTpYMYt|HKCyR*1`&1gwSXd6bBZ=ao}sk7Es=^AOQ4h#ZkvTst;Gwk&)T(_?qxy-l6 zpWHrU05-aC--oeBO4=7teqyyQ>02i-Mygqu0$`;XpY+KLQ=}wAaHNH?6PAoyG>2`8 z=wymp96u136JjRQ!KY18(V7jx+&}xE?Y>rQ@?8%fC&Q39j0(hg4es({q+N6dOg5x- zfjQqc6=gq+oRvq?K9lx%33`pk>jo6lA|M%?|Lo9ELqgW7V+$&+tGa}Igi8DjU)%zd zfh=s>gt&&;1BYZ^WG*0)0Tn#YVd$zw>RHK!FPz5JOkturl)H`O(-Wt?Nfk6viIcWW z&^8>4-XYw%q0cZpY{Jqs4d7;cHiAsmMd{;eZpdj+E$rM9DHml~vOYN>Bk-6E#&4$_1YV!`|#ZcP)Y@`ac{~1LuKwqxB0JLXV zHnJ$-1;&L^5w-_-tWo{!6Kd}iFL;J>#>N0EVWuFswq{fg#%o(|G%%~5sm#+3GBM47 zQ?{*0qFU2TQ|22%l{t$RYpE_y!5DH8ix?Al<4LR3;ILK3)}5sqCvgmR*FeB3mq^r@ z{gxg8Qfz)U&V%&`i-Y+*(q^%%Gaahywk$JH{a|(1A%^Yas+p@9xy;G~V-`o5#DvXj z_m{BO`*1xj%a2POn~On$qnox~2Wc)|@(cyckU0diKhrJ6m`pcx9&M+plq3_}wy#Pc8a^qO z%vu}i8!)HrCMt$CluPjFI}o5&xm7r(lSB;fz@msW5o)q0OtLwkeDj#DJqOQ4O~VaI zvoJ<~2@_X)H8O8%ap2KLi_K4(*d$I`sUhsrp)VM#;kRES_YwBxV^;pNp!@m~3TjGA z?hljnAUxF9>o0XG~ZQ~=j!DCqSiOi#ZWSz#=bl$X)+BzIYz zvTs0Na3$3qY4&kKZ3TPt0Pen^GuVKz%o<)Rbl2+!|Fb&HF;fhI3>pi=wc#dU4g7Ds)w!bMh>YqNd0JT!`35+bu|TWp}2nu0CJ zVbS((p;4NqJ9W~`l228qs>i^YO4@pO^em2VtY;tkXqQtAzM5S1nLf9CP$o>sncy3f z+FHSJMhHJMErvc*jdW!C)nOb`Dq9jSe_r$lu{GdzAa^kKX96qa{UelG%WOePIS!c} z^e)P_5w3eSlbZ6zbuhywKgYIH<*cR1axi>!>DqRHjebG{hqJGcFHM^$CeU5fwZDGjxnet+18}oLfJ5c zyAU%j*4|$4;WO+?Cfk*7kQ`j-bsI zqFaZ)W|n1-^Q&r4i>hmGf(?%iCfjR^kpNVR0uQsxC&b~KX+-FU83V> zXtj~eH$~bS(Z64TVw;}EnIgQT^qQVa-ZG%xYQ<*!x?wcncJJCHV`-e=AHPd+i0?mr zm(0q|?IL_!d2fsKZxrfY1su%MoPv#2xw&DYLfTC?8@lXYRmLMK{uSb zSZ_%nL+K9lX9k0{)0CFhHrsL*Ih;_Zsd4p9aR>Jwz@`uxA?&cI`oUJ+E)opQL->1K z@|Ta9l{mwBEZ5~mmq>?9qT?OQt+PnGH$B6-LSJ=Bv*|Mz@f6cBbJ%9^Q#uxm07V_e ztSoV)Y)F+M4py$knVuO8V3|T!eZ`PHS6GLQ)tpiXJxrK*%I?X>XYv+w>@;Q2t|HCP zHlhw+%;U0Ot3W*;nozhX$`N--)5P}Imr!x_v82K7HfC||-;2hrh4Z6#?pZxBw90nt zYLC7(vZvy*W9ID2VK$XN^j}l?J?3MI>2PP6iDg;L*=FW;VNdSRSth~4sLVzFpwcN~ zU>-Ji?a5)-xtQu#w&j|No?Q~2d@X_RndbI_QurD7A)gVkO`dvBbM4d z0El2(Bda&qkifG;xaH8c3p3;BpVOwtyNIG<{ z0fw!8p)SkG422XxhTVj{+DRWse!E|H|n?*{W4cudEE{`id5%cH&_+x(i#~-f< zDu_vB7d0j=vg_mRFs0X&48e&H0c6|QT`iuz@R=e!hP__S7ESeYt$5ZzhcV%z7f!?p z^|aY1p;H?PcWHEU9eTgnOWoI*-A6w?ZjFgUvv74j%PBYD%xe0!=_>R;7%Y_`W8iVw zFUhAOg0@^Y+Hin>eF^1>F*YFG$aKNfom$<;%^-7706ZK z=W(s(*>96Em|5RaQ8vS$dbmvqXykS*A6a^Jt!!-Rwa+j;{c2<_uGg9txcr5?xZkl# zfZdt~_A|#)Mw>5T zZtc7nX%eft9lruJnjCPOfg_iID|}|A2slN&qMS$AOHu?n+w|B%LDWp1Lz74zlQOwD zrHC25H-*)wi7;u}1Mx7z%xo(WNHBfKg|uObs1EQ?na-rVrT@6+f4ZhM{pz1?Sx&$Cr}y9d(=D0m&;Ip?FMs&$@9()#@BibUe|>}e z_Wqhb{QCZmzx&mXKj6imZ_r=<`Zxc4|N3)^Ra+1#omqO{OtK1}uc>OC*=HH9Yh)4b z0>NK&I^j8i#%$-+&f=I?=sTe{a<>ZuQ2*jc1 z{X0ls8F}kFKBEJ6oBaW`cWw|7@hnn^B=ZHPnVWg#&f0Tiz{Y3z7b;<9l~Yg}*>HoI&vK$Z9 z^mJiS4jq2IOU&9cwY9EU?w;iE?w-_!^~^dwfy2)46azJ;x7zmTEb`w8MQU30m~9*I z0RYy`*psT-;r;{2^s41*ZkJZ7W7eEhHc}CFcudvN#>%}77!sD;|KE`YjA?=ps~>zs zQF+8|^2St_yG=|7Yus;GYSXt3N9fg7-n0$~hY-sj(mN!~_dHVWzah*)S<{o%6a8S6 z9th7bPq1wzGxP^2EN)M`O&h0SgS$g~0oJi4gc&^~<%^p2nqe{+>qT;?Yffn++-v$B z5+u`d50aUV21AB&BqNoT(&p3DDu=Z1x)6~7%)YnetgvTo5oSsw5hcr~VS7kB9njia zRq_dxmC2FY{_akW$gFh{WU5#e zEaBw5X%lVQ-E;bi4@^jPklIqyE^n{)`f3Tle`D&Nhzk=kl($fr@rSR1n*4KxyGyhp zH#U3L+-8C;7r7(Zs43Nj*cftO!gIAu8wbqv?5cDVvo%^J42h6RYEdqno(?;~bHf0# z88***%e!Z8S8dxJqV|&AePfO0w~QsTJ@X(*8fWkv+C4_AFJX8(+d&+jFKaVny2L9e zU}m%&^hOfen9A(z~hLxhz;*(MH4>7LtDdYVos)96S(U^}CBPyicS zVN}cs&V0hmVAT#}Q#1}TTXHQG9Pz3B>+mCq^lI{h{Rn$a6YoDoK-v9E*u}YnfF7ab zEGBTnicY2ZlD5!R5W#V@A>UoQ(H?Sj5$L|Hp51%G$z%Yoj1hYUcvn7UR}W-rg$$35T(kkG5uTwojj($j7E0$UFf9J9Iy!~Uh4b_p(R1-A?2N9q2dS)s$8 zR{;baW4XCw=g-k0?>At%toZtU?kA)F9Y0yp^u*$&*dQgeX>`KiKqfP#WZlg@VMbP4 zQhRzRE;HiiVmHs!M9jv=W~10OHN-^#)8HuEyJFaefmWFpjxXD8cf}Rg1WP5~T-C#c zh*S7cPj!1F)#&)ZYH8F?kqJS8kBEjogX}N_wqC3mD1mD9=Rf}145DBArM$$9Y*5JX zL}uNj{86i%CS~M^s}s}vDwU(jy^6$iO}|0?P1CtFqQq2UOd}VUHZfR2-RV*>@k;0g zz{lQ^eQmZAMP0$QCg+jo!w^@}c9cdcib4)hO4^K_DA$k;-hF?J8SyjrWx971cO13* zd{!4TMp~K~d0dUDPoBm;ij+5`s*%R<#vMYsgTuJ!HVPjaX=>W?)OKU#gD~c?5j{)fyUS(n%7LDd3JqfOMOdP)Gwd>Eo@HFh~lDboC zDbg+`f=_-w&_oZXK6ypaN1AlkJ8jRlmc2%4u^f|taVgQ%pz?N|WrkdvmCHfumtTKc zV=mvtF;WCNwoYSvNM~VnE@!`ce8+FWWb8%uSaSk9;5Q@=d%| zD)zWqcAdjCpHZb{jU1Xb?q|MT--#TmJ+^CZJ$ZHl6me-}&C{xOc2?6oQ5c3o&(oi5 zsd^C3u)3CX%N=3LmB`UBYNLmaiHs>#3!s4xm#+ugssJkN?AKJF9J50n@BQ!pL=JamO(B0AV>@ROt@ z6ntqf@hBKYF_M?L@$Dp!KVlB49f(}Yongv&(a5oNgF><#5MvUa#Ut}u&(1lrkDRk> z-MTl%bkkORW0uDOU~;=DSdd-q(@r!a05cXlgft}MPoZMVJe)|I8!b@L6jct#6UE`m zo(WY~7pR*is@Nz}b+SJ^Z`U>;&hv;$gIGS)@P80to_eryT1#W3fUjdHOn=Yd0*B+p7EP2 zh)^*1s@XqQm{?}%ctkicyPU8g)`rO>VLLPRkf&{A7vi|4GCV7?!N;V%GC?P9IU6|J zE6xFyqYve*S(?Zjhf0&foL^&6mYsqW;eDrN_t1A-0ZlO@JM_u33W#UUXt$;17}s<} z?nwMx^j2sK;V~74K+M`~!+r6~BNcg%gv}(DkVYy&dB*LR2;5A;l>On9Bwvf2ni|7%7#O4IPx-vKI=p>Tw_A@Z+@Z!H_~I09yfX{ix9`@>$*l_a)7+0YKF4&}_)R11Gp?-B8NLMN0 z_iw2<&w7M%b*Q#H@|sMDWhBy=H)6v zyD=l_PdOCVx}*&vHLkc@)I`YPzG_)Hg>Z~Esyld3ZJr$rV~Vh{_8Kn6a_RnbX~{DP z)1YIaF|sU@HG?cI!A9DGAy~0u;%UZxnY_0?o%bldds<1%54Y`(C7T3k?mF$8j21kg z*5L7r`0t2#G(Bvp$n36cASd0a)6TTCtkkkou^G(6b-6s76J2&qWuW+ii(e?7g9AsF z(CExqml|oDWO)k9EH0)ISnt~pOF=+_3SQbS*vtZJg7u!3Bm^Az%b!NNt0E=E%&+Ep z+h~18hKVE${;uS$Q{YXA_T`yOW|hV}4fR9o(G?u(ZFBp`Vh2#VoDpHwI1|~s1vXap z#O;*RmWBtHoOpVfvubl@eA<-6K0@j1aZ$_E^sa%%Zk-7li2DE)e-4kI%<8snqm*}! z2d6H-()}Rx6;y%i%AI?&-sC9UB}+C{H^w>)2iHnlttF&qh|27d zWXZr(p5@}VU@mA+p;|&6gPA`=_@-wmeaiPkm_sLckon!^+*LWqYs9Qg2)Pbo&-Axf z*m(uJyP2P&5cHf^?IQH_b~u|`)4e>9Zff0~OyO>Z_`vez$m?r)%AZMV7Pxp0t##f6 zJsgzq2|IVQ1%=EN9((#tAdKRu1+dgAr#M@=U&@eyGaEJo*f`O%t^s zg+9@1zCht60*OnGM|+J6OXC+S{DhCwysqq<-U=eB#DxLd!?nW!JA*$d=S)Z~JvGJ2 z5PR2j4Cx-NT+Qfit0#yy5#?pUUhX7{Y>7BP4ybcI*5d=(SD zY#f3SXkdsB z@FJlnu6fD_Ib`#M3Ntyo{2eXzssw<}+Yb-0nnR!^cv%TFnsGNZ@7@eqYf2)a3uOi3 z78vN;t4aPWGNA(t>ysiysgo2=F9+uMzc z%ZX`U904`?x1vd2iO^`<>tVV8n;2Xr9&w}4pRZ&1t! zZjUNK{m0SAUyZ*We=#UT>tBp&zahoyFHFn(MM<-oVEBuH2n}3PCaUfK81swq*ARd9 zJS6fQznEqa4+QTFW_)hKJv9EayaG z-!?p_a)7RrK7j9^sBR&Sd;)nJq24}m18FLJp97Cczh8l|xbqVS3SQvvlIr%j3*X9} z#7#7s$&b5GVb(wH0t@vB$mBA3alsuq(E@eB3~OlpE8e&0^69ih zmM>y#-@hvmwG)3Hg!Nw^Jut_0r(S!>~bj0xOgWBk_eFNhN6Ey$V_Q2zd>(>(a0wk zQacP(HzI2U@&37ipgmTC`UW@Wqp8BI2C(X*NMhRw#I)n90C6IVnF~QU2P_4VID`qK zrj|5Z8Af#U1o4}#ak|%W0WvjU!z#g|26O;`M)iD^g43e9dNw?a|+mQZJs8T5Pj$F^tWWGFBz^KkN-W#fgJ z4R0*GcHo@$FBVz_)WlmCw;w9jVz7m3&k~Xad7yc-WQf?k>8YxH0N}>I39Y;wx#72+ z&a+XWauL=vA;D-*76g-cyY_^>6S{j(!=bokRNy?Wy=$b|I>x93!~RQbGe{s{d}y*F z9$^Uw<&|%dWJHtgN{5u?JG%UTS z&?3SXp`PFN4UXv}zKjUHZ*bQB;=-Tr53_l(ztD%C_BX*j`nbKdx3X{Bi+lyqIWnCQ z)DW!rYHZiLaS~ht zHk3GHo*XKEa9r!UQI2FW1fV1y$UeC_9rq9<8Bw+2#eUsndxLBb-S6?|kD7aLjpVv7 z2O1g9wO@dwU}klLpPLm~r(jO&3x0dm$2%WJ37374`eLayK+S?rqHFsA6|<%o8$fMm z?w5gZ_uJoC{Xim;G@R5+S`-N2Ww?ODBeB}Ne%g6fzR&Vq2;P0{%X+TQ$GTr3=*ypm z-*&_AO5EK83S=fHOJq%hkngY#_nb466RJYP*EizkAOdjLZwE{>vZK@6!^kvyeSG5y zi*Mhdn&tJ4?FHJ}1KNJ~o$CHHsyh<*%9*(LyHBq+>g%V+Ek(kvZxpq;pC=NN%_(1? zk7S28euO}cNcw)ezVWAtyB6}iJ%d;1H;sTrj`Z~bIuW}ej!~ei5r+C~ zTX@Iy1AzLjDq6X9S7{1r+Qrspn9kQ9VY}_er(q%@B!B(PMC6C4JEwDnY2v=W2NZ?& zCP!x6q{FV6qJT=Bm?Ci;5KS9p|H{L-6A$saXN=vf3uvaH^S?AQp3REi{P^E*M#Zn* zzy9{e+nwq^`-gvde}8FW)RM9Ld;gS)5%Th>dU8eMkoD*)G{y{z^TAgL=*TNyfB@Bc zcNSh@r?>C|y|@eSP&xC?&m1nl%X}^l!^_++9z#G|y}JwnLva80nHwyeh8O6?Yj}ko z%RIkxqT>ZNZ(OrrC~6+Y*Wow>dJ(m9`v@#u zphwr?BNUFpkHF+UyFf4QLqd`7dAbid$8z@{0yd0P>Wu^lO?MBXV0RbdD>`^lPNNo7 zRS}I>_aT8?Air7~7w9{o#xb{Rtcbn50hWh+Fcrx~c{*&kZ$z%YFQ2Z^>5jwqPuJ&B zln?>C>vtAX%4dDM&IqvL??jnJIvEe>JE3*%W2~OX?c2Qi?K^~QD>%y*KQ+S=oLC&Bl)ASmwVPxH_`p5b;*Z#g)T;#9cfhxE$ZD0k1O{JB(R7q z!t1z6FH*a{?F7YV_3#N@4Y!IeFNBw`Kuh>m9C-@#-+OI zA(~3(h3uP8{05X$9H5kLUU9P~VoNS=@mlI<)L^{&#Q}42I2_}EmGtNt2h_fDbB&Xs z&AV@0um|V(9heSgIPdOpd?QZV=^qDF4Snt-0~Q`~uCG73$bk#oOuG8WZ&;+0`~|Xa zN)5PQXc-?`Kn`qH@rLr#w+p}P}1a23yh;c&>K|IxBv2+Km0%c z@^@!9xVImF_=9PjKmPXpUwxZzA?=LfFZ;P{z+*Vwg_ zF9w9WxWC~Jd~Y!M3#x~V%#cKGv_t^P)guy3v1hz)CdD!~ij)bIG6RxDp{s%r;o{vR z<{Kgr2EfUiQmlcu$|6X4fdK8X1WHqd?~p2-P+)Ep)?dhqS>v`YLA{{P0}DQvR0%Sf zoIRGg8$(KkMXiuAjP?+kFG@M$`mFNbL3h*+(~L(IFDiwFIypOKVGWoK8!l3OwIEP&TPgt{7RXLg_}EV8wU*M zeMHb0kjiNb2-Qt(?2_n~9%+^diwAS@yA*{)wK#0EtSpkH27_RkAp%}AMyW|bY=T~p z0e|#i#!E9Jv>s*0sz8JdiH3~g2ZX^xRJSO%QA{`yq@naGf{ANP4O;66J2pkmfbfD+ z+la<0!Ukfn^U0uaRcs#OuCn$io>cT3;%OnUgO4^5dF2fT1S|XU=L!^P23x3p zj9}&Fl1g=N3DUF_tOUr5d?0|QSRzisKw1{UxJo0ja}~+Ob`EkJ@6@{P3m|m71+*iC z%W=Gr2%lvcY{)YRG9op|JXAJbGb9}pI(n9#RBW*zxXSypSCBDT_A9voQP>*IOVLt` ztA~dYhnaa|+P^c%?+o%ggZ$1Qzca`u z2KhQ~a(i>-LG6~t1bh$uvVUJc-8b5}B`Hax5R;xtQ$j`2aslyOtJ8&Zjza|{@hy13 zRC*45#HvPZqe@k{LoqUV+-Mvkn;g69%mN-N&;#wg<~yIfx(TB9zcc9X4Ej5R{?4HP zT@1SOBC+$lqO0-hf#}%8b>sI>P_*=EBl3m%R=S{&9n!k);66ak8E00m z27@*bf8BeqV=-$th@&JmC%%{#2_=#m@x9&XL85mruJ!wEAh4&2CR?lnNJe4I4u^|# z$ha6T{-hXoJ?Y?aux? zs^YSJAQqa*PYPN5>jSW1+ zcM+jN?$-qbd8w|l03xfN2el$i5T!fBC;^h$hbCC!*a+mSu`dgM1i#2^$b_3J2fIjv zS5>Z$ny4A^(Q%ARXvKxgFe~hK+yEF6*C2a7K$`HB z3{&4Xc|oPUgugW?!&t3~{Tyg9@!=y-&AxS!e}C;3P_+BM=nQ)kVw=W*A?j zqoGTWrzAxs|Kv(rUP<@RE2}I!uo1aMVj=TZSmPbQrVEMyjjD2wjhTJ>`hm_;%l#td zF-q%>E2k#TPX^NbQeb5j?i=VfgwJK&oV*S?K}BK%-L0oHBY}7xZP~8f6yAQog;ol9 z@T?6F#6_jzoi^8P`e?3sm7b+hwdKUv>63xt+Fcbx-hpd2$odfFO;(A7PPCoY)vgzy zgNg5C7Zp}!)>?EM2VNofSS(wWJ7_9vkc;CN+H4r- z=Nzr&F9snB zDrf=d=fIu;XZ#$kEl@oekNTDwsK=8ng=K1IYH%Bq${FEhY<7M@kZrrsy1UIzUmBYK z(G%EJ8mO?;I{?##mH~=0B(!cgJ|Hj8>Cp>kV5ftQ#~F2(@6V+uSarDg5R6j6ZFFc4 zwo->$|DE2u%>HOqvie|oh5N*MgyGt3MGbh+I?X?@lNUvqGUgBVgWQjkBjw@UccM~M zX1#^ZnmYYp*(XSc1|W)crJ=A@FoA4$jLdbNdw|Fc43rOUKb=3IlFz~IeH;o;79Xe5 z3^!1Bo$LmbjA?Z0`r8^hJiP)*vyu}8q9QkU9J{E%)!tm|YakpugQ6WiX4T_GnG!8b zZ=(=PO7j7zxf`$?6OqhfXO57jZH8kQH7I;j1)?FQq zM)71!Ivp(0s0u~9ky#duQ!3<@fAE4R(yQbH$VBdF9T`9O-WzRbQ|G4zI^6|L zq30B;$^c`5oeqVGr0C`y3y;?by^O$xGW9x}3All}jQmOLy(q|p)>r7l>ftFF3FrtA zQ7u;$ap|1B7tTOVgk88y*OL!7Qo$G;59mr;vAsqnR<(&Qi9kpN$3cuCbT!JaqPV$%@4pqm4k$RB<%`X~ST<4$IfQ+eerM zQ!69rzMV$%^yrbJXiCvQcedo%6-a#3PkiH5EwK+*_F@As8V_nBs)h0?&<>P$)rFF8 zAely0uZJf50DN@JS*2$e>K|u_5QaE{f1O$?mjL4-NASpnEmUM-JVqN zFPF-_s2C@SXdCd@FZY`j<1Qr{MnhWf+cz)4{X|*fi4*&KY$L*^Vwfh(68?&3}gL1>< zGj6Z78eprj#1yt+$kvuU)|kNk_0vF&A;*beKxReBX&H@Zls%Y%RtPbf!!YXDY_I6R z1U^4uGiwz9p;~kDzECnZQGm`ju=(~uioEk>6TO1y2}FM#d^0!x<0tnNEvnFfb*?jYuktp`jyZ3Wdd3dWr!QW9Rb? zpq6?BGfFK0t>(&WYKwI63}bn;x}5k1Du1!EN?#ICwJs{4C?8T&((=t-Gq!{}6P>kU z4~(O_-e4^d$bzW?p$q*&noAlZDI+RWA)eAC!8CA2_|@$o)14_sSkOhx+*Ega#Zr%x zOua01G_=9H&`njKHrP84l}#oLK>`y9(@PL%*WZdQUbR57Xsi0=Ayn=$1IuZ8pd2k~ zx}tnPK?&9L05VybblhuEM{5All2Y0$yB4EBRLySk?W&j6zLWgpE31mehS81$Lj{>& z%Wd?FuT_a8X+Z_uXo{5ddf9gNBw8`0rC|-e1q`#T+A=EIgh=1=C2CY9&hq4gDC))47h>fR`Cv9UikT3e`V+&tkg6=U%nms)bGV!;4MdA^yNZxuZ7BKtP; z^5`pDZsK4WM#SF|&HEQ;?NY+9UCTVOc=eJXH_qKrt`N&LQSMvF9yO@2GG~~csFuM3 z*UP_AZ8fBFIL+D=V>gAyEEj-KW>_HOMFdS>1p99?&kEIJYIm=_?cC(CoNYFbQgtSUpfWCmLMnk{f6S!GZE|M-8eMJePvE84kJ~w)_ zh~F{VU<(Al+d3SGaO_TI^gy;30`EcK6lbVOKGq*6&a#?v{z*1$Mh8c;h1|1R(ah_s zK|2aTH&GE@iMoZL(j5u60}S8JG`#9gxeYzeZE1kkGIRU!zF4{L!QEZW1p?y0>4j?Dd zt+u8A+k5C$Ak71q>oq_)%X=!e;hyxlK|Lpl~e;Y62urAKANLrgb^&5#ukL z9{l2$-`u!g{sQga=;->lpWa>cgiR6qHcf(MaoNf4Pds!o6JXnAYw8Ya8@ zv3pUfurUVgMQZ`I^GjIsq*F7jQ8*b7H&iSm76K#e8n^3WoVX(|5t69oj>W~sb$|Xc;mJ!hZ{BTIV+os8*ivB<_JLx z)Hit?)Z81WGntp9(fPwj(s3`uX4!hS2HkSKnNC6e1|T{RNb!U3WAHd;Guvc}Z&Qq_ zjwm>SZhp^r);1}-;b+d>>dCF2Tv5=P^WFyJt7vN1nXg4OpF0Vz5#k99A zr>1>r2OBWEFbHicBB(`F|Nr*}2V@K z6@D`#Tf@iAK87=D^sg7xbX=4br-taZDx#q8vFhS!ZpNyb7H#u) zs{|CL^EG?+DiVU>;elj?o*zfrE4sssED0Opu>frbBLFmeJv(%6jjKB;)@4elA7a~y z=2w84dLHt)Pq7(Zq1YN}(%eHHCiSK!Q-@>04B6h}!#laBbTI=CYc|u+5o@LsPv?ZO z>Yj>8?|~3z6qa;ukZGflqc8Ukq5+EDXu>}D+a5cfHmEyIGn<7z!gi0R&9Wz~utc{% zZGvBCrmW($yftwg1Y{a;7`oRBB;78v=X7#+sG#JedsWX0Z7@=$W>MCFa#Hho!=w;j zK|l~KKgml?1NX}qFz@h@wtJGAwY(fH5k92>!Ms4InIxe(7Ulykr!f|4KVXjUJ^d0$ zj9jotr|heR%R_`2>16kWRHf2@BjhZOFxYJPO`z>Tjm?&)e7H-4nCw*8{9KutKAh>A zDEE=GJJ0Qs&hRzFFylofl*XE*Qeis~8m1z-gLNzxx`mR1{x!~@4D+N>K(~{lDfncA za9TKTws#~4r8;-8EJrued-`(DJ{xx$eK5D%!f=-YVh5~`*k@ldt~qHojdU0HCv1yr1WfV(y&KnUB)smkx_^YhMZl2y znq!|SqoD2;h3%kq}%GU10MG)6cw7Rpa+_tf0WG%%c=fE@lgyx zTSPQjIIj&Fp#b-|qj-VhWk~l@Hq&7G=M`4HcifjxS11HMsO)?pJ)orL^V0_m4>R9* z^xH^WOk{Hwcv%~)q+Ao6u=+;tiLLH4%FmBHJ-*9A!GLx50V7P_FObWe4;ZgNqv6-( zT`;wg7IgV&VucOO_?R6Gmjcx~i_4{PL*O&2FD{C4`ruFl8Xee={=n2P?b|a$s}mYXXMr98qK3prI^bwHW#gsv17rJ?tBlXS2}NMSbRsF%IR!jTG? z#=ay}=AuM!xAu5L0SiY4WWT_^kjBh{^xVrBiKL^HBGG17s zvl&JKS7f|QU9%^2%<9=PiCQS5e#x)sPcv_-V3|S6BllWQo5Ah5JW7HhY^KPT!cf6l z^8_OMw47Xpoz)|w2gybn$xw(AoEn@XOgdTsRzRu09$s2$wdvGp)Ys?RnF>Xi>6!fr z#zt$8Pd`8j8@;Y4Sb6bjdSy@OyP$@l`uKP)vpmA)&h`17HLvmfPTVqhe4dR09=~&c zI^MZvdwwUZ{HN#dBVWP%on_xJ;_pnmmxpbG0_E|MO$P#5N^IIm2#xnuoIECVW@dr~ z=7O~F?&KSE5+R=HEi(4NN(k%O!&gDp`0Ms_b{+sYKb+o7JzAoeVf?_)-bN-HVL8qT z_g>H922Gr21ayRp-owB}4S?oQBpqr!#s!ivTq66hn#rth+sX4FDJn7sFGY-A+iTJl z8V+EEVR-4y&>R43)lZ7yQTovfs9@Veay&8wAz|w6nuSLwE};;LBL12;LSO=I-`^4uz|eBRg}$sfD4LingXm z^mTTb2$~v=Jr@D(2tW`v#c5GmKzcx9hc4Ia?12$9<+KvzM?_P{8`tu6Pj;Xs%*@q8 zaMpNQ!c7!_yvONK)r!$6OSwNo>8ZIx!A7&oryF$Uv@TFF=IwVCJ9GfC@ZzbM=o1w@ zI4cb+tNJ9n9O&Gt9dSbT8so0SS{jsSe|U_*oSpco zhbK$Ih+byVN>*J#Sj+X-jsln2FTNVxO$}VDMw06ArNGB|zl}*~iSuBJ6wFJ&YMgJS z%o5i8!tGKj6k)yfU}{J^5N!KNoMt%rv!jG;W6qR6eM0NZA;3+(MHcd)BabW8d9Qm( zgpM6T*-W4r0vK9GK#>3?Sjmr|=?4}mVu1$KcXjsD%hpm%i*h;A%#7Al9<`Pasym1y zuJowxELdx0Kd3vnM0W7kL*H5Sjz{eZ*LN1&%u&$7^&K?p3vnI_4=$nUg20vEqpEF4 z&yWmEFLBNn3eSR9X3Ym#v%t|~`&}v_?IeZ#afJslYPN{G$vL!tR4DmWcosJ}q`4&L zLi~GOCA6NleGkDa-3BQwV?M*ud{hOVp@j8EMF6mx9+eXqbt6A2Cm1{0dVIc=-5+7b z3HtLpOUci#OuIZjo#))oUwMI_f~#>)Z)73e$H(s@U%~vH`P6I9cb2^mWX7H$RiWLq zoyAs;PecIrIv>czY^7Bm$SUvLo(V9IpkCK?7P3^vU4>upIQplrv~N<{Q$j-7Szw@X zFT#}_qzuFvIh37AO?W*hJBV=9$o4su9e7f<-_#6c2T2p5l7+T|e2A3WgSIoN4(yw@ zgAhj~kA=1~aUC?Uc4#}#P{AaX4s8b^sF})xwu4*DaW{c>H|LmxJHvO~ecsK*>#6Q2 z;bYy@9rm*K5J;#y^3(SRbw|OVep7egpFPwNS9jp=4<+DKcd(7qeQlxcEF_48%gWWA zh2*APAJm4Ag8jzEa>{E1i}5qT=WMh zQZt`{gI&Nc+RkEv@Vsd|aM(-uT-6<3Ox`zj2jb)y==u(%QHsFTq3nC(ee{H^O=e;f4z6@$?C; zNJx4Ybh1?!r9B<|Tw-WZb?+*#R`P1|q02ZbSDtsJ2UQvNsr97lu;GmHRC{>K)zN0F z=%VYDm-CGH_Otf#LA3|2xOvnDY+DXn)bX8FuJlH|3$R{BlE)n_XJZG&9M0nT(j1CE zU3e9eOlZ$7501lfL03|vCmU{IJ$)1tkd_`D>_N%xM-PSE`54R>WVbl>hn7m|68*zBjSdSfm zcYAsM6MTI|X6UYduQ@UMiB8CpBO~gxOskwdNR)2w9n1`=5|*_P-fs5mZ>BUKwkfO# z#bd^vax@!rUk>7(CK1kWjo%5I;?^Ju+~|I_|1Vl~zG4$!{|{#qc1lKkX3IrY*#DeSVB-SCIY)_VjJ36R zw#B?nkC}326_?exSjK<`TMLmEC-8~TA7z_jBa6ci6B;eo?CU-OUWrtATZoUH* zc}gzZN%4I(9dSK#qQLiE9BF6n;s`SGwK%%+vF9*YVORJxgY~6($iE#g;(c`g;J%X# zK3Ma5ATAJg_RIJ;!gk{N-Zr1mfje%UQ5J9L$_*@YN1o0InLp}!$mt;KmhBi&vF@(;=H>a*ZF7DH+Ujvuju~y# zXbP_CbKl;d=ekBzMmb38z{cjK@*CPVs&}+@!7Y+y=FAU!VEn_dA{*n&GWLaj2hX^ zXk826WuoHRAF&m;hk-3tTBz#+1#EkmVS)^xdo@@~x%#z4^q%$wFEgV2s;Ey7O1q8$ zf_=E#d4=5<98lH?f%E(FMpmld^Y(3&UmMC|yUY?-AE2XM7K4IUxR7?omS93gS=}|f z7>Pupl*jdjyv4E0!+NW^1|(`$qK zIOGN8Jg;v%L3h##=sCgjedj`+m;4$L@5c%H9sds1Q_hIeRAB+@f?6a0a$_k+z;-J+ z3cTRLF}I`ch;8K-xG%kV{m@czk)5!aB4~;LueMThfJRmgYR$d5PslI}XQjbX+Bs70 zn6iTz_L1iWCXy*NB7ZRw*F|UXh{;A#h${?bQm4FGH>gX3%NudIY23anh-Tepi|hYY z3pUa0Boyzu!p^Ur&_)k+*2o^U1F~7PRWKk-JGI?0+;CAO6*+Wp!&AFrT9*gf;4IY} zANTo^1vYP4U`Ek!{@j25=%kp|?UaALK}pHvCeUNDwS=vj=XFmV6U) z@Y|+&lZFTA&I%lO%GdLCryRbn_mtR3q~851$AQ7RR=Vi`7uX%w0X^pk53COLxs>Nc ze@J3qr+i9mgf*MrG75gbkp!^XO)6|JOmdeWxJbR+f$oxq#iez44~cYv-4`6tQ_7uh zJf-3J?W^28XL$(Nb1|nJzGCZ?*a*(Fk#8dhZ#n1d3kPZ}ha2(uG6*+$;h%IUX7)&5 zx3!$GJB0=Gl-TDRPli#hl3dAA>F;FQ@orsaN~A5Lj*-#DdDtf{?S z-uD5jISH-W9&)#E0x5rtfN-vu_S z9Q#2Dfjtp2aGC1@8#}3sq*sBBTx(S^JOwr?DN8*$#qhwgc6qxC?7?aq^)`}OB3^);gF zC2(lL2>(!&C>4+*ADjnNrQC5kjG8@nRxv$;hsMj z_F#Wx4|2=TZB#hm=N%q@R({kwp?Rko`36Xu&tssV!;I1k33sqi0;sA~4ep>V-AZOL ztmrAM%ZPYqXA-ylkU)Hk- z7sn*i;B=T}>uGc-RDq|N2^1Mr%BmU_DV&~J&{08yuPnTn?dsl8cP~IIbaz1$d^Lip zBoEwryh_8Tb6A{d>-G}AG~Lx@I{I8bPqY+pS98lq=(xwi!CY0tBCHKhYEOgL^SBdSA`*mZwwNqx@g!Q-b#IO zD(;n{LZ_}!fczYl5xFm@tW9nXXbaNG32AzS;WL7n*_prbz5l;Y;fMl`8|8;CJBowU zP`-t*I{oUgYD(#yne3BS=N4jl+COZXccjR9X`bi@=qsYb7V9x{yhl2r2iOJ}@$gEg zg*kD+lF7#8INp;hf5!VwtM~iXGogHHI=5R0^@9I*I4UI{N2TI^FkwAAh=q1v zA@hd+ZwsD+WRT=7J9upzyO6!N28*=}6pJA&cX#$SW+4Gqckk{qN3S`qB(vI}T-Rzl z@h<3`o=uLJE~bqt@IiDZ4AoKkMURZlBvTJA+$&l60(Ns1tAi;d(WtW(hyY; zOuPapW~^)!s?=VtBB`>~E8=|r8UCNyy~Qu~vT&2g*~nD^tx{Ns0H6^A(UxTpw$k>( zP78f;Vqra+BiOQLkA>Bhola=V6IPDq5D7WSTG*Y+VqwFpt;fQM>=87cb?+UUvLz@` z@*Hj`?%otr##w06{jnmaQns309GJ1sH}&Qj=1116z*gptOp#8}K3Bhu^a}PG>+e6u z{c+>5cv3`1g=XKVfbvG<(oS{1rv_Z$E-~wCixOaP$>8id_545h0vSYSAgJy^k_NIc5$WCD*B$;E2lFM!gKLfdOscva}b^wR( zobdv`R>=#Njx<%HWpj;@d`J57>zjz{zlOa=`TNgSQ;I$NMgf)EmO?;HFVV1Pmg)sV zet1@fwo82*Q;y6hrqK-}R$_52(l>O{AFXUW# zcD2j01CVuF4n~E8A4Q;Z2}U(?avNq0CQrf=s6C2UoUl?;6q8K{k0?`l_xKWWnctmw zKo!#i|k&S!OW2wJlYWA;R%M4^~zp{ zTwEZ(R~Yu@PPP$7c+&e*Snngb%Yqu8NnUH0>*xo_;kFmGj~=yx_Fh zCd5my=HYA+PbWDcqv{e-v5TK_Tc>k6&)ap? zuzzNsYm-{l8G+S4x1NfV92=4VHfzK(YR0Alw!NY{wtT2~AGRPbe|l6chj)r&}8Ae|T-A7*{)Sx!x z!C;y`P8t0RB}LWZ02OhXbhU-bj9&jd$gjI4^Bw$$yeF!qt6qJeX6W zXkWKE(D;mZ567kME?8Y&aAJ0;HVZ|8x0goj_RhkA6=lG*J|yYCTJi^R1rI%I1Fo`YuB?W&aG5^)mYB{}n{U)&;eJ)l=u_WGNe#UgD{yE& zkL&&z!X^TCaTl;Q;p+-S>m4J7$r6)(Urf=p3EQ=_NS2tw)&to3w2QGXnDp!ps4Bx3BQ9o#yBX)6*t4>f^|C z6xcsr_<*ku;~C)*xtAJB95|x$tljhN8q7zcbqz00hHFInDx8NN@YyG}OqG?1XRLaN zMI&EdK5-OJmSjq`dZzS|5(sE+Nzl2_+JtXRs_I)2Qa~k}NmIuYVGTbyzFl+k6i35H z(8$P1UM0L`xGcqu@s@P!W zcI4#+HH{E1AMSLxAx-nsVjh=$#g{RNfijqjw+AmxJt-!e$4AmYF8Egh=gOWb3;Rrb zzLn|O@R{|1m#(ZU>M?NJi|a*XwY0dc0~`TnBGH{;y^E0|9k;pT%5+{GCse{t_3pJr z%Yape7?&A*WTben5v5y=ti_) zX16CBcbrziOh@pLsnLHtYHvQu=S=4{@$?ZS2CStVmCM{B0NL!~88U+=+k}SATKKFN z0G$u8j&g>w*wR?~G%i(pK}r)fh=NZDk?idf5?Kc6dhD=8(pJS$S6W_CpZNohJP z{3u(5S7)i%buS>eN0rBaF|E-PXJ>W|OZCsmMH)(IzaZ`pYxG8s9#J>Dv1Zrk)2t~< z*u(w8!(Dd75_9g(8cg#S?=rnWIqA^-am`myG_{gOKZOK(OokS0=fex`q?``>2}0b1 zX9{L5_{gFdsh0_S0Ta4x`B7bQLI-$%3FUWcK01;QT3WCTwui?IEuvZ2gK@>bB;Grt zb-MlbJd0|}!f1Z;)6kmDchM)E_uM4$#2}!LvXqUrd04Jo^#_nMMTnd8p zZ>p@_XMTINTOU^1ckU_N@i;19UqT5pA?t4EtfSJQRxiJcm$q{jMB-JH8^@TH>!Gxp z`KwwVV85(YgD(e&6Wn!UMfTGp6Hgj=3!@R`JI8;nrkQn_eDy$0hm7rSlJ%K zF=<%xOPD|XVkBFfP%&t-GH-g7ht%|qpxJO8j}K-{XT0?atfi^hZr%k zOR2c8Mjx>@Dx*qI7?ccI6G5f5e$}I(bZ&+{U%*~&!}~{F7%6-7Sr?VQz5+s&?dd_x zHKm{AYu926|c*--#pM4&e_wp?Oi+9-Qe&?=5Ro=z^PSv5A2`{hq z?+Nt5;|=p9ioTnw*Rz{1k5DN#%U7|wCIYcFz3?}j$vd3Q-t-DxsY{b(hYLl%;8mJT zX1EAFO>%n4Qo4ADUnn#KrOK-8v#r43a94HTzG8S$5BGlVUa%acu{Chj5xEVh&8+G4 z<~_0mbD#0qM94=%p$Mx@*G;7}t|{UfHAPdlZPu7H<&v`3ojPIJORwLbPEJn8cW#*Q`Mki{$B>N=G%+gX zRrD~O4&X}FPet~w*agLklgDLp@mtM;xw+8&1e^nnPMn6%7oISF*>UYalA$}hdO5E; z4@ZBKL#DC`clf&MsNNa9FiPX@oV_1lhOqwpw6XjA_?b z6`IjpLK=3Nk-V@_CQSB>H2Z+Lol;b5?>cfeb#b}A@r3Ey9-BF!l?^2>FFXo|Ts|GJ z;JY*WLmeZ}#T+kuT=fUhMdlLf4^j-j-hp+pOAF%XhORPs-{j~VR~h+thaC* zX}dbt!wXNCKCP!u=wf=|{f(xx*8K2o!NLlx<6+>rmh**=tG=Qu`?bE~C%PmAx=c@Y zPLR>NQP@(*;%H{a3D@92ygx%{Z%+{{p3^;0!#mH)=gQg*Sj5_%;)oU#{xtox>h9SB zSF?9dxss&A2p41uieWvu8IU#Q%&Jf+ht@_N`jeYp&+*<;#f1Z8&@nTlVXV^8mcVC!k| z@Fby-AunEh!xT&Lr#D)<%g8TM#WK@iM;Q6Fc%H^+OWfAh5sgL7j+R(?T=NH!g}Zn) zG9&4H@%)LkJckm&1*!T)3r127E6mVdt)2snvYKJpqOPQ(YkSBg0^9U35r>@11qred z;ToQ*jP#46OA{I<%Puxs#q&+;Vu%aw>bzSdezy@u&+>*&aA2}%C5@it-5Sjn+j3Dp zF8hP{I&zB8a1Sg{`t0a^gf&^3JhG8m7S_XKj}~54`)BjGDuXvvsCMX|v$QD<%{Z!e?J=t|3{10CJcMDSTf;`_e)l3i z{j}&0A`1Zbd^9?Ybp$jYc0c_4M-*SwoGWf@a7TmrE#_F6kBCHE=6e(h2UU?wjL!& zng>SF0NP>EuND@knY-S1kd`51^s9LLpe+9|(-(GDGz7hm%aPR3w=lT}cn9BTY)Lrt z?D!WLzgH?kW9h?j`|(uKJwCHf#kUhhqgLqsr@hD!K!M>Q>Na>o`3^N4nfx*vAC5O> zj7589z@~pWx;PhfL2lfi%Q)jyIqiLW-xa1W>**6}IQ5VbJpl)1a^}b})GQk=61LC6xSGB7blp2X zZBjm(Sr7P4fJ(%$CDg}&7A}ls7yg0)kI;dNYnw5$qJyKmmRCnFK)_6I-2Q3y4Aj$5 zhOW*XjqFi1wc{2@NzjYJ>(W-+A`D#Yba&!_Cz%6V-w`uLe_g_iIeOKdz&N0gl>M?d zX7S;JbzS!G3OjdO(0ZW?OdFRf=Y@EliR@m-0|__aq_if<{IM5ncuwtJxc2e&0lJVc zq#aTVMmD)?qJP71gxI>$hB1eh%kVo|V2C`WxoS;U7;0R$ho>ugz(z}9|Bhk1PtWFw z>0Du&xSkFuVj|56A>N20N7=bXd>eIlwx9cRmeBYhzpkq}VduK9Pag=1&s^~s(|LvQ z@0;|0)Fy$mBg3}`XUA^MHE!GU04-PtV51**{|37AV(w<-`!%msWl~UBp+%pdY#fJ` z!{=TeV745$Vb7;|C&`29@f|Sg;oLOlbAw&>C!htlv5aNmjc81w{mBCfO((Mnh~}NL zG~jdGiHY-l&p0~~{CG|5@l5cVm{J1&jfh@HVfUWVH`6tjy>paipME;b2wZXdKbNZr z=T*bmN+m*K~sJO`ts?Qf6Cem zD#CiU6Z>O)eFY>z6p37R!kSgbt2NKd#XZsGq%&4^=qk+*V$H)#j3S*wZ$!adFVQ=C zJPfas2G(8u)3s%%d0Rv7FJZ6OuQJmg+HY9C-WfMBaq_YiY$~bU-bb_WI+W#3o?vQ( zmo#WIHfTmK-~?9*nG72Cyj|1U07XmQeMff9iWLpE_Rz!AdkYWmDe}c9t;#TvQnJ#wj$tWr!z?tvK91pS)tTsbzZ)%8fUTQ$APtZcZOpv51`m6 zIQ2K2(u2kk0|Bxq#o()#N!-j8`}z_}R?}C$D|5kCu1cSxkXQAPJbEX0km-G|U@kvj zjqH(FgR0xoV38N=0i|JTuYcQ^mRC_T6T69hA=u^)Vx+XZggJ55>=neAefKUPues}H zRpISJzF<|qoPw&?qS%5;w{J=XL*_JLevb`kkxEV!rQvtD?D!vFs@}30aphLxOGv8s z`!XF>$lkASfPplIORTCQ;B--s9K3sYt(Un^p4r&UfHo73zFyhJ&T#=Mvs%b2D0%a& ze&Z&yBhYD82E;|%GNSQeNQyG)Ndg~f zHr6VX0Zmq+Ayf}%s9&%67SD6}s2I@}ek7b-6?Wq5?fsXz5{BW;k#ClXFGQvfjIi zN1(R2l7upvUv1?qKU?5l=clk2F5O3qMYu@?yjh^&f@gi$(c3D-O8jJ820ZpzYC&yk z-UttR88N7O`0A}wS+wCs28TJPK3}I?|Ir4)CR!hM->Y+go=mRMa4dhGqGMX6?qYbU zr5$V6+^0OVp@J5^JHUQ+^CawoN;t$mwFx34Z9bEE8=sM%JRxedsW_fka$YS z`q62a(XV2s@;D<8D)Rn#mQwOQuL&R5y~O(9B2wQnqL6)EZk0ivm*~nCbLX(EklfG1{Q9x^;h@&5kRZI? z!c}K_7D%fUs?5YoG!_ZRe%G?LGF)8iF#75tA4l~?%L5AfxJ2Q5DrqbGeK{$)eIvdj zn#Ea9zR4F)RyG%T_k%Aazx(_gZrLYGEj9ZlzG>Z8S6VnDSW7WGaLnMjXiN&=)sxGo;i*~^32)lzZ6?zN=NC}HQfaT``o9wR^L{(52)64D&%KR|&k)Ttl-JMDW7HT8k@8@Jh3wMskITvv`I&2ea5yURc0L zcy7<(R_Iw-tf0}6%O0jE6T=OZ5=IZx*_1U0mt^Vg3p*)zK|}FRSDj6`J3z}zP&Z0B$UNLTd#|v^6av6~qGXKIShZHPxUSPnui-2>E74+WwuLKlWu1~A2g4PC z;4ci50%5n|>j1c0Xn#iX{E*4gKi#m&huLe5ORe%Yk@ZW86-?;7t1q+|Fs=(o5TIt6 z)c3<61yDEbNP9n*0@|RYg`;TcvX$2AAw-TfQXSVr`AC16rV?ni7;Kqq2ojWvDZVMw zS7s68e5@5oIUBrAUEY?;9 zNh*w4qYKq8R{PnxwQ+}m0H}9Y_5=f5uCJn;9^`eiFw zDPqA;jZ+G2xdT>4t=La@JN-y#c*5nMzaycQNA^+SZL>DOVVagc9RP_##2)Pj7~-nH zL0J##*F!}kczo|cJs*uH7jDeLd-j&r{oLnYy`ie2c+qb@sJLN_M(A(8)3Tp@Gop*8 zVPAjz`l~vy^?3Rs9*FVyVg6xWZ{v^G0Ke*wF^9)bT~mFxMvvq75C8dB2TL8MMpe!u z)R-}nKOGE+l#%Bhi2F>q5{FZp(5@00B|rS}(?9+3zy0y2fBxgA?|=B|&p&_u>33f~ z{rSrufA{6j-#`8E{nL*>fB*FL$DhCa`G>Ece*W?=KmX64{?m^?|L_0w|NJlCfBqj| zzeG05n*sF!+*+PeSf0jnCA9pX_%eUx*Kbl=Htt59pF$&IT+q%5|6GYJ>`P%*N{q~% zpW1Y+or90pl*jxH#MY3Z3S#NF0I(apAm@yOgtEU2B-BdCBt=KJ-2sY^kiv{oM-Q|; zz`0cn5K)6?;5n43(7p_$ZGF1g()Xs#o9>U;LzZ~OU=9Lck?zejdwzyJ z=@w^l(A3gucA#a<4f7x+?aiyJSc3%n^QdDOezRfiQbr9)kUHxMZv~$wNpQ?oEokkq zAGxn=(X{(DIJ6PKo*Z6Mw1+&x#XGN#-^4*C=_g{hbxiFWirz_LUgYA#&GbS# zgvxpeWLy^I0dFj%$i~+=@OChhhJ-2*nG>95WRx!*T)){lJC*UCTCJmJy$-6ZvC&_9 zb2jTY2!QdJ^%AcxHt!V^d5;fwm#i&9?SUq~nsA2SkZ!m9>tA4xgMNDK6z)iVyCXt6 zbo3I$j8TfVaM;&Q<*tStSQrmz`06P(8HGVXVQH`z0$)Ru(JG;`sXbn4nAky%qWEAT z1}UZ*a2@d?;zl+uPAr8Adj^1%wO{9WpYgpAZr9?M*Nw>=Tc0@R(?a!$wo!ado5)^A z+{2^oOgY8?^R6+8ye-+){n;+ zoEzd-5-GPa;KCU^{ybp!>9w6bCzVCO`DI5k2x%JVWUlFBzIZpj>i5Sk)MID_z^dAy zRXx~!m^Lqmbv>qgR5!zYNaX()Upqi^TSU&q0?bGFEHv$)3}M-%BgXcnB?c1}pwYZ< zlSRn4l;MzSIzM-5zKtuK78h{7;ny}8mbz&-R+NM`l;lU%T2zn3aeSz?y~)FYjP8sK`*18dPd0u%6WbQN(0d2)6G=h^nTfIo3)`uIyeD|IS&*JjzCCrkiQmt zj>E~j(WPN7{jfmaX7!iaEneFZ(ZCav3K?g32>t_iuz;9;uWG5$Zbh#qn6N}KNh0pe ztC0BUV+5i! z7IGZ*9>OZ|$;OiWnO@;E#lz^#UP&t*bqYwET{U@OWhI@>h0DN!5sijFOQsh-3TsJrpxP3e~9U*wIAA zxIlKq(MG2giIWC<5P#MQ95l}KQkK#K1n&Y~%vb_p>EPlSAS%?&2LDUqESVbU9n?^} zigFu8B|#bl+#m^gfi%@^3KFa$wXCB^btdux>>$Grv<*jE@a~t0JaC|?I_kz9MQ+JB zcw?Z%-JXHy{}PGPVtS(Zz0NcfJSlqyL{`)|pO`{?e2xkqUnOH` zQJYg;0ycnIlv=2%&qg$w!r<~2T&Ns;t!FAm7H`MR5Rj(0ph(;CMJS5R4+^+%+yL1o z)h64n$?yWjX;_%LKqrU^xinBfW;2BBiIN#iE81ohZLmyaVRFp?zAi|~L@9~l<}6UU z8L;C=%Ui)-B#6N^XFc?Vs?|QMB?Sk>I&ANabCRpZTW9cOJO9UdC9e^^gN@5}IKlkg zd9=AmxYW~4B<>i2Ob3IB!>_M zKm~Pdjm``&g4RzrA^@U8pU?sq+`l#Y@PUVzoNx6Xiqvbn24qqZzvZdC3aCVBT@Vw@ z6%52HjND-Y7l0aKaF(|hB09uzaiJ~miAa_Y^cnl;BFXYDY$t9Rc7--V?5KRAFM=96;>fW#Jk7rTt+aBrP9Q}tUZPp zhgE%zW?Js-UMWI&>XoG6@izPEensyx1I<9LSBOn z1*n7%v2#3QnPEtJnIJh2Kdx8^uNWeYC>X(2oUMA_Co3gjwRH{#TVNNXF++)rVgr-V z3k6=adr4=5rGPDD&N56JV25>=S9`L}4fA6|XeX%vRH=S{)cD!6KTYNq{QczACjaTc zkI?lBgQnYfle3rSen}kn9boZQ6x@*B)gNA{i~x|0YtL_Pz!HH4>k5KVO=Mx{k)6~5 zl1!q(#MDkXmgR7Guj}fyvl8P^itQIjpMUkF69Y17R`^6hwB|{}uG&I5KQu9nqGx?A zZ2MNP_imA0Q+SGXvE*h=ZbpCLc|2!KIbXriT-D6uaf=s;E|0 zNV4zgefr*Z`rcXgR4(V#mc0x4H5Sf1*rzviLSEXk!@_d)}ch7wYm`mOuCm1zn z`Mu^Nfn_M{f5_Xc@{?iuz~O9+P_;$}{HFO#S~=u!o&LFA-3=2&TMi8uxr@{k6tE{M z%=S9Ke`WooJdvKp6SdrUBt?3$qz0@8je=!Vi8H#|BTCT@5yxUnz7JT~e2;jI{%1IY zxRmyx%G0cM%%dxagLpUR*n0#T?0`ldTLdH2SyEp}rcPdA86I!4Qw=RUVW+-XUBoW< zSprOcH8jp+-LDW}QlOboB0^0xvl9xPpe$ zK$>{KVKT&nG$~jfC=Wy0;r;dLZjg7o^@5T!Z?zxGgUfNm!04XcKz0Fr*QJ_O+5FIs z@crE%5hedG|K@FE{I5!fD3K`e z?N^mWmr#G^LH&BDN6!w_dU;SQazyGWGFYSkbI?D3s* z!u;f8L=PH={rKaXbci?awYNXq-h5l>5amrWMSDAkD;?sR>bo_1Nry;CPzT%pjp-2I zfBgTxeERYGr=S1u<K<{+3)2k5#_C2E+WozRhthsus4 zn}TH`b6L-#YH&_LEg7+M`~pl(c3wWeyBH%r^4axQM4cHy89bZPGg|TuYn<+B}m7^ETvoN8yXyv>nroM=U`;+cvLS=&=eD5Ik3 z=Tg>3hsjQPO#>>7On&3f9p!+ZTX34s6(4CCO7@@$)RR$n@Ea;6}m})QSo8*NuJDk=Sn1o|JnHQW}0u;=Pz*EWJZ7I zN*wrEcC6166`#OuoGBXDjGwKzc?Mc%YkIBu@N`(g#8X5js-9e(F_0UY-ahsSm}PAj z_R}t`3B|6J%`I(*3f% z)84jz0f~68JR}|1yN*frCZ3e~zM+DK(gdI=NsTff$JXn>=pdG^`SNZg_oc(o*9unb z3MphxMBZYb8ft`TUR_T$me|!g4YaZq&Abk8T=fDVQwJT8Ky~|&iJYda^*r2YQcJT4! z>DvNuM*0I@Y?r)f$Urf}JyXeWOP{SP7!J2*cs%!G30k9sKU8(R2Msjex*nXgyE-7j3G}*xgVu?-^H^PM69?ZA*M*;9K|IjR9%qj3uEkiF zCBUrWR}>qSq+uwW4bGT0VJVjpnD*YG%8B+aSZ=bM`_yS{T8LNL{T964yC`=5{@>oa zqyG6K>X*p1b4S%DvbaO*MN)z*0<`5cR`O9T=A()FuAiQ035rIvKnr6k;f&N4ygAOP zZ~6|ePLpx6bj85E0{VMg0~)8MKnw6R9~Ldd4#hy`}Dl0 z+%MzX^$!!P`QPwZ=1`7ZQXbPdQhr!DK2!PGmVCHZ4V72N^1`Gvqb*+Ad=MPc-JWh_ zILx^G8_*Zpsm3%CpW!L3`GY8)o4+A$>1#j6EYe4fm^ zwV|oV@&qljHZlOj_fYb|>NAI2!QqPOZAw?6lifmiA=AJ90+rnWbNuRBULLf8&`;uu zX;cBMk{bB@(0N7&y}CnZ*4}Pudb^|mr03s;sjpP-mkH3W!{(_R>$e*khdU^!-~r~PMglkXY;17Sj>mCo zMd*Fi>?6Nga7`W|A}2uzEXABcly(WjjZVaI;75<8TbF9wmo3nXvRx%2<5U!bLo%TS zrkznld8pL$sZ=!RX50brrmA8+jFvf zPU5^g3=p1Ku`m4Lwv%qCY678oAQi$SVZeH9xAho@4>z{BH}!D96{{FsbF0p0TFm10 z$-~1_l3`QWurBP>h5}!X6eVf1xfd{?xIe&>N`RHO+=7L+Kx1&h&1KNQGM+nFtPJ-j zi4Q+|&^^;WyVpVWmSkqFeZ5`9cio0i!5sqKF{#1L3E_3IE3RKJ3$JIY4o~s9-pvNV znVe;FnGc9?^kvgoS(4&_4FK3Q&hIJa=~}Xk_{A=QUvH^|kb*^<-8y;V7wT2iA$vL2 z3w<1i)(ds51#K4UV3v@#`o&k8-oASh9Y8}eOFH0`g>YOKNG7y1nwWdaPP|*$XWHiv zne;c>?b|I?Z6>8zaTl48fhCHG|F^17UVUwfIH|ijrOHri>j7I;Vg8loV#qH~<}i>D z;+d36C=%VYu4wFOSPR~69d-($UQHv%8cTkm^i6j6kY}3Y%j=VXIg*m-JyKl@8CJiZ zJEPrkpli_%qKtQQ_xX<@iZ}iR+PBFid|7rN{4eb2N}&dazDu%BcKED9WL05#qQlYs zpg0z#fTv%6C|cdz5N{v*#K5~Wtv36D*|^H9*^#ECH0z%fPo7^a(sFyocOX$sE6I+r z`3A}(Mb)k~LkyH;PKKAQAt&r^+8|HDm7PR?aF;_}4KByL~sV7H->R zx4v#QBvW`Q7fla~FeHu}8)>C|Ra6~M@MZ|^Bm{SNzrmf0yF0<%-Q6KL_u}sE?hY5% z;1XOD+_U`7o_*M}|F?asnV!>Us;B3x?&`0q?&h?4ilh=bQgD8Cjpr$xz-=t6tebD3 zVlV8Lti(-RNH|n=8x+Jo8DYkk7Q5T2x1>yKN|;C>(nkd_ombtSo8?*0Sfc&k2X?#PYEi z?U`PrjvHRb4m^(eJ92MDHZa>R(j{<4^-VS{@8mP784QM4Xtw}4nt1~|G)rM_&3Va@ zSHOFkepDkNEpFVqon=eS$%XH3I;A|?oHLU#3$~nOi9rHSmPMa@Kf1|CazsdS%q)5@ z-)Qj3}d6F=+>?DPMh|sP4Aj|#N>`6XK`4P)oKs=jKMQ2N%k5e1=uuP zA9rAo9~q$M?lejqR{%&^?XH_D})U`!H^>s zqts5!Ffm=@oT6L4VO&{j1vp$~%Gbty>QO1)PL?MG$b7{;qd>_@g2s@K^cTV#;i{Am z8>L2$MKzFI5~bxftUbp%TWzA@`HL{sod4ZdKlTBZ&ACeZ2bsFATMuFx51KjA>eicm z-!xqE)}8$f;U#7)#6;lvz8_`uK8vh$hQG$L|ITMM`e*0tz$@;qiqHfr%s?y)#gn7Y zMtwt@w3ind^E;6QHWrR&x8Y2qzv}+e!**khgh5Pb(aU304m4jZ7s$17m}`qRmqO^m zVryle%pjzGKCIcg0_w zfT+V(ha6z;=9@h~gdX;%wP*ped+}Z8fiw0wGTO9R${#t7>mD!m1n8P{^oT}Tx{;1< z@#E)<_01%ocGZr)A2HfObNpvu8WQ?aea=B&=o4=*sdg)KAlpIY%Aj}Cij7%j#0;R< z?`D6DEoX{(y-2^$=^>(-qvCLLxBZ6p>hUPueKf%~qGA>oLif|Me(kK--TD!k5f>X7 zy7ObDriYhoxoc;LmZ@Gm?@&jU^Gd{;D($$w zG#d@In5oO^t}t~VCn;VfP?6m)Yq}c&&qG^eAGG+iHy^g@p4dk&xj(Y3WgbrkrG)Q9 zFg3i1zg-7ZJ>K6`<*wmlSrBiQ2)?PPQmh$sGur8&FLB48dRQrL4VGz- zwF9ujr?}@;XRrYXD$D5AwWma)ZjPdu-uOotKkh5k@}}<5+nMwCpm5FVtdoUFiHb@?gXYQ8GAE3^#Y{VSuscV5okdEuGE6Rb^#0+% zMQvqGm0?}1r`N(b@WloUv=_+irH64lenE<2hBrBULSB@sr3Ble8k|lSTs$<*JS%zm zIa8%5Q8NAeit13P5>XbpP*g;8C}tY(2E0IaT5Hv{c0Yll z3RG*-d>NW)Y0C0ySoA{kP#3ri1Cni!uN(j!SyV8-J~wU5-QLTXf;en$olISm&nhv) zQW<~eNXE9$0|q&IeArg4hQYFM`(mIgt~B{z7oi{(4(8Jx(O ziB49^yo20|^tQmLc(;<;DkDY(TmGycOH+Q-x9ge1_2l(P}}dmVM_a~($)NuKG4%ylaL8>XsmTP0c;sB&=^4ydea1%jrX?maD6{X{H_x(R4f?wW;P zt5*Ymo*%_kq1l*{ih3J4CfccJ_*ohpMduTzoADx2wR=2dG+zE)cJ!YMOcK6RQOg>dfa?{@Pih3zKx&u75wc z6NBzI8jKyv2(KsH^Cyd}>SU0s5pdsE(t~dx3b%C4`uSD;&6(Lnjp#1d>cP+4D)6xT zQm7ODbtG2w8GcG#l51vZmnxcub=D%&6L+e?pK|`aUaED^@2Lq2Mb(T65Ue*kyDl&N zUY_JGJYA0whcRoc;nbsV1IXhGRC+^Nn_l!EwLW7i+BZTR%(gMw@`FJUL!75i`O#25 z#rT+j8MZaPv( zAjSi7EHzB!zw-QwuQ4vp^>((Jwz7!=_obvRg0Rtkr;jTkHYcagUD&RhN{Za^W#IWH zrh84;%JpJcO;nwH5UWhdW1TFWcCi17kd#H*)}MBXVy9S*J}PY zT1K5V;DwBQ$Ey6}{r=bbSP`wjtGm+=D-s#gjs>@E`|>K#)&an(&S}HunAI0}F6nVq zJrJjtj8dUz??#X~R48)HOg;l>C203*7`GiTZ=xLbGc_%O0!c;)71_zEkgSf}1)neW z%^f@5yU8}Dc@%qHeuhh8e=HU*gjku{PUrePOj{^mvk zrObFBi(J-@=cbt|*kO~B<8og6_CS{u=t9xF9NOD!USToaHd`Dvi^J)&t|sP99amP^CVn{q4jVJT^5ZEp}w{#R6cC1{*z-;b69(80+QNpNiN$E-mZjYZXCw>x<@T z>Z7mt6S>6W?Fm|qUov9{hyoyjZ_f8$AwjdEJ-2cBKHxYjup|@}4NF>%**v?Jt#&sU z^G1tW7ZUcVT3B!+Ykf7q#(>^$v07Nf$3%ckvqkGtp3QMf2M=Lvx#|4~!z@bD)o#-~ zNo_gqmzZT?h$0igNaUtZ&i!Z-*fW}nl}uqSK{C?T$Gz&phSaHLiB6hN+F#N2Q_CaX z(ls=hc>C<9)D{a5U=N9Hlo#(#VR7lmWq+E9EpmA`?x3HT2`Jr61{&?N)#dEyql9o= zRT`d+(A`+GNn&s(T>z~IVws~WUrfA{1`GNyraF1Itj{62PkW<8L!-iTn{O(u-M9zT z28fvO0twqq_PyaVR*q`Rpis{Fu*J1SDsRnweStN-;#W0ePnU4j$TUw84gIl^Qgqn% z24@jTrNDP5I|9p1*1R2Z{bULUl%E)Eg@@pyglXC;-}&`M7oQ_uYA z{K{(-bRV!IM;{_RuIaQp>U0+&zvpV8=GtgdnrFPFm$uf>NoEuq|3x;nhZl2a+H2~e zfPdpnxBaMkPC1e{R)n@}RC*qy)WPMp4_ngs0&>%6eC9IG6NH>Zx0|B5TcE4?7}0G$ z$&EN?5GSxcE7>wqw$@iV+=gtuaEi35qh<7zxtFNJ5i6?=mQyED$duPm8arlGs9PP^ zro@O@$a{#qr}C$*#t)qIgGCQt@T-Z1yBrAXtT5qq(@W}i|AMqcmz`hb?6jgiLX5c3 zKP>r~kF7|9C}xH3UolL{lnuf3RoOSqYsQE#KW_~#jJj_XW4_Hy(d!cXR8z%n71pCP z&7YbFs?;1bTj2dho{_l}XoGhv759yKjB#=O^MhA=_?~kosWwD2|a@q>$ zetV{@Q}yG**I00PW?w@HzldM!^48SW1um#EXG1ZV9qDE`JxnEc!dJjb_j>NDgG$X? z0l5a3XoF5(Rh$xwyFK~Z5}W*ZGNrw2xWoyU$7`JAdz|~K5mT6lBT$<6_WAHvT^}CC zPMU=C$6A+GaIdGRvO`Ayozh@b1m)0VqPx<4> zX>IXI?Cj61yi{M5fp$d9`LRYjzThf3kEo1yqj2-ah8MSX!jEbBD-h)|vSIxH$Ih4Hwj59q~y~*C~L-MR&MD4pTvhqC`yA@pt3nN>E?#3 z`5KZ*>eCB_8m^qD{&d@paw{pgV887xq--BB^}ArvNd<@>sz{ebgx3svPQ3#D%t*3> zWh5@~Jte?c3^9Try|hGfN5Mi__e(}irsJyM)&gRis`b={;ONY9NRQY7ew4R=C1j>7 zbO+rG)=1cL{90O*&GmO;FZSGQO=qp=pX0kdQhQCaxCBNk~v+}Vbsu)E9g=7P+nT#vxezOtg}-U!Y}tF+RJl(@ zP}x%8nkwk4S$3|jv>ggUv~9^3UmS*;omQb_tHxlmJZ_DXKDQ}6COdRtRmB0v+wt>n z<}`Vo7(e=_Ruu4S>l|M;03L@mN&~)_8&IRF*WKyCOtNNDoc`uY2H|gwRl~0jF>fX` zg|p10#()xXav%hv#J(dd3He&;AE38o?P&~cA$~)F?t@fcD*nWM<+^(OT&@*=u|&YNU*P8(|?+4B6V+6L-gnO>)T8s4Kf3rGcu zk)AGXq3D)Br>XobUM}nna#cTa_>9~g&?`ehDl$S+olw`9|NiWQ`GwWgu~HRsz{^ng z%+J~VCeyhcflkG^JIpK})@Nv^(NL@APY%(|J0M?}G;8hzWi{s2R7B-P*Ty~YWsuwE zX-7ABo;5>NDmF?#Bt{gIs2cD|EIfdMtDow_1;5eak+qzGEe*x_AXhiwD@$H}udsVB z{&r?K_4|#FO%#z(N+WQWr8#b$vHqO}=-)EBx;%_shOlpnZ=M(0)a-qi3^`E^s~)w9 z(*5#{jH-}DeCYhu*+_5wK@d-x7y3#5l&)rjLJ;t|t<;Ut8*+9oQq&>qmA)TVAQl$I0u9{<6+$|z{AC|0@ZPzi5;%cwVH}LUGm0?`-(ZhW z%CzW+vt7Ypf*vfelHvs^au8~THZ1jg$hVr?;llN|hw^uKV461=H^_*S>apSpql4vg z;x8XVkW>ZLYT0jHfbOl`f!(mz@X`?`@;(_$OyUj}poxymyHODG{Hn2i2mi!}I!=kE z_ibq0I*puWQ$S9*O-_bKlQp{#K=@8PE6(h^JVb{r4`Qy*SLQMac`Ft#YrC`g)%~Ku zU3;Kqlb7IGXM2H!YzRAbh<^>}5W1ed8=b}HEnU*oJkYMdjy4MD3%j(f#?By?_Rau& z<{Knk$H-TarwuJ~L<))wzc+ zgq51%r>II5LzJ~;(ptwle~|%=-;{?v@tN+3R2njCMY69O>pVETz8IGewLVZS4*cZ% zN!V=IeOwjDbmxx5m&`{hc9e?kbN9TDLgy2n4Qv3T&}r~77JP#@A7OhWXgS4EO&tSJ z6qPK-w^WQXbdg*#=s9#4degUrtDwQKvh=QYfRLFL5n1;Y0ogtC11S^L9fY&G5DQ#g ztQDAD@-zxk8fbU8N7|NJKsFC?Kb(`agp)o)LPpa}?ZUfK#n<@jSiFbOY)tw_oNVPQ z47bECwH7%Hy~Eh@i|e#HehU~mGI(z^6*^AL)=A)@m|#ha#C{G_SA>I9>@*GABr};T zdd}a@M=7bzYOqn$#3)?G^{s>XYoZ)gff$~qu%%Qh;^+{jy0d^DiPYb^*IXlu+J;y> znj3gRj|K-c9Cs8q2-W-GYHPMm85B_*YZ^mC4~NP0ZVaB01$mwwi&8o`pycC3#+JyS z95et+d&RUF?EH#$1P!4JgWD<42zX{kS#C>#pMP}iZkqWcS%9oZW_SLFNN_`u9(tT* z|Bu)Gtz+>LzF>cPtzrKZ8vnuvFogF1BovcIY>*G?+hy(hb2wf~luy3-+*PPe)4Tf2 z%4txpcEf?8WDOy1Z741Mnvgv`M|dczwL|1&>C;s8oXGtZ4Qd#!DY~DVK~6@l9%x9* zQoVO(ya8L!v2L#0X5Nv|WKK0qw#mT%8ng-(+w?@0v91iJR`!77yKGw zQIf^d7K)dMWIGVnbAKDL@(q<#HLzY-q!jnQM-;0tf})`5yxD5NPj46ZZXxTRXv5e1 z>c6@kPE0(hgDIEbsg-TZyl$4dtlHwLFKA~tJI1mkB>x=0i8pJ{t`#%idvlq%&KrGr=; zzq>nfGfQ2OS_5b&vua56uqS2!-M$a|DvLLpj-u^V5i>}OL=Hz0gMB)_&(e-K=k8IMo8=Me(g-{=ebkp)?iQ(rJ>^=WblUYh(!PXlHO0AysJTFNUo=-TD?z zh8}1>5}Hx8@0ik8Tl|_Hjd;BgL?9-36pg-F`noVkmE05eCHGOP`)IWAX?!1Zcat+= zbb1Z70jV^3plKFb6jp^57lY^HtwiJ+WqBBlE=+25p=|rNb@q~%bC%KdrAuioOmH|3 zMhJn+$odKsvl7vsaJZe}D3e`-IYMNn_BJz`=!=o7P*;WUkWmhLG4wY*&I-=q(Sx1g zJCDr29Q=iMU!2$&;iYGTzvnk^NYQPhO@1Te{HociO3sz8^HU&K9uj@q=ks+sM$#0H zCu^6P7GNx`yPLxOV#6|dQ?-4>kor#j_xSgdOzK_mwCu?q-Rn)hZSk7tly3fLh$06O zKUT}toEFTCS3%0}AiN9P#|qu?+Nig2@e91woC@<|!9H*BgHG&jGe%)N>iL?DG?Be(7?e;$35YnJ*aofLjhf#jrV5Efsh* zz(Y!Yk+~)bfq7y5j-ZiWKIZ|Rxfp%#Y2t1;oJ{sEMc8mcntL9^q7~SsFJY2I%5ee| zH442a^*hv@mGA2ILzC`3s*6^e3wr%;L9sq7JsfdnIIj?bE99PJoIUzc8NN&5+N51Z zD?%{M5|Ng*cGydC2!4W4>djmSIl3w+-2CqZEKi=`mz?hgzjx{)o=V)Yb6Idzk7D%XnF97gfTzn#N^DMw}p4rjln|Xb=nHs@Uz) z?B&^0f>J_6rjSZ47y0a_5CGp1TYSmB!qW8FbjV_`w^pvB@5-bq$27&6{zglM-az4Q z+9?wKr(J3{Mn@V;i>Nf^7zWmj;|W0hmK5K-;n_l4QY{rshUAO)th=K0;5cogkJx;t z#|&L3jvt27By14}S%6_)L$v`cCW~@n-5C5RTS_Thn&ZFFCQvv{M9HncPppJXd7C$k zdo9UOkGu^ue}o2Mfr#>{8S5Te30`t;{`r|%kJwdaD(XO%g_kaHuqM+InPW>!w4{KCMZZEv+u*x-Hu zR<+q%3H9|wJ$!E08t8b0JogU*7;gH}x+>yXOW~Rtcb9)1XKmKDez$NfkTgeMA(Lei zPc@*;5L7C_9Td4na`F)q8DC{OqfV=-KQwV-;&`I6w|vO_Q?FOtv3Y6Wmh6KNOG7R+ z+p4;{zEfaNc!D3+5nE30TwKDi(?5@!yR|SYZa9!4S#-OSkHybEgeizm zELq!6rK(4aVMm>*$*M5SZHZ+PYTj(ycY)&HUQeTb>0rq#P!`;>C4dOEbRN4}4)T`m z)E|jAqZfI7*o_XbSOGQtW=PE_9onD1wzaxr`_6YqG`@UlBGn{3rY6|#$s$b!W=D!9 z#E*H>YvzPfnqDVTmxFd8ftgV6--Ll1V+$8x?b0~+#3mM)~UYlhIu2(4ybWQEUfO$$NF5`9{^ zJ(}W+>51TYu48aY9Z^L^UsK1RYI9OPhy&F9dODl!ywN6dxitH7YsxE1NuNgV9c3S? zPxML^*eT*=LbbYLqkjW-b7^c>nB<7-$)u6`#VVV}bIXf)Keu~8f|jX8kturu#%d0Z#*Y26C;aN=k> zPOYG)CitX4!+mBbxtQ)(?CSNm?b+LlLh;g?xhXWgmpgYRAy7@|Aol>Jy7$S)ub`+T zT|@F~l_H%pFxGrIXhA?exK)N@vNac;3~GW4B9KYHX1_5vxse=;B5B`Z_VBUQL8m7O zKXBZ|HsNRB^J`@YTN?^i-C>PLw~q`DRB($&R)h4GN-J#%q-)$zMSG`IpzB(c0DNq+ zg2JWbc)Z zdkVEDyZE(txIJ`Bbvl5$Hdfz91t;D0VP)=il>5K`!%EwuK;-^8T1&sj1CIn1K;fp} zwd(bfj$2Q|g}U?|dKPUz;fL~4yX&ly>iGBTeKqQ81bj0Pe0~ktq>*ipPC)adcOI`$ zDia}3M$2Xc8`-$@vuq2|b?Uj=p{-2YGJXx4v|9l#^JuqQ)Sme=i5|0BG@fe5A?fL6 z)kSXkka+6IOq|3)=nwf2iPMNRB4K_tBKBo%;GAD%UCx2BO+)>$+Uhd`W@hvzk=gRT zI|d*UxIhljocawB^XEfouI4NKS*;Gf=D?|N{wu8~tob{>9XH1EasAw%SXnqBr=DyG ziDb2;Ma9Ayhl@9hVJCz&L0Dmefq=ANYhh1iQjOcn;O6>-k6Ny}6<{}bmdqN1u$7oR zru$k%1n2s0UvtRz>Q`3x8K=MWg~_+rkJ0R;cIRu!sxBPW{ud0pMW}NWot0`wsl>HU zc4WOOcwWk;iyM`uI+cvx2y6}YNWS(J#_(l-FpfD?MUJK!`c(S{R;>k`7T_Yv=m`hR zEgxv)3O~|C^x^hzy3%dT8dn!5U5e#H--m5beJdm5K4p@+Qa~}BG zm#X&}d_5;YRJPv$dr$MHCnk_riw5%RR}$Gd+v32W_eb=`<~fY)ysfeoBTkO8Dd%Wv zC_ptUs;LrntRv)KJdyQkgfm%;oQu1)IJoG$F&Kl(y*^e{09psJIghj5!{uG8zsA8srf-U+hT)*wa{J+FhdPljM@*`@aC+H zrkg?2vA+jHTgwhsv~8@7ardjNt)iC}b!0F~KPO?yIZwIftnGsR+x*qdUtGD)(9IaX z!(sj2VrLGG2-u5~q06E?b9z1Q2zq57js}8b;Ug05zp>Po9(D3XhkvFlEZwgcy(&|+;`ym)US+P-HV72sL||^ zcjx5wFp<(9Aq9Kzt!toXAsUbl+Q*jIOeDV|UO)TQm}y$h3FrzWFVfJ~hG4kn%J%91q%=%abrfr$`eV&$;=_bgy zf4aDQXd+biDp6VSpcB|I0nv*ohU^CaGpmfmn95K{hcKA3<0=J~H&+@7Cy#znM?26Q zU!NnfcQ=ibcc1S6m6-x7%EZ88w|ir~5!T4SUff7!tFIdsj{wwQUMsN;OM$JHvcHw8 z8dz`cG>1%9*iYAqTU}l+!SmzaCTfP6C9`WKvHO>5_N<&wzY#n$>XT(1Vp20<8S%?t#+gB_+}6Wcd4R& z4t)R&iSr>b3kM4x?Wu@1j-QN-tZM(@xrX&|=Q!cR{(R&hCI2jbKejM8XF)$B9}ELe z$varFs)A__*G)tRK$<7{Y^om4AsoV0hr~+-qw=5g@=m(=qL%=T{W#B5O7`BTV3NKE z-dLCM?sIEDT3|T#=>@2y^(v>TdJVjY7onENt(RagaqpSVwWf6z3E}wc@pHV;Ppna< z!)yK&-f@x|CZ-)pte{iIlXvDa)0;AnDK)F^ElpP9VO=*v@WT!7qZVK6#_vnH76e)K z^lp##q@d)y)@XTRahl<{h$G^wmK*iv%VOP@0&WaUNOBs&Lm<^?IB3P5k|}k%(r&ke zZ2O9zU1RJ$q%8Q;i#3_UHjM_{AMjJf$$vK*kNQE`zhq#5o)H=ljcJ; ziFgr+G89;DPF+EEpQOq*f&;|)Ha(gI>H67LlaO)QD5w)O=5$p9L-ilQ z@I@s|V!e{pRJZ^Na@JIgZVFMx1xz~WrkfK%X=TD^+`klA^t#?~5NQEucp0@3h02W~_6B9K3EMH_2&(&1h`&=FHLh{a$E5Z1f zcl9Jlb2?=q6cCV1cDJ7UelqhBeJij~%D-utqBS~|>6k4t9Zw4-2G=jKDH@cInt zef@Kx_@meNU+4DgAy>cyN$=b3N4W3>iU0ElS3cl*6a25N_jxMdZMXLy#;WGOmD=|J z@@`26R3tNdQx{ifGb6kIBnM+_BmgHVE9rj%KR*(SoSD6at0gHL8wVE>i<+0C87YgR zv9+qJ9TJNIDS!isMZ(I~)y$ccMZ(s|)eLB6;$Uj_-%V#1S5gi(9wb3Qr2pMJDw1c` zf#!v^t|W$Es#j2ryTv`GXr999HyPO|BwDXrG~)Y~{XwL^Ly;LKloCn7T_|(fk5FG@V9DFy zBvDc&In{@crSfFH=!%qO{na-V)eHRwpY_$SArVICvtE+nXNhrEvUbDFFCqe>v}o}d zcnX2curl!+4Zj-RxpT+i(*kdnDpyGY0?sY(UtVk0=g+|h7oZy0i02%8%X{;;wzb5< zm0Cpst9u)d&a59*LJQ5`LAP8fhTLJaDcu}ACJ$Vv-6t5qHbbi~_MQG4j0xjh!>h+M&|E&YqbPBiiA=*pL;3um{8Q#5Suly)5!ZP`uWoHI%GWs3sx z4?7Rt<viT^W4lX zRAj(?g`mz^De}p(sE$b#gM{Cu0n;G9FaVfp&k)0CeRCkj4l~}6st|az4zO!wm*QP^cLC*%bJIf8yhD`Kj>1qAJ9QZL<9(uF)brVMbRS@PFOkR(#tBQbxY51lhY8 zQW%ML-zC){)#&3i^EF=~sv1Ql7o|WM&e7VMdHtR5#i^}uiap*5ZYb_{PRM`uD1S~BvsD0ylSfEeuP_g(vn$6SmU^;pMmF)y@5t$; zwDoz^yaH&Gzv*|!cMgdOsECBy%LV?%dMYAN$lO&P7lFLm)&oA3t%E)OCv4;Tf3b~+ z8;M25!NHZ3?W0WviA5Sns>=`H`2L-ZQ-T910TdNs=MWPIvH<{G>}+hJTwEaUm4sa`n*P3_ywOL zUqUN5do}X0WQ-!_ token svgToken.height(), svgToken.alignment(), svgToken.baselineOffset(), - svgToken.linkTarget())); + svgToken.linkTarget(), + svgToken.text())); width += svgToken.width(); tokenIndex++; } else { diff --git a/core/src/main/java/com/demcha/compose/document/layout/payloads/ParagraphSvgSpan.java b/core/src/main/java/com/demcha/compose/document/layout/payloads/ParagraphSvgSpan.java index 418f16b17..2279d7616 100644 --- a/core/src/main/java/com/demcha/compose/document/layout/payloads/ParagraphSvgSpan.java +++ b/core/src/main/java/com/demcha/compose/document/layout/payloads/ParagraphSvgSpan.java @@ -17,6 +17,8 @@ * @param alignment vertical alignment relative to the surrounding text * @param baselineOffset extra vertical offset in points; positive moves up * @param linkTarget optional link metadata + * @param text the text the icon stands for when the page is read as + * text (its {@code SvgIcon.text()}), or {@code null} * @author Artem Demchyshyn * @since 1.9.0 */ @@ -26,7 +28,8 @@ public record ParagraphSvgSpan( double height, InlineImageAlignment alignment, double baselineOffset, - DocumentLinkTarget linkTarget + DocumentLinkTarget linkTarget, + String text ) implements ParagraphSpan { /** * Copies the layer stack defensively and normalizes alignment defaults. diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index ab832886e..c45fc5e32 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -60,6 +60,7 @@ Payload records live in `core` under | Inline images (`ParagraphImageSpan`) | โœ… `PdfParagraphFragmentRenderHandler` | โœ… `PptxParagraphFragmentRenderHandler` | โŒ | | Inline vector shapes (`ParagraphShapeSpan`) | โœ… `PdfParagraphFragmentRenderHandler` | โš ๏ธ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius โ€” single-adjust preset) | โŒ | | Inline SVG (`ParagraphSvgSpan`) | โœ… `PdfParagraphFragmentRenderHandler` + `PdfPathPainter` | โš ๏ธ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` + `PptxInlineSvgRasterizer` (simple layers stay native; arbitrary clips, exact dash/cap/join styles, and off-viewBox art use a transparent PNG fallback; gradient paints use their primary colour) | โŒ | +| Text an inline icon stands for โ€” copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | โœ… `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF โ€” it replaces glyphs, and a drawing has none | โŒ the icon is drawn and its text is not written | โŒ inline SVG is not written | | Rectangle shape โ€” fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | โœ… `PdfShapeFragmentRenderHandler` | โš ๏ธ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners โ€” single-adjust `roundRect` preset โ€” until custom geometry lands; uniform radii and side borders exact) | โŒ | | Ellipse (`EllipseFragmentPayload`) | โœ… `PdfEllipseFragmentRenderHandler` | โœ… `PptxEllipseFragmentRenderHandler` | โŒ | | Line โ€” dash pattern, line cap (`LineFragmentPayload`) | โœ… `PdfLineFragmentRenderHandler` | โš ๏ธ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | โŒ | diff --git a/docs/recipes/rich-text.md b/docs/recipes/rich-text.md index cb500297a..3524f4ccf 100644 --- a/docs/recipes/rich-text.md +++ b/docs/recipes/rich-text.md @@ -128,6 +128,15 @@ both take `alignment` / `baselineOffset` / link overloads and a clickable form via `DocumentLinkOptions`. `SvgIcon.parse(String)` reads inline SVG markup; `SvgIcon.read(Path)` loads it from a file. +An icon is a drawing, so copying the line out of the PDF skips it. When the +icon stands for a character, say which with `withText` โ€” the PDF then carries +that text where the icon sits, invisible, and a copy, a search or a text +extractor reads it: + +```java +SvgIcon check = SvgIcon.read(Path.of("icons/check.svg")).withText("โœ“"); +``` + ## Emoji / shortcodes `emoji(":code:")` resolves a GitHub-style shortcode to an inline colour @@ -148,6 +157,11 @@ in the optional, independently-versioned `graph-compose-emoji` companion artifact (Noto Emoji, SIL OFL 1.1) โ€” add it to the classpath to resolve shortcodes; the engine itself carries no emoji art. +Each resolved emoji states its own text, so a line copied out of the PDF +pastes with the emoji in it โ€” `:rocket:` as ๐Ÿš€, `:heart:` as โค๏ธ โ€” into a +messenger, a search box or a text extractor. PPTX and DOCX exports do not +carry it yet. + ## Inline shapes and checkboxes Geometric figures drawn from geometry โ€” not font glyphs โ€” so they render diff --git a/knowledge/api/backends.json b/knowledge/api/backends.json index 43fe600c8..492e2dd98 100644 --- a/knowledge/api/backends.json +++ b/knowledge/api/backends.json @@ -23,7 +23,7 @@ ], "counts": { "types": 70, - "methods": 375, + "methods": 376, "constants": 18, "generated": 192 }, @@ -1060,6 +1060,41 @@ ], "stability": "beta" }, + { + "kind": "method", + "name": "writeTextLayer", + "static": false, + "origin": "source", + "typeParameters": null, + "returns": "void", + "params": [ + { + "type": "PDPageContentStream", + "name": "stream" + }, + { + "type": "String", + "name": "text" + }, + { + "type": "double", + "name": "x" + }, + { + "type": "double", + "name": "baselineY" + }, + { + "type": "double", + "name": "width" + }, + { + "type": "double", + "name": "height" + } + ], + "stability": "beta" + }, { "kind": "method", "name": "document", diff --git a/knowledge/api/backends.md b/knowledge/api/backends.md index 4957342cc..3c3488dbb 100644 --- a/knowledge/api/backends.md +++ b/knowledge/api/backends.md @@ -28,7 +28,7 @@ note: "Generated from the pinned artifact's class files. Authoritative closed se **GraphCompose version:** 2.4.1-SNAPSHOT -Types: 70 ยท methods: 375 ยท constants: 18 ยท compiler-generated members: 192 +Types: 70 ยท methods: 376 ยท constants: 18 ยท compiler-generated members: 192 ## com.demcha.compose.document.backend.fixed @@ -116,6 +116,7 @@ Types: 70 ยท methods: 375 ยท constants: 18 ยท compiler-generated members: 192 ### PdfRenderEnvironment (class) - `PdfRenderEnvironment.LetterSpacedFont letterSpacedFont(PDFont font, double fontSize, double letterSpacing, String text) [beta]` +- `void writeTextLayer(PDPageContentStream stream, String text, double x, double baselineY, double width, double height) [beta]` - `PDDocument document()` - `FontLibrary fonts()` - `PDPageContentStream pageSurface(int pageIndex)` diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java index 144df85ac..5ffbc5003 100644 --- a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java @@ -412,8 +412,8 @@ private Rendered buildDocument(LayoutGraph graph, FixedLayoutRenderContext conte List pages = createPages(document, graph); try (PdfRenderSession session = new PdfRenderSession(document, pages)) { - PdfRenderEnvironment environment = - new PdfRenderEnvironment(document, fonts, session, letterSpacedFonts); + PdfRenderEnvironment environment = new PdfRenderEnvironment( + document, fonts, session, letterSpacedFonts, new PdfTextLayer(document)); renderGraph(graph, environment); reorderedText = environment.reorderedText(); PdfBookmarkOutlineWriter.apply(document, environment.bookmarkRecords()); @@ -539,6 +539,8 @@ private Rendered buildSectionsDocument(List sections) throws Except // One registry for the combined document: a letter-spaced face drawn in two sections // is one font resource, like the base font it shares its program with. PdfTrackedFontResources letterSpacedFonts = new PdfTrackedFontResources(document); + // Likewise one text-layer font, however many sections draw icons that state text. + PdfTextLayer textLayer = new PdfTextLayer(document); int pageOffset = 0; for (SectionUnit section : sections) { LayoutGraph graph = section.graph(); @@ -548,7 +550,7 @@ private Rendered buildSectionsDocument(List sections) throws Except // Each section renders with its OWN backend's handlers/debug, but // records navigation against the combined document via the page offset. PdfRenderEnvironment environment = new PdfRenderEnvironment( - document, fonts, renderSession, pageOffset, letterSpacedFonts); + document, fonts, renderSession, pageOffset, letterSpacedFonts, textLayer); chrome.renderGraph(graph, environment); reorderedText |= environment.reorderedText(); bookmarks.addAll(environment.bookmarkRecords()); diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java index 57d238731..ee7750670 100644 --- a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java @@ -43,11 +43,12 @@ public final class PdfRenderEnvironment { private final Map anchorDestinations = new LinkedHashMap<>(); private final List deferredInternalLinks = new ArrayList<>(); private final PdfTrackedFontResources letterSpacedFonts; + private final PdfTextLayer textLayer; private boolean reorderedText; PdfRenderEnvironment(PDDocument document, FontLibrary fonts, PdfRenderSession session, - PdfTrackedFontResources letterSpacedFonts) { - this(document, fonts, session, 0, letterSpacedFonts); + PdfTrackedFontResources letterSpacedFonts, PdfTextLayer textLayer) { + this(document, fonts, session, 0, letterSpacedFonts, textLayer); } /** @@ -65,14 +66,16 @@ public final class PdfRenderEnvironment { * @param session page-scoped drawing surface for this section * @param pageIndexOffset number of pages already placed before this section * @param letterSpacedFonts the document's letter-spaced font resources, shared by all its sections + * @param textLayer the document's text-layer font, shared by all its sections */ PdfRenderEnvironment(PDDocument document, FontLibrary fonts, PdfRenderSession session, int pageIndexOffset, - PdfTrackedFontResources letterSpacedFonts) { + PdfTrackedFontResources letterSpacedFonts, PdfTextLayer textLayer) { this.document = document; this.fonts = fonts; this.session = session; this.pageIndexOffset = pageIndexOffset; this.letterSpacedFonts = letterSpacedFonts; + this.textLayer = textLayer; } /** @@ -148,6 +151,42 @@ PdfTrackedFontResources letterSpacedFonts() { return letterSpacedFonts; } + /** + * Puts {@code text} in the page's text layer over a box a handler has drawn, painting nothing. + * + *

A drawing has no characters, so a reader that copies, searches or extracts the page + * finds nothing where one sits: a colour emoji drawn from SVG paths pastes as nothing. This + * writes the text the drawing stands for as one invisible glyph whose advance spans the box's + * width, so extracted text reads it in place and a selection runs across the drawing. The + * glyph rises one {@code height} from {@code baselineY}; write it on the surrounding line's + * baseline, which keeps it in that line for readers that group text by position. Call it + * outside a text object; the graphics state is left as it was.

+ * + *

Experimental. The text layer this produces is settled; the shape of the call may + * still change in a minor release.

+ * + * @param stream the page's content stream, outside a text object + * @param text the text the drawing stands for; nothing is written when it is + * {@code null} or empty, or longer than 256 UTF-16 code units + * @param x left edge of the box in page space + * @param baselineY baseline to write the text on + * @param width width of the box in points + * @param height height of the box in points + * @throws IOException if the content stream cannot be written + * @throws IllegalStateException if there is text to write and {@code stream} is inside a text + * object + * @since 2.5.0 + */ + @Beta + public void writeTextLayer(PDPageContentStream stream, + String text, + double x, + double baselineY, + double width, + double height) throws IOException { + textLayer.write(stream, text, (float) x, (float) baselineY, (float) width, (float) height); + } + /** * Returns the live PDFBox document for the current render pass. * diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java new file mode 100644 index 000000000..a6059b878 --- /dev/null +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java @@ -0,0 +1,244 @@ +package com.demcha.compose.document.backend.fixed.pdf; + +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSInteger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType3Font; +import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; + +/** + * Puts text in a page's text layer without painting anything. + * + *

An inline icon is a drawing, and a drawing has no characters: a colour emoji drawn from + * SVG paths leaves nothing where it sits for a reader that copies, searches or extracts the page, + * so a line copied into a messenger arrives without its emoji. {@code ActualText} cannot supply + * one. It replaces the text of the glyphs it covers, and a drawing covers none; PDFBox, poppler, + * pdf.js and MuPDF all extract nothing from a {@code Span} with {@code ActualText} around paths.

+ * + *

So the text goes in as a glyph of its own, shown over the drawing from a font that paints + * nothing: a Type 3 font whose glyph procedures are empty, and whose {@code ToUnicode} map states, + * code by code, the text each glyph stands for — a whole ZWJ sequence when that is what the + * icon depicts. The glyph is shown in rendering mode 3, neither filled nor stroked, so a viewer + * that cannot read the font still paints nothing. An OCR'd scan carries its text layer the same + * way, as invisible text over the picture; PDFBox, poppler, pdf.js and MuPDF all read this one.

+ * + *

One font serves a document, with one code per distinct text; a font has 255 codes, and the + * text after that starts another. The font's dictionaries grow as codes are handed out, so the + * file is complete whenever it is saved, however many pages have been drawn.

+ */ +final class PdfTextLayer { + + /** + * The longest text one glyph may stand for, in UTF-16 code units. A {@code ToUnicode} + * destination is limited to 512 bytes; longer text is not written. + */ + static final int MAX_TEXT_UNITS = 256; + + private final PDDocument document; + private final Map glyphs = new HashMap<>(); + private TextLayerFont font; + + PdfTextLayer(PDDocument document) { + this.document = document; + } + + /** + * Writes {@code text} as one invisible glyph whose advance spans {@code width}. + * + * @param stream page content stream, outside a text object + * @param text the text to write; nothing is written when {@code null}, empty or longer + * than {@link #MAX_TEXT_UNITS} + * @param x left edge of the glyph in page space + * @param baselineY baseline the glyph sits on + * @param width advance of the glyph in points + * @param height font size of the glyph in points + * @throws IOException if the content stream cannot be written + */ + void write(PDPageContentStream stream, + String text, + float x, + float baselineY, + float width, + float height) throws IOException { + if (text == null || text.isEmpty() || text.length() > MAX_TEXT_UNITS + || !(width > 0) || !(height > 0) || Float.isInfinite(width) || Float.isInfinite(height)) { + return; + } + Glyph glyph = glyphs.get(text); + if (glyph == null) { + if (font == null || font.isFull()) { + font = TextLayerFont.create(document); + } + glyph = font.add(text); + glyphs.put(text, glyph); + } + // Text state is graphics state and outlives ET, in both directions. Out: without the + // q/Q every word drawn after the glyph would be invisible too. In: spacing or rise left + // by the run before (a letter-spaced word keeps its tracking in Tc) would stretch or + // lift the glyph, so each is set here rather than inherited. + stream.saveGraphicsState(); + stream.beginText(); + stream.setFont(glyph.font(), height); + stream.setRenderingMode(RenderingMode.NEITHER); + stream.setCharacterSpacing(0); + stream.setWordSpacing(0); + stream.setTextRise(0); + // The glyph is one em wide, and an em is the font size: scale it to the box. + stream.setHorizontalScaling(100f * width / height); + stream.newLineAtOffset(x, baselineY); + stream.showText(glyph.shown()); + stream.endText(); + stream.restoreGraphicsState(); + } + + /** A code of one text-layer font, and the string that shows it. */ + private record Glyph(TextLayerFont font, String shown) { + } + + /** + * A Type 3 font of empty glyphs whose codes are handed out one text at a time. + * + *

PDFBox cannot encode text for a Type 3 font, so {@link #encode(int)} is overridden: a + * glyph is shown as the private-use character {@code U+E000} plus its code, and encodes to + * that one byte.

+ * + *

Only the dictionary is kept current. What {@code PDType3Font} read from it at + * construction — the encoding, the widths, an absent {@code ToUnicode} — goes + * stale as codes are added, and nothing on PDFBox 3.0's write path reads it: {@code setFont} + * and {@code showText} reach only {@code encode} and the embedding flags. A PDFBox upgrade + * that measured or looked glyphs up while writing would see {@code .notdef} here.

+ */ + private static final class TextLayerFont extends PDType3Font { + + private static final int CARRIER = 0xE000; + private static final int LAST_CODE = 255; + private static final int GLYPH_WIDTH = 1000; + /** A CMap block holds at most 100 mappings. */ + private static final int BFCHAR_BLOCK = 100; + + private final COSDictionary charProcs; + private final COSArray differences; + private final COSArray widths; + private final COSStream emptyGlyph; + private final COSStream toUnicode; + private final List texts = new ArrayList<>(); + + private TextLayerFont(COSDictionary dictionary, COSStream emptyGlyph, COSStream toUnicode) + throws IOException { + super(dictionary); + this.charProcs = dictionary.getCOSDictionary(COSName.CHAR_PROCS); + this.differences = dictionary.getCOSDictionary(COSName.ENCODING).getCOSArray(COSName.DIFFERENCES); + this.widths = dictionary.getCOSArray(COSName.WIDTHS); + this.emptyGlyph = emptyGlyph; + this.toUnicode = toUnicode; + } + + static TextLayerFont create(PDDocument document) throws IOException { + COSStream emptyGlyph = document.getDocument().createCOSStream(); + try (OutputStream out = emptyGlyph.createOutputStream()) { + // Advance one em, no marks, no bounding box: d0 with nothing after it. + out.write((GLYPH_WIDTH + " 0 d0\n").getBytes(StandardCharsets.US_ASCII)); + } + COSStream toUnicode = document.getDocument().createCOSStream(); + + COSDictionary dictionary = new COSDictionary(); + dictionary.setItem(COSName.TYPE, COSName.FONT); + dictionary.setItem(COSName.SUBTYPE, COSName.TYPE3); + dictionary.setItem(COSName.FONT_BBOX, numbers(0, 0, GLYPH_WIDTH, GLYPH_WIDTH)); + dictionary.setItem(COSName.FONT_MATRIX, numbers(0.001f, 0, 0, 0.001f, 0, 0)); + dictionary.setItem(COSName.CHAR_PROCS, new COSDictionary()); + COSDictionary encoding = new COSDictionary(); + encoding.setItem(COSName.TYPE, COSName.ENCODING); + COSArray differences = new COSArray(); + differences.add(COSInteger.ONE); + encoding.setItem(COSName.DIFFERENCES, differences); + dictionary.setItem(COSName.ENCODING, encoding); + dictionary.setInt(COSName.FIRST_CHAR, 1); + dictionary.setInt(COSName.LAST_CHAR, 1); + dictionary.setItem(COSName.WIDTHS, new COSArray()); + dictionary.setItem(COSName.RESOURCES, new COSDictionary()); + TextLayerFont font = new TextLayerFont(dictionary, emptyGlyph, toUnicode); + // Attached only now: PDFont's constructor reads ToUnicode, and this stream is still + // empty until the first code is added. + dictionary.setItem(COSName.TO_UNICODE, toUnicode); + return font; + } + + boolean isFull() { + return texts.size() == LAST_CODE; + } + + /** Gives {@code text} the next code, and states it in every dictionary that lists codes. */ + Glyph add(String text) throws IOException { + texts.add(text); + int code = texts.size(); + COSName glyphName = COSName.getPDFName("t" + code); + charProcs.setItem(glyphName, emptyGlyph); + differences.add(glyphName); + widths.add(COSInteger.get(GLYPH_WIDTH)); + getCOSObject().setInt(COSName.LAST_CHAR, code); + writeToUnicode(); + return new Glyph(this, new String(Character.toChars(CARRIER + code))); + } + + @Override + protected byte[] encode(int unicode) { + int code = unicode - CARRIER; + if (code < 1 || code > texts.size()) { + throw new IllegalArgumentException("No text-layer glyph for U+" + Integer.toHexString(unicode)); + } + return new byte[]{(byte) code}; + } + + private void writeToUnicode() throws IOException { + StringBuilder cmap = new StringBuilder(128 + texts.size() * 24); + cmap.append("/CIDInit /ProcSet findresource begin\n") + .append("12 dict begin\n") + .append("begincmap\n") + .append("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n") + .append("/CMapName /Adobe-Identity-UCS def\n") + .append("/CMapType 2 def\n") + .append("1 begincodespacerange\n<00> \nendcodespacerange\n"); + HexFormat hex = HexFormat.of().withUpperCase(); + for (int start = 0; start < texts.size(); start += BFCHAR_BLOCK) { + int end = Math.min(texts.size(), start + BFCHAR_BLOCK); + cmap.append(end - start).append(" beginbfchar\n"); + for (int i = start; i < end; i++) { + cmap.append('<').append(hex.toHexDigits((byte) (i + 1))).append("> <") + .append(hex.formatHex(texts.get(i).getBytes(StandardCharsets.UTF_16BE))) + .append(">\n"); + } + cmap.append("endbfchar\n"); + } + cmap.append("endcmap\n") + .append("CMapName currentdict /CMap defineresource pop\n") + .append("end\n") + .append("end\n"); + try (OutputStream out = toUnicode.createOutputStream(COSName.FLATE_DECODE)) { + out.write(cmap.toString().getBytes(StandardCharsets.US_ASCII)); + } + } + + private static COSArray numbers(float... values) { + COSArray array = new COSArray(); + for (float value : values) { + array.add(value == (int) value ? COSInteger.get((int) value) : new COSFloat(value)); + } + return array; + } + } +} diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java index c0ca9501d..3ae1096d0 100644 --- a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java @@ -270,7 +270,9 @@ private static void renderShape(PDPageContentStream stream, * Draws an inline SVG-icon span: each resolved vector layer is painted on * the baseline-seated box through the shared {@link PdfPathPainter}, so the * inline glyph matches the block path fragment for fragment (flat colours, - * gradients, dashes alike). + * gradients, dashes alike). An icon that states text โ€” a colour emoji, a + * symbol given {@code SvgIcon.withText} โ€” also leaves that text in the + * page's text layer, so copying the line carries it. */ private static void renderSvg(PDPageContentStream stream, ParagraphSvgSpan span, @@ -312,6 +314,9 @@ private static void renderSvg(PDPageContentStream stream, } finally { stream.restoreGraphicsState(); } + // On the line's baseline rather than the icon's bottom: readers assemble lines + // from glyph positions, so the text sits where the text around it sits. + environment.writeTextLayer(stream, span.text(), cursorX, baselineY, width, height); } @Override diff --git a/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java b/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java new file mode 100644 index 000000000..0fd49e13b --- /dev/null +++ b/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java @@ -0,0 +1,291 @@ +package com.demcha.compose.document.backend.fixed.pdf; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.api.MultiSectionDocument; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.emoji.EmojiLibrary; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentLetterSpacing; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.svg.SvgIcon; +import com.demcha.compose.font.FontName; +import com.demcha.testing.VisualTestOutputs; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType3Font; +import org.apache.pdfbox.pdmodel.graphics.state.PDTextState; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.Vector; +import org.junit.jupiter.api.Test; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * An inline icon that states text leaves that text in the page's text layer: copying the line + * carries the emoji, and nothing extra is painted. + */ +class PdfTextLayerTest { + + private static final EmojiLibrary EMOJI = EmojiLibrary.getDefault(); + + private static final String ROCKET = "๐Ÿš€"; + private static final String WOMAN_TECHNOLOGIST = "๐Ÿ‘ฉโ€๐Ÿ’ป"; + private static final String RED_HEART = "โค๏ธ"; + + @Test + void copiedLineCarriesEachEmojiWholeAndInPlace() throws Exception { + byte[] pdf = render(p -> p + .inlineText("Launch ").inlineEmoji(":rocket:", 14) + .inlineText(" by ").inlineEmoji(":woman_technologist:", 14) + .inlineText(" with ").inlineEmoji(":heart:", 14) + .inlineText(" done.")); + Path output = VisualTestOutputs.preparePdf("emoji-copy-text", "text-layer"); + Files.write(output, pdf); + + assertThat(text(pdf)).contains( + "Launch " + ROCKET + " by " + WOMAN_TECHNOLOGIST + " with " + RED_HEART + " done."); + } + + @Test + void textLayerPaintsNothing() throws Exception { + // Same drawing twice, once stating its text and once not. The text layer must add no + // pixel, and must hand the graphics state back: rendering mode 3 outlives ET, so a + // missing q/Q would leave " done." invisible and the pages would differ. + SvgIcon rocket = EMOJI.require(":rocket:"); + byte[] stating = render(p -> p.inlineText("Launch ").inlineSvgIcon(rocket, 14).inlineText(" done.")); + byte[] silent = render(p -> p.inlineText("Launch ").inlineSvgIcon(rocket.withText(null), 14) + .inlineText(" done.")); + + assertThat(text(stating)).contains("Launch " + ROCKET + " done."); + assertThat(text(silent)).doesNotContain(ROCKET); + assertThat(pixels(stating)).isEqualTo(pixels(silent)); + } + + @Test + void iconStatingItsOwnTextCopiesThatText() throws Exception { + SvgIcon check = SvgIcon.parse(""); + + byte[] stating = render(p -> p.inlineText("Tested ").inlineSvgIcon(check.withText("โœ“"), 10)); + byte[] silent = render(p -> p.inlineText("Tested ").inlineSvgIcon(check, 10)); + + assertThat(text(stating)).contains("Tested โœ“"); + assertThat(text(silent).strip()).isEqualTo("Tested"); + assertThat(type3Fonts(silent)).as("an icon without text adds no font").isEmpty(); + } + + @Test + void textsShareOneFontPerDocumentAndStartAnotherAfter255() throws Exception { + SvgIcon dot = SvgIcon.parse(""); + List texts = IntStream.range(0, 300).mapToObj(i -> "<" + i + ">").toList(); + + // Every text twice, on a flow long enough to run over several pages. + byte[] pdf = render(p -> { + for (int round = 0; round < 2; round++) { + for (String text : texts) { + p.inlineSvgIcon(dot.withText(text), 8).inlineText(" "); + } + } + }); + + String extracted = text(pdf); + assertThat(texts).allSatisfy(text -> assertThat(extracted).contains(text)); + try (PDDocument document = Loader.loadPDF(pdf)) { + assertThat(document.getNumberOfPages()).isGreaterThan(1); + } + // Repeated text reuses its code: 300 distinct texts fill one font and start a second. + assertThat(lastCodes(pdf)).containsExactlyInAnyOrder(255, 45); + // A CMap block holds at most 100 mappings, so the full font states its 255 in three. + try (PDDocument document = Loader.loadPDF(pdf)) { + List blocks = new ArrayList<>(); + for (PDType3Font font : type3Fonts(document)) { + if (font.getCOSObject().getInt(COSName.LAST_CHAR) == 255) { + COSStream map = font.getCOSObject().getCOSStream(COSName.TO_UNICODE); + try (InputStream in = map.createInputStream()) { + Matcher block = Pattern.compile("(\\d+) beginbfchar") + .matcher(new String(in.readAllBytes(), StandardCharsets.US_ASCII)); + while (block.find()) { + blocks.add(block.group(1)); + } + } + } + } + assertThat(blocks).containsExactly("100", "100", "55"); + } + } + + @Test + void glyphSpansTheIconWhateverTextStateTheRunBeforeLeft() throws Exception { + // A 2:1 icon after a word tracked in Tc (a Standard 14 face keeps all its tracking + // there): the glyph's advance must be the icon's width, not its height plus the + // tracking the word before it left behind, and it must sit on the baseline. + SvgIcon wide = SvgIcon.parse("") + .withText("[wide]"); + DocumentTextStyle tracked = DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(10) + .letterSpacing(DocumentLetterSpacing.points(4)) + .build(); + + byte[] pdf = render(p -> p.inlineText("SPACED", tracked).inlineSvgIcon(wide, 10).inlineText(" end")); + + try (PDDocument document = Loader.loadPDF(pdf)) { + TextLayerProbe probe = new TextLayerProbe(); + probe.getText(document); + assertThat(probe.advances).singleElement() + .satisfies(advance -> assertThat(advance).isCloseTo(20.0, within(0.01))); + assertThat(probe.rises).containsOnly(0.0); + } + } + + @Test + void deterministicOutputStaysByteIdenticalWithATextLayer() throws Exception { + assertThat(renderDeterministic()).isEqualTo(renderDeterministic()); + } + + @Test + void sectionsOfOneDocumentShareOneFont() throws Exception { + byte[] pdf; + try (MultiSectionDocument document = GraphCompose.documents() + .section(emojiSection("Cover ")) + .section(emojiSection("Body ")) + .create()) { + pdf = document.toPdfBytes(); + } + + assertThat(text(pdf)).contains("Cover " + ROCKET).contains("Body " + ROCKET); + assertThat(lastCodes(pdf)).containsExactly(1); + } + + @Test + void textLongerThanACMapCanStateIsNotWritten() throws Exception { + SvgIcon dot = SvgIcon.parse(""); + String longest = "a".repeat(PdfTextLayer.MAX_TEXT_UNITS); + String tooLong = "b".repeat(PdfTextLayer.MAX_TEXT_UNITS + 1); + + byte[] pdf = render(p -> p.inlineSvgIcon(dot.withText(longest), 8) + .inlineText(" ").inlineSvgIcon(dot.withText(tooLong), 8)); + + assertThat(text(pdf)).contains(longest).doesNotContain("b"); + } + + private static DocumentSession emojiSection(String label) { + DocumentSession section = GraphCompose.document() + .pageSize(240, 200) + .margin(DocumentInsets.of(16)) + .create(); + section.pageFlow(page -> page.addParagraph(p -> p.inlineText(label).inlineEmoji(":rocket:", 12))); + return section; + } + + private static byte[] render(Consumer body) throws Exception { + try (DocumentSession session = GraphCompose.document() + .pageSize(320, 200) + .margin(DocumentInsets.of(16)) + .create()) { + session.pageFlow(page -> page.addParagraph(body::accept)); + return session.toPdfBytes(); + } + } + + private static byte[] renderDeterministic() throws Exception { + try (DocumentSession session = GraphCompose.document() + .pageSize(320, 200) + .margin(DocumentInsets.of(16)) + .create()) { + session.pageFlow(page -> page.addParagraph(p -> p + .inlineText("Launch ").inlineEmoji(":rocket:", 14) + .inlineText(" with ").inlineEmoji(":heart:", 14))); + return session.render(PdfFixedLayoutBackend.builder().deterministic(true).build()); + } + } + + /** Records, for every text-layer glyph shown, its advance and rise under the text state it met. */ + private static final class TextLayerProbe extends PDFTextStripper { + private final List advances = new ArrayList<>(); + private final List rises = new ArrayList<>(); + + @Override + protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) + throws IOException { + if (font instanceof PDType3Font) { + PDTextState state = getGraphicsState().getTextState(); + double wordSpacing = code == 32 ? state.getWordSpacing() : 0; + advances.add((displacement.getX() * state.getFontSize() + state.getCharacterSpacing() + + wordSpacing) * state.getHorizontalScaling() / 100); + rises.add((double) state.getRise()); + } + super.showGlyph(textRenderingMatrix, font, code, displacement); + } + } + + private static String text(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + return new PDFTextStripper().getText(document).replaceAll("\\s+", " "); + } + } + + private static int[] pixels(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144); + return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()); + } + } + + private static List type3Fonts(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + return type3Fonts(document); + } + } + + private static List type3Fonts(PDDocument document) throws IOException { + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + List fonts = new ArrayList<>(); + for (PDPage page : document.getPages()) { + PDResources resources = page.getResources(); + for (COSName name : resources.getFontNames()) { + PDFont font = resources.getFont(name); + if (font instanceof PDType3Font type3 && seen.add(type3.getCOSObject())) { + fonts.add(type3); + } + } + } + return fonts; + } + + /** The last code of every text-layer font in the document: how many texts each holds. */ + private static List lastCodes(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + List codes = new ArrayList<>(); + for (PDType3Font font : type3Fonts(document)) { + codes.add(font.getCOSObject().getInt(COSName.LAST_CHAR)); + } + return codes; + } + } +} diff --git a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineSvgRasterizerTest.java b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineSvgRasterizerTest.java index 1a7bcc639..29dd026d6 100644 --- a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineSvgRasterizerTest.java +++ b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineSvgRasterizerTest.java @@ -81,7 +81,7 @@ null, null, new Stroke(Color.BLACK, 1), null, private static ParagraphSvgSpan span(ResolvedSvgLayer layer) { return new ParagraphSvgSpan(List.of(layer), 20, 20, - InlineImageAlignment.CENTER, 0, null); + InlineImageAlignment.CENTER, 0, null, null); } private static ResolvedSvgLayer layer(List segments, From 98ac0b1f3adab170c0d4e29cb26a4914f7bfb5b2 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 18 Sep 2026 11:33:23 +0100 Subject: [PATCH 2/3] docs(api-stability): name PdfRenderEnvironment.writeTextLayer as @Beta check-stability-doc.mjs requires every @Beta member to be named in docs/api-stability.md; writeTextLayer was not, which failed the JDK 17 CI leg. It joins letterSpacedFont in the list of @Beta members of the PDF backend, now seven. Verified: all four knowledge-pack checks (extract-api --check, check-stability-doc, check-claims --check, check-routes) pass locally; javadoc:javadoc -pl :graph-compose-core BUILD SUCCESS, 0 warnings. --- docs/api-stability.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/api-stability.md b/docs/api-stability.md index f7b2bfb80..2b36f1333 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -57,7 +57,7 @@ matrix. > Geometry identity with the PDF backend is a design invariant and will not > change; the API shape around it may still move in a minor release. > -> Six members of the otherwise-Stable **PDF backend** also carry `@Beta`. The +> Seven members of the otherwise-Stable **PDF backend** also carry `@Beta`. The > package is not Experimental โ€” these are: > `PdfFixedLayoutBackend.renderSections` / `writeSections`, the low-level seam > that concatenates several sections into one document, where @@ -67,12 +67,16 @@ matrix. > `CreationDate` / `ModDate` and derives the `/ID` from metadata so a document > renders byte-identically across runs. Determinism is off by default, and what > reproducible builds depend on is the *behaviour* โ€” it is the shape of the -> opt-in that may still move; and +> opt-in that may still move; > `PdfRenderEnvironment.letterSpacedFont` with its result record > `PdfRenderEnvironment.LetterSpacedFont`, the seam a render handler uses to > draw tracked text with the spacing in the glyph widths rather than in `Tc` > gaps. The glyph positions and text layer it produces are settled; the shape of -> the call โ€” a nullable result, a face bound to one size โ€” may still move. +> the call โ€” a nullable result, a face bound to one size โ€” may still move; and +> `PdfRenderEnvironment.writeTextLayer`, the seam a render handler uses to put +> the text a drawing stands for โ€” an inline emoji's character โ€” in the page's +> text layer as an invisible glyph. The text layer it writes is settled; the +> shape of the call may still move. ### What each tier promises From 8b64fb9fcc47dbe0cdd48e803810a98d1dcae6d9 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 18 Sep 2026 11:59:24 +0100 Subject: [PATCH 3/3] fix(render-pdf): close the text layer's state on failure; pin right-to-left lines PdfTextLayer.write wrote q, BT, the glyph, ET and Q in a straight line, so a failure after the q (setFont, showText, the stream itself) left the rest of the page inside the glyph's text object and saved state. ET and Q now sit in finally blocks. A call inside a caller's open text object already failed clean: PDFBox refuses the q before writing it, so nothing reaches the stream; that is now pinned, and the writeTextLayer Javadoc says both. Right-to-left lines, measured with Hebrew and Arabic: the emoji's glyph lies between the words it was written between and states its whole text. Reading such a line back, PDFBox's handleDirection reverses the run one UTF-16 unit at a time (surrogates swapped) and poppler reverses the code points of a ZWJ sequence or a U+FE0F pair; pdf.js and MuPDF keep the emoji whole. The capability matrix records it. PdfTextLayerTest +5 (13): a call inside an open text object throws IllegalStateException and the stream reads BT ET only; a Mockito spy failing showText still leaves the stream ending ET Q with balanced q/Q and BT/ET; Hebrew and Arabic RTL lines place the glyph between their words and state the rocket whole; a ZWJ sequence and a heart in an RTL line stay on one glyph each; a left-to-right sentence with a Hebrew word reads back in written order. Without the finally blocks, and with the glyph shifted off the icon, the matching tests fail. Verified: full CI reactor gate BUILD SUCCESS, 3259 tests, 0 failures; the four knowledge-pack checks and core javadoc pass; examples 87 tests green with the committed preview unchanged. --- CHANGELOG.md | 8 +- .../architecture/backend-capability-matrix.md | 2 +- .../fixed/pdf/PdfRenderEnvironment.java | 7 +- .../backend/fixed/pdf/PdfTextLayer.java | 33 +++-- .../backend/fixed/pdf/PdfTextLayerTest.java | 137 ++++++++++++++++++ 5 files changed, 170 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6610ff78..625cd65f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,8 +92,12 @@ follow semantic versioning; release dates are ISO 8601. `ToUnicode` in blocks of at most 100; keeps one font across the sections of a multi-section document; writes a 256-unit text but not a 257-unit one; gives a 2:1 icon after a word tracked in `Tc` a glyph exactly as wide as the icon, on the baseline, so neither the - horizontal scaling nor the spacing and rise left by the run before can go astray; and keeps - `deterministic(true)` output byte-identical. + horizontal scaling nor the spacing and rise left by the run before can go astray; keeps + `deterministic(true)` output byte-identical; fails a call made inside an open text object + with nothing written, and closes the glyph's text object and saved state when writing fails + half-way. In right-to-left Hebrew and Arabic lines it places the emoji's glyph between the + words it was written between and keeps a ZWJ sequence and a U+FE0F on one glyph each; a + left-to-right sentence with a Hebrew word reads back in written order. ## v2.4.0 โ€” 2026-09-14 diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index c45fc5e32..125c282a2 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -60,7 +60,7 @@ Payload records live in `core` under | Inline images (`ParagraphImageSpan`) | โœ… `PdfParagraphFragmentRenderHandler` | โœ… `PptxParagraphFragmentRenderHandler` | โŒ | | Inline vector shapes (`ParagraphShapeSpan`) | โœ… `PdfParagraphFragmentRenderHandler` | โš ๏ธ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius โ€” single-adjust preset) | โŒ | | Inline SVG (`ParagraphSvgSpan`) | โœ… `PdfParagraphFragmentRenderHandler` + `PdfPathPainter` | โš ๏ธ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` + `PptxInlineSvgRasterizer` (simple layers stay native; arbitrary clips, exact dash/cap/join styles, and off-viewBox art use a transparent PNG fallback; gradient paints use their primary colour) | โŒ | -| Text an inline icon stands for โ€” copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | โœ… `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF โ€” it replaces glyphs, and a drawing has none | โŒ the icon is drawn and its text is not written | โŒ inline SVG is not written | +| Text an inline icon stands for โ€” copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | โœ… `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF โ€” it replaces glyphs, and a drawing has none. In a right-to-left line the glyph sits between the words it was written between and states its whole text; reading such a line back, PDFBox reverses the emoji one UTF-16 unit at a time and poppler reverses the code points of a ZWJ sequence or a U+FE0F pair, while pdf.js and MuPDF keep it whole (measured; a reader-side reversal of the glyph's text) | โŒ the icon is drawn and its text is not written | โŒ inline SVG is not written | | Rectangle shape โ€” fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | โœ… `PdfShapeFragmentRenderHandler` | โš ๏ธ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners โ€” single-adjust `roundRect` preset โ€” until custom geometry lands; uniform radii and side borders exact) | โŒ | | Ellipse (`EllipseFragmentPayload`) | โœ… `PdfEllipseFragmentRenderHandler` | โœ… `PptxEllipseFragmentRenderHandler` | โŒ | | Line โ€” dash pattern, line cap (`LineFragmentPayload`) | โœ… `PdfLineFragmentRenderHandler` | โš ๏ธ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | โŒ | diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java index ee7750670..13112b533 100644 --- a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfRenderEnvironment.java @@ -160,7 +160,9 @@ PdfTrackedFontResources letterSpacedFonts() { * width, so extracted text reads it in place and a selection runs across the drawing. The * glyph rises one {@code height} from {@code baselineY}; write it on the surrounding line's * baseline, which keeps it in that line for readers that group text by position. Call it - * outside a text object; the graphics state is left as it was.

+ * outside a text object; the graphics state is left as it was, including when writing fails + * part-way โ€” the glyph's own text object and saved state are closed before the exception + * leaves.

* *

Experimental. The text layer this produces is settled; the shape of the call may * still change in a minor release.

@@ -174,7 +176,8 @@ PdfTrackedFontResources letterSpacedFonts() { * @param height height of the box in points * @throws IOException if the content stream cannot be written * @throws IllegalStateException if there is text to write and {@code stream} is inside a text - * object + * object; nothing is written to the stream then, and the + * caller's text object stays open * @since 2.5.0 */ @Beta diff --git a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java index a6059b878..c694206b4 100644 --- a/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java +++ b/render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayer.java @@ -90,19 +90,28 @@ void write(PDPageContentStream stream, // q/Q every word drawn after the glyph would be invisible too. In: spacing or rise left // by the run before (a letter-spaced word keeps its tracking in Tc) would stretch or // lift the glyph, so each is set here rather than inherited. + // Inside a caller's text object PDFBox refuses the q before writing it, so a misplaced + // call fails with nothing written. Past the q, the text object and the q are closed on + // every path: a failure half-way must not leave the rest of the page inside them. stream.saveGraphicsState(); - stream.beginText(); - stream.setFont(glyph.font(), height); - stream.setRenderingMode(RenderingMode.NEITHER); - stream.setCharacterSpacing(0); - stream.setWordSpacing(0); - stream.setTextRise(0); - // The glyph is one em wide, and an em is the font size: scale it to the box. - stream.setHorizontalScaling(100f * width / height); - stream.newLineAtOffset(x, baselineY); - stream.showText(glyph.shown()); - stream.endText(); - stream.restoreGraphicsState(); + try { + stream.beginText(); + try { + stream.setFont(glyph.font(), height); + stream.setRenderingMode(RenderingMode.NEITHER); + stream.setCharacterSpacing(0); + stream.setWordSpacing(0); + stream.setTextRise(0); + // The glyph is one em wide, and an em is the font size: scale it to the box. + stream.setHorizontalScaling(100f * width / height); + stream.newLineAtOffset(x, baselineY); + stream.showText(glyph.shown()); + } finally { + stream.endText(); + } + } finally { + stream.restoreGraphicsState(); + } } /** A code of one text-layer font, and the string that shows it. */ diff --git a/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java b/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java index 0fd49e13b..2f35b1261 100644 --- a/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java +++ b/render-pdf/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfTextLayerTest.java @@ -5,6 +5,7 @@ import com.demcha.compose.document.api.MultiSectionDocument; import com.demcha.compose.document.dsl.ParagraphBuilder; import com.demcha.compose.document.emoji.EmojiLibrary; +import com.demcha.compose.document.node.TextDirection; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentLetterSpacing; import com.demcha.compose.document.style.DocumentTextStyle; @@ -12,16 +13,20 @@ import com.demcha.compose.font.FontName; import com.demcha.testing.VisualTestOutputs; import org.apache.pdfbox.Loader; +import org.apache.pdfbox.contentstream.operator.Operator; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType3Font; import org.apache.pdfbox.pdmodel.graphics.state.PDTextState; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Vector; import org.junit.jupiter.api.Test; @@ -43,7 +48,11 @@ import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.within; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; /** * An inline icon that states text leaves that text in the page's text layer: copying the line @@ -194,6 +203,106 @@ void textLongerThanACMapCanStateIsNotWritten() throws Exception { assertThat(text(pdf)).contains(longest).doesNotContain("b"); } + @Test + void calledInsideATextObjectFailsWithNothingWritten() throws Exception { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(); + document.addPage(page); + PdfTextLayer layer = new PdfTextLayer(document); + try (PDPageContentStream stream = new PDPageContentStream(document, page)) { + stream.beginText(); + assertThatThrownBy(() -> layer.write(stream, ROCKET, 10, 10, 12, 12)) + .isInstanceOf(IllegalStateException.class); + // The caller's text object is still open, and closes as if nothing happened. + stream.endText(); + } + assertThat(operators(page)).containsExactly("BT", "ET"); + } + } + + @Test + void aFailureHalfWayStillClosesTheGlyphsTextObjectAndSavedState() throws Exception { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(); + document.addPage(page); + PdfTextLayer layer = new PdfTextLayer(document); + PDPageContentStream stream = spy(new PDPageContentStream(document, page)); + doThrow(new IOException("stream refused the glyph")).when(stream).showText(anyString()); + + assertThatThrownBy(() -> layer.write(stream, ROCKET, 10, 10, 12, 12)) + .isInstanceOf(IOException.class) + .hasMessage("stream refused the glyph"); + stream.close(); + + List operators = operators(page); + assertThat(operators).startsWith("q", "BT").endsWith("ET", "Q"); + assertThat(Collections.frequency(operators, "q")).isEqualTo(Collections.frequency(operators, "Q")); + assertThat(Collections.frequency(operators, "BT")).isEqualTo(Collections.frequency(operators, "ET")); + } + } + + /** + * Right-to-left lines. The emoji's glyph must sit between the words it was written + * between and state its whole text; both are what the file controls. + * + *

Reading the line back is the reader's job, and readers disagree: PDFBox's + * {@code PDFTextStripper.handleDirection} reverses a right-to-left run one UTF-16 unit at + * a time, so any emoji inside one comes back with its surrogates swapped, and poppler + * reverses the code points of a multi-code-point glyph (a ZWJ sequence, a U+FE0F); + * pdf.js and MuPDF keep the glyph's text whole. So these hold the glyph, not a line.

+ */ + @Test + void anEmojiInARightToLeftLineSitsBetweenItsWordsAndStatesItselfWhole() throws Exception { + DocumentTextStyle hebrew = DocumentTextStyle.builder().fontName(FontName.DAVID_LIBRE).size(18).build(); + DocumentTextStyle arabic = DocumentTextStyle.builder().fontName(FontName.AMIRI).size(18).build(); + // Words with no letter in common, so every glyph belongs to exactly one of them. + assertEmojiBetween("ืฉืœื•ื", "ื—ื‘ืจ", hebrew); + assertEmojiBetween("ุณู„ุงู…", "ูƒุชุจ", arabic); + } + + @Test + void zwjSequenceAndSelectorInARightToLeftLineStayOnOneGlyphEach() throws Exception { + DocumentTextStyle hebrew = DocumentTextStyle.builder().fontName(FontName.DAVID_LIBRE).size(18).build(); + byte[] pdf = render(p -> p.inlineEmoji(":woman_technologist:", 18).inlineText(" ืฉืœื•ื ", hebrew) + .inlineEmoji(":heart:", 18).direction(TextDirection.RTL)); + + assertThat(glyphs(pdf).stream().filter(Glyph::textLayer).map(Glyph::unicode)) + .containsExactlyInAnyOrder(WOMAN_TECHNOLOGIST, RED_HEART); + } + + @Test + void aMixedLineWithARightToLeftWordReadsBackInWrittenOrder() throws Exception { + // A left-to-right sentence with a Hebrew word: the emoji resolves left-to-right, and + // PDFBox reads the whole line back exactly as written. + DocumentTextStyle hebrew = DocumentTextStyle.builder().fontName(FontName.DAVID_LIBRE).size(18).build(); + byte[] pdf = render(p -> p.inlineText("Deploy ").inlineText("ืฉืœื•ื", hebrew).inlineText(" ") + .inlineEmoji(":rocket:", 18).inlineText(" done")); + + assertThat(text(pdf)).contains("Deploy ืฉืœื•ื " + ROCKET + " done"); + } + + private static void assertEmojiBetween(String first, String second, DocumentTextStyle style) + throws Exception { + byte[] pdf = render(p -> p.inlineText(first + " ", style).inlineEmoji(":rocket:", 18) + .inlineText(" " + second, style).direction(TextDirection.RTL)); + + List glyphs = glyphs(pdf); + List textLayer = glyphs.stream().filter(Glyph::textLayer).toList(); + assertThat(textLayer).hasSize(1); + Glyph emoji = textLayer.get(0); + double firstLeft = glyphs.stream().filter(g -> first.contains(g.unicode())) + .mapToDouble(Glyph::x).min().orElseThrow(); + double secondRight = glyphs.stream().filter(g -> second.contains(g.unicode())) + .mapToDouble(g -> g.x() + g.width()).max().orElseThrow(); + + assertThat(emoji.unicode()).isEqualTo(ROCKET); + // Drawn right to left: the second word is leftmost, the first rightmost. + assertThat(emoji.x()).as("%s: emoji starts right of the second word", first) + .isGreaterThanOrEqualTo(secondRight - 0.5); + assertThat(emoji.x() + emoji.width()).as("%s: emoji ends left of the first word", first) + .isLessThanOrEqualTo(firstLeft + 0.5); + } + private static DocumentSession emojiSection(String label) { DocumentSession section = GraphCompose.document() .pageSize(240, 200) @@ -225,6 +334,34 @@ private static byte[] renderDeterministic() throws Exception { } } + /** One glyph as PDFBox positions it, before any reordering of the line. */ + private record Glyph(String unicode, double x, double width, boolean textLayer) { + } + + private static List glyphs(byte[] pdf) throws IOException { + List glyphs = new ArrayList<>(); + try (PDDocument document = Loader.loadPDF(pdf)) { + new PDFTextStripper() { + @Override + protected void processTextPosition(TextPosition text) { + glyphs.add(new Glyph(text.getUnicode(), text.getXDirAdj(), text.getWidthDirAdj(), + text.getFont() instanceof PDType3Font)); + } + }.getText(document); + } + return glyphs; + } + + private static List operators(PDPage page) throws IOException { + List names = new ArrayList<>(); + for (Object token : new PDFStreamParser(page).parse()) { + if (token instanceof Operator operator) { + names.add(operator.getName()); + } + } + return names; + } + /** Records, for every text-layer glyph shown, its advance and rise under the text state it met. */ private static final class TextLayerProbe extends PDFTextStripper { private final List advances = new ArrayList<>();