From 70fddcd6d4150a9ad1b7f76c4e33f2d0654c12c8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 4 Jun 2026 08:18:28 +1200 Subject: [PATCH 1/5] Faster matrix construction Also fix checks --- src/reduction/palette.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 7d4864ad..b5f10f2f 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -267,24 +267,31 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec> { for line in png.scan_lines(false) { for i in 0..line.data.len() { let val = line.data[i] as usize; - if val > num_colors { + if val >= num_colors { continue; } if let Some(prev_val) = prev_val.replace(val) { matrix[prev_val][val] += 1; - matrix[val][prev_val] += 1; } if let Some(prev) = &prev { let prev_val = prev.data[i] as usize; - if prev_val > num_colors { + if prev_val >= num_colors { continue; } matrix[prev_val][val] += 1; - matrix[val][prev_val] += 1; } } prev = Some(line); } + + // Make the matrix symmetrical - this is faster to do afterward than maintaining symmetry during counting + #[allow(clippy::needless_range_loop)] + for i in 0..num_colors { + for j in 0..num_colors { + matrix[j][i] += matrix[i][j]; + matrix[i][j] = matrix[j][i]; + } + } matrix } From c8bb050b0a73ac015f6fc53db894f0ddc1b90d11 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 5 Jun 2026 08:39:49 +1200 Subject: [PATCH 2/5] Replace palette 8 image for better benchmark comparisons --- tests/files/palette_8_should_be_palette_8.png | Bin 13421 -> 32762 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/files/palette_8_should_be_palette_8.png b/tests/files/palette_8_should_be_palette_8.png index 2bd7902d8fe201f0b06638b21d5190adec32aa90..6066c494496b165bd91d37cfad8b96e6dead003f 100644 GIT binary patch literal 32762 zcmW(+dpy(M|5qyI8Y*)AlnM!n+-9yJzrgO3u7Km5l%KXHXf7v_iWhMPEe1> z`?KuFD}JxsjwkGFRu3K;vRJI+6&CA=#bO>07VD73I$*IFEY|+f|K@ZSYxn4g$zoAi ztZf!+lf|MO95IiMwpd4G=HVghh{QTtXC1Avj#gPmE3Bh`tfS?_LmKO7p2;NA4-c5k ze+T=Fea6=Q;mRRn@nC;#o3Xb~+oK*Vk@mNCshfMW<&E8a^8P%HvcJ2zL?e?4bllSJ z`o`wQ^7ifmZHh#m-(20@UR&5C5TsC5u zyGe6Bb93F@@MZM3**{g2%bi1ihr9a=7myQZ+<5ESH*8l9zBvb9*V;WW(b}3jg3p}! zG4dUkI$KjXj6|S22WytzW9#7^z3=*GhU*(*C*i&2Gd0ca@A|4j#|oo5$NNf4GZDW# z@~2+^{Zd-j692obEoUa6ZS(~uvokl>ZRkTqaV7G@#M?UL3k1fiGs}4(EvxK%^!IW| zV^>&n_M`q3*Pny7y-BSfKK@7?v+Rj~U4?oNt1<76wg1*<*7YW|4F2Hvn@25aex*$< zNy&kg?=0(jYQz7Uv_(Fs?S{p7SyXr0RkSC3Ds!#MbS_00Bg4R7%AV!dH%3L7z}w0L zhYf!Od*vsmB)<2`%nwd2D)t#NEcjUq2J2P(L!;^f;|fy4Vhh{{QhZU-knh^XK6;sz zS^f~Xx3^2;C)bp~FHZeh`7hqOqhEw&mAboU+4X9Cc29fM0Z7XIWZm;P=>12Fo)6~T zci%aunRYyWm1YHbsuKD(!KlUTRgzMy{ex#oiV-$px)p|?Sh+wm`4_?dfLz^2F?vrd zwH`)FKQ~af4)?g5a@QPs!{M2dX~4cd zGrIOfTu=V0%401_TcNu^2?Yxg)f*x*_Y^PdUAQH6>8i&0YZ3xtidXpM`GjsP|I}(Z z4uF$j8w*<&+rOsUrfh8CaFcs_4?|pLVW1awYc1UqrYhdE-gj=S^ot87bLQMWC#-(1 z&#OJ;KN*!5>K9e0&gB>4WG-T_CQY6-`0vrrQ%|p5zb4>jV+GLP=z4ei^@Zn2++bhM z(W<}5-&W-q|Gb6P;ho5++Tab8_>czcrQ7pafYN#sYk1#(rnY@rUF3cWi!oeq)ZNCKY3JbJ1*_e?dr|ey*mR{*<~5U;%8Cl)fvz8={q}x~ z`_dBJ0tN_P`#V;p<~P?Gi-=l98&uI9^oH#vWAe2^5BC;2{{1U0Ej4uF+I(cydDOJ` z_C@lG(9nIUuWM^QS@5C!9Cavl^H(ftv+C|8)oWu8M2C6G+lFnM)!E=HLD5H%!VCmE zgM%9i0>Ok3@wn(nwz-!#A#Y%BIH=q@qY(@}Ja@W1gpgvxz4^v~n5+BCqQcVBwe1Wh zot=ti$Kg^_)4>u$6P**J@GK|<$Re95$_0VnH~l8p%M zr|Lyy99pq|Ev|(fah3wntLwH|os2mst>OkwnA0NK+jT;y^SyoUyb(nYu0VWgK7Wbl z@vJPp^wPPaa$M)!%^C~dh&C=w8&_A?;kcpt3B;i9do^|~8;(XnQjd7MRCr|-6{IWa z;D@^=87y&rg-@coS{Ll<#Ce8RSTdG83RmpkLrZ>rG*$%H^EiauqPbn7Sv)~gnDhgj zMdBV>0?a5ge}|aZxI`7ZPZ?nn0bv&NOVO zZ1Bo}iOfI7ZHOHVD)R7;tFbM`3OiJvi=}V4+kF>gKxJAfH!x~r9#&1Ut+AR> zC0cH#aBv-E`SRl;qAgsnSh)Vt^wpDH+D;?GWJg=y8-J7w+<+9r3aS z$I2A~Qq6C=v&FAE8qt|HAEQ;Q6KUGY)pPLVV}R$R!;A2y%3wx3llKw$P|*u=>)$5^ zim?Uckz1!MRRovcsc77@C`#7|&@?ArA>^u=G#XC?p1xrU`eo59wb$VP#8M~l%ArYT zqJ~&vxa(6-9h@$VbZ`{^quh>5j*N6ELKD8$UW4%C z!%jeL?K;A~l#)C<-PH=$CuPCG1?XCfJmAh=Bi>F(8m&4BHuTDIN0-^u906w*34%>Xa;>+I$hFYbjSmdk)Ysu_2 zbX+(nSkMuu(10v#jD$TkCdX;jOJ0@SOGa}f7$wLulukx`e8kjrWS+1wZWKhCQBP?w zRV=kvF0Gz#i9^enF%6~Aqa|tfb-p2%$``jjeSbhSitCoGmTvVNnyVP4ou{aU)l9cu z#y<9?h(DRv2;bv&-JDG40TwA#TU6*cABBYGXt)@*eOGBx(NSwU#dp&6i36_IA>aw* z3-+KeyiSCs;-oZe(hpZ?2o4H@=!Hmbr#}V8JJ+4TeRawG7Zi^G15Q|CB!!=BW(KoG zh+=X!PMzDF@R~WZ6Py5x&C+ko`n+NqxS1`LCXEi(UA0%s{dv<7m}-U1_?-i1 zGKSjn91#3}64{iFJn|dfrOWWqL=LD1cyRqWP{;*p+YpGP7;T456*yC2(OBa&3FE5b zQU8WiYw6hq1OBxR)6&1(l`00=S-o&VxzZU@5X<8Bbbb{I(LG^I75Vg>0HH10vWNK zE-y}3a1k1nJJ+}mK1x?)5iB@My{e4DEu}6pJ5@x&63c=g> zjPF~0(c%!?1GFDr-q+n`%Wr^R7002%*gg{e+>N%lS0QQw@|pg$p=|fa%d9waslrQz z#v53XY2{D|_>Clu!s5mJ8TKiR74^Ca!p+XJs5t-|Ww_VmB=g40ub~MiiER1V689D| zrUr3*vQ)W==e1X;|Nef{i}1HLBW7im@RmB48nmc5YUIDYX;f(-SUT6i+43JX>kRej z8JuB6W1)o3=hWY6E{Tr57iuDDE8;_WTn|_zexagBdI#SKq1>zRXyn_ZX@y>JF!7>+ zHuG{-FOmkp^Iq>vm}-$~ticJL+q2?nuo z%roIAH}WL$ERz8d_y#XKOU?26-TM7jwL(Q8ywT)VB3e?D_O=|Z@oBx+YYGE659O0v z*4JPvi!;tDF8_SRPITfS-f3J-+U(JiIf+6Zuz_E>FTsVWW1amBP^ zik3>m(a~q8iXTN-Hz68;A2*U~N<7VWWblpFU5~u5p%#>34)H2FiU3Z97KWx2<^O_p z2cM;gF&g0orY9Jdc7$N^90Ibx9p7l{d6D=$d;+Sf8mYsJIY7`Ko&gS)=2;qFbT3t= ztoiFl$sT|usqEYgRfnXe)j(c?#vrd3%sxPXVS5o#=6PH;;93IL#~~+SXDdYQUC(1d zCa5|;o&0SpRhmk{wst|KFr3%!6m(eI)gm$CL)|#LMU>x+sw5;X|MH@^ucd$_l#dE8 z&<<9L;>0A6DuVvQ4F4B&GGdwsmWm%NL34bBwqQr|`j{sx-AW!39&6y%sQfa^yJQce zi3UKc#GSCTS#r$L;XYikha-IaJA-t@1h7Q}Y;%uHRrF`oU7@D8Zh%(;0;EzCe944G=e;K{V$Flfzhs}Y4j4B5g0_s|8+v-MwolJw)7s_!lmjJ2lcjo13W-^GY|B;D&gz$@B=OlW199`OxQ01H&nf| z`+CqQd8O#qqXX6~;IeCJ6aWyn**boKf@?$idBH%cB-t?c%Md6=f#Ct-oBw~IMicc|4T|2fK9b1Af zwBdmFmsrSY+)2iL^t+Mux#F8^kKMbEpGScan&Lx~8mNMJIz5Q=ItN#hc11f-Sn{jO zdCb#ahO%?6&G&N5;OP>QfZERBe5{ZP;r*DIvM}EABvtPL(cD~g+sd-vZoh8%rjdzw zN18?vz?0%G)zL9ejktiwfCeSiL3-k9qdiB&VHWQqEnbbjYS2#uL{JGMOPth16*NGT z_Hnu*qXaEkou3%GPw0y(wNf&2Q_oO@2NJ>nvef?|tinHn%Xhc?wz(3uwX`aKtSO3J z-GX65=+~+IJczpmAIzP4)zkg?#wjM>|9-RkWmfu@pHt6uIQr-aje-F_w6voKlOr0f z9$u!%5YB7WEtQXUU_!TQbrY*2cS--&uT|h1KO(Y+lS>7Z(bz{G-1rtU%oXtxOqH>M+q0~Erd0?8>SeLEf@hht zk#|1L22w1lZY4C53z}KFtg@3h9<5$;9wFsANB}=OFMIhO2$kG$VRiY zRw0DTI4mw*zE4b?47uSelVx94f`VdGPC(8Pr1G_Kz`V=xL}tYA*2b1lPopIIuJ2=| zIX5yV<<}IqXwQd5m@p_NlJBEt9sbx+t@#IbNTG$!AOX$>; z(rV=*lIP^5I{l{eawdI3lZH7!wPo&L=^nnnd)g+jNI%0h><7lz!l920uVXu&3581( z=G)(7ymEAMin>P330s~WJ2)>4@<$Y+-4lhvJYv6SMIb)>y@KH5zE8MTQD;@vXei`< zq#bL*958tfvF{DdfI3!8(!EX-H^xd`z~$0^*4BTH8iv5t7y-6;esf^42Oj{Zhl|s0 zuhlw>8FpbK1UuMQ7m4Me5HgDgU5)T_RD;;qoMT7O-=d7(EO$_@7N@xq^Ds?04OoF&{rE55)qcuJWtwQfK-#mfOX@QY@Vb>os52*uW@ zg;syQ8125I;Au>;0=dWzG0Bqi&(TZBW&+vqjtQ!qLBRnjSlC@!!}>nIVG zf$JN&9cExq9?Ioc5NPX(F|?tNQ@~Qy7jbJF;l?L474ff#PiXt4rTZV_6qb-Yz?w(X zO+MD;O{mecl9LMm;$)ZQ(dK{}R3hNe^rZ{Jot7>)ada zK&lojY@t-tlmlb0(MI8+Ht;Q+@NmgRGghR4W~mFP4t|L`Ka}%?rcj>cw+oF&Ops0@ zn&0Pn(^k&}J_S1>1y}BIC>iBF#dt6kYSPpW3pHSmn1}lr(vOZ)s?dWfc;?NKvF&1O z`taODX7a3=8dYJ)c{UyR1^VaK+^(j+!$-|Vgb{N;FtbIt+j?a=KCzcesRI9d(_-mh z`r`_?Uelj3kSQlLTyTbv_{c`B=onBbE^!NF<2f1jrsMLkgvLdvPs)2kP`9r&{<*D# z!xYM1XbW^Eq;se*Mwh0NESZxF+}rJNOQ%6*fmIW?RDBs1Iq_@M~nBQth$*tEmicQNHRE- zE3&gzFL>A2$SwF!h-aKb(WqSfn=z%|QbCn=3xSUjQs9?cd{R`iiGQyEw30NeNr>X$ z$Gym{RuR0(bvQZBLe9=D8T%UvCY(9|9EW2<#1C@pu@&`j64+;ndo_<;JmI4+mkHFKh}mW${OjydcCPmbYV}?Kf70ZYG2%O@V~cJBl_;GTV8y?1~0c>5zIt3)`)hG?--igeLS1+|_5F`it&b9Qu4_wh|R)h3EPgjTG z`-Wf948fZEI)fRSO^naM-FbS!p-geB;jqEWB6DlP_J=zUs}d`+KwWb7@Duqq#-uoa zR+56(07tY&MCOa#y_!RbYONI$P|mP!bb-?s*mX>5S?ZHSdtl8Qfvn9JOm*yNfU5Ob z_zb3IEl;r_(ctiT)5NP|afE*%f+k=*ltZ^!In66hVFfJw{pZx=3w#pn@iQ?1^L=nO zf20~6rPF5h^4JD&b#Rqy-R>U^E6ekYs`FqSh|q5M^QQimlaiQntZdM4afy5Iq(55^ zZp_u(U0{m;D2YtA4@e(lzSC$Xjf6rT@% zD5-OLeq?FsLQB#{j~HdGcih`3vt(#JI6~W?M!Vx9;`7+3x6D@gFEbb=ujq%q)_ccIl6^BeSL<59LZBmk=SN6$n@~E%(q6)ecA)h+ zAh5B?BYgESu;#5m`dP5Vv2&gV#JYG8y^ivZ(5Hbn8dgQI**U18!nDbA!EBcI(83En zkQEUyG|HuXxjvFL`Hdl#SnG&VJO(IWLu4_KTMWKhZD!{B*Hdm+jSJVYb_9F3VYTvx zL51=T=h_M{#fO0x0I~-03FkE$TYk3|+6S!OMzhnT)ArEnI&JESCa}k_i<@3{pF(~V z>z&(_AlmIR_;kX3bXjloT zFbQHBEqy+D>hXK&+Z+jJ2syV=Vh>W6#c=sM$M7-YhcpP}P6F{%OH=JUFX{o3m=ys_zq|1yU3 zCm>@q1K`Vi7``;;+nz##mqbYzBKSx_waa_P^U|t^tZ~iJ;Q9m$>ZC-wc1ueHP&$!2 z*sAc^u_^(k&qBKIl7)xfZEljI^Pld*>~~hS5jqVMvXfrk9Ge$_r_nRILq&3>VqRm9 zqg`7LHl?a;U9$^DtB;!e&eGKIcK3n3=Yb-QaZQPVW1y#=7?!X3a;v-(UtF*d?cGi7 zFstHgyU-NP)Xm=;J+QCRe7d!_9SY@WfI(tCQ#`0^@gLrta0^{LrWk2n$WG%##1pOr!DtcA-DshMZ$6?!NwW0#%?( zDZ&KW4j-TA!6Y_NbA%cu!P4A2%N|2MU-&7&xciUvDM&NW)vH2<0}H8gw{P2m>sS-( zw4Bre*WYlmLz3iAjDU8rL}sjo2|)j^s}~biMTik zBF=5Vw-%{H>WOIN6t8vGtFye9E*&LOs;w0czMyvh4lz2SR>iWOtIC$3%gh+QOT=d; z0+TU8l929`VVQjz;tU5jj_GScQHKK}6D~U3RA^rj7H#@~6DHu`!9S`N=@7 zOvrIeh(NfII}#_jHD@4!=Pev1+=Cs_K|F=SL$`@Lc^W*MlDSw{mYmw|HH#T@q8rT+ zuV4D44okX0RczzdmE^aO)@ga!=w1u_n}zk#plf<2Szi5tE1A1h(L}&Hou+-iI)yp6JAY4s6OoE~Y>f@6b?HX; zrht0%UO*9&0wd&|3g&kQh;37sd0dBh?WRsGgkFlv|{jyQw4e-pK(>T{2 znljwoyVnr)*kOF`E$L#m3@O>1DXklEvck0`Xz9_A3kUQD zY{sKsb?as5em$7q*UBV>B*K05-C4>8Ki27C(b!Hxhz{hB)*EPyE!^xI$AaU$ae6_X zdLV;jSfX8QBkd*r)1Yn&n>ip64@|yv^z$;7r^&$-cLHRwtmXblWNycnPw^Wiso0|B zpxVVJ))Tv?fA}kwgHMW<;W?C7fC%}=xy7h1vG56r*R&)F0Kk4zlNlimu4=Ie=o7hJ z#PFKyH1esjNB{VUgaM8?m23C!^6*$#`Gn@bH$xS40m3_mmjwt4yXv}(ebs1;5Fbk0 zbn*WC{xrzd>xa;!9N*U5=22;w`!k*RIere5VW!k?BvAv#u*a?2y9NKJR#=6Ds#!@- zri%fCwma8GSBK?Xge;5+DgQfub8iQ2cbuhVJH=cRK={(ma6ov%46!H+>Fq>1Z!GlL z-PQ8N9bWJ1tLX9T;z<1nRt%btmp6cTO4BOK-gt$J%je64SJFg7;>~>{bvO$1Xi@;e zmHn(rANJHQX>i@cO|3AhHxARr=)0TuOX;PthD&-Gxq8vA(bGEEz!R`~fLS+AgQ=Ue z*4iW*jvMxYY}5v8pU_pvAwyEv`LcM2ueRA0E7Vg4wxOq=2VV?$r(s1;eG2$m;p2rP z;_e8By*~ZN3}uuRjZUMYx z^L5EOCE~W`=I1GMl1iV??3UI9@Mh=}$$6JHyC-;GtJUro4voscv&?Zk%*?nWA6BvUT_uha3x5zpFgqUCSmE5G&ykq+A22+&g>RPYWLK@&-4w z&fV8P%xB&Q(HW}+uZevc>xe1#$u_~+16@w~A0y5=Ny553kt(AiJo$>~k&AnbifXCV zXH_*qv#?vz-DQwZxQ4{;Kyp#F#6n}aEh#niS{-G|vR;r7$4C~e&OQ*U7(G9nt++Wp zm<(DyN_b<*k<2(1qKpA-xVTbwp|O`1a&rw_hD~s5;PG$%geVpg&?-ga1w*xo+KrUk z^?rf@>8p3Hwu&M*HDe;r zXa(w!HOOnm$;h*$0-4FsPYx}T8Uf4aO`|Ec>_9rezx7UiRoz6juc~jKUO*ip^J$QA z!I9~4TPJEgW^SaG=3ArvqkQ{GI?t9y`TArAk0_^k>J8dx)WOWeM?}whtC#CM*1X9K zX-n=-69)vu`Adp{&p@vaq@;|FagT zJ#6S{tE!CV3Bq*!Q!cK}14AYHpxQA*2g08O`|F%=o98>X{dwU-_=&!KBawecZ!Q<6r0};D|sh6T?Fi{rU0=2dMOj@uNhcQezoi zU--n8`eb$1Qc#J+t)W2fbGx>p8^Yo=3l`5OBpPxCr z#Er~gq@DQ@M>pEja!DtQo~EjZ2k3k9qvfUAJ*9Wcl?%is*NG=*eZY-^RR;Wn6>ZFX zs|ghEl_PhtQ5noXt-0@jf00{@T|4c}R=1E`fJ@VTdYFC`SK>sM-isIS6JKQM{O)!i z&hM$fj6S^F@dZ1T?#t7YK<3`ML4DGVmQ6t{?8^TomSmgZBjHex{Nm%d?wgS@)(#C- zvvuvgexCp$N8beqJY?+V$<{cDueI-zI^!<*ZLO@nRC{Z6A!NG_3nE&J*tStJ9x*Y$~4D(z&(oJwGLS0{h_;w0#wH_IW zc;peNYk6+REwM(uuG=bIyw7*#?IU-GLcewaiSkkW>W0%A}^gz!9&-o1Vj|_X)b9JP5yawrVgkxDR6Tfy3WSF8U`FHs%c_rU&=v81=;7C=C zXfy(p&goW+?>|$Q6Bi-QyoQx1mB>T;ZygO@|o)3L=m*n+G zgF%&Cu6jxQW;gssB6Bi!NyD<+MMxECgenA`vat`;>Wos**K%_ev{1`9m=?)nB)H9= z4u&t({I-ZeYdridy4vEA4dm%l`j8UMoAr**diXedJA9tP74N;K*dI%UcD=2>*`Qrg zb+*%^PoF&p)@0MU4>m-bE{90drMf@yN>oTga#h#(U1Ia~Kphgm@^d_%XsUrF=!DC! zxi42zphH`EQjMM~G!a8l8xX#&)MfR`Tug-do7Drt{2bd(pz|AK5>-fJPxomSb`87E z#0FP7l?QDt!NtZ@Yu}CH^tlvHz}bV;AiM5P%FnEu3Xe-pm7{Xv8H*Q20aQZiqrZDfCbMicggpkPF$d zjy_v}tB3zYM=hx!!GOktH-!pAfnz=E2~AEKu>f7Q@(E;x+FmO;XLx@wA8tAPbuK>^ zno?PQ*3TOM`B>(B6`Iq3)#X`G$3{vLEC=R5-Ff?xOt!GtIwofD(|Zied!sFcRy6U$ z#YtwC;#u0-V+;C|Rh=Jq%`VYeCzVq|_-NRgCdVXCY^ZmZuP0VW49i0fGW0tDPW4n& zxzyqRv;$hng|*`2FNvXf`I-+;&@LGw9s5ej!q`*n;Wb$rmL!G7Ts;?34(Q1$!jy3) z*fY<86Zx#Ta43?U!R6l+R#hV{8+Bmwv;Zo|rU}QSI3gS)o|HZu2)vF=ePS%b-qjHxD|Z4&RRN zILaD0nsy5d)B^@@O}j*t&r@cFq2(IQSW-eXe#PSYXZMNjp2TVY&6I^k;U~ zFf<&f{k(}BCtM_X5OvO2h4Qi(Kj-@k|Dhk@UXYFzie-Ge0Lg+Jc|8<>?w3?0!7Awi z1&N-3@DI=}^{g3~ZI{Q22I6~IkRZLlb=327^!}Fm;Gy+2p;$W|lL0}+PS6S?Klp8O zBOC5x15I>fQ-{hE>sLqTy1EijzsJV!F`aq9l0jdB?u=@f){&#`i=bZLSe;I#CE@gQM|?s)2EPfBrS*U7YlxdxJ$%?GOI>0OHx5gz7j6}&jKFA zU8XKSNNa9-{*FP?DEpN$GkdsTOY^Acpfm2n>bXqnZuhj9j>(7D^`QD6{-~=~EHKlE#PUTK)Y72cCV4lqhI1A@(1EQt*!xB`VakJ-8qm zZgf3G{cd6hxqe1`nseKbD~#+WXX9|u^!~w&nn?dQjy!)w|t&QR1`{FGbngo;agNSPdicGrror}EH^Y4JnO)=%pD)0Ckn!zXO-A+t@^?r$e zz(_GrHn}9mg~cy|`lOk^DnPsV7zdA7=6j%SS%)q&7_*by7vW8;st(T?HV%BuI*(O! z&W6=&MO35#^O`8+wHa}#;D(XL95|U9Iy?UX0oc1q918#b^EqEiTi+jPR2bRD|xhgAGMU?kl5Z)lTv8`D>89GQpnyx6m-;k73irbl1q9#jk&pb9#j0(LS*3w zVwiE&ni6y!(yh%&95si2mH14PaJ|~Bxa@*i1Uy6mIVkzI4m9G0x{WBm^@FL{gWxw| zTlXof?o&Xo%|pYcN1!1DY?Y00P`-n&nNUt1eIE=#ySQD|KoO>pqF7 zbcK6T=5oJBl~VjYX6Z|(BX2>VU{jqxgo~dcVWh3I1vx8FsHjVBN7jnj28x&_CVzt{ zv$iR?v6i&|SvD;~_)^fY!p{(T!rp-X7kaQro&@Au+>9QxBl137JXzb2kT-3fui+Amp=F3Hb_5rKk03oZSoIwj}@nscJ1 z@!my#6n-5*HBQezk~9e>ofUWSH*@WpYm(msIuD@&CP5jJ*$B<%$}d9U_PB5E!6(}k z9|XmTznT;mq~3=)zo;o2YDhoNXj~nD#bT=yc%%wJ+Gpb~4vT0JFApBBJL zt9p@7*1sr>HP0&(J!ZR-BgHPprcEkK`})T!X3;*t=4fA!_NI3H;6J2roJY%g?`Pvv5H55-^Z*`jIi)$?&gB??CH(HB^_WDi? z>J1`|fp|Px@H=Y-9!4>r0epHqZ5+RN_Apsqu@0v}BWb7d?Wd;Gf;TJwoK{n{rKjrC z4=(`|hpMC>r&~^B8F)0J(Y3LWtPgro`<$+hkTGG<=^eI6vy6k!PD#JG~?T)gY8 z7;U{rt{o;(`^1A4hpFaoDoe{0Spgw{97crti77>r_(|h1gYb8Mq;OlP>Kw87f zT!;CDG(pLtEX2?kfWKg33;#7xamN>4g3hyXYLs`e+(tyit8Fedh+4zb&6=7zXV?}3U#GlB;1&crZO#4dPj_zIg5Y-Q%TKoa5W#86zt_*R&WG?}nAoQ6wa3l2- zu};-zr2*jVZi*MW~G$9#-9w*OhguTP-Cm}Bn!XN8gD$H-wa zWb4rV-DJ_bvpnHuI)c)Mbg6Lg;`jo#v|4bl$jCJq{*spocP)eVUAn143wt%4KY^vH=H^m6fgd0+6okXY0Z~4K3KEX<5b7;-_N>|GgV3 z+}ZrkMPdHb@50;5_PY1Q=FjjOgkMYgppyUEra$$=`xj3HR7@8i=(09Xcbu=*{vwmM z@>L)<$-zB4tn0r&$Vf$>8oXoJM#S!#C^LU_^z9freD7#(eA-9Z|6HEfP^$wobga^j zuNuns-L&_K!xf40?vt%$WIgGXxbPq>aLYQsN8F;)S_%E7o{KD-pmq^;izG&B5#G6z zqR0zwY$po^o~WA$3~5U)(iy;{OF)WPjh=UN{V8cjkbIkKFacN;ydut&Z13ix1S6ch^YJH9e?*{;U`ctTsL$j<2)m ziF(FQmnj|?q(_i%qDJ`}$9{amPf;$l7R<-`OSJe9Kya72b*Wb&-6LUanowpN^hT@C z;_5IAK$pJ~e^5ISwfv!*Z~}VoT)h%lDBMCS>m!nyrPi|ZkJn|&LiSL)=d@X7Xk`ksyH{}sy-mIM0_Ij(f(Cqpys zlK{4LMMZ=43EscSv6hd%j6{e%8P2H_>~ty1-Z2Z=GTTDAP&AJlFgtAs*r?*k0F%T1 zVI7uO^7<{Zpz)6l6)5)rFIJ;!eUBGo48{82y4#wcs?8M(xNW?Q7rnCk;CY=uU;I6P zmnjqip?Yaex>uKlw0biUq{t3z-4l@b>zt!iVvSvgT<+7S^f!FW|4a*>fDuGZtV59&Tk(?I?Ri{c9WI^ zxCiqb>4=ff{}U%9W0h-~OFwT)RY2i+wM6yT4CP_5<^!uxjY+lJ23T z>76pb8Aa2~_lP8l1a&e3Ohs4Cpc}n>|3l zZCDo=q>}_D47Kx`F%lf3?5o&Ok}I~b{EF<9P+X}m%muIYp#z`^GNgO*o+*_4hG|sC zDUMGqvE?PTF^E-)azM$HwdD<)UaNEimVwyu75;(TTxs3QLYIpL1Lv8viF-^WH};pG zU2VWW50Ms;yxF*k>|gvopwffS_4i|TjJz_Sv4-vg4JLk{iC!zBgqqeTzFhp+6goDV4{5fsQ_ zMd8y9%Gtt<5uU)`zU9)VL$2I4FL&j0-)1DTwcti!s)&aoKX3hIro&45}ry?O9p2lV1fs7C^2$Gc&4)vkqqjwx}|a-Gb3lM0PcK8%bMpSkJ@ zub*u=M~LE9&Y~fPmTy!GZbb~w6xPPD@7N?8Xa&%tDV_1E&(Q-ZWb@+n!Kr%+-~*Tm zVS3GT=(FW|1qP{$tN!IMS~5PP7qyhi*vYF?ZXf&4eYe^R7}Pm`cbU4`SGhK+2_&#K z@KTn(G^3s$z2fOUivpnlB@WVc;?&EB(u>W1j0S{qMv!S_391j#dNG$v9ak(tn(zco<};NPA_U+-v)kZ zJ_Yj5I=M*9*zHOd444)Um8RrgDef+m3Q7+ZL!?rABW9ZYb#a#Kyt~e|qO(eviT0Y& zmSo$>jkZs&&vl_oD`8Ad6K3D3<6FqsKs%4b3dhpM$+UuX41Z?-Kw6}Nf zt%!e7XX{?JYzHm2vL5?}@rgt%_sEp(-Lufu5#`;qb#(lbDsIbx-Ez1U`S0VIOCTJ- ze8;%GyR09pxo?!sq1OvY`3SBj!uf(< zSy1DTaCc`{(4Y4(PKbG*Uzer3MFIdMV^{~+nI?SO$kNCnmIlo4GE>b| z-ir9Mbl>@6l%MzF@f%l$scKee55f9=kjEG|KBxI7Lv0Q^L%Mt))zDB|d&x;{d1$#x z>mNea0Cy0$n!^3sqsT$^%czkbT0)vqmevuW?3dkp;D3iv#LmsMjB-Sr&t^U4-2izm zOP=}QlqHI#>T#Hyg76#;s@dUKwap2yF1t2z_Q(gSFbEF@n5A}Z5r3kC|7zMe?3-Ep zEjUd}(Z1;2{X^zWb+$|j8bw6e|Lcp@E=+N87%)~*Rogy4o6yn-w zq~}`yWJoL`wC|ga)AEf>r{zwqhZTZbtDr@;LtL=V;ikbU-{hU5Q??N+Q-w%-p%evf zoZJc)Pn=qYpo>s9>(6OD4Suo=HM-e1u@`ltZ6G@PB-Y(tQsf&{nSA$bDY$Y;b7ZYM z?I{>vMQo<|*w;EYKj}RgaZY5z{ZbDkWyJ`>p}Fd>Sl-xmV$WDI;_JP3p>xaOS7V;0 z84YK7NZ<{BeaCUmmC)|WB{MD*&fWRhz+1r{oH1J^_7AFS)yr}8C_M9gVAO`k>wPvI zw|l5N&=isGEf-n1J0LCd0Kj&fsyjG#$+&}g1Gb2@?dzuDF`TbJO+5ud?!r|cg|Np* zREpGA`un|AQ`bA20FC<5DXHnzm(;_a)-H+by$y&W3F6Gdb9_3*McN*3?jGFk+sK}- zDhU7I4{!Hkta;JGye54OwZp)yAtm@pJSdmksv8(_bEdys$U z>EF1|4Gw|R;mOe5;eaHo!+lE!E{Zx_2<0MFhpE5uW0tW1rG-RXL- zbpi;R$01_8KyUCp&JrK|aEzqfz8U7Wyx3UI9`ec}J~E6fRv^8Sf7LVFiLifXU{K

<(?t`^EcPJfW54~sm(X>$GNpU#J1O>r(<9=LgMtvRwV4VRn)rtHwN@; zt#?R##2bMS@doJ-)I`0qUr0*T&ocXRg~zXf($0UU%3}}A+Z@ahsbsA<;Gdq0fvO0G z$II#OdRH9WGJLXj(2#rNAkWB0mRE-^=_E=>&%ITjN9dhb>yg_Dz11EiNS;B5QeJsL zO3=QgZ#TJC2GEc{gBia__d!q9{yD!?oiIB&r!?O)dBVj>?M%bOxCTjdVM_fZ{{Jev z?s%yGKW>EVB9xhYBRga}M5ttEJ3^9#aOXHHip#F-y;sK}8HYQ2uaiT@IkL`~;f%9> zexLt8@5lS~`Mh6`*LpsmZ|;8vGOan`r$A3o=+Yk(hRxWsUGc!99}eB|9t=(2ozOBL zc_%w?={$lEeZ|b)rLO+JTsM#uoO;_bqZ9z%1praRu&Zp;1k2&)?ss!BkQEA+M_G)& z&jD6Gm})HQjSW*IsR$SK=qlZ8xQWmBM=#==H+nThoQ)e(ZW54F3CB2`%;Iy*TcbP@ zyrv7%X7}#}EqCAB28*WyZ!Y_kf_%U9eo~19J>z|JVI$11n-dYhNfgoWE0=bN$9kax zXdUT@oy5j330~iZNnbuf>MRVH(RY01h%>ie*RiP?wm#Z3L;YIN8(!kJ?U7G#rvz37 zvC!k>$&Y25o*>f13wLG?m9+4Uj{9$PJrR$^xszkZ2iY#W1U zNaqfO@a4nFH_NZcy%RGd%6&;&rhj^y5hXgl-qCSe!pSk!QtqV9uE*T|S^gHQQEtE^ z4~HxjFk3m?$@B7pZ?;~LbU&KRs~;?(xQV)!J+!6k)3rgB`mw5zi*y%Q#Xq%ZI|%Me z8>$2+{vN_p`>Lk3CrPCxjygaOr_ZC6HSsOM8y2hqnT>|+udXsPXCL7U7n&{;oDM(Z z?oV$stWhp4Q9zvNB~Fyy3VqJ;@M&;AZyTb#`F;?qnFzt~KgF!L%Bp5Vxyda0L(pPkuS1M_+NThr3@5llRLT(3fE$hMSY4S<01pp<$FcznjQW4FI$o?p=u|3K5O` zY=bz(L>=AcI=sC^*tm_vf=D3w@s|0|0t*`k<6fo$XMQEnxe(RK4@YyQpHzzuguHmi zpb=@UMd@DCXz;1u;$8u4&aMQrdXLKy$fn4j@Kf{yPixt|`$gcmE1WKKx!24uH`2S? zDysIWlxWwQ+Gcn89d89tLSj~_K1OcQqXfDagV}Kv8z7R7tq-%c%Rw4LL>Y-_| zA;({Rngm&fE)9org>t>PM6Tc6cIvk`Fr3O6<6O(sS8-l znTGD)BArZbWK9OTJP~q2u5MNPY4UV5!0FI)+XOr+^y3;(llQ$;nh>b{%&YB8r~3@K zmh%S{d~~>#;GyV^Q<;-daS8Y{N^Tim_D;0CF~J8;2)J7*bWqF0_q!?oqCoGQ#QnEwHiL1vH4#AVyB6b>4o%ac4_ktS%~d&33W6F|0?JEklZc=bK};QvQ)4f z%B5a?bx^-GFDS^VpyMZuW|w}#)(75NWlLzv+tx}I`PNk#kJYfi;zLg5OVAPJqs`g@ zK-=t$tM*EB1km!Gx36l7@JNH)T=QZ->wW)|C7iC<_a&azdEZgg(R}I869@kPRs&gi z)$=}3?{#kaxa@NswSF9GCwR=>|6q-Z+88~kbG^d9mNZHk%p3gurVk*7JGlX6G=j;o zJ%*I?`6KqZYu31tg}{-GrJy-EW)EjKkJ;wpZPcm#4fK4W!*)I^hYPg5H~0w0Diylh zuE(*D5^H2!o>}b*Qk3;N`Wi^G&4lqHOm6f=^ou7DO>s_A&ES#?8l}0ykPhSCFf4IG zpNZseu_Lpp+#a}kK(W)z_veV4(y%)*uV7oR=?EVxp!P4~fUB6UeI-)ac&hL6&9}r+ zM}Ui}9Nqk3(v5(Vbo89DLyFT9lr=4Df^WGX&6!YY80?tFP8xk!8A^e<4;EW3n&>nf z1p}v$HCcs9nEH5hrVKm&H12}RC`1W*#Sy(hs%8@kjr`aw6?*eq;i#(0j0y!LlV{>S zD0`rPnTI6LfbK8kH-@CC!YlB>zI(N1r4{4-M5S~gcx&D`RNOZEXOrbXcgwua-pssQ zT!WcNl%|~bn#ybvdY$O8ik*rlvwH)E zjea7(BOuC)7&a3lg%3R54YPdzN^e=(<|=$|-5#O&pd2M5#d;T38OI%fHF{kxP7ugV zqP+M;m=tYm{fJRavx#2{%%(=cR%>pJ0OuAwmfl6#uuneFhSKJYVpPvxs^oojRtSyI z_j|r7eKGO*5ch$y=EdUo09#w86KXc)EtjoliASGbG+dDkDdBawJY zYTD^7H`df>+~mTM@%W$ExT#Ts=Q*`?dzA%tZTw*h@kkcad~Ke>b^c3ctsHVDd@?Jn zILr<7b)IFfGeEgQ|FF~Zb!hvWecX@F?$XgnTN5Ga$Zu6Yy>Z+2Iou?1laHZ?J6@>s z`6zY~qa5A@P+Pw={QYms^oeouFVlCBQ z%D~;jQ=2#cSe&UGjWjt&&C_`v6}rBktFNAP@=DwSG(#;G54R@I{@uXC;^ zK6Mw&vMWO zGi<`ikh8q%X$uM~rQfJ2rP!A$aB$n-Rf-V8*TItq=tdO;|KD@(izVmY`H$ksLx^6? zHBW)p`z#-jcFOk&QGK|V@k0Tf$8ZS%x_p|jnTDcEX z@&o??QYOWzcXoSTA-q2|RDP2rE}d;3d;9C(+6Dm%AWi%%GeNB67G{rawj=7()oaUU zE9RmDtF0Z9o|lJSmz*T(cIK=$BJTp;l=5Qf2|QHO!V;^D9_?Vv=8j~xz?>KH+qw>$ z|7VDcojp1)BQXm46QBK4;RIA=6zLy|`$px)6ZJOa<}@j(*p3HnNVF&flGM)eiKk;8D1p$NKyT zin!;kqyaMit1jl9n47@%rigiT#E|UzJ%n|Nx3iU?cF1D7wqQ8qGKS8KIlOnUiWrR; zrVhi2{sD`XRwmC~eR%5k+RzMf9d8>4qoGYi6moY|Kk5G>F z}lQ9ViS<1bZIp{^L`-*~4{a`|NQZ7*nKdaiOh->9=NJ%)cToBL$);ohug z`W1&obGcA3S$rYF#A2+Noi|hQ=Fa4s-%aBcj6~>@Pjy*8RW{>v;P7MhdIZ9AcO{iB zAA;MsZii7v3=ijL7_I1HvLivIy5}tVL7=l^)!v16t}?!x`M9f}BqQBHxw%!KaRkiC z8r+iaZe6Ap(v71^^?$p9?^2n zUMeCTIY^4`w2UXOD-1ok23OHT`NzUUhw8N*O^Q$}gX!0N`MD zH=5v->$}&p-Ic>|wPXdFU}W@;oTr7M*ZT@M>TGXNN_>N4N5ht!(T;OFlgxUb*Xn#f zePdyM`i`@d$3<2RQfx-``5q?NtgZX2v_1%l+VAruWiF-dyCbH@**}1)6!?*P~E0kRf`GyLiX;@dQ5v9 z`9ji(@-o;&T~Q>e(Y(2T^>fP~N_bhpyHw}HC(HHHM~Hg@mDW7lsn`wB>GzU11ya;O z7^08ff$e)z^59MKEwvXueG7vf7F9mUgJR?0h_mbkH-?L^XjOxtg;wQWiT$);2xF%6 z$(u^gw9G#KcfW&D72W20{s!(+};46QK?Nt0!YQW!(L6q6T^lO7R557({ zKgG?>DHvwF8V}q*DoFGV`Jj^cZhvh#FFVGsU5#`4+$2_w=Bb^|2<_C|zP*$5fikHj z(*6n`?np{5NGVJPQ{pCldWeh_6EG%Wc&dD$RG|uA4v8ymyHKIc8sW6UukL4>r7*N{VJK0+v#|X7#EZj3Sg>>NqeZzN4X3; zp5bUYI4(Z-51a9Y5~76~zxOY{C9yP}VQY}nTMww3)rfG>FKW_e&3WoScFJaVp5SA) zEv?$!;byakkDX>|Qy+=JcVsQ-h~TH^KL=$=9_iJsLdN5P33cs!e}{etzF`qiAqa*J z)FLx_zZ9TQm7k=PC;PyZuB?lPoWH*V4aNLr?WGTB!2jaXc!uJSDlfR-m$osFJtq2_ zw#UOJm*@+u25yoDJ#3EhEn{)A3Eke*v)Wr;F)ClYS~#}b7Ex83=d<=oPhVeNv^FX9 z;t*Q|u*ZSIYc#IrgaKFFZxTCOxRRLs1SQ=f)yg{FkWR%vTYrw2Fw(zD(Q>dB_!|C7 z&n5)}fMA#9b<{pz(6v_rB)hG|RaC-VyNqs*6I7XN^6qEK{+3GIf!oWra!ZD-v|$6v zQlV21@*LO%olMCh)>X|FI~91R`Ak7KgF^ZzncIbQK0p)*GVUQZ$4=0I^n*3uwdRP! z4X5x-v%3n2o^H#|+Yj{HlbJ?0>$>FkL~aDw(%B)fj***Bc|DC2q`YdE(#+vD)2oKB z1#<6K+seG(4~yGHx%(Y8-+mUCSx>G&GIAp9U>2d0GH`0J3s~tL(?}dpY04(}OL&e| z$JH1m*<=WhuC2>weJdyA9Q3w*J`-icwg>#cHix(*B{^Lv$>Lco#W_@$yUZsxUL6)Z z|Cclpz+0Q5-7L9qwpXd{kSi@N(B$YqFt0tu)ADn$P=@^Na@Vm49k2qpX|dO;91^u@ z*$*FoqD_w5Mauqc0n7-8xKBpzCGHjt+Wp%7QKu(>BM}fl)uf!663*+ncb6Z6BfStnmt`K6{%u!6jjCfj`7vaPBG5R_zhi6O%jL_Gq62069nEnZ^(F+@5&vy1$3r1)uAXBr(&^n29Q$(x@5N|G+s zVrrf!)4F0x($V0{ZT#HA?Sm&Plo?Z`Ein)ZQuTz9EnK5{aBE_WC`F<~{P;Y;Yxyc* z_LspbXLb4`C%*_-{9MB-THbxK!l5cWRg0ygaABb(?@%Ein49vbp_j1W$q2FSjQHl- zZvW6zv5e$CstOq1FfbtQY%`CSh!Gf5s(dT9;}*(2#x3w1)#Qa+MDuvl@<8Nlmp8-W z&`fVoRbk*Z60(94w?x>{&AdtzU*_`|2}AVF+4bZF#}7@fm-wX81NvGd4m9m|Br_A8 z976vqC6$y!P1&n%rC9s65#fq}3#^&64g8cNsEptQZ=qRQ|LwvArUwr8tBs@K zFR``*y*fjg56XoOz#IIbS(W_nCR?vtWtUG)csJxU9)1s4?@oCq<;i@iPHXVX+JE&H zzevN49DOdaZKAY{_W#>2LJTTRbMh+EWsgd1iM$oiC~)wW=Ql*=t&--A zoFJBoE#I{fV56p+vX(=$<5thGZ9|7(ZrdbEZTS5s_rTa4VEhm>NnrO(Cn*5WlK%b6@l{*A}ibO+WyhyLAE>iID24hjh|FWS9gf80>Pxk&e_?Y*xIq~%{} zmWmU^(9$MY#iwu>!b4wrVbC9FYV@CUK=Zcl46Op+?dXj7gmgKqbN%noA&b6b8{0)) z)l5Y%m(}@o8J%|CI2J5$vjyUWX5M`d{>WdHqf_r2L)M(<6L>^DI6gN{g6jeHIe8QQ?Va?nQcixPK3DD+uZvCof$UVi(KFBW|`-WVVLb!ax z#!YNAYXilJ?8**4kF@nl@qSn13Im#lHre&%D?09$M&ncpL3@04T{_b9I zcF5O~7rC;|1F?5nGxNV*9-ZiON{N=40n_GVNq;VFHA^zQL`GsmFr z@XLQXdeS^nhC^x9%__?-dc2rY*-oz&_W>P`23&&3=9M6I|-s z83Bwvgj!82!EBTPK(tM-ayM1^e5CPl+}t$58mEzcPX<2a{n03M$mRO(_ao5A9KXk# z%AJdcZgaPFP%giVc)W7*)9^Q6ZQtdR=Y6c#TDW#1DrF!{Tmb)UDqlgY$4?Zhr(O(n z*=e_$DWj4rE`C*$U6ZZ&GD)7a{=gpwxyo{@o=*Vr_Pti}u{`iB^?<$QOfNKv@x`?Av`oBJKdfsC`r3yMPO{Q&iS1=ax3*p|0Cx#PN?bi^OvPrnQ;dL7>f zrb-g3ay>XkY^7)-m;g%Dbpt0(z#fv?MeU`jacf)Gd|HAn<WjvuRbt1?8|h9Y3x{G@3G{9k30-=Aum?w|@6b&WPP7Tlsiu9lm_kXtbEEm&d! z9Q6P7oMgCwHUVQmZAgvU2VRMtco6wB%oE0x7x-(>f`lj@=?Fr;e!FU|30%m$U~kJh>eg&PYj z!`v(`{tNtj0VaBo9-u^j!SHKH(@MtK&I5Ezi6kwcn`qLCjo7zp7B4Kka{8%%=k#u1 z7^XCte2otJ zeX8)HJcer|cYG|nGjNDl?;&TH;1vt}anweAApV*r*SN#-zN1m`u<6j9tD-z$2-x{o zqsg#-CkEXW`Zhy>i1i{t>t9#toxfxtTRH^4D>(hhkwNAhuSaqIBy$PEH*h|djac>= z4B^c>}j=sLtlKH<&Ao~pjrBlNi%mr z`>2JVsN)hDVP40~)hh0}qMP*6`8a)ESKvgLKDsy2Juo-x#HLm|0nEX}9GTQ5Ky) z9^6lTpw8#J#gZ-J%^|lY z6e=J$_3MV8HZb(vpY=GBSd#-dnHoS0Zcj#-K#H!SJqL&eN|UnlkQ#QwxT+7JL+O_!X&daz^*VDJt85u-k7_x_- z6m}eLc})Op@*2d0V+M7gxqp%)4Kn+Pp`}z8*z(;s(Z^xoVjZkEv9*31$d|>}AiCya zg--q2BXmOx`;peg{FyV}x*@c|y@a>q|04Ag<9I8!Z%C6itT3($cqC$?7pT8oN z9TmF?{nJWqcysKM70H5~-iSVFGRWlDBKkAhUwY2Ab6DW&)+xNkp+3zyaZQn)y|TCG z6lWH4Sa1&^@hqRTLu))96Eks_sXL%Z^~cKDc@6>Q*wX<q+tP_)$S&@Lib`z9jCcUE4pF;((!w7Rbr;*0bW68ppaQ2HzSVt88_J4m^2erf`lWzpfw-5Uzh+(^5-6>74!sT_~zPrFv zg-Ed9Kgs=x7h~FQs5B0u#Sz!9_;`!H{j%bc_0eWB*4K_qqoW0vE5$z^-@`ijrhh&S zWjt6<C zZPwQyw*$~BVwl;dMQ-mbU_bd_k>gEx)n%u@U}>JB{GqasNNy%;A;W)h7oa)9!%x8a z_x-XuKu3qmuYq_M`g2CPyeO}d+dpcd*P$J13aJm?Pe~l8zFkF#RqYKKcu9?r?A!a+ z!d_z2TKflnz5<>i_MetaD$%HE zLRxYh1q`H2n1Ej2GF;>A>$TVX0!yovAL{*ue7yyBHL;f{>&-XH!Q3W=?Rg{ZOI?#p z7TxUNKx~@& zy@oKQ5qMQ2rZ6WSnWZjBWL!yn?SA~c%y*q%e96K%0g>N&K|a+_k+2$pcob5pyWwvE z|L9ue$~)UIfX#-OcVBX<&>hixt{Wx?iax7<*DzE!9H!WxW%F~4nJ*+)o%QOx{qrA(EL7Aw z63MbvNsDv}kz**ImA6q1Iuh|=AP<{3JjfL`X>~n*Ho&TC zsh3xK8`i9PSh?20{Wc46D(`mS1O=5M#b>kbss6WbEALYvr3+vJCnKpR;Oa3gu5Fw%H3rTZU(91M?l{+*fq8hlR zpgPgacs}TLN&!`ezj$o3Aye`#7{QeDwZ9-oMVRAkx%(ax9i18=Ibq*}gl4!O=V98C{l1|KGm5 z!SSKSPaT|F{RJI^4Y+|%j`rKmN^`R+_+xh-!`+G6R9J0Fb*`ibOT2_~UFmnZUNoWob&iiM#4$7`GY^rl*PtIvQ2Vv zz6GimU!MkZ+3+cveW5P>X^pdWspB}K&_m`eE3s(F+?u)dB{2|`i)5dnR2HXiGV4Rl zaAEd=Ur8!I6FPnanl#d^^VhaL??CJ?zeYoUPG}EGF_61@UK8ov09;9PH=@4%DLQyU zh&*|dsyxg4LGgk7fd1b(b=;YkpZewAAc_@$msiHJUkUd%fypx#V^z-zsY$sZzb%Zo zG1pRo*U6Y9w1=?*!H;&Mvs14IlePPS8+~Y0fvwl4wnL;HW105O($3Hkf0GzY4AW&lq4rqn25~lQj?mmIl-bXS|((< z@`G;=+U@iDHc`c6oEHH^la*$19-P}mSPzEwsAMVTeFMG{nzw~iE9O?}0E(s5js zzk*&LzuOX0J=$8=EKGfV10PuVl}Gt!GsZ<$TFGc%=evTD5&)`o2j&F7+;9*v#NdEV z7DsfCc=4ZtXBR$B_zKb!5`(WONQyP*HxozKV82h=!Vd41r;X=|5Pq)%SaAI6DLe6V z3?p4+EyshmWEFZ{hp`_p@7D*V-Pa~h4VB@cmsLCHan-kmwc|_T6~1eK1Hy;vEk~>8 zQyA&jjs2`*6e;wVT&|FPh39E3+8L!$1yZ}o~BcO!z9!h+p@}gWy!QQ z_P+H!#dog8k0zI!wMmo)XC3vLA6c7o^PI;DQs9%Xx|`=aEdiGIU&pZ$CSMsTs(7>$#Ml+Xl~;39aC zqWOmbB~A6{yaWLa_16bhtrk6mS63D1)AN_Ya79JhcgQv+1?HyMuKLwIZt%~5hxJBZ zPUd1yhBQOxQ*#*!TPPaTnaACq&))bRBI)-Ehdt6|nU=b)9g4StM7)DU;GtVyDZU6t z9YVFv??UzEXu*DtAgLmX1I?g@A#eAWrtbxMhO)OERt|jeDj3yA`}`67DoRTDt?OP< zzJu{Y>pgrcFmKy|J3t%Y<4c<%r_my7lxO4hUCSaX0N3}>l3T!Lg3*iFgYI`%B6*W< zm$B@|BW+l&&+~=s%7GQHnF6_%!S071N%CZ7#TSxtv9ovMavni_DlbUW{y_Fb4^e9*rD z=&RdYX>B&=n>ObgwXT)p&Vy3E-qSAkB9A`bjH_4}&sY5yjP|9CP`>`P>g{z|nBZKS z66mb_FDB6!ik1J@?+jQ6=Tp%I_-j1p=G$IeNZl4^iw#l#2V)2F*x-ychHV#&UjKqt zTaVIkXo&n)ejd9MFhcjje{ih{u+{{;FcRz>XI;?@wbqsvB=LJUDLX#w-@t86d4Qz5 zJbxX-RQU?4uCpwe#`4#ar)y)+e}yuOdxbSv1MJ$HPepwmGv7+C&nen1ruUb{IFA@? z)9m;EUX${zSUO0@uxs>adee$}~(BFlEf+Y~2!d-iJkj1?syiiGaG z@n55+1rhiCoP?>LN!$Oqa_VD78~*{SH~+pSefVeulk)4@13v{( zKPJgl{1u`|o$jQyTMc=86k71Qv|(pmchK*?xGHJe8xja|KG4~FBU!-^ca?wodt@SK zbh6YQ-K09ZPIJx${3KP0DA$&E-3D~hb=+BY60K9)!|u+539ZLEYzND~=1D{LZK*}E zBdtXoDFHo>S!1K#aG||g`ctRUb-qUo&^turwdo2&%A7KxL!Fps{wlZcr8e8cuBPk@ zBG2!PY_7lazodDPOz!)-2kd6S>d_bC7hZ!R(H)vq7{Whm2^xQYu;<7F+DGi`9SZKG zg)hQACZCO*%yG*X_&WZli`^fB_>+Wf*8?Ao z)bYCDPE2`D4D(w%crW$Yjb0-1KR3enrVa#>-8*10;FYx`eE`+L=M>3-d&l(6hA1bF zW6~uT*=+5yzO0{^2`O;F-WtGua@;!+qC*8FH+cf&mEN_$SQC&^-@`*bs zO_@8cdwKdq?$cq(!c1-?>KP5BTte9CUIYFb`4=6Zacf%UZnmx#6ck(lpE>W(DcF`C zdd+8pm9WqZC8_+0!Ofk$z*p1S!z`-a_J6drhy8KiUCn;{d*#huM!(u*=~Wh}e~Xk$ zXPg^>(H|ih)`V}=dHg1{w0gV(k!MznP4)QgdMebnY*EqAG$G}-yNVOdPdxuHj65Pp z6n{TGAdKpr1(d9RZ2dZgP!wI@3NFM`b}Hot0LCAP;flZVOlmV{n`s>K+s)6}H6xl8 zeiL24V>~(-H@RC;HhB;vi^*YL*%tGzNz_s z0-urxugwB8GxL)6Gr>H&<}Vc%Dg9XN(dw(T{@14;x+;qyKR70J3H-H-CPzG@3DS75 z^mhotzsj;y(Kasi5TB4V?jRrlgLq8IgJc)rjTGQ0pzqPg4Z75sfT1hSm)l--XL_xx zZ@B|i(+j7aB++J&BKro6bvyYEuGz1K`dG-Fq&VWs3Pw*^Y$0ZA_`^*nkW{ zEg(EK206Bao1T}FRe1x}1ZsxvAM!m|A!?sDvEno);J@x3`cmzilTz>5$F?x3>$HfJ2NA~ERLD-HfP!XSMF3d}hKLPuJ)NvHB`kl`T{+Ec@WPjy#;>VLtbF-c z_4}Rp@PlT?LmROv>-^ot7pn^IgN1@@oHQ8A&9u1-ym`ime@HX&p#?ht`q&V6=fcm* z>UlEQx(Pf=v3QRA?F2pC^3FS+VTxVfzxck(tmNplz72_*mtGrOo)Z%)BGq7d71+bW^A0t9zYI3fZkH zBgP-FW3}^|(K_d8%Ju-EbMYg`bzq9Bn#whcH4HxV*mk+u6B{W%$Q z9jN9O699Myr~>Z17MuGHz56?Rdn|11L}-{sYkOw%!C`X$5v#z(oBE_B>}aUgtzC0; z53J?i?RuB|gs0j3!(iEE=B4Lx>Q?sKweJN_q^3&df~qR*ubM`_@K9l{s{y{zql0x_ zjQgfkz@**H(s5OvRRiS|a(cL5aO|3jEO<(nwRF2Vf6Se5g8gQ?jmaKxBV;v6tUQ_W z%lQd47Xuf|a{D&9#Hi(}uRR{!SINr>Sekq$?sfp^3~sm4 z-k~{B$MH6C@;tk5n2MK$Ep* z3@V_-(u*lOt^#{|5;ijR-nDJk0Qb4HV!qm2-`R*Km{m(rx78(XB;D0))O|u8RF5e2 z`8)ylsgb!3R;jrys^HOW&%(gxHsum8C}r^Uh)ebFV50zBQM2+G-KX|wd8jZc zwmd6wC$^?+0s-wHC&R}UTsg0%o+tWwq(@k79#FbJ>iL`ie*{V(D`z!rkkp7bO9RcX((Z)1 zu?F*XMEt>hshhj{cm8|}qGSB7fafE|4jhR&ypMfuxkI#t-yjc_A1al(ZoibcF=x=^ zwR{JdvJHtHtFkKOsS>GSH$!$>p&5y9MJ-pNsxW4^n(fL|~6bqLHXF z#cuD=_E`zg==OMf45e!BU%*;1R{ZGr7T6Qq$QWFf^D53ZTcbuG-U%*I<1G#yJX|`$ zFB+~1Tnd*99aW6qfC>@wC&rLWN@KW)JN^ZoAs+qTeyPTY2Bn!8IYn6j8O2wAFxC7M z{|h2L(_jqNM^(QonPACVYS?gKKBGl&N$pdgTK3mV-t40$ca)ej-n(=rFE zFsfvJDO(&0xu)_M74cZQn7>A4VSb0*+^zuBpld0jQry?ij|7KRs?cV0Xt}bpQSk-2 zCx2GI|5?+>0j;zK?O%ymJNP`7>u)Lfiim)DD;*%nT)OmwkcK3oz3jo^BQaqG8Jd5d ztv}A#nNffU6No!9FC%jz8hY+y*g?G-po{-kHHDCHuzvpKW^_HV#?(-ZOC>W)trUhg zO2r7gBkV1f`>A#ky{pD4b4#wg;CA`nyPzGYfzhkNf|hu-_6+`Rt>KKIs;4#21|nG0 zGg=dRn?uaf!?wdA!`mj1N<$Gf9aJqv9GaIElB#UlRGKz!&xIMTWCTHV(Tr|z%cMU~ zX!79+t9tpLw4@eE-^1Q}%5y!0AjT@kj(ArSP>yr2Fb*T(hQ_S>I2ivh1fc{{aHJ$G zn9ZEN9xM^|p;d{;VRv0W5Oa_1N8P8AC(L8)jk^P;+|MtPC04H;KE$)K4xhg>MEj0m zrn_A7HoPjs6IK%qz3NKd5;&Xjjo+M)_fO~rei8gQU1QO7rP~Rph�`$Rv@-J{w%< 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 Date: Tue, 2 Jun 2026 07:56:04 +1200 Subject: [PATCH 3/5] Replace mzeng with new ezeng method --- benches/reductions.rs | 10 ++++ src/reduction/mod.rs | 6 +-- src/reduction/palette.rs | 107 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/benches/reductions.rs b/benches/reductions.rs index 0b7cf33e..a3b925db 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -262,6 +262,16 @@ fn reductions_palette_sort_mzeng(b: &mut Bencher) { b.iter(|| palette::sorted_palette_mzeng(&png.raw)); } +#[bench] +fn reductions_palette_sort_ezeng(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_8_should_be_palette_8.png", + )); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| palette::sorted_palette_ezeng(&png.raw)); +} + #[bench] fn reductions_palette_sort_battiato(b: &mut Bencher) { let input = test::black_box(PathBuf::from( diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 5881a917..fcb7e6fc 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -161,13 +161,13 @@ pub(crate) fn perform_reductions( } } - // Attempt to sort the palette using the mzeng method + // Attempt to sort the palette using the ezeng method if !deadline.passed() { - if let Some(reduced) = sorted_palette_mzeng(input) { + if let Some(reduced) = sorted_palette_ezeng(input) { if let ColorType::Indexed { palette } = &reduced.ihdr.color_type { if !palettes.contains(palette) { palettes.push(palette.clone()); - eval.try_image_with_description(Arc::new(reduced), "Indexed (mzeng sort)"); + eval.try_image_with_description(Arc::new(reduced), "Indexed (ezeng sort)"); evaluation_added = true; } } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index b5f10f2f..1c33c457 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -151,6 +151,28 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option { apply_palette_reorder(png, &remapping) } +/// Sort the colors in the palette using the ezeng technique, returning the sorted image if successful +#[must_use] +pub fn sorted_palette_ezeng(png: &PngImage) -> Option { + // Interlacing not currently supported + if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced { + return None; + } + let palette = match &png.ihdr.color_type { + // Images with only two colors will remain unchanged from previous luma sort + ColorType::Indexed { palette } if palette.len() > 2 => palette, + _ => return None, + }; + + let matrix = co_occurrence_matrix(palette.len(), png); + let edges = weighted_edges(&matrix); + let mut remapping = ezeng_reindex(edges, &matrix); + + apply_most_popular_color(png, &mut remapping); + + apply_palette_reorder(png, &remapping) +} + /// Sort the colors in the palette using the battiato technique, returning the sorted image if successful #[must_use] pub fn sorted_palette_battiato(png: &PngImage) -> Option { @@ -364,6 +386,91 @@ fn mzeng_reindex(num_colors: usize, edges: Vec<(usize, usize)>, matrix: &[Vec, matrix: &[Vec]) -> Vec { + // Initialize the mapping list with the two best indices. + let mut remapping = vec![edges[0].0, edges[0].1]; + + // Initialize the sums with the first two remappings and find the best one + let mut sums = Vec::new(); + let mut best_sum_pos = 0; + let mut best_sum = (0, 0); + for (i, m_row) in matrix.iter().enumerate() { + if i == remapping[0] || i == remapping[1] { + continue; + } + let sum = (i, m_row[remapping[0]] + m_row[remapping[1]]); + if sum.1 > best_sum.1 { + best_sum_pos = sums.len(); + best_sum = sum; + } + sums.push(sum); + } + + while !sums.is_empty() { + let best_index = best_sum.0; + // Try all insertion positions and pick the one minimizing total cost increase. + // The cost increase of inserting at position p has two components: + // 1. New element cost: Σ w(new, placed[k]) * distance(p, k_after_shift) + // 2. Cross-pair cost: Σ w(placed[a], placed[b]) for all pairs straddling p + // (these pairs get pushed 1 unit farther apart by the insertion) + let m = remapping.len(); + let mut best_pos = 0; + let mut best_cost = i64::MAX; + let mut cross_cost: i64 = 0; + for p in 0..=m { + // New element's weighted distance to all existing elements + let new_cost: i64 = (0..m) + .map(|k| { + let dist = if k < p { p - k } else { k + 1 - p }; + matrix[best_index][remapping[k]] as i64 * dist as i64 + }) + .sum(); + + let total = new_cost + cross_cost; + if total < best_cost { + best_cost = total; + best_pos = p; + } + + // Update cross_cost for position p+1: + // Element at position p moves from "right of split" to "left of split" + // cross_cost(p+1) - cross_cost(p) = Σ_{b>p} w(p,b) - Σ_{a best_sum.1 { + best_sum_pos = i; + best_sum = *sum; + } + } + } + } + + // Return the completed remapping + remapping +} + // Calculate an approximate solution of the Traveling Salesman Problem using the algorithm // from "An efficient Re-indexing algorithm for color-mapped images" by Battiato et al // https://ieeexplore.ieee.org/document/1344033 From 4faef9c4e33d74bc5101cf4beeebc2e72c1f2008 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 1 Jun 2026 19:18:19 +1200 Subject: [PATCH 4/5] Add pairwise swaps --- src/reduction/palette.rs | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 1c33c457..ff53bff9 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -168,6 +168,10 @@ pub fn sorted_palette_ezeng(png: &PngImage) -> Option { let edges = weighted_edges(&matrix); let mut remapping = ezeng_reindex(edges, &matrix); + // Perform additional optimization with pairwise swaps. 50 is a good value for max_dist to + // keep performance reasonable and can actually be better than the full 255 in some cases. + pairwise_swap_search(&mut remapping, &matrix, 50); + apply_most_popular_color(png, &mut remapping); apply_palette_reorder(png, &remapping) @@ -551,3 +555,50 @@ fn battiato_reindex(num_colors: usize, edges: Vec<(usize, usize)>) -> Vec // Return the completed chain chains.swap_remove(0) } + +// Pairwise swap: for each pair (a, b), swap if it reduces cost. +// This is an effective means of refining the result of another algorithm. It's currently rather +// brutish and not very efficient - there are probably ways it could be optimized, or perhaps the +// ezeng algorithm could be modified to achieve a similar effect without needing this step at all. +// +// `max_dist` limits the distance between pairs to consider to keep it performant. A value of 1 is +// quite fast and still provides a good improvement, but higher values can be a little better. +fn pairwise_swap_search(remapping: &mut [usize], matrix: &[Vec], max_dist: usize) { + let num_colors = remapping.len(); + + // Build a flat weight matrix for cache-friendly inner-loop access + let mut weights = vec![0_i64; num_colors * num_colors]; + for i in 0..num_colors { + for j in 0..num_colors { + weights[i * num_colors + j] = matrix[i][j] as i64; + } + } + + // Repeat until no further improvement, but cap the number of passes to num_colors for safety + let mut pass = 0; + let mut improved = true; + while improved && pass < num_colors { + pass += 1; + improved = false; + for a in 0..num_colors - 1 { + for b in (a + 1)..(a + 1 + max_dist).min(num_colors) { + let va = remapping[a]; + let vb = remapping[b]; + let mut delta: i64 = 0; + for (i, &vi) in remapping.iter().enumerate() { + if i == a || i == b { + continue; + } + let weight_diff = weights[va * num_colors + vi] - weights[vb * num_colors + vi]; + let dist_diff = (b as i64 - i as i64).unsigned_abs() as i64 + - (a as i64 - i as i64).unsigned_abs() as i64; + delta += weight_diff * dist_diff; + } + if delta < 0 { + remapping.swap(a, b); + improved = true; + } + } + } + } +} From 144b0a710c2d2fda3a43ab3724706a06aa3306bb Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 2 Jun 2026 17:36:46 +1200 Subject: [PATCH 5/5] Don't use battiato --- src/reduction/mod.rs | 44 ++++++++-------------------------------- src/reduction/palette.rs | 2 ++ 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index fcb7e6fc..ffc3f8fe 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage}; +use crate::{Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage}; pub mod alpha; use crate::alpha::*; @@ -134,43 +134,15 @@ pub(crate) fn perform_reductions( } } - // Attempt additional palette sorting techniques - if !cheap && opts.palette_reduction { - // Collect a list of palettes so we can avoid evaluating the same one twice - let mut palettes = Vec::new(); - if let ColorType::Indexed { palette } = &baseline.ihdr.color_type { - palettes.push(palette.clone()); - } + // Attempt to sort the palette using the ezeng method + if !cheap && opts.palette_reduction && !deadline.passed() { // Make sure we use the `indexed` var as input if it exists - // This one doesn't need to be kept in the palette list as the sorters will fail if there's no change let input = indexed.as_ref().unwrap_or(&png); - - // Attempt to sort the palette using the battiato method - if !deadline.passed() { - if let Some(reduced) = sorted_palette_battiato(input) { - if let ColorType::Indexed { palette } = &reduced.ihdr.color_type { - if !palettes.contains(palette) { - palettes.push(palette.clone()); - eval.try_image_with_description( - Arc::new(reduced), - "Indexed (battiato sort)", - ); - evaluation_added = true; - } - } - } - } - - // Attempt to sort the palette using the ezeng method - if !deadline.passed() { - if let Some(reduced) = sorted_palette_ezeng(input) { - if let ColorType::Indexed { palette } = &reduced.ihdr.color_type { - if !palettes.contains(palette) { - palettes.push(palette.clone()); - eval.try_image_with_description(Arc::new(reduced), "Indexed (ezeng sort)"); - evaluation_added = true; - } - } + if let Some(reduced) = sorted_palette_ezeng(input) { + // Skip evaluation if the palette is the same as the baseline + if reduced.ihdr.color_type != baseline.ihdr.color_type { + eval.try_image_with_description(Arc::new(reduced), "Indexed (ezeng sort)"); + evaluation_added = true; } } } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index ff53bff9..9b4fd62d 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -130,6 +130,7 @@ pub fn sorted_palette(png: &PngImage) -> Option { } /// Sort the colors in the palette using the mzeng technique, returning the sorted image if successful +// (Note: This is currently unused as it is outclassed by the ezeng method) #[must_use] pub fn sorted_palette_mzeng(png: &PngImage) -> Option { // Interlacing not currently supported @@ -178,6 +179,7 @@ pub fn sorted_palette_ezeng(png: &PngImage) -> Option { } /// Sort the colors in the palette using the battiato technique, returning the sorted image if successful +// (Note: This is currently unused as it is outclassed by the ezeng method) #[must_use] pub fn sorted_palette_battiato(png: &PngImage) -> Option { // Interlacing not currently supported