From e569dd59a02ec1c39e6183dba30d1b3821857e00 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 21 Jan 2026 10:44:22 +0100 Subject: [PATCH] Misc webp performance work MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit *- Dynamic pool sizing for WebP and Katex based on CPU count - WebP decode: use RGB (3 bytes) for opaque images instead of RGBA (4 bytes) - Add new imaging.config with new encoder method option; this is default 4 (what we had before), if have tested with value 2 for a 30% speedup on my MacBook, but that does sacrifice quality in some cases (quality/speed trade-off 0-6) - Track hasAlpha in decode response to enable RGB optimization - Scale image processing workers (1->2) on high-core machines - Add WebP benchmark test On my MacBook Pro M1 Pro, I now get these numbers compared to Hugo v0.152.0: ``` goos: darwin goarch: arm64 pkg: github.com/gohugoio/hugo/internal/warpc cpu: Apple M1 Pro │ cmp152.bench │ fix-webpbench.bench │ │ sec/op │ sec/op vs base │ Webp-10 162.6m ± 32% 145.3m ± 21% -10.60% (p=0.026 n=6) │ cmp152.bench │ fix-webpbench.bench │ │ B/op │ B/op vs base │ Webp-10 41.69Mi ± 0% 123.79Mi ± 0% +196.92% (p=0.002 n=6) │ cmp152.bench │ fix-webpbench.bench │ │ allocs/op │ allocs/op vs base │ Webp-10 9.963k ± 3% 18.797k ± 2% +88.67% (p=0.002 n=6) ``` For general image resize benchmarks compared to Hugo v0.152.0 (note that that's not the result of this branch): ```bash goos: darwin goarch: arm64 pkg: github.com/gohugoio/hugo/resources/images cpu: Apple M1 Pro │ cmp152.bench │ fix-webpbench.bench │ │ sec/op │ sec/op vs base │ ImageResize-10 84.12m ± ∞ ¹ 40.70m ± ∞ ¹ -51.61% (p=0.029 n=4) ¹ need >= 6 samples for confidence interval at level 0.95 │ cmp152.bench │ fix-webpbench.bench │ │ B/op │ B/op vs base │ ImageResize-10 32.37Mi ± ∞ ¹ 29.25Mi ± ∞ ¹ -9.64% (p=0.029 n=4) ¹ need >= 6 samples for confidence interval at level 0.95 │ cmp152.bench │ fix-webpbench.bench │ │ allocs/op │ allocs/op vs base │ ImageResize-10 11.34k ± ∞ ¹ 12.03k ± ∞ ¹ +6.07% (p=0.029 n=4) ¹ need >= 6 samples for confidence interval at level 0.95 ``` Closes #14370 --- hugolib/site.go | 14 ++++- internal/warpc/genwebp/webp.c | 29 +++++++++- internal/warpc/wasm/webp.wasm | Bin 686191 -> 686647 bytes internal/warpc/webp.go | 40 +++++++++++--- internal/warpc/webp_integration_test.go | 53 ++++++++++++++++++ resources/image.go | 16 +++--- resources/images/codec.go | 2 + resources/images/config.go | 67 ++++++++++++++++++++--- resources/images/image.go | 4 +- resources/images/imagetesting/testing.go | 2 +- resources/images/overlay.go | 1 + 11 files changed, 200 insertions(+), 28 deletions(-) diff --git a/hugolib/site.go b/hugolib/site.go index 2f04bd4a8..0e3660c86 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -201,6 +201,15 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { rolesSorted := cfg.Configs.Base.Roles.Config.Sorted versionsSorted := cfg.Configs.Base.Versions.Config.Sorted + var ( + poolSizeKatex = 2 + poolSizeWebP = 1 + ) + if n := config.GetNumWorkerMultiplier(); n > 1 { + poolSizeKatex = min(n, 8) + poolSizeWebP = max(2, n/2) + } + var logger loggers.Logger if cfg.TestLogger != nil { logger = cfg.TestLogger @@ -261,15 +270,14 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { warpc.Options{ CompilationCacheDir: compilationCacheDir, - // Katex is relatively slow. - PoolSize: 8, + PoolSize: poolSizeKatex, Infof: logger.InfoCommand("katex").Logf, Warnf: logger.WarnCommand("katex").Logf, }, // WebP options. warpc.Options{ CompilationCacheDir: compilationCacheDir, - PoolSize: 1, + PoolSize: poolSizeWebP, Memory: 384, // 384 MiB (4096 MiB Max) Infof: logger.InfoCommand("webp").Logf, Warnf: logger.WarnCommand("webp").Logf, diff --git a/internal/warpc/genwebp/webp.c b/internal/warpc/genwebp/webp.c index 2a51fa07f..ad9d32a28 100644 --- a/internal/warpc/genwebp/webp.c +++ b/internal/warpc/genwebp/webp.c @@ -68,6 +68,7 @@ typedef struct int loopCount; int frameCount; int *frameDurations; + bool hasAlpha; } InputParams; @@ -79,6 +80,7 @@ typedef struct int preset; // preset to use; resolved from hint. bool useSharpYuv; // use sharp YUV for better quality. + int method; // quality/speed trade-off (0=fast, 6=slower-better). Default is 2. } InputOptions; @@ -316,6 +318,7 @@ static uint8_t initEncoderConfig(WebPConfig *config, InputOptions opts) } config->use_sharp_yuv = opts.useSharpYuv ? 1 : 0; + config->method = opts.method; return 1; } @@ -410,6 +413,11 @@ InputMessage parse_input_message(const char *line) msg.data.options.hint[sizeof(msg.data.options.hint) - 1] = '\0'; } msg.data.options.useSharpYuv = json_object_get_number(options_object, "useSharpYuv") != 0; + msg.data.options.method = (int)json_object_get_number(options_object, "method"); + if (msg.data.options.method < 0 || msg.data.options.method > 6) + { + msg.data.options.method = 2; // default + } if (msg.data.options.quality < 0 || msg.data.options.quality > 100) { msg.data.options.quality = 75; // default @@ -493,6 +501,7 @@ void write_output_message(const OutputMessage *msg) json_object_set_number(params_object, "width", msg->data.params.width); json_object_set_number(params_object, "height", msg->data.params.height); json_object_set_number(params_object, "stride", msg->data.params.stride); + json_object_set_boolean(params_object, "hasAlpha", msg->data.params.hasAlpha); if (msg->data.params.frameDurations != NULL) { JSON_Value *durations_value = json_value_init_array(); @@ -611,6 +620,7 @@ void handle_commands(FILE *stream) output.data.params.stride = anim_info.canvas_width * 4; output.data.params.frameCount = anim_info.frame_count; output.data.params.loopCount = anim_info.loop_count; + output.data.params.hasAlpha = true; // Animated WebP always decoded as RGBA int *frameDurations = malloc(sizeof(int) * anim_info.frame_count); if (frameDurations == NULL) @@ -680,7 +690,22 @@ void handle_commands(FILE *stream) } else { - uint8_t *output_buffer = WebPDecodeRGBA(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height); + uint8_t *output_buffer; + int bytesPerPixel; + + output.data.params.hasAlpha = config.input.has_alpha; + + if (config.input.has_alpha) + { + output_buffer = WebPDecodeRGBA(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height); + bytesPerPixel = 4; + } + else + { + output_buffer = WebPDecodeRGB(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height); + bytesPerPixel = 3; + } + if (output_buffer == NULL) { strncpy(output.header.err, "Failed to decode WebP", sizeof(output.header.err) - 1); @@ -688,7 +713,7 @@ void handle_commands(FILE *stream) goto cleanup; } - output.data.params.stride = output.data.params.width * 4; + output.data.params.stride = output.data.params.width * bytesPerPixel; write_output_message(&output); diff --git a/internal/warpc/wasm/webp.wasm b/internal/warpc/wasm/webp.wasm index 14c4ff841ca053e062ab48eb87ed1826692e51ec..a464f0d13c276e77cc9533efd99dfb7d6ca09518 100755 GIT binary patch delta 32213 zcmd75d3+Sb);Hc&-91Yt%Y+0Hl0bTfEo`#LzDOEb1Vr5T1+N%s(#k|1Dk z&_YFxhzhb84bovVLAHQFf(Aqh5EUg+(0D0>;gi(2PMtb+ z>eP0sy1MgyLuc!B=i)Mr6_%81Tbb*R=Td)Hi>~V&eB~Wec(b@S@Tgh)Gtc76NG+R? z$@+1{TwJBn55X!xg+L`hZmveK0? zncI7^Uu_>y4Arbk%rRPfkEmKUB6*G+Unhi(2-6UrMwpJU$eCkm(|v8t+#^y(B52#Z zIlfGUd0LLI1;t}?e67qzyO1Q6#BGYr@yD5pxkzvFgv#ge`hb}+EyvFc1}mFTWyW+gfYj8dL3NhnQw$CA z4ToQ<4~3)h29OyU4ebGwO+!s6Lx@l6#7!foy<#{}dB@}&QyHWv95(K*R3$tSP?ijTq2-pPIinUJL)j|IB}tQ3&-AaFEn%FE68$>zgjy20|OiGzx@(%+gh5|fV!0@0px(TytD8_s~&Fpm+A9N&T zJfuD`gV9DVpF%1D*e1 z-aK>-m}8JKhoeEwTn{*GQD#2R0p$!H%`UJm5d}JJBRS3TnCRl?B-rIOYO?%O0hb+b zvg5yb?P#tplaGZ`^K>3SMIE8ak_Kj8jCZz%K7o$n@^h*lbs3+7XQKgNW9kQf7kCV9 z6zb$c21D~chW>)8z)aMU&>R~k;;DIL1I~j? zW@d&P@VY_DN{y=hoh!n!O0oR7 z_v5FL5S$v98qhI29$bW(HJ?P7kPPGM-IY}udN;WH=a{ehJ1a_mh4S{yF_$#Qj2v^G z|94pHcaKc<=9o>ssQ8IU1o}h9@P?1(zxv6CHN7vB6@~3jBxz_o2(FDY9o|{8Og)$u zpWa$Gu>zb3rG6f7>XFn-;!~it9!^F52zm^UHJz+=RrDwuR*>^Z_L#PsdOEdP~i zdTB5=q?u_ZGm`A_YGAEc>_oK@E^+!f^$Qx?hHB6fVkDW1+Zd|%SZHi>!!riWOw!QI z#AaqsHfZUxGxS8jSNB&3AY-l%sXcS-yhbO?PV<<(6g06MS|Ebm6WRs9-;Xyk5 zf(9G&8U1(@U>0BHlxkP^%ArZ)W=!U+Kgay0(h6MN3*$h+8bzex8oRHfDEB z3+d3tZDd>mGiMm?$-V@bHJsmLB#?Skipxk1?rWJANJ-Q%`ZQA+iw5CDo{~eRlvP64 zCpBPi_%fPd^LnYj%bFxZeYyO~N@FJSX`zaPS=Q8W{gpJb3j_u<_@edz0!0dhA7ymF z9E#>L+V?~EGh)pFXe|5+%_*^%y%6u&9M7V7R&yM)k`q%?gddZV6EjmJjycH*U%N98 z$Vn=e{5dCng&7QI2>nTaRH#3a7ie1sztN|--5;4R4$TbG5>CSeeTYVbAH$cdjz~UD zK(1h14XKS7 zQpn?l-8Jx_!|{>+Seo11=o_2{78*N-68@A5(f>X3|AyBt2AV|F(u~v`Utg+(YqH;q zo`f2>j1-z0b3CYA0%j;wi!7~0WkOxF;0vkgo&02{)0p8g2U3TjEtL~a0@k%S+N32T zBTdDJQ@dhDm8&>SxaF}vPz_%{ej_am{rI(pI=p)J>gPXxec$W7`m2}PGdXq!zVD6(X{ShVOH!F0<(2Fd}nNocy@4V>lw@w z{Hk?#pTpkiDHJTmrV?Wgn{kXNIP}>38jj%kY3(n>+LLUgqXOyD}n0^Vzw~% zwm55bjkr;8pp+@LqjXNws96EIu4loInnHVr|FWSlk6_A>ZDu!=7HXtc=H3L;8FFo< z)8`}q4ePZJ8wVe=y)fBQ_C7IqX?p8`7HTQX6Mkn@tGPlA;DTF+=^kz7L=z@se42`P z1X{#_wnFu=xKMMM%!j={wG_4vnEhoFl8+x+$HCmfd@Md>C|Uz;Kl2W_l;mbopS_eE z9VCQ&QS`f3=Htm`Y;={3OtV3uS;(##^~ehq<=Nrh&)_SdlaU;pL#SQg!rC;RZL|$e z?A|)C>JXh2^y4nnW3|L3130Wn8#=j&Ly7K6KB`kn?lT0<1HM*zG@BAJZ-G*p=!3JA z#n&dn%RpF>=*K`Hv@Ox+rLa5^hEPp%hoyx~atBaJZ$HCfy9VSh()d8%cMQ+Pzgl8J zTsq(A2nWe&<{_s!=xvh~kbV%B^nUP_nL!4}vW@L{uG3y}ZI4Da3B&x3;|MGj4*ZJv zt5_-=%}a&!UmeX$1)azsotBD_JByXV5m_m)-WrZj5@sU<{R)8>6=%c$4c&hZRUkQ5 z#j*x33kopWZZKI*nnAd97@&1BPO*q58O|ge157k=NTlsI)r$O8RNfJbexTD%#-P)X z06A*g+F5jHFQJ}M7B#TjPx#$v~OmZDUK$0XgI*s4oWq`y0J*;75kb-!T4KA2Z95t_bY^4DHlU=Jl%x@XO4!zr6`pKn zk|RXbZ5ydB4H&a6wX3YI5*D;O6O%8bK&N6JKwpw;F|25=lAAtmzKou7k%7I?jkL5O z1v3a8x0$snwEged=jn3eMiTx?HV$1v!l?^xf}yAnmXg7sL@{~J4sWgOqz)Xed@A=( z4ujhWsj$Kc6`dmNAJk!IVn;YmAz^U``3tgFEz*23XdP+-ZGx;=(;or3`p28GKCYsu zk^Tg#atw)GOpLLU;eVm?NI&4LG}nJf)BYq4l_IH80=XH$=`5UmOnn+mOQ$h76`;9J zBekVYQlCG|3#XbJan!-%WIkpFFv3w)*+;$o)hb}JZw1k?T$WQDX01^XN{Ol1NMgWD zCI^z{Vm>~dM$0BJ;&8yMRA|QxLMcp6S~2fh8OQ$Ij?<{9A@SJFo*?nZ963Y-tB=!M z%)F~rnseyXMsAt8+&P+6X=Wytn}9sraL}fcCt$+-2!z zm#`vI2U@VG*}{6u-W!^{<7n}N(d^R>d%}j$TxSl}iWus{apq1rn|sY;ikaWii1XH~ zbclshk<&QYwpl(2crnGm^dTOaV8A91heI1!@HaTn^a!`vr4c;lpx?EQ$)%_qn_T*8 z6ZgKQ%C%0WY=Z-YZs+h86Oniom<9n3RI!a;b?nj8U@o4H{ASVP^t{9G^lqTJz%d>@1W7Z@I%Qfqa&Xf%9-9F4 z?-}N@CpfKLx|xbDnKQyKGh(Gx$((RRkVfkZ9cI0!(rJl29>N(tjW(oud#reX9#-P4j73T5*_U(%}2;yTqNL zTJV?lLj%}3L!W+14CDZ5gOzy9=1%mxbt>sWt1HS<{214mJWLEaq{nzm)ewcO+=zz- zqvK#nUo6zCQp4wEG2Y!OPSMe&XZqyXflj^6WyruwL4cmA(5e%zSOWaxfRB--WEJQa zww(%Gl>oNJu{rW6(sq188h|$eUYp-VZ1L3X$S=g=3jc`uo{YH+Bol{xphHC%@rbp9 z6Shq@Elx~GOQOb_m69|D{R&&Q;58lFGcEW?$Bun)O4D75$4(8dOm`)L6qrWKIHFi| zWh8^LU~A{4r#@cnn1Tm7j?0MmE>Qh$RKab;dw0UJbkt5Nd40*jp`AKAl4;~72d8vO z^CX+z2L_=T6eBsfu#-OkdDPB0TA)UT%l9beD>^5jmOv(9J$D zYqT}xnL9mgqHYNDDBcfL5*3z!=@^HtG{wQsv?p2J4ED}S!f9j;rjb#|iUQH%n>gw& zS42cgxgWCR_L){hk}P-7NIJ&pM9W?cnb|}>%QPP?hD=2>g%(2>Nvxw4IkXsR&5NO( zhHeVq&|dM*Nu;#UYIsJPoDJ%@Tn+66Y#3?&jK^^W7QWh^6hjgOxK-y+6x51inEiZz)cc?7XtzY`bokj_Hcti~SL8SEYJGsFyIz zP#*7>=tFc2?qeAKI4#$Lxm|{1$ZYM>C7@x~Z&xJl!l~-J zNVBDq&Q&VSZYX&aJyL^n-w$Vuapw9K@ZRl3YhDWCaNX8z1F#Tr82|yw?cz8MwS5cl&P#jfP&^WY5l6ypQY;b@f z_moA!p53|xRBG%xaykCO^}uRRTQ@Y(3EJ;Q4Py*i+tpC8GbYCbZ7FxxvZXQeZH6sA zHRxPG>fYp}u7o`yOv7hjW9Y*piew`Rvj(0V(H6m$N3Mq~6!xc`O2i*Q6{~xZgMvOD zOzfUKSf>2P4KPl6i2aNWunNWH2KdVV%?217UU{gG4KSx7upySN7g@DWvsJO0ZXyQ` zb}J$)!vf^k(N0b}dd=1i8t1iD;h+Z1^XQ=!GL{n~GM0wIwG`qZRxv_4#jq`dDdPuP z_R&8XOh-#q#Cqtv(rA9ge!i9arw21IXD_Nu^NCCVr)8)WX)Cpawxo6Rc^TD)9L?qf zGb|(MXHn-cq?3JE){1(53p1CSCt{FI_WW>p)EirvJ7c1ci~@M{hT61_#m2UU`TIFf z<^F;!M-%!eor{HzqL!RfV+_ftnxdkpIayg&G}JtS|6Ef#aZ;jdDo>nXYzMW1Z0vk& z@jZyi2B1R`S|G_FRC@SUjC99T+?T<8VOGgoHFSESW(Ozs>>@m`>1lCUybl@=`zxMp z)&}41*(Zb3^BGJp2A{l&^uP+8*g{DohJ!)Y>pwEo1uyH>C-bi)B;eF|z9&Ao(9t@$ zxmVK7a;7Io)4n?a&K2;6tOeDgOi#d38@!$DY~j(T~`WwsIyTw%dTlI}3P zVn|m;V{Ib0=47I*8uHE~w-}mXzmIzuk-Pz9#+4)F!EoC_W;O6dsC>8kIg0?;i%G9xH93vF2SW(zBp1RIH+$| zteAK9?a)Fq6VT07GXY!W7)-xNe6erlHFA`wSTWFf$-$C(>)_PngAF=Ou}4$0N-zc< ztKp84LA4_eY>wz`8lwU`20FLVf=T__r{(MPoQN!j8zxTK9LHn>?D{4~#v;*cZXeFWwjXTv~U9u8x681bd8c+e}bF@LiD< zX;eO3zCymg43>Q6`X$Msn}neiH#-Gu2Rv8CHG(Zsmh9j z6K-smvFQ}IN1l?U;5u5-=Ua5@p77etijoC4&WUH$!B&%hkNw)81L5$;-R74inNv17 zS#e3tef4pyp=8IbEe@X77%a#i#2SLz@`vNMvmg^GoeCCkR#&p_$x9il3p(d_VCBK? z^ZT)F!Q1C|;<=5%`SWiB=?6*5TF?cgk}DTn&jX7aY0%?1a@lDDbfc%TiQ{Sw4XB&(16m|e5zsRdEdhE~qQyW$qD4S;pun_&uYy=o1n;z2oV+s zJ*zLlV84E~fo6Spb;mRFP=7ch4-pmzx2+xuZoa0YzUUWjE(vyDaC(5{L=}o9duhoN?@nQ?DtKz$yR52Yg>?yIyMm3S zSF&9tLIX!YN-RF+_5|CZ^ZFri2gW@1GFONe+FsW`3 zeqT~|1%5BBBhHFC%GDr$r`8j{S>Fx6@2T&_wwDyuW3_82S^rIY#tMT+zHQHbD;a+> zmIV_|#ZHl>F9ougYF{cQV7j{o?P8 z-OqlrzGPT-;-qn~*yghwvqE@XEHyba@hE1kbiv@}n#aTt7xU>Ln2W{VTx>Th7v0@# zAcDKxEE~acJE(WFT=ureiDARoUh!rO8_p`NlQC>+5~~z9w`Ete@5H-p*`=U5GTBEA z)K3jIm%U`&?qdNrE4GexVMpWGF|lm`>%f+XZw9cVi0vN8`XKmYARCEbh z*bfN)IhgfEuxc>tj^MMwtQUgVA#5muu|rr71P={iT@buB1S%i1jt*geF!sARdI2kD zRibbh3$kj_KAVkTC&k^_;HeU?W;-G? zMJz?nu9G+XF^L%15zvVd#~JeffG9p6&$3vZh`EG~YjeUDhCve+X5h&gh7a6?6Jq)$ zta~8B;_=K!qXx`Kqvb{>Y4FekJgJA7N#Han27&xWZu$r`fZ`|62+)o|6F_?cx!~zQ zAP=A;fq4L(2oxp(Wf58g(3wE)X{*-+_OvsQC(94%LyH#XSF;PTECFv<0*gWEMgX&> z*_{B*kmpGrfF1;j0D2NA2Ixg#89;9WnCQ&&36ui#A%L0F>`P$VF9B}$BUAxWe*%>N z0|=}I7)W3hz#syf00t8{2+eTfg}ooZPy!VI7ZBJ6FpNMsKsmMr7z{vLB&r2^U!oeI zt%L$*HQ+W$tOVLFQ4!D%iIxHFlxR6ng+!}>c1ctWv|FOJKp#j{3REdk1JE9cP6K@? zQ6tb^iJE|_6ltnlRBWFl<^g>qQ9jVe5>)|J+o`89o~>!uvfa*}k!28bWA-~u= ziDeYZ4&#GoV*}01w?VAANc?as+yB=bG3Fxci@z~7BE%II;tmT*01LaF-ND<1%5jES zTw#_tbCF%3TO7WNwH6oO#pYW*?qV0{tkAk=3j58CFy{dlAIk~_;)eCyY<8VDBcyMF z8Ip`i)RyB#nejn3l4pbnDPa~ZTsUcd?=$Qj7IyP6Dr9CFF->To{jz}?feuJ?8t4;= zu%?;^B{~80sYG=^pGkBK=#WIUKoUT(+{t-7?{kd>?Ef6iNJmswkznH!dG4>``{isR z&->N7Yy}gX66i(K)#z6nHY2^)ant7NcBhbFRhVTpkF*wm!rd z5$`#qoiRr)u}vL-t(W}9@$`d+_2LLx1aw%^3W1JDG!N*gMEO9q66FDXE>SMf7ZNr7 z%FQn&Y6Lna(P^NsBx(TqTA~v`f#VX^0oF-$45(hBS|D4k8lZ0^?O+5|1AQxLRX`^s zssuVIQ3cR<5^V$eUZQfK28lKS{UA|k0PtTDt_3c#5GbS}P)I|dkb*!V1%W~e0)-R=3MmK_ zQV_^iD-WtRN~LQ-`$Z!3QuEj05~2&6ze!XG)Fcr)o%y>&=vwB#B|3)8e@K)ITCUu! zEV0(CW1qK3Z1|1a&p@$j$Gu1Mv{kf=ecr!$a4(Mx?jr239=9_CJt5J&2rVx{TMjfw zrY-}@muN9ifkcZUsfCf$e4x29H5cefpm;2nk-M4NzKlBn3~*Os4hVS?<^iQmBIHCl5z@nkrw zVo4VN+qI!G9A@E@Ds8P1Uyvy-g z0M@QTadl@t5OY*vXa015J99`j&7=RF4C}fse6OAX;WVqbXxG%j@MF(}Cx63?8_gJV zi5St7{|aYxSTEkv)66(Q+}fMB77zB~6-YFD^OkIhbxm)6FKY>j^$8$;|{#LD9-mXFw>0HVdQ6wO8S1B%5^tZ|k)d=tf76l*|iHDz>AtPZi45v$cG zR*P5=u|kz%2NBCh3~#R>R)rWMxjiX@s?#GdoH@fB^mGL-h1Ys_0N*4ncHdxr8QUbb z4CQIo(ZPHv_WF(Dv7vmZP5dD|)Ev~~VMA*F{|iG6Pm554c}>=^Vf;ew$23FT0NVmQ zCZM-QY4eLYVgNS3cqog-QY>=?E!GOb7AHg}i5C zxGr?vRn|=m8o@K5^)(}S-*Z!+g$ISiHY2%D92&us{}acy!C~Qab`oW{96a$)#0}!J zsZQJy2-8H-DBc<+Z5YMdjE_Zz7UyJuN6dFv5j*)db(=}(ja3^!i2kv5=Xq38rB66mr-m@J+f zjq4UL!)V?kIZB`>!tncOo|+m?q3n3?w>f+Fi?|=zFTIHO8XjVrV#I_qhwX)2PUu6o zy3b6aglKVFFXC6(jgleyj^TGinjg1wLfvQO7}-NA#_%4I)JW%Q?mij99Ltj;9L@Rf z7|XkN2-ioQLic{ou#f{fxos<3V|iO>b6{+gZIB~UnrJ;v)_%x1-Ya6fXpv&pI6jJ1 zSry~>Y>pF?ahLG(a18R$CHw^I9a)4+BePfaY&_SMQ~yDrS0t(dS}IYgwfItgElaEn z@6X`(8t{871iPHKV{1i+%Xwy3>~LxR!J;Np@kof%hMH(jx-F1Ec_g(|+Ap#PsR;!PNv@mv|Wjuw*fA~OG3 zDRiD^m-6uyd_2_gT`3D2Vh4yN`?=}D|8q-5R@F|TT*>}W&KZ1b%96*VF?EG(NtQoIPOG!krSl{V2{7Xq&TewdIxBkte~N>#s*sOzAxwFWf02lu z`RkkfK?L`$33WC|F^yU%lTg8(+ zRoM9>v*(ZGhlRO{pAPn%F~S~JVVlJ&e#6fZ5$cs^V% zzF5x(oXwlH;VfRfYC*jBY&e&9?b*E0jd*|k6W;8NXUT=W;I*FE$P@4|LOi>PZ^Sw9 z<(v6qh#lC>Kga1q>3jThl{Jdz-{;w^$vX5tk70vz?IFaJRsB^w);AKhnMMjsSiA|3 z|02UHaf&>sbEm>HF4c; zerr9-m>@i8%em;iD~v&W5x2wRpq0J;?LIb{Vet zV(=4ZWZ`IsQb?adEARm?W8UU2)|##?m0OqW;UiefhIJto@T>@4Gx{HVXzPs+G2tfU zhXvzFy=HzPs`m1|XAzGPF;(oS;=7|#oM8W2yu6Qp5k1k7KBAMxIdt+KX$%r9W< zwBY-B$E=VNejV2n=_aIuMrp)2@S1TZi5b66T)Ll6Z=Oo&nmr|<570x3Pxte+y(4Yx z!^y)kc0N=5NyEQUX2R#7>uqKvebsQ+6OZ`g(KpHt@J=b;oz0u85wrOH0M^q6;roOq zqnWM#pYS+5ixDFa^4rgC)b$5>`i+t1@U=5q^ zU}_oLz>#&wM=j7G{(gv0iMF@R2}_c_8xHZmw{PwvwD95_Y#AQ8V2h|s!FuDuHb({L ztQy|(rU<7b^Reh^^i)tCn+~3@o3qIm^^(n8ysUb5mUOf39KJ%yXPv6yDod&f_hww< zwg=P+;XZ=lctUhKf|ccj7;Oi4*})t;c*72MB1kw9u>tks2Jzz&j+;>;{V0waio{h% z;pP>I2alpx6p1BAkya$$w}U!6h_98zKDB%_X0k_W`G;x%4?oD2QslK77zVGpL`HI^ zLH>@5j@z4whI}8(K7?krb|0L`!Ig_uauOsVU7O+8ekxW#+1q zS=1iGsl_qtr(^spnu}|`=I~KPyW@O#TUt0VdVGn7V-^Of1to`cv z?eKVBsKfSipRg!aB{J&yiwHK<^BD+we8UA+!kypniBv|`3BEG^Sh$_&6{{2C^Aqr{ zPds~4HdNb_)KJz1C;4R>j9d5vZ<$jRncne!wpl^*gc+gvoTw@@^Rc)^Q|Xm9@>ipc zk|+&$wpm+91KKSh68A%NpuisvqUvO?@3e(R3_0epmK@ofVN0<0_c5-8i2O4egPa% zod(<{iH$(pC29iNAyMvJ)!ZpjB~XP#6%n)zXqTjw1MQY*6VL||l>${tv=(TOM8!ZK zO0)_nuvfz6fK?JL1KKB15zt2xEdlyiqQyY;^%6XI1lliAA&{-sJRCFHC_jSofNTYG zfouhvki%B6F#u?5cpAvoumQ-{@C1;pU>%UH;4vUu!CD|&!5Sc2!Gl1yg4IB_f>l7a zT4?N0OVeqSXQs-8tBvYs#iDB)^)Wm{f2)l;nhh=9j!(R(@)nE3MAOpzL*FCN9>HrH z^!2Rq7|?{&8D@z05!{2qEf_p|5a(yA-QuG}5bf`oYWMz8G`uPi5$kjDnOGKVscMu= zI;l{Rd0(bFB3hsZT02AS;SC8i_@naaO^x`Qp&mV(bAnIZdp50`U!5FHLoLN?e)Xql z8dMZ3+NlT5rp<4EW?rXRV>vpg@5aE2n|i1>B52)H9R_1w*He8!kIs(I?TN#E>n zqn~;eOTfq>L(3gY7KUlCzuF6v+4lab3m1~ahXd8Y2x11Qy%7wxgQ(L3se0s!RFi#k%zH~P8ZzIoPwjlW>9?eK< zeRYvKAesghtgd6!HVg(IGge)zBmDOSb#*M-VbzUl7gk|?a-+IPXH8AvUhT;ii8r2C+kyDO z^XeAN^7CI%@ybl0IJ{i#ifbb773x!Lr}f+l6*tK$tbeXiUuCR9v?)eks}vK8)eA1G zmMa2X;)WAb=f<5B^I(_+160}AK?e*q5jx$Y278rckFfaQkYRWnDOT@G;<)oai`_f)HCY?D<`tqzP~$Hdkn>TU!Jj>_P=T6Gy>|E^Wn zVnCOCuD-+?t;@f_5`P_*Pcl_3-X_Bi9|s7zx9im;_;TCp)gdea>pK;PqrXHW+dO7@zs2N|jP;+WUZUb^dXB#nTp1*_ z6gBl~q3C>46|7urIjMF;|ExQy8VS)kLd+G`(eKpj>F(B`2DN`S{0x$bvnt%7H`3tn zV*&Xe5UsZx)F&@MCm-5FJH_A<_3EWHGH>x=c^l9dgSib`kzz6Md@V1E>XcM#&-vQ$ zBzR2tOE`!qP47hkMRaIvMwM?}1np?Cq@vob;E+DnJMe7hI-E@m2<>`XN%CmiyH@L^WsXT;hZAo-mM*vEB?|p zI2{t#ikUfD-$@}2(6D%cmA;18tQ8K+5kK=xZc0v$pO&ICsbD0(V46Setb{3$i!T(? zaa{UjEe%_Pev`Ea;x$@&R9Q74_gZ^ zx43tTHYILHlxjcyzoMG;(%&^)$-ynfd$eEJQgQrVtp|Heq)gTNuodFssah)f`yErY zDPZ|%s&*?KR`s2xb>w-EiJR=udVHF8g^oV`=>r;d?bLs06A(=LhjtOh<=g+zo^IP| zlUyaDwiwr$#d5ntkE>_R^f_@P=fn|U&a>JT0i=YU=HevVd{uhb*n*nNBx(S9&Gx#% z_qs$lzcb&Es1~WqCBjjSxk4hG1DJ10ln1m@A{=tyLk{G{2=&LVZIOU+i^VpfRc3`A z+Ay#kjNz_Y<_lUy z3?|o@*R?Fn>gT_%jbiJqhhEp5bdEfGg?1Z)#MN4=`1cAeC5+E|vl-uPW0QCmyos5u zncpQod{avZ6OhRz{vAa?PM0`urQ`@_cZr)uU8wwgT;9hoeiukNV>Lrs?);6<_ zvGJ{IG3851DdNqw&8)|xSX;jai%7DX>BB8a--kfVv7}KTIz_CB(4nxUmTH?Ax>@;pZ4ZZda|=wde3N#ejZ=iW z88ft`r-;5$_^t>}6H7MBc1PY6@o|KnBJ}r~^Y)M6G%@A9X8upTS`_{yg40B7IhO5^&X<%o%lW<>z5K~?X(OpD5*d($f)=-`cxH8;sf4@@&=a+UavT=n z{4cFL_iJ})36TlWhkG-a5^Fxy_Bf!yo3)x1Hze|~yeKm7p2p?8S#q|;RXlUHL??hA zlc)|TPog60#qTs1(#LBHV8nl&a%c5DF9)wcG@E>pAq2wa#uG1RhaM$ks zNqY*xFF$EdB6$2~Z83tjjoNc|V2Fc_+6kQYZvRE=o}5xNTgoyVGYsc+xJlXrX2LYH zNVwy5_QCZTqN&^(dyZBi~KwVGuMiaiTY#u+Lc(Q#F|9C4LV#UDDmKx>#4bI zBaY+5O)d2H*g4K_pOlY7z6@b-=uXT&%w=5n#Az5W=sZP^|4dufwX+)@7*+Tqns`c^7wQkLEe)_OimAKo1z4qv5Tuh%_J z$74gT*7IP_Em!L=;a1wjYxFEuDhjUAd*kWBrfc*Pbd{;s>gOT$^0oR3OuA#P(={j0 zeL_r_sHdZd$rJU?Vky}xH{ZVK&jIAhKWbK}!--vfEMW?CyPw4(_r|FZJu`KFKN=gpTHkZjfIH!n# z4D|okr|WorBEFlhKN6_jNPA5Cb3;Y;3;ObFTC2*`;Q@Rv%v=VLo12H*gyr-p5fe;C z3_VksI?X)z=KeSJ`~32cZLBTBqbu?m^Fjc#4p8WCZ5+ht3c(W_@tQU<)`1qlr_u2K z3m6;ZYbs6eg;8S|!TZH~4VzTDtkJlU-q(@+5U~b|$#=BzksM(AeQms2VKe6Uz zqC9w(ZKvc@iX>ebddV9Pb15mrhUdjNFyQp18?*8ce4v+fyY=szbzdj!Q{E7SZZZ;P> zXbE(|9(=J_%*#mx#<-i99-f!?K*^+s2us4@l_fn`0R5&btqqXr*4 zpwfPOMDKJJdN`R-F=>^<_xfo00Zp2YzJY^oiIF2;EyuW!WP1cq2f~=3fdv5+TLi@j zsM+GZNA*?{{zpBXGM?4bB?+fvc1oeBc~l>edS-V<54Ib!R&<-G55ex}?wPvr*G#X> z)LUlSRW|sz=&ItR5+B*q$u9e6>fJd)EucSg{ki4QXYk09200K0zHE?V7M7`@kB;J= zbO^hMie#7kTHCosTqHgV=r{Z9A%d?|RD>IXVY*Zwz!$%#QCn1q>vHwUv5lKCk`&p9 zm7*#aqp4DSo2#!&fCZ@ia9wvix>~I$o~5V7kXfXAQ!A=x;SYV3iXUg`N*3{~8u)1+v7p@91`Y}s^DXt0ne(Z6Lkt`slL2^GYtApM+zp2^pD zw6ir2*~7{a>S~d_P&X22bmH0)mP4{awYYboo*sufM|yUZ5DWF(=%({Mt&a=W z`041*eNOkF%35QC`rQm|ympb^K9ue7BH6}y&xG1op*Gg+&P5~5P$NU*_%r%r=d`hG zF*zD-Me*9OJ;PvHwA)xVDC+QkXk&bG5kFs3m;#q)H2Cq9{=wdh#2c}=CYo)mF zIlT!jJoS0~e#{nMKd&1%ES^i7jHT$~g&~A#)<$?VB=;~yMU>nqH#D^WXs{8Ur9>sy zMv@PPlB>k6FX-z6g`ouJ@b39<;H9oosGw3ALPS^h|$)O)=|aD2uKQkq+7< zvEUWGl_-5he?3rc@72r-8B$F!j$iTP8DVG>hv_YZ97RP%e!GqaY6<@)l(#{KD2OUv z7b4b%iRxy*_6F=~HY7~*ySu8ztI^~T<--jqla*RWmg-|!mxJe)!)2SuSgH4o0YQGY9Pt}g>VY_LVuhd&C@CNvU#Z`S z0!FOTCwO9C`Ce}7P4AdEO(edHWzDj5AA3c7YU!O(u3D=9)SA4$3}lYQ9u|`lKKwb2 z>Sn$oTCCT{f%)e3`aKBttk)lPzw!h3;hR@TykdiXDX7af!0&rSBy7}MBj~(Q?;jT` zbsRbQrPkj!>X&mIr<8BjFG(w`lD>e3+uiu%Ux=BFd*vG6D6-zu`(XjP<_D9x6V19QJt#PSFuY_Gqj7(K?P9* z=@6iwDPXUPzwnlfzms6%)g-*MK0Y1CtUXxKH#mMd!gJ(31^kH@ziu9|PJXDr$P>P5 zfeTUw{_bD8`M%fOXFa!1|I!V&?$JZ~X(&9sMz3Sl)(waC*GLgotKXzU!CSu4kFwaE zZE<9XN4my#SOW$*P9@+xqT5) zj-wUZV6~d;_+4dNt>k+h&%|fce!)CR_*xy~s-}A#PM15z6B`$wfXaL+wm<5?(_ibv zqmGqMNA16uXRS-zYB+q>%bAWpRI#}L0+)*F0!If}1e}tiJK^>7Y`%g1!dy-m5vNN&ax1_VOJR>EPvvOo?3O;AkXZRr-s+q z63TysgnCN7=dcn;wurf_qzZ2!>4sffNQIRvZ@gz6NDUT(AS2wO(GZa%b7KW3QNAcqqha*k%1;m(}JiSr&p+XYIep zC_BaVZ^;7xfutLD?Sz=z52kb-zpZS@gMDw^5}RTdSVYYAlv>!atsu+ky^55Q;*O>L{Y5deQoA zD(V9<;B7}n=86xX>cp?+e$=q8?6dpM-_>7)I?6<1-X#{kjf@i46GldyM;CyC=5@e5Lh%-stkJsAJO?3c@qxo1AoF}HKb)Wov_i7C1Jo$2dE z*}&I-4PH4v%TCRv)ZOB;63G6Bf)uAgq`6gMUWp@}y(yL>spXL>?%_oL9Y8Jro;dc2 zPr)%{#Xb@py!L9NaM(vbf8n^Hrd>!haqUKGt6fjPyNud5SiC*#g_q*4zNvQyTSC!ENLWFswfo-q zadXMAPcAw1+RYW+hT0vBQg;dVu9TFCfEQGY3*MC`yn(2@#6RssfnaFCeiC$O;KI7H zVZX<`GT}gibI2U}9YIx6OXIB%USW*nj@ z=57k=#T*1f3qC)LamWasVl_WBx$$UOLFVQ3B-88XTnn|i^(db$>1|cs3Sw{ z8|l=RMDd=O#)mIE2(m*WdPB*}h@c=Fyj( zd_B}w2BoeLMJNE~dXIv7QGdf2)dlv;2YsXbF(s@+vRY}vt>^aA5NJ5{076`~LYp!78e z+SY^j&De?89o@Wt|FD`nmbtokVm3dxK~Wm;Gs&u7??`rF;r(}+<7>81d{yq~)cu1K z+{0oO^Am(ElgQ-qmpfKkAHR|I_(|}16?35&xW$p~E~97*#r#0rw8hcB?dj)3GQ+tEMN zs40|p-cQ`q;I9S+@1^ov!HZ z{4FY>v$H2`*@)6vH(jl;9>JHjXTlwOk-OW(@CP?omnFM@W$ovqJK$-Eq9y5S;~IRW zX#tex*CFe8q9*HV-3D=4qB|Kb_xJJc;k+S~Q*=*s$EGwzvfLNW0znhSrHSsAxEFM1 zqPw26N!F#wZYNJs-U`bf7iNb1gEn)6$ZO@EhKFiDwQ{%MTzkukY3=T$qr$!0x}VeV zYG|e3oq-D0``umIj!r<|%0}h1(fIY4;ye_uC|jtqD|EF`^lImx!1jq*?cA*s?tsIL z_i~W5hUsdxwW6JSb{p)Sx|!}SF#9#8`vtm7d}@??ksBML{7c=Buw+ZW%-x!^(N^Xa a?%nNitn6i);&p7C}KosR~*yLQ&MBs1>WWQ1$nIGP~O?=sD-}&+m(`Zr*wF zBzf{AdGaKg8ADCIe`xGoR&B7t@)~0!b8UVy<2PMi+&h?jb6=U8V>o4XZypycbr|yI zD~$L+fZ6}e61Bhb9IlNq@(2kwglnR>R;NFLHGmp{DuBX5gJ1>051j-{+BoO6!TEs8 zs&y1gJljxgIOpNds4ibdw}2+<2Tk?!?wZ9#S#p7u;oYYjd5E-{1-@Pg4?7BcSqK{t zW+N%v<2=g0NLD@Fi2+Q{d|;8f=CRIu*CXQ{aymnwV#{y2Ew( z%x>C5VNLaWx@$a-=ZP16{WZ;x>cvZh5}2C@EYqhFd7h=$_;v9Io9;ItpeFR`V-mQf zSsGU`KZK$pRc_WAjF3Y3nklCl9sUAxg`mG{+dV!x=3|bbD zYUT`dfRfawLv&W)(<~k3Tc%%$ua8NH$s0gsWVBEWn~s)HhA^L!$s?=+beM_8n^Oyf zHe7>>EZ$!$OGY_RHu}hKM3gNwAM2(G?^M5rOvo~Xd%$jjl>*Y9F03=5Q@utcP8D~J z>j%xFB_>sCr&9vjWsObH#GT_waJw8gbbM>PFW{kJdn{9A2o~;kZ-WlCp#qN)usmpu zDFSX`pc?T|rWkY_A7mt9JS09bgV9DVpGJjf;*$igmwH*V42y|fiQc4EPCBL7+ks>| zN|BOeZz7E$%Mh*t$ovQM7GY>W9mBN;I671my1`)=C5m|gDD8L*yTSv>s1O5*O>ev4J+dpGe)KI&hvrz0)<)fq~)LYX&v@dsmcYnPSO}ungC}1qT(g8ihvXzh=1B zh|cJP8vHq@Q=_i1DBO~NS3{XHk2PhthnOH2zriTlGmGXL&vGRJ{Ry| zG9zq7NYwdQA`cVRFw{{CU=Cm8RAR3hR6y2Zh^6PVp&W*dQ%9{DgbAR*Y7U(0H)wSN z`7l{SngAvy0R3P<1-p1V_1DSxF`tIn2k~gN^=ra4J^`cO(q@W>XraOyfDCS*9=z6T zrDH{3%4l{B;lnI24dML|nB`A<(;51EAX#?}CJGWJi$k8{TN_7GZowj=lavrn88c-) zbc^-owbT+a*ED+?8QBo(868U)2o0>DH`i3g4+U5gBbo)!gXw# z-=g78HWfX3RZ7=@D$3_Wk(;1pKeU{Pzit>0c?Eul8V^e4J{k^w`hor4#Qo5I4zFit z+k|b*mqLUc%$Go6E%U`wh@P+<-Y(&6tqi8pk;5~5PM*gD_{$4W9E(0yTjH`0gyr|-^ZWg2 z`G4SF(#2?Z+RJ_yiU(^G69LU~VTQT|qXGRV{Hz^6QQ$p`v>^(4 zfe(|+au)b{*lNL~rX(;9w7Sm-s{=ZPREs#;gh_Hv0@Ic5T)sGB^!k!W&&Z!hQMjh* z#6Ax_NJRxy3_8;b&P6ni(u%pi6NM$bz@G`K2&mcTL~2rjzo*p+byz*E%&9`NGOeCx z`SmO;>zN{80qEfBbS~7~$%>omOM+G-+1*wWiAR;hTj|pZNM&&Ws)&pu0w^}Fz)G@G zk_{Lqr_j!(CbcoEcDC_i5q5@o6KPtg_NAgxY6VuRfSvL=!|DLDYQGh4S8bOTh*pi_ zNL{K#RE`2w22~&io7DqT6rE^wAA(V8xy3N><0oui+^}Sb4{VF)P&}tC4h!XgnTqnm zKsiK4pp5_n<$%8(Wq?cPfL|AhTaGaLlR>J{ASFl8)}m6WrQ3s)`MS~Yi%b>_ESe+m zgH>My5G&^B3iKc#hD8cSr#*?AnJ*DT$~oPaL}?|84FgiR#=*l%uoBS$1t!M6In$R8 z^(Gde5M4B5q9IQr^zO703NQ@aW8m*Wx;UX(ZklbD3q6eleN5ob$^XAlNv5-uC{ekb zRys8zQMji16ETv|f_N*1ENB51G%E>)49#*wJUyC6w_I4Bf>jw=y?msnCM*Onl{8>z zSLJ}afHkfgEsUwiNEZ9|jJ}tL*K@MVOFceNEo%tBoL253e3HfUcg$Y?)W26CmKRP8Ed!QiL$7y_j@_WQlVx~ob&r#LVm-L1Qz7#NJ7@Oy8TL+2qhJBE zy$i7bquT+=j2T!3p3mwYfZLU7Wkb4b<=MEcEZ{5)WavP7B%&TG4%u;d*4aLtT)P@Z zi?{08WKrGWxYaG{s5CgBKHC+sU6`0Aqw%zw26Eu3Oe44ae^h`D1+Z8J=yv0+j$(U~ zaE9GR^07SW|-~gV0VZ)p|)Te=4gI!OLi)A1V78}G|XuHiirJb&6Pv(1fV;#ompmLg=7ViTO(59g&oyGDv5f^;Bb82AG z9@=IP;VuaNjKbx^I0g*uhSZ`6srqZp@DnRY#2j9ljQNdlQ8Gq8!Uf4b7lq4`{Tf;e zv@{up0O68krA+c88B(yBq^UjxMw8jgD?F{f!0@Wa}e;P!bQXCO7jwlv|*)I#lzg7@$nvv036s1;TD}AVCZXNRc8z^OYIx5D>EZ{ z%|InoeG>F%*O!eTR;^wS?R?t{N_Dse$?o(Sc2VdylFm(|l_a{=?>-w6>sZFH0Q!vy zh#;w*l-RvFh~y2Qa)={cA*7dtq;Ve~(S~a9RMA!WEGWFoRGP=sTucjx=tBMKP*x>U z&>9p(r9h@K9zc(VN0TO&sofG2&!L2~$d+DC%Lbzj&gC$rrl{8;?f=d`&y0-JzbqTa z6yd^Y2v%WlMq{u7A#GX8$RE|c_4cj z)9R{5)qKN8=9#*{I1dfeMe82{wOGapk5AY0pfrC1HCZWl0Wo@}!d-zj)v|e9n(IHL z8Gn+7QGn@f#1yED0M0YXXT#PTs-^P_I6KOsk=RNmCC{JbRYxD#+M&l)KAd|E;dPGi z81)X-Yk^6>TZo1QsD|QD>s}g3>%+w)D$%+pVLa3=7ZTscX@maeM75>tb0N4jSW18$|4v`8Y&AAQkzOB zq-{I~hn3|0nzpp!@odo)&aNj+2X}!WvUa!K}|Gc1LQrV(}52AhA0ae4{)iY zQqA7-fxXc|-HJUW9Y8`afCtf!*jzfMR-kQcYQ+n!+`AHce(Fu63|oqsgw5LIUq_) z1sn!%a8nQOGyvL>!Kc#Hgn?zmGJvka_{oM*wGV*(m{PRYrRC1&z*>p(Y@G(K05@y` zKsZf^yqU05va)EopkCPxy)tJNbXM{e&{-S4DuXB8|hiaaeJ|oxQrl^E=wB=PB6x_SX`9 zskB=4*OI8Y(}z$+7#@tSC(uWf#Yz=ky5#V}q69C@8J`{JeMI-CLflj<&bt{}RL%;- z@%mDNYjb)#QfOwT1i#72bf*aKt;3;m%}NO-_4Wr)p1nY(QisAxn}tQ%Z6&B)fKtLb z$FXyF`ykrDo)a|~;uNSbR&(lzSMQUYu#ju^=r!!xukq#*($kE5YV zL+pjw7>kZT1kW88*zL=D;vYg7G2nwj!p|N zZ17{SL~<~wg$>6qj<%JpEit^Z{b^cwWowtFCVvB0E1R7_9v}5M4jnKtlk9~}=_rXL zRx%lRUTpu`~CRxSF;rk~5{XA1b1@B;5{7ZlU%>7wrbU12Oc`I=CVdqs9=h$>l7#YI5<8=Z(S|l_QsV~V%1_)4KT6*Ax&jap(@G|u`O_-td^+Oj)=nvKLr0)`3v!3e}U}Tr&-Ed zPz!lv{iKk#Mf06Fy8oaT9jk)#%WUBn;Uyo5*refR8eRnL6eu1n>7S)z?GCQ!-v_6! zkfV+~iNEkDu!z%k3!Sl-cA_z}7n9TWH#BU}$=^V)sTE)KDGc6n^#8jS?sV;a=wT=M zQMkf}68OvpDy`v$I+PlX9p!pr=)mwGC(;3I2A&xNLnQ&q$R0i&xInsTid}9 zc`7XST!ds&BhTtc13g(3;*udA^{9a!sgJg#$zpSStYiUPHla10JlK&Yi{DPThx!Y$ znmib&KDu5cMI?)PCmao72FA1$4ModI%c`N_?g{+omeLWJ7TZ#F`~?l0)C;P!ThS^H zVyXk^G=qE#QV7)^c>qNDVH{6}z&wZ=^#BQ6k32mucxX_cc%3f%U^Xx$IuFifwZY+o zPwA}FWrZS6T}D!1ZBFi>Wnv2*7T~f3NXCvc-b$o$C8L>Z!q{ zENW-sfh4;Os*#DG;C;Y>RP{4)%uBIW6$2<%I}R3U9G)TYiutxg4y+{T3nnm|HQ^ES zgLe^!(!^>ohGqndtfEO=Z0o` z$r6B(!FQ*lE@*0K_sk-bZY*epQja^N0UT5+Dh$RA>xW($GHk>tZ7qUgYhARG#nF7x z!cQi_L95Dy^9dWy)Oj#AG374~b5h?fKP|men+_c`CDooBUILgtJFN#B6fGW;$3qvN zv|Mv<0UEVlW5?q=09)9l< z4}xOmPXN9keJ1!qplii$wJ854mngDGd- z)uFAq*l}U=00B$Sx+$hC3@9qA>$s;-^O|vfp3#;CCWZFt)MeDNrtzn-lJb+z*~8eL zVEnlsdLGQ|uhGLzV0s|<^|@Wz2s#MK=bh9>07v=w^JepQl;Fw>WAb3qAx}#1@P%F5 zbBVuw+@4@v5R=WRvRT1`3liILl^0#mm9?Y%&();_`&`tCeHA?8q9+pWkF=4AqRilj z7k3P{UgSxrPY$=7iG9h6+V|ok3E#9)8;F`#o_oo69D414*)E-x2Ax-SW6Oelugqi> z*0xv)di~`>A}i^Up_K~tqNv8+KV^-8XWWJ-$8oiQ6kkp z+6Sa^PswFGP~1#Y92=tfO$4x3h^kgD=3xcHnJ&=73e^EUqRA# zshfc895q08jtZd1RB9Q}<3PG5=9f1-o*5N{i$zgE7W~FVP_fSkny*j^&=U&H1A0=S zVj!td5s;mu5XjEaimL1!%|HtjUlWj>w-IO|Xb@ykdC?P2&dSQ?$vB-I3_khP8rBdz z|LJR>-&Ieq(RpKQ`OVKU#v6~7-%`3AEaj&!*~Y=+eYqQWPI-B-Go^lX!2pI`D$XX* zi)3iaf_JRw)vZcl$qrop%`fXJ1Dcf(bs^H;O4X3TL z#h}}2kHFs038^v9aqo`0s@f7esZ}OSHww_Igw+|WBG_~Fl=S&Fn-k9=;7ZkPrNQ*Z zi$%d_R$IaQS9?3)`XBLSHkS)EbqY>jot9qmr`Z++-J$GY(`v8N&bG9CbZ8o5HNn>^ z-efi9S680PSY@zm&4sM8++6zyN5owJ5gJ;z-jCm7>n}oAoL~JCN2Gbf>G&P_7R`{$ z-#V2cczomOhDx!5zihUmsYAESL27XAyLtF+eU~z1Z#^2viptY>T+Whfjxm~)c=DEZ zluXo=7wydYGjiD{!(zzT5P8UrC~x`nB&s}T*CAA1-eKQ(Hss+Htn~0r)C!D&g82r+ zxxW*rYBA2?BmLNhPf$h|1yO2iYB0EeM#jB3Xi-e+sT|uRBlVr~&h^6`nB|kcqUnFn zSH1B2lET{*Zu)8fMtAqGQy3D4H4xL44a9W6`dy(YI}~nJ*nfyp&pwoc-&+)Z`Vfi; zzISLiejVRjfZxl$p>hhpA02zD_2ttKcW10H_~dup z*|+6uzsqE7ZqRD9g3G>3WWRvmVMz|$w&N*cx@W?U_Q`UqvB8a;ajma#Hn+U|_nUM! zH`I+Y{CivW(AmG)x1pzXtYoJtePYZOvjS0}ObE>(Y+HxmQ6D=rMA+Klyia*#aDv4S z*%ZfoCJ3TfX2!FvtW16x&rUy3FZ~{t%eKgCJS?A8gQ#IaP278AHn0J*dPR(MzK){_-Hl&!Qj!X4}vR3 zL*yMHIhy^!*mqJwa8@JF5-i9z$-RP|!9J6|eDKuB$@wU)L_V0$vJiVQpY>(i2Rf?_z4uWD4Xa?{Rs0Z*9XaML&;4naU z0*wIl6oY62=t-azpcjF{lx8k+2o(YJCeZj(=+H#=xHHhC>JQ69V!(cjKEyH)pf7=9 z7}b6RV0uM=0%T@SQak_y2*AXNfdonc1`&X*6oUyY063YzQh-wklmVPdU{wnaRfZ6% z0BI-zSYk1Zz!HGd2rL2^P5^dQj3BTDlHr^STR?zO1S$YV6Icb1OP~y(MtR*kfHo*p z3-p#k+kiF_3W!aBn-sAM=xv3tHi>r>nh&&Dp#?x&6j}uIu0o|iTNPRY^qxXXfwn1B z5A?o52Z26Nr~zoZLWhBBHKnRXzz-F%3Fsq*nt?u6s0Pj6VW-m64fVc^ZFQa#U1nhq z^QOX57s4FiBsa49x^KcT#sW=N6{+Y$dlANM#sD17fr?WFBw&RrmburkY*s8UzlL>Y z#i2W|VJR%TP?;7VUY<}R1lzo38+b&KeC6+K=U;Qgi=xm|*D^g?h$~V^T%?dBuvA{h zuH%tXoDmjRge3ti!*5{UnygR`!Vzs~?7!HaI93?)-_8;}2tmBVn;jM}NrVN)GflRf zUgUpgJ{!Zc!-Vt*ixH`d1nni+8|?27qT0Zd1H%w`BgTBH?@S~USpE8lF;+ZVG&ua{CWkuiq(eB zT*>w_n7Hn*Gq?adLbtrZynI-*-Ghdfgf65*4$=D86xtWsOoejoD)xNVl`o~H!GoQL2FS6qf{KtR}e#3 z98;(eZE95rgHHUW5QdicU7;Pw{D(q~$Wf>^GV?;$SFi&elIxFg`w#~kcs!ve4u&SZ z#|{jQZ11q#L?>_wc0BjlnSt(Cs1-%qv?d^%wg9OQsMPsD#R?SzJ*ZGoG_^3A+Kjvp zsnkZGxj@)eEeNgE`FB}kqo$-ZD#4X{p2H{e)?Y)py?H8}N_k-)e$AwC9S%`ASEr*M1emBk2OMg`bW@GpheI5UicmaP z7Y9_)ML?e`G!N(tLf)l1v5G>O*q5IMOLSIW{`gQkbGU4B4gR+>LdpI3b`x&jfk8YI zLF*uXG#youC2`TtsXLyx%(X5Q@nW7#IGKOjE>Tm77gJ&==@fnwOM|?WHAC}Vf#;O_ zS{<#SSOtTp@--ZrdwJe4UeG1154TtnZE(eJWZ&RkiyHaqFuovAGn-zzBKuP@ThIUY z-onD#F1X+pWr)=wR+>$*rHH+aSbZmol_K^AVrA(RTYy+8VkK!5D?#iL#2PzLtQfIY zJndDBCu!g*A@(g|HHj2!zE{uR6F{`gL(xV=-=kPO#TpQ+qL_HPZPHNSB>n!v_BiT1LB%@l2<#N$cei<0EcV>oRU zK0KSJ$w$xNU1G{gh;l`X_Z&~c_AE*|zAUfI8N(A}3Uo(VqXotvPr_YulyqW&uaDsa zPl_~+9wd!vD)uiF`(2-ohhsJ2xK*?!O~+IyoPefvAIq)e6Ee!0@jM%?S1jL*rvp3J0Z6fcU)~rb==j28 zHtqVB9=Wl{7kP_q<7+%0@Lz_^k?}k;$`UPBc0Pyq?-6OKK8>F1iLDS%T@>K7kIot8 zP3Q0~wtQ*wg>!gjYD~81JdhurqZFWqbKC@;9m^FhT%I<8k7YF>aLvY%sMII&lW?#x zcp_f*8ysEAmqwSBO|x}Ts#cLLK+h|*4QR1KOG6X?#wW4ls)%b2AK8L>DrDsayc=60 zKf8c;O^Y57+Nro+;-XY0U&zzgQrYW5e)CY2O>17fR*Jbx1z3U}5%l7}KS`qeDoK8D zA| zD8j=Ll@f_iiX440cSN$_(Y#0&s+O4LqjtI~1y9tebRGaM|E>IJ62jg0b0ZEX zH~Syu87QLpQT`|gx%hED)`f8UQ~dK}jJG$I@!JvfUCwXItSMsZ{F|Qd;v$hAGNgMx z96HFV-L> zl;5Iku`Ql2RnOyinvbScmCRkmEqQl2?~X=3U(T=03+KdBvap99d&Wl^M#a(0Z(m@+ z2_>Fk7dGMiI|}>>R8Gg^ve>1k|8J$oogi=in-O_?hf3b$3-nH_9#oyKq0{lg1f$=( z>5cm`DJuDmiP7W&T&kAIib_7XLzIRl;k7oRw;oTAx9NFn_<(l&9-Dse8vb@{*#^qE za4kP2hL=`zTp5?ir`Gae$Me=7&xrn|M52#o6+?w|Pgiv(cuTE*KvS*+_ z2|`-5{MX6*w(;xFi5dmB&%zswl!&bo4#@3dARtD}B)lcp9pu)C7+(l$g!QD!P4qiheg006E;v7T=1zwSJlFxB{fG1#x1=eK9a>B z^6keFcgX8M;-AM5@m!wxL|*c7J0d-0SQk2L2OrGXqR?GCaa_(0$~B+xp3u*MPk3sZ zCh(334h#J_xst9=`HVL6h`pjpLYL4*!DFBDRfD5_=EDKW5_U3!W0U`lb`RVQdhS?6 z)0gGD?!Vigh=Fih9q*OC?|9xqoH1f_{7}a`%J=GcLVEr293_e)1daq;pUB^K@oP`) z)ako<7H^#`AKA^bDO?r~w}!(8gqKIV3y%hO9KhmzUc4oT?o1W)Jq2x@>GM#k2k%ma zQx~*L?LeuCc;_*kx=5wUDSPai@3F(sqCNZ{%pVy>i2T-WeAe`}bo_*(?9B zgRb=|mTL!B*+Bq7(%xw6X}H$Q<@Fp7EXj}Sak5Y%6Ar*BE0F^az#S@)7al-biM-Pe zO6}ksJ2-rRp9xcR%IEw8eKam3$RUDF$Fnq035%JnT`jtMgf3!<7A|P%>_|~%-d8;B z(zf~S$FxTxZ=a=`Mc)jq)ZL;l2Nmk=sXg#4CU|XXXvJ6jOR}Z&8#sJPxu=2W zBj|ESZKp;Y;%^}K{ULsB=UUh#UnU;KWHWsoEte=P)AnR zSM^7Fo6b{g?b;m@`s^n@unVSoVh8;R41gsa^gH7}SxsZ!mxei!lY&>tKd|GOVw?P1 zias^3*lr8P06kepeQR|R#}o10oQxBQdV!UWGo#;9^uIBzY7^7+0dN=#)AX|uyqBg= zXNTpej`~T6P3x$ik2Co79d(Bzuu8f8ZI@%-m+U!hW<4-{Gi6xgod0zmp;2JZBL zK2)d#=p%*Z0e!4cG0+Z$ihy=1R0w2?)fz?3QPc!v3)l!`3wRjF7O)`zXiIny$d<4k z$d+&~kS$;xkS*X2AX~s%AX~s~K(>HefNTLb0oel90NDbfv%@`22WIYjbS7)M=zdlz zzv!aR!L9Z`y6R`L#L~C%SrT16%;Lbvtg`ej$zj^vcyL@jExS#AYw7)BS#c*Qd-(MJ zu{0AjJnoVeOGAU@gFgL?SQ;LwmwSBr0B^V&i_cP*FWV%(KC=IKPG>iL`|-4c-Snxk zG!p!Z?)opWG?M1(9{MN8)5i2{pVuk2h1U1f-;DQ^?qmi1?8kZxIWeGT$)^VCL!hq@ z2kE)cSMp%}Rx`FZeCka;G4zDSg}xrDU&N9yImpDTT}qCqmE%v-2f>cbI!$-s(Ia{P z2z>;Cw?^oL5&UciIU_NX8|0*s`V{y)l_T|kBN#ABzY@VCcJSROeKI>38aG=13=U(c zm(Z`&6HC{rug}oQ4eeTkXXrg+X|RcM^BMYkvG7+vmUtjJ) zXIy)w-iK9$9=%eZZ^F!Vzg8cMVCuE{TsZc>UaK$06MFKg8}u9m8*k88om6@NYt%5< z{=23Lg-@dZaXd8JVXAL9(5!IE2vI6uyis4xUJ6}apdVr=c zl!I^9^CI}+7~BwpPoLh#^Q^)lON_aK9Cj3s}||qaG5e^k-h=ee#|pE z-fJn8PrR!4#WBE!SM^6(D0Kd7I-VY@2n{OJUtp|4?tC2svPwGN&`0;%q*exc^cp@+ zLmZv9Zi$efpc)%Hu$91=pZy64+w{BtQ=L{CJ&GL37SX;b2W^4ii{972pvv$4K)(~p z47G08E6q;PPJ$M(2f*8ecwrBqNPe(Wza$$gN>~->VA{6g2cJG6j%6Y7iGEr<9tgUt zUf+sf>;V-df37b6dyc4uszPQZF~tu(nf1++ihK3FLhH^TdwOtZ(&^EGae(RJr{JeTyvl zPG67;-+*#??Ofghe=j>ZP0s&TFOdhn*Ci{Hcl@CD#1Jq20pBf&%>m{R%JOpzydyvA zL-XK%P;uB>(95Nn^ae90pX7ffhHgBf&mE01|MNiOCx)jz4h=S%nYZ*aHJ$M4yNk9g zrSiv9jG`E-Q&B_rooeK#z)RY4ma!SLykwklivj1n>1x`y^&Xyh`v z=n`W%dm{AdB}Nfr3qzM*YRqEnS-JZ%V>dFrIoViapsbsx7+o>f4LELz~F&Gb#{BphVzTPi#&_&OJ25!Lt6<#pLd0qGn&+<$IJwlW}6c{*h zw$uD{EE`SfJk`j6k3M9o@l!(S7q&*}aJE)X|EF!>O}Xa`~Uvr7aZULpB%a;DR$`B(s#Tv4GXWOt)=i=%Uy0HZTeZtTL`c@Op)j zDgRidYEXF4n{D{-QJgLxe-j^x3G;7{(laGn-Nt`%6sOB;R=4p#7o}&)4`Z+%isVg~ zgF|inH$~~0^7$D2aTKRZUSZ_h^7!RHDwG}|f4Y36B2vyEJ6Vz3(yWZsl_`ftak{*| zGQyuJUr_YW+DfB}u~$O3uQlH1D8%;`bTnnXak`Du<=XYIj*6Zxzl*_Lt0VMGd0{oy z%P`N}C_P=Si@`rcai;8E)5br!CQ?qid^iSIM{%ZXisA3Op^g8F82o4yXUa_*)aXK6 z(xn3pvSm9hBI1lF$uC!eCqmyBrDw{(M$DwFjqMWM@}iAO`J|)}I8KM94sG0IT+b2q z*=+p66E@fj$eFko{_%+Hzt!lL@Kzh?$Psx3kv6uGemWu_Ceo%hQqvK+j!18}k$yfR zza!E+ZKPk0$jk&DDNL$)S%}3-cBE8#2YB?g86KQK3>F5#p8Ij&=BON;u zO4w#Brxvb#-`LF)n~$;_jUIu+Sz+T*`9iIcpHO_v_Hi;mD-0c}HGa^sllgpyF$6)U zoknL?6dJnIc%Nl9ejf3L+~PN2`Xn&kR}syhhq`=f{KJUZjL>V$+d}jA81Fk!`8A&# zp@fmqYwwb%F+Par-0oE-7!Tly*$V9inxjx1P?167INYc?K5@OrZ`7s03&V*!GjkHY@^(qh~}S5imu zr5}W|jv4(^(@SQn!Ys#3%Q*v{lX0uS<1Hm}Ly`&SF7#`X`7uu|QBRK1n%a7mk6}$^ zVu{?>!JN}=$!oa9e~p}Gh;xP({^!Fp^EZVVSIMFjb50_PP>ZivwU#ar(^Adu*n$pE zH7Dt(H5Bu{c-pF71r}es7(N}YVpP;`l;XJ?bRN2zioK4{@%k13ytIl}V97nhIr|u`8&*Fi#j7!ZNwp5Bs&B3@| zn0cvLjvP=RABRLSoGpI>T~cV@~Ugh%+8Bcf>??GugljUzzeV25a3bEgP|v{G2b^M72x%O zNCiB$f_rahtAG-s6|B4AzgEz6gJ~Mrf*zd)U3Ege&_CYhfsP?@6qkf%|I54_Z_nbu z#ry>m;FB5V4a|!DqL5a=^Td3W2Zt5$C#pdNz{QBz~{&?|AW@+-fs7Mw77{v$=|@R58tzqDb-hLW`YLDEr)MS_5`KsM6^FqypLz#UDET z^n#k$gh-A30?da_v4N0Vp1+D^#A9R~W_XoNRLG}qHAi+*f-y385ZI5vpdLcevU-#MM^TZR1nHgkAy zaM%j10if1+65OK7?pOMrq~*Q=r+BDEUVXcH7&GtOJIrx7CRuQYNv9WK4BcgRjfb9WrcJW`F0=r5IoIt`X|RkZxSB1f}l=2#gdG8n5KzF+s~U z9((1u^QI#~gZg5!35-lSJT#^!7(9<-vjqX0*UQ?w%@LSGo>}HFPo!L=;Kpc{SpW;Y zc9uEKL8H29z3hLFdG<&ens%l1MKPruj9bWHY2%O)SSbT?(tjd6#dN#2@_Wo)0Za!H z86wiX=7cQjP0*Bn>C-lNr3ZzmchND46xp6Gq#8^hnuREUVnyM3cfvGL2Aie^4W#zd zq%$%i;ZNoXWs(B|W=B_aTFeia!!p`W3(N$&yKCjafH@L-EMK8%o%2^rk%2?s$)?x5 z=;M9VT+$dHAKP?HYkWA*q+WTr(Cp6dfUy`J1oYgoAGXVK;ERlc&44ny*16Ft^34eo*zvvSPC%tB@!^L+&p&C&N;Wc+kv*`MUVQ za7mm>GLM&DPI$)F6|Z#~>+?6zN32Q>jYXUDfx*T0{9Wr97I9o0ZBbd$y6%BVl6qc5jQgVY$gsG_yC)dR$*G7}Kgp+Gzr)SL7IE}A; z#!T{;4IDwLgGFuM+{w<)4dg{T&`JWSeRlN7BcOskMLykM@ltjyk>i3%0^ zwNl*JXwUm`Ul`uD!3Kqq11k1I%*9wL7!}_T&RefSYAA?xVIpo$mY5ldrOV+sBBKwF zMv0}NCzqIKvp!qY>z;lMgIL2ihVWq{QTH=#7osGK%^+$`aNnR_p+PU1>p2EQ<5H|* zOXc8KXk!(c{EAu45Y2iGp6XJ0<7?(S7%c;rnWrGQZkahbwanfon4(Gn8q2HXj%DVl z@gS(rgCm}@+zcdu6ZVfLobRuUh z8_XW;ROuV4Z0dAsi+D_KU1N?1bH}yjzYyHF)|?gh*pD1fQ&FP3%A5%5#NNnuoUSv<^yQLb2a9vI)vW2(VQ5`#p&aI$K}f1WPSzqo^PA?I@u4Q zrQ1x-ar^Yz2j((%E*guj@!@SgyIKwXeP#pO z6iV4|zDR;p9x$&kAz=E~=6>c0brDR2y5N8BB^pBfkeTK}j*K78>EL+iM;tI9yz^%> znWa-dski6p(=PhU-e&q_uvjYlA2qv%HvD3~N5ksj7W1D7j1z+=WJhWKoa=#RMEXDDdA$w;yE)ErEINov&EAzPr zJshWLvkXnkacEkWQ_~i>G<>8=0z=byM`bE+ToPJ2#j(r{VK1NND99*T z$K2lJv`(E9@Dqn06F-`GHMDP<<2Rj^hxXp=cp@>o_H*V=!8hg@*A3I_aJu5+-JXQR zB)s_dxqLX_=;^@KpnFM@WCI+&(C&caJ|`}Qraa&nVc-sG(OgFlsN=1afxFYTOT_0F~^N5Y(?m~C5~|}-m*@9v&@mr7E0%GN9W0v z>$y99(wOwyZrxe^&xxydF1e=LI*tD2{gSJRU6k6~|AFP1JF9p8qwLv(MPF5^REttu zDE0Q+p15lLj_Uf+y@NMq8FpTcQWweqDqxlJ847mEwFr1iwcNd2)&8rhy#^%|?@PJt z!H=qQ%eJph-I_K^l|SWYs{OB0^C-22Qg7dWO!WS!`qteSO~T>Xg0jEpPmwy8Fue7lnHEwNvvbb)md!rK)`v1-sr39x{rkCVMyj@#{zSD?YT4&qtGaEi{$|72z1Pm`VYg1D zw)|CUA>~~t&nr{4|C54UaxMbi^0s`nOx3;_(aDwXp!|}TZyxx^+tsH{^geg`vt8`+ zHC6jxrPd#zwo_`^^Iv?~^X=+KCp$~#?z8N?HI%wg=Dx0KN08wtCDJZ=7f8HivwZ4x zB|-?%$(37B%#;B;`-C=BKlws(#*)I5>|*j%9!f2#z5AX|YpR5`m=}s^YJy+TV9>qaO!D2?9yw9 ztMac>ODT1iJV2$lP>|s$B+^3Jxm>k*1O>a~r3iS-d-C>js%sn7mG$JOqP%*4O3Sw0 zSN5~(vQ%A^TKK~EL#}(hI_`x>Z@u#FR6A9(>-vyNY>@~u9QBQ=u7p*pE(95l8Y1nI z<3Qpq@5>ujQH39%$eQ0PA9(hK>Q{g7`E!W>93EN3T=`e2g_OEmZa@+2CX$uDiE$cgNPUr8SRMFM8?YU+(U*KWq_*_2_mv{Y^(lheeU6abADU*?^`;JbyLH+a+&8z*|0&kE~Y1?KPrS%6IL= zuMv!D`4|P|U3$fpCs*d~K6Bmh%agm=YOgs=)$Bm(l;sa^DOj8PBVR#my!X9t)6{6TK=BWtfq^KG>|TcZMYAu+wId|LLx?YRT@d$t~a zFDtCV2CAG=Cry|#Y7errIfW%Et$OO#L{wVx;fmJ3kLMu9Q++DAl|E)_+X> zICsl!KbF1!uVg!KA*C*q^;B)8{N1iSdo9(zSB_Y#YM+GY$jZ-9LhY6de!XjF?hkoa zeE;3@uHo9hqS}%As%Ofj``7K5ncHmq8&7`R`%HF=$?NB(Zo!Q`^-%G z#lJXByzo7EQZ=y>VOly7)lXb1K@Go1`;2%xYT{66?nXx+o`8>Dx=oH`TtSt}x88P~ zl2==aO&Q1!vYWeV+D!c2fxiI$8Y?xe34e>aX_^-oi;wokAD-QHp-~?EdGVLJM)rKi z;ScOanS1cJ7k{7OZy)~l4CnR9ihIPor6xtqc~T@yFOqQp?+@H z65Kq7K1^`E-V>hZoKsvsb;oxRZoAaAld)l;36oumop6mCuW=36nK#t;de;cf28RA| zgXmGP?*BNo0 z+ojI9n|DgO}$LM-aW1l8!Gp9kITlkzomQJdA&El7mrg)`P$U4hr=F?T!a+7 z8HBtmn5l0HP45vmyEFE1P5CGU_rs^hJxz}@uRSYnejN5)6aN->H#0)-|1GW)XG23@ cUl_NwJC1VRxFhZ&=6b+&XPk8189(6v1Hgs~!~g&Q diff --git a/internal/warpc/webp.go b/internal/warpc/webp.go index bc5d9973a..a241324d5 100644 --- a/internal/warpc/webp.go +++ b/internal/warpc/webp.go @@ -34,9 +34,10 @@ var ( ) type CommonImageProcessingParams struct { - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - Stride int `json:"stride,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Stride int `json:"stride,omitempty"` + HasAlpha bool `json:"hasAlpha,omitempty"` // For animated images. FrameDurations []int `json:"frameDurations,omitempty"` @@ -132,7 +133,7 @@ func (d *WebpCodec) Decode(r io.Reader) (image.Image, error) { } if len(out.Data.Params.FrameDurations) > 0 { - // Animated WebP. + // Animated WebP (always RGBA). img := &WEBP{ frameDurations: out.Data.Params.FrameDurations, loopCount: out.Data.Params.LoopCount, @@ -153,9 +154,35 @@ func (d *WebpCodec) Decode(r io.Reader) (image.Image, error) { return img, nil } + var pix []byte + var nrgbaStride int + + if out.Data.Params.HasAlpha { + // RGBA data (4 bytes per pixel). + pix = destination.Bytes() + nrgbaStride = stride + } else { + // RGB data (3 bytes per pixel) - convert to NRGBA. + rgbData := destination.Bytes() + nrgbaStride = w * 4 + pix = make([]byte, nrgbaStride*h) + for y := 0; y < h; y++ { + srcIdx := y * stride + dstIdx := y * nrgbaStride + for x := 0; x < w; x++ { + pix[dstIdx+0] = rgbData[srcIdx+0] // R + pix[dstIdx+1] = rgbData[srcIdx+1] // G + pix[dstIdx+2] = rgbData[srcIdx+2] // B + pix[dstIdx+3] = 0xFF // A (fully opaque) + srcIdx += 3 + dstIdx += 4 + } + } + } + img := &image.NRGBA{ - Pix: destination.Bytes(), - Stride: stride, + Pix: pix, + Stride: nrgbaStride, Rect: image.Rect(0, 0, w, h), } @@ -294,7 +321,6 @@ func (d *WebpCodec) Encode(w io.Writer, img image.Image, opts map[string]any) er } opts = maps.Clone(opts) - opts["useSharpYuv"] = true // Use sharp (and slow) RGB->YUV conversion. message := Message[WebpInput]{ Header: Header{ diff --git a/internal/warpc/webp_integration_test.go b/internal/warpc/webp_integration_test.go index 544c45e24..7093947ae 100644 --- a/internal/warpc/webp_integration_test.go +++ b/internal/warpc/webp_integration_test.go @@ -120,3 +120,56 @@ sourcefilename: ../../resources/testdata/giphy.gif b.ImageHelper("public/anim_hu_edc2f24aaad2cee6.webp").AssertFormat("webp").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations) b.ImageHelper("public/anim_hu_58eb49733894e7ce.gif").AssertFormat("gif").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations) } + +func BenchmarkWebp(b *testing.B) { + files := ` +-- content/p1/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p1/index.md -- +-- content/p2/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p2/index.md -- +-- content/p3/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p3/index.md -- +-- content/p4/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p4/index.md -- +-- content/p5/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p5/index.md -- +-- content/p6/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p6/index.md -- +-- content/p7/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p7/index.md -- +-- content/p8/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p8/index.md -- +-- content/p9/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p9/index.md -- +-- content/p10/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- content/p10/index.md -- +-- layouts/home.html -- +Home. +-- layouts/page.html -- +{{ $image := .Resources.Get "sunrise.webp" }} +{{ $resized := $image.Fit "400x300 webp" }} +Resized RelPermalink: {{ $resized.RelPermalink }}| +` + + cfg := hugolib.IntegrationTestConfig{ + T: b, + TxtarString: files, + } + + for b.Loop() { + b.StopTimer() + builder := hugolib.NewIntegrationTestBuilder(cfg).Init() + b.StartTimer() + builder.Build() + } +} diff --git a/resources/image.go b/resources/image.go index 070727567..b792c2f1c 100644 --- a/resources/image.go +++ b/resources/image.go @@ -30,6 +30,7 @@ import ( "github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/gift" @@ -394,14 +395,15 @@ func (i *imageResource) processOptions(options []string) (images.ImageResource, return img, nil } -// Serialize image processing. The imaging library spins up its own set of Go routines, -// so there is not much to gain from adding more load to the mix. That -// can even have negative effect in low resource scenarios. -// Note that this only effects the non-cached scenario. Once the processed -// image is written to disk, everything is fast, fast fast. -const imageProcWorkers = 1 +var imageProcSem chan bool -var imageProcSem = make(chan bool, imageProcWorkers) +func init() { + imageProcWorkers := 1 + if n := config.GetNumWorkerMultiplier(); n > 4 { + imageProcWorkers = 2 + } + imageProcSem = make(chan bool, imageProcWorkers) +} func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (images.ImageResource, error) { img, err := i.getSpec().ImageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) { diff --git a/resources/images/codec.go b/resources/images/codec.go index b70d05456..fd5f22433 100644 --- a/resources/images/codec.go +++ b/resources/images/codec.go @@ -129,6 +129,8 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error { "compression": conf.Compression, "quality": conf.Quality, "hint": conf.Hint, + "useSharpYuv": conf.UseSharpYuv, + "method": conf.Method, } return d.webp.Encode(w, img, opts) default: diff --git a/resources/images/config.go b/resources/images/config.go index 9475096ac..42e07450d 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -131,11 +131,13 @@ func ImageFormatFromMediaSubType(sub string) (Format, bool) { } const ( - defaultJPEGQuality = 75 - defaultResampleFilter = "box" - defaultBgColor = "#ffffff" - defaultHint = "photo" - defaultCompression = "lossy" + defaultJPEGQuality = 75 + defaultResampleFilter = "box" + defaultBgColor = "#ffffff" + defaultHint = "photo" + defaultCompression = "lossy" + defaultWebpUseSharpYuv = true + defaultWebpMethod = 4 ) var ( @@ -145,6 +147,10 @@ var ( "hint": defaultHint, "quality": defaultJPEGQuality, "compression": defaultCompression, + "webp": map[string]any{ + "useSharpYuv": defaultWebpUseSharpYuv, + "method": defaultWebpMethod, + }, } defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] @@ -171,6 +177,11 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima // Merge in the defaults. hmaps.MergeShallow(m, defaultImaging) + // Deep merge webp defaults. + if webp, ok := m["webp"].(map[string]any); ok { + hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any)) + } + var i ImagingConfigInternal if err := mapstructure.Decode(m, &i.Imaging); err != nil { return i, nil, err @@ -304,7 +315,7 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin } if c.Hint == "" { - c.Hint = "photo" + c.Hint = defaults.Config.Imaging.Webp.Hint } if c.Action != "" && c.Anchor == -1 { @@ -378,6 +389,10 @@ type ImageConfig struct { Compression string + // WebP-specific options. + UseSharpYuv bool + Method int + Width int Height int @@ -442,7 +457,8 @@ type ImagingConfig struct { // Currently only used when encoding to Webp. // Default is "photo". // Valid values are "picture", "photo", "drawing", "icon", or "text". - Hint string + // Moved to WebpConfig in v0.155.0, but kept here for backwards compatibility. + Hint string `json:"-"` // The anchor to use in Fill. Default is "smart", i.e. Smart Crop. Anchor string @@ -452,6 +468,7 @@ type ImagingConfig struct { Exif ExifConfig Meta MetaConfig + Webp WebpConfig } var validMetaSources = map[string]bool{ @@ -503,6 +520,26 @@ func (cfg *ImagingConfig) init() error { } } + // WebP config with backwards compatibility for root-level Hint. + cfg.Webp.Hint = strings.ToLower(cfg.Webp.Hint) + if cfg.Webp.Hint == "" { + // Fall back to root-level hint for backwards compatibility. + if cfg.Hint != "" { + cfg.Webp.Hint = cfg.Hint + } else { + cfg.Webp.Hint = defaultHint + } + } + if cfg.Webp.Hint != "" && !hints[cfg.Webp.Hint] { + return fmt.Errorf("invalid webp hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Webp.Hint) + } + if cfg.Webp.Method == 0 { + cfg.Webp.Method = defaultWebpMethod + } + if cfg.Webp.Method < 0 || cfg.Webp.Method > 6 { + return fmt.Errorf("webp method must be between 0 and 6, got %d", cfg.Webp.Method) + } + return nil } @@ -549,3 +586,19 @@ type MetaConfig struct { // Default is ["exif", "iptc"] (XMP is excluded for performance reasons). Sources []string } + +// WebpConfig holds WebP-specific encoding configuration. +type WebpConfig struct { + // Hint about what type of image this is. + // Valid values are "picture", "photo", "drawing", "icon", or "text". + // Default is "photo". + Hint string + + // Use sharp (and slow) RGB->YUV conversion. + // Default is true. + UseSharpYuv bool + + // Quality/speed trade-off (0=fast, 6=slower-better). + // Default is 2. + Method int +} diff --git a/resources/images/image.go b/resources/images/image.go index 1e78e8bbe..dfdae20c5 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -321,9 +321,11 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi } return ImageConfig{ Anchor: -1, // The real values start at 0. - Hint: "photo", + Hint: defaults.Config.Imaging.Webp.Hint, Quality: defaults.Config.Imaging.Quality, Compression: defaults.Config.Imaging.Compression, + UseSharpYuv: defaults.Config.Imaging.Webp.UseSharpYuv, + Method: defaults.Config.Imaging.Webp.Method, } } diff --git a/resources/images/imagetesting/testing.go b/resources/images/imagetesting/testing.go index 418c21685..b20dc10d7 100644 --- a/resources/images/imagetesting/testing.go +++ b/resources/images/imagetesting/testing.go @@ -153,7 +153,7 @@ func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder { c.Assert(err, qt.IsNil) c.Assert(len(entries1), qt.Equals, len(entries2)) for i, e1 := range entries1 { - if shouldSkip != nil && shouldSkip(e1) { + if shouldSkip(e1) { continue } c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "") diff --git a/resources/images/overlay.go b/resources/images/overlay.go index bc5bb3e78..2a750efc0 100644 --- a/resources/images/overlay.go +++ b/resources/images/overlay.go @@ -33,6 +33,7 @@ func (f overlayFilter) Draw(dst draw.Image, src image.Image, options *gift.Optio if err != nil { panic(fmt.Sprintf("failed to decode image: %s", err)) } + gift.New().Draw(dst, src) gift.New().DrawAt(dst, overlaySrc, image.Pt(f.x, f.y), gift.OverOperator) } -- 2.39.5