From c7eec6d352510daa9acf607f513ddd9388539b47 Mon Sep 17 00:00:00 2001 From: nertyog Date: Fri, 7 Aug 2026 05:11:34 +0300 Subject: [PATCH] fix(watermark): case-insensitive Windows path check, text font size (wm_text_size), and fixed pixel size (wm_size_px) --- image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip | Bin 790564 -> 799809 bytes upload/admin/controller/module/img_opti.php | 2 + upload/admin/model/module/img_opti.php | 1565 ++++++++++++++++-- 3 files changed, 1469 insertions(+), 98 deletions(-) diff --git a/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip b/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip index 24a5721bd5e9636f30853424c24427c45020a5ff..39dff49cc240af7648bf6bf12b14a1813d9be644 100644 GIT binary patch delta 40540 zcmY(pQ*GOfH8XXw>(r^LtGcLt z_Sq!`ECZ)32+DHc5Evl;8}_t`2()1TAvgX1(3I|fn85KLrpxz$<0kIn{x9*){2wOM zgEFDo&_e4k-BKkzNrHe(O@o5aC+6cfg@PIj0$ARNu}VLO#KZyrH0PKqPv^6)6`LS7 z7viZwgZe-1Lh>aJAyoz_FW{5n3*eQN_U4h>X;$XF;8kP)TPo~{X!SM>$9XOiV5+b$Uuby7|?n}X}b8H@EvOZU|+qHpX4nNG*C;3p@%kx zs?5XS{98AjIn&?!7w3**sU0m){ZJ_;+BqQe)qlAjwN`lSM*H!LVV7OOkxgEznCP%v z9VMd50(iQB^n?=V%n??~5#}%h%JJv!B4>@$(dOnVkAUC{hwz5*n&2v-uD%Qe@XuTr zZIQEyh&sKoP1Rr695C_*F@4f#RV{XnlqH;L?2>|>lT9T)cIQ~E%0S?%lCfY(p4Yn4 z6LMrYvwf1%2VMkKW-5k%Jva_*U2$se`kit@u#>?i8#!6mfa`8~p&O8g=S%}=C z#hi0}jj<>7ePqQTcgWSlOJuBuC`MP|zcm}zo zA3V(yO9R06qgP5fYLidnX^0e;7|>32&Ta>sB$8?+V}x-vE?slhRzuGdmQuC1#Xp0b zwlItjNRjA%y+UaOR6*F0s9I_{WJnhEO1BjyGHVs&+={GAuM|o@-!`x7sykT`jB!*( zv+`#v$y29gDvlqU0&@O>OPWtTohH%|&Pekmt`!mo99czI9FPqMpTFMFgkp-q9BChN zT*XrQoD#6bb+U)p^4PnL(65Atxz|qtX}c+b2j-b91uAK4-l@u_AlkZ9>jJhTIlgRi zfm;kXB2ROCC=0mF|1%k8^{#`{j~md zq&tiFH3*QnR!5ZSO7P9Xyq$>{2qpq;^)JQ=)ALmNVU#s4qj6N18@Q2w$VC4NtsTbT zak8+|M|WK9W7TNkb|M8PC;XCwnlaMaRd`O``-KkHOl8O*j8J2m*!Jf=2jtf9CFf;h^9py#9YDr@vtq9^f!89Z3(#BpiXvprnp!Ot0 z&of7hm%L$Efgcr;3hBBy!|HJ zRSz;mG)H`pHOFLz#y7`u2NlO}63?I9s&s;nL)Am{Ck@h3oW`+M{RHr=mP`(#*|8eR z30c*`>{9(6tjbOId3r9PBjTHj;pyGv-C-xG_?U*5Bb2T!O3XM3Jz4*TN9W}1LXb`U zd{Uw6mRK!Rvv5)g#F7=N=ii7K!3mUsLhS`vc+9pL^D=gE$VfX;(H>EML!G_<&Qv-H zd#YG7$}puZ$v^jeZ3cY3lThYXT~;g=dy2OWzMnoAW0TSg<<@VuIF7Ylyls);CU%Yj zhbm}Ps}piwMzj%%ewX68`8PTLg&`l8q~TTCVJ+V=a5ZV+>2( z;atZlf*aZ7@T$|>Zrx+pWGgy~Rb!#irpub3v$h0cbFYHgmzLk7@GcA*Y@4N~#}F$G5GeEw#?DoUVjxwFDhrv~Z5A16u? ze0Zla+(vWdx++pO?|nVSZ)?a_q5l%_wTlSZP15mTGXR`T$(8g~+^&c$N40DF3*)$k z(s35OL^TQ+;B&0s{Dm~GSSXWA2C7}(!&3TffrD<(nuH|XZ{Dt4KC?fI{zbd^$%Rpg z_bY#$wAZ+&uqabPiD*zIv1LRBDYdSzH+ouP%}I5z*(t+OKG470`Sb~)=D3$;8Y@7& zY%*n!g#qeUR62v{0rfXj&3NIFBy6ebyo8c-OG(FZ_92(sDRk@wURi0MFoXjd>ZTcr z6A$5_4iiAM(WR&`RPS^w)r^!ohSu9#%bUYPse6l`71%!OQR*FK{DiY*RuYHwqI?dO zl)-%)ImjoABUSdyG#T-Q2Umm+59R9yeR-zXI>0$Q9!Y8)vLV5xKq!1pQ82)OaSE#n zvRUlZc)e?Ah_OU-Nn3o|O5s<&{lu$O$Zt|{7Fuos!Zfj=s>p*=IiWWeCL3g{M`gyk z(UDg$UxlV<&m<&#wu|xFWhX3bM-=vpEt22d6{hZ9wptUPC-}YX`YRkT0?sWY4IJ)R zuRIazOavt)PBSKb6hq7Q{g1SqNB~&fz4{!fPI|LoHwf6E~LKVka( z2RtO)|I71|yJhK;?PdS(d;YQl|D6i%mJRQg1#Rn=1v4>6w)~ITaZ-Rzg2#b?K+-^@ zGd6(H11@{cyz#kPY2E>VUH|Y#9cqd{*3M&uQmvTujTlwBY2B2iOIDjR7!yfqc8auE zj1@{Jz9+K}0!sgbxtHF-ofFjSWWDN8T*v}t3bP5=H|=IMNQVCg2)xZ;GP4wRao=b3 zBSYox1(n4a$r;dQ?j>G#v(A9<^2Rp*8bC}4#EU(3keX1>5{~o#oa%d>>N|bhzvJ=d zy`&Pn=RBkQ_Wdh+NGZKJ5Qt2BU?7U|amqu2J~*|)u+a9^ zcMyPzu0x+bB z;CDN{GQ>adOifI=3Mj%Llz(=HA=^0MFQlso_g2<%7kl%K;~#I%SbjBtRi-~LfzkM$ zLQwAD72F#Wy}*>uI((9~tig&HZ@yoP9VtUQ#zRD-S`Yf1gai@#| zVv=@vXaggZmu2akr*PvUNIH-z09<*ld=s8}(Rua~UR*L+qdu3uv7eGVzhyHhPhslO zn#m<$WwkWGepuvKzC{=ynsCetl0iaR$ze&N&x(^M-ze}|oL`bJ3C3fa|JjbieibJ- z&4p?v%y)IK_8yJYSWR12O08nR?sC9PHKy0#qvZJZz!Gv2U*4pZ)o0DS7q)PI;NO}Bs z$;dhJ*ihj5t02fC*~KLf@hWC}?BMb$j%c=x1#9N2!tBZ+s4hyjiFE#;PSKq80yftj zXom*(S%P|Z9M7IK611rkH;WBf8Rv9d$pMyKdaX4g=(^rR>MV-^fJXn(Zfou>;%}Kh(5ooDVA@QDdyoyQRg3i{G9F@A?Brs749H7;(p;>Bh6>2@0 z?V7L*S8m`I5RTpKF_p*d<&5}q}G z80{*rL4NP+_W4UmHwa~)W|rA#NrSJqlH+eGabnQiY*;bCR5?9$0!{QdLIzs3oyJQU zNI}o6U6~X!8Q?QSv~F;rac^z3H+0gCC>W|?k*+PMft znnz6B6Qu}f>*HICeSRt0-o47t$rIoG9a6}}@%;6tn3duIIhmp>jJ5Rfx>d9FR>kk^ zaAcuvVfOC&`HTtN4vax$=nAo_&K8l%FU1KiB76J+uH^P&1L;gS(ZunnyeCwH2s^?Q zfyBMog%!8kCU!{}bWB*wkh_alfS+8KUz&e3`+6P_Mm!<|Qz5Z&NpyLgBbhJhGbY6h z?;45pYOn+o}(9}g+Nh+l*Q+bU1p(T>gxDpKEQRzU*>H}A?YdDwN z>&yTkunzPCcLcss5?a?$ob7tJxMVi69*)wNal??8$R^qM=hpP)m19F#K#;CckC3S3 zO9dH%EaC&!&!|bKn(!3X{)SLa(Ht@$nI}3@gj@Pq8k46!kZ50U_f=0-xh#Dc z+_@9vEe?xP)pzM;V%@2mZ3EcG*S&=&Cfyg9~i1e&S(bbFw10FaX8~1 zDHTJ=b%oIm!!_DToY!)x`m0s54+YO17EyNXs93!|L;!dOUvEn;s4~yK`u09LGb$NC zAhaoVM-yG}syy@+dm$Ua`|O{7gO4ztpkEqg3X-X3Rw6iFiZ2j1uJ&uBIn217aVa-e zIANmife+_sY*1WPgtsgq^?1tSwa@IFh8bqbQ}N6}W2YKZ>6}jQ&uP*(O&?JA*hS1t zJN{N2GUUd|>dOEXefifYnzrDCx6?Pk@9`>n`wyAnfYqxH%KC?N0G9CfYSGou$-;~2 zRCAA=3db0yotNqOP_IG4M3Iuv!U&UiG9CYNa7PV7Hvht33B7`3=YHg4xVJ2@d@|vC z?)JXQRC&@`k)xB4NpZd&$n%Xm+RJ%&Uy;`j{~UC$%2fA;8Mnc7`_HVSaZX4;RiczFUNUHwsdQAd z0C6n$Dv*hpS@SG=WwyRglGGoN+4iBdb2hR3_KHK&!6{Ni}s4Q$M&=s?mjwyp!0s)SWJ% zU(}827h78FCvF)T!L@qZsB~TuoNC~l<{*hIr5Rs5ThBesRcz?{{BHp;)H6rKY$qr~ z&hagv&lf$>+bEVzIk&2MkM3Y)4J+=}5>X{q2BOzDw7A!PeN~ej52?9PO}JWQ2|J#C zeh0hJRX`=5e5bh@u}4pKvXbspQZZeo&=!K(I@@B_iRJnv7|n=c@<&bNLdTNsL;1K0 z{`XGw(vl{1P43mwRd6w&ZsL$SJNg;M9rFrwo&Rw|)=Wo@R(q**c#+YgnT{iHo9Yc- z6YSM2XY9=sp)Zi@FJ@o-5>ZZmgm{0I5Ki5_*?LQbH^99oz80_eLianPp+) zpN%vyB8 z?$MgZRKiWIvw{=kV=7%M5yU?^$3vQ0GMgyDI2RXVWcFn$O1$Kw_DyP4c$nd&CYba( z+rpUX&~MJLIh{oX7ZfV$KV;>HM)h7g$-37(xbd)Nunt9((pX}C8dvBt>eUiw;b>{T zfP&LST6Z|aKD)ce%|9%VE;p9w?l2=FRxeZt?P z&KEJ(>sMKx0v1jU+ZB@nS$X4bsA=?8@?xa!0uJQEa-@Ae=7A)Z5K%K^lOOi)>W+igY%9Cb9Kay>css}88yh7}>ju)=?tS;($dHJ>lNq;|Q zFZHBKEe{6*mN}&2;;t(drNZ+HeCcIx77bGTm$Ow+KXf-@%L5Y&vLvCqe99K;;-8Vf zd{!5X)mHCTxmTfXTsqfwur@bMe7YVsb=7vS-8J!}^^vgOX6Zc2`&;_Rxww1bUBOz} zK*g@j2piHtK}r#CEtbE-t_obLvNQW7H)r^Kxi4G+Q+Ge}o48T|1rzn%D>LE6><8Dy z2`hS#shVxAKU?4S0~NQh?;TedOZJGBgEo=1@b3J=Rxa^C_ zJMU+J*$Waw0p61cL$!R!$RJ*8y}{@@31Nazw;GM-ujmUzrsUI-m3xM?e-@EQY1F1> zZ!=8#7|jwTgve0+0;e0@YAm62jGmuNnTg4R_l%oYuBqJ88kf(NN>OU8A9>4<=i-*e zstD?2*6a|ut)J2K;u`~Ss8c2re0cvDv|H@~XqiHS#MLUE>`WO&@yB$e2JKiAZ__<= z4|Ul&BgqE6%}<#7&wImo8+lC2mXEZ#RdH5U@(ONgtqWCC*on&cNkd#%(MzrKOB1-ce(-L+kC##zJDH5(+V~0iJ zl)@$#*V~z?A@ng|(Z>?_Xxf$1FJ(JG3&gKsG*?bWNzHXVg_XUG#smWoDrfo{U8`C* z1upD)R&mEq@Q4IoxjD^>A)dY5RZfh!z*^Jdvr6VQxEKm*fs^%@m4OLApWX!p)z0Yb zK0;E*-E*B?$3&7BX$a5Wsgo)xpVRvz=6C{@uzn#B_TPP%0@@qM9|t)st#u9n`C+o? zl-Qrb3nu?CB~dkUN3dcY2q^!;*rb%&4TrKcp!IRW3POWI@1wLG-iAx!KaPmN#Vs0g z`6PX2y9vcT;A0w`BC&KUJ3cT(AU3OM-1B@4(-A$g8c7z$|8 z$K%}sjf>lj9!0LlX7IIvcMCS4PtISEPZ%%9fWoK829HVm*1fE^JdJYN71rF^g2Mtz zJ%akJ-c2&W^2^C}hwQ=#Si^fT0O|a0rRjzqf)X!?S0$t_5`Efx-0qqB+w-lSyIa1I z@3c-7qG}2%d!g!T3Hu}5N2+FfH}{$-?~<613+}snQ8>5%uwjRD(M<$!5nmh<5zYSi zZM%5UQ)JvY(vL5)i0e6IGds_tARHLjHj?0t)HebCD#{HEQA3G`!-Ir^;jol*s3r2d zJHNf)Ue6xLu8PGes{bpAQeE%7^59%*BwhYH0@pl&By!Nw(SROq_^{tG=Qfc=?AJ~; z5cZ$&kQR5^s;a3}z@i!;tfHWq4CIC25B+B2_zVMyl&WFqp|V2bwk^aC^x%;P;I_Ml zwQI5VDPzw4s3e_*hCe~A@cNqESKKJUNXo-Cq3hrr&k8f;G&0C#;Dchy5OG#1+!S{U z^is{+uDZ@=Lai`ZnD2FQps%-@-0&srs@+2jxZ%{3082v*&GrW16&s-R(fjAr$Tm;e zplGZ^X5p!;Gmj+n7}kyp%Q0E7*-Uy{cqZidR&-`}3f&0c$SvJm^%CR5>IBKY=&ql5;H-eV9z= znD7IRNRlIW*^_62&l74Sqb2CFG60;o(~VJFw(3*J(yNn$7H?!1oF_ z97b&g=$c$LWFhw|?=*+Tr{PZdBgJWXyU@V*Uw8Pqr@4;vf554LI@wSEtj(t*)ez$!cgC*~4t&>o)`TRhyhvmU89-*PhoAOG4<)f&ketR=+Gac~{7v1_wDRA%fToLGCRADKL{Gf&B=^crcU zMgpQsii6XnQ{ z@MBGqsy3$!3kp($!ee`%{o%>mU*KvBEeBDh#&Ld3CJP7w?{j;E30z`xCI z920QC`7{Eln&;tB8;p0-h|b$5&7ZK6S6C4&tK<4o?E%^M>c60lQHVi^b=~K^!q3T+ zwe02>MoPs(<8KcRL$1coj7CORIyto-hjGO-hCuEY4H8U2ue#wOqybaj1A7`ifim#mft8%y1xpX07LCs3)s90*bG+>b zCE?+8{$*fp6k~z3K$s^TquFGqwu{um)M;`*Dl_N6opGgP*$)523iHGYDw5x^0-O<2 zfzZ%KnHMA?&{T%RTZ>^;#*>l?N(1*ueq(|OM zXjJhO+1o$C=GNXL>k?6x9 zcIh;TW3k3=yNUauQp=pO5P+AI>BuB59`^ko&Nmgx6D5(b&$WAbrjo%>ria={#|#Hk z4wd`Zeqhgk+7wtF3|63pbAWNTCh6^TjPS2;pKU{JWD)!kl=O6J>ReC9oEU1s&ZC&U zL0vYFERC?+1ol45yVMDatUj#c_l{bv$eBg!)XS#A62-Cvt9ivNJV4KvPJ^2X1UV|~ zw4;>QNQ+8_c?zp+mDi&zj}GmmmiSN?N6;ff4;wj{trJLdF$4EKE!6O`O}8K;QsxY5 zrd*(`<|nUQe!#LV#*X&Qs6G_Ifx#Ta1W}b2Wem2YEbJRTe3gZf${#6H74y^MQ?0>y z<8xfr>k(hky@xjAbO7ZW}GJt&JgI?Iym{0O;RFQ zpxP=H@b@_vvz5ONIpIk*O%>g$hK8)oji5#GAC1X^T6l1RTGj*psbH!~x;kHk?%xJc z>Fj?fKnNru!EpyOKk)k})4R7_M>!q;f}aE53`F=9e~tkrs0k2sYy&?#h|I-IlIsqQ2jvNE)!m+&l(>@pnv zUIlU(E1~f$F%5t%yzPF{7QNCy-6;_o^KuHFHXCSX^GS_Id8qDvx+4uDpPXcmRrc~| zaRJtN=)})49u_JOqyK&Hp@)Be#3ifrxG!D<+44KUY@YRV{Rrs-(3o21W{(+|OvdV2 z_5cj$a{2*BGPLwLG8iJA&jD0&YjSn%=OWEJF&P+i@c>12u?1G>1qI1s4=WUJ(piFt zft0ZK^+5zfX327VUfyeC{39$>)hL2@-)A&qa@H7%5_7_;F6VJJbk!uYA=N89S_lvu zokp@Rn2ci?tdn1--y|I|amlk3_+>ZAe$P2ShO0Yh!?559xC{p1vkFRqGc(D%mwncP z!hC`Y^?-&W@5%6?9Xw9|vodZkX>`jWQbe37e~w^odfhV|=UgBmR+-qosYY~yQ@UZD zio=>YBOhWam7M(|Lib_cREc2F&)gEbE}U5`V>FO24x<}Xzk`r^qGAd6)|X&LYsKsO zkXWj%pF*@VWD4X>ofWdvR?bvtz6HE85UNHs7%&dntP<7Aj2gLtL1UBCAvmlUlF(KZ zjx6MJd%EBadE1+pvYQ2B7j0vx!{R_P06Cq8L9xE*11ihbC>`^0Vym!LRzEd1Ro&MZ zDht{u-6Y%vYh1V2!tMh1)6P~N!hz2fS48=8abH+~NS2$o=e$CSF`3+CyJlg~~)us;lL6+?#Jd`Xu_T_;6D25GJo#&gi>1+iJv^#>_n+uV+3^ zfG`oY6I~&gu+H+k>MIA}TY|tE@(|zs1dx*rutdJ`AeMdiy!3+^w!xH27!Ri(VzYU* zC}cTv7k$ez0>D z5ikAv(^LfE^I~J6+Cmq8tj%yklgAWuqfWOSbhmA-fWgm^GsITj{^CUB2Xlu2QU0DyQYuQ{r3b;N@%?SOCJ#6vp(B~_>Tn1(e5iARR~ zvA4PEnoghM?)ArFf1lVey}f+Dqfd&-#%DS>3pS6_kCkoR2Jd6eR^k3Svh@TjwS}7x+O&1Tds{2K|Di z?I2Zp@wspd#ABbWyhsxfdNZ}Jyavf-MIZ~|b0f}bU=&K7BS=-TB_4}Sa9+0g{ITo6 z(_(fo6Mr~R**Tjx_>DX6eU}TZ_0P**HX1eCi6!;4-EM(beOkY^eX{{f7scK@TjLWB zL$MF|N+yW@#GsGOsq>dp9FR=tNsA8yJ+I*u)nucx!RUoa1Fh$cUM`CODfB+w-L^$t z5i=Xo9D$uC7>dHp$6&+RqN|7wz)`#E#m4cb^RKlvo35wuk+~7oHS~x;9tlq}K2`AO zXEu3x)=L$_r8Fid34lZn#DQ9uK4YN~57rqeM6r~&i&z{;kBeM%0PGeRZ9b17Er!z{ z9`SS{#Ca$Y-iV}BgoyWVC)>a0*>rcEW!PG;W-n+ncX$)zjup?GE<}7jk4^9>&DCnI zsra#qH|mh?O9JF=YGVHDr4cjpE-a#Ig(mb#kAQOC!DuwABI1BTr?of% zjT6l#;Q|uPrr|)`0TRBP5leZ>?=J^csZ-6_hPFaMDD5!cz0!*h(K$j|QDNXIl(2tI z3p5%w=*YL0xDUnorFWKzq^8_Fx3we?HNRr9$XW> zj86!aS;?o)WrZjZB#t}Z{LtB!&I7KesqkvC9lnznev8Gx@NLgjze6fY3< zQ^>Pzc@IS50FqE!w5>B29F*8$*$htVE6PlAdOI?-p3Pwgws7xwedF#)tee(Ivc2wZ z68?tD$Nh|(OmFC@(G_b9_*M&e-PY66W>dL=L42&LwT70vXLYnNOBpOfC&u9p({~6+ zkpqH%8l-<9@?$;<*qIw;DZENIOmou8K#(Cz8Pj3)0Q`PdseY0wBLqYVmJ^T4r~j+X z>2ot5x#xoM0o#dGWKH+^XR=1pau@0IFo ztE;bC^GuP(Ocj)WNo%OSA0L;$D;tUWI!O)bHp^HKbnWK<0(Uv63@KPqDZg^~a%20t z{|?Op3=I;axa~DF12%4wOgSXBi|&YpsB-WS&>rYOq2Ug$E=Av5T;UT0G@JCSFP~sx z8W6P)?pL_fHwTqgBm4Q9u$dq}S37VwZ;X90x8)tyfVEYwEC1+b9+})3<+z$(_0d2? zuZRWKPK*e9^cXdvUvt zl_}d-0$2SQF%&@htmtxVW;;o%w8arJ7Ae%Z`$6Z`5Yhx|Hu7ksqIvL#K3CC5k7& z@4YZf1|nzE#@aGSVWzRTwHUoc(|YPueXi^#;uTBBo%e%N-#u|R?3{9ME8&?LbkPCu z)Hp48cISfH8hetz+DV#8Q$kCo~2zexb z;l#X+@BXep`QcWN67n;+td(%w~a+FQ;QLV|4)%r75o_!ksX(y%n_FPX&Feqb6aO+7B z@-srDpnbPT$PG4Izz!lM8k@uP9*au=M{7**`I=oop+oY9gqNkl*Ne)%JpM)jAnWL; zQ$HBii(4ipQLi0wF+6&F*ZHI%31-8p3jUoQ!M7-Ci9F#l9BA_Jli&ZKZ?Js&|M<^K zlLy#9{lf#%QwCWL1VMSY>?U-XUf?8|MfGc3(@)I5NQeQ2iD1{y4f>?e>3jEeAQXiD(4`naaS zI`Lqz(wkj}!91U!0ENwNh0r}LkhYjRf2rSxkda%qi!D&g5vyRgatVI zo-Z^Z)^?plroxg%(_usMB`>P_Y`5f%gc2EcskMo+1oKkA+xL%ofy5*LN1+fV9%9Ry zsf2si&Uv_p`04O?G&WH;PhF+;iEh`65kZ$qzly_!YI}&!l)hWi@;v5(?jnB8@wpkd zfIDrozCcy|@)awQ=I4i792&*_&@PalmP)Qo$)vMw_>ist>NuznaCdLmogD5j1zT?@ zklj#Q4fG(9LI0Jkaszt+A_v#Z&sS4#mOOOnVVJedEzG+hsPF;V7!8yBT)@k z{CrG8jPla-vaf)%2Wv-vJ_>2lj-zqwe_0E~;c+V^n?mPqSF`MZT2|91q-449B}B9d z9UCS^u4bz;YVfPXB`|&^&1~eb$hp1aU{aN1cXn)Xc^^)TL32|~y?^WqK;&pO7K>kQ zik+<2rEi0(Ukh55ZwaAUl{5EEYn8%u(bvubL}e%7WaePRe*F|w#KpKCij@wRRF8@a zzHPc!o;k{y72>o2#2zW!TI6SrBc}4FRAwEAo$79{ruVeT^lIF`Oq9-Vx>;vLT+%*B z+VQ%_Nj71yvHmQg0cbQjLPYP?J;#@4WOw8?!)xz>pmk@*a+SO}UnAQ^!yT5&NX-iW zSGt!0iXzCpo)S46P&@sg97h&1l-`i+NG{4uO%($1eD*j%>{0?U@#tmBgVB+?E<>CC z{h&|Wv^iWo$R7@(mE{@`nnbK9TQ&3Z&w7)K2nEg=($a|8V6&b z9YKFRD`Nc-WAQP2;D`@-!RrwuskEAuHrZOgtZ9_}MkvmX^(UE+Y+lRhQwPH43d|Dk z!V=FQN;f9R>Cz`IF|EH++x6*Wp2;l}&3xh_3HB!2%UI(!3Ce!dGP>Zzx_yOYC+@BlqC=KS?-hof$#9Qu(+PD_ zzu}0Ct<8E)0$~~9N~{s4TvNKbR>tXE5evc&E|?LZF^!g~S+LwV2^Ve@((ll33!hIC zr8U!54Nm$*uj|+Vz ze$Zd@byz%}!mVjsXw$2+Y8Fmg^;MuBkf4*sP0#F6=t=!$Xne3)-QD>)X77eBZTh zjUUz@p4p#t@iiM0B|H!#y^RFlQHVMf>e~R2t8AX?+9eo;bp{rn`#}alLMmbKre?&J z>{(RMd@Hf{lXQ9$t}N|1D=fA!v%~p?@T^VBjePa-e%zmU13h$AEREXy$keJ0vkBnh?~M))|0>%{ zI?M_{Clte~xV~1y>wnKavj1@!UT>leF{8_3ysmdbLw|dIL=7_HTS|;kF%CdPd1bwu zcHq9v1x?Wqn*~un`mxY9boi?W%@;d4*a0@J?ZA3HhN~Sn9wL2Q*CJBS*moV2qLrNv z^{Z90b_~VLV!>RR#6zJ>u2rCM}yLfza;=T8nO1}6DFj}t&+j_o|)_ZJ?g z+3v!lN=&dAvuYQk;iJeC#?{OtTqWIPg)vuW!IjFI%67ITLKFvBoglmepNNn8mqA_j zyw3LbjKNV>Wt66m6&@WQ0g>-j9i3d$N~-eYv|)Jx7TtfMaFOg6EBiD5EfB`4KJC!|%u##W6rWLgLk9Wy5e+NvB7O52~KLsva#N)7PKWtjL5elE~32CXwG zV8ds)+cCSWuvV+qQr<));_AMHCO(;g!c>)gg+dV3AO6$3EcH!qR!?IX^lNe-PFq#E zR-j0!{(c^vmWuy2u36T*6bW}w)ApmtB^Pz;)J|pth5MGU7ZtOR9wH0xQ;yPfcoCwk z3c5Gj?N$h(y%JDqYkCVsN?sQM09qCJji>Ge1^dMCY2^E=rUc{dF7XDpyW_o@Ei~p}QIT{_E3FXP8^nB5hsCe)feFTBE;{chg}`VXXOgQ%SS- zk}^%N>Q>ZqPbCvbIG0W-VV@BCJy1(LXeuy`vTL#zts!CxAjr}9~#TQFo@1l^? zgvi(tr288?af(>w@LsG}X0;{#uB+r(V+PPNn>RGRVc`39en>)f5-zi^{nss0&Ovb_MfoU#8yHe;khYg>ZAzW&~mY>C=Cma7pyHqyy)#CWr+EoN?Z+HjEg4b)3a%>j{371*;UR zP;DZDh_fyQx3c^<*7h~JGFZE22|X2+e))rqB!_pZA@B6MJn)XqIecW=uWWKIl{XI- zNeechqU>O~U$Bx*wx=30>f=urFL(em2=B1U5v09I0p6)FKjq%=-*Br4VBjw)G%BJ?%<4G9O|7!e=M?^tdP>f;5pH=wsw#4Pb5zsC@$2U?d_(| zw=1Jzn&;TaqA%hrsvk;~_1AtyN@AxLH!-M-ID)FX<|d{v)LESIqW_z`SXu#ii?nNU z2vX+ESpLE%$GRyG30IKIyH#e@&83B1Ug-HKD@y0w$7$F4E)4-Q)Q+b791RIWeA`jn5{fBy1=3@zi6+zg+4!djyQQ8Xw7G<$lftuw9^1%IV~l!eF? zT_s2KCHwVv9m_dtBGp5DF^%==hWUD=5{J_l)TuQ*k$@Nm6?^5&F!BNT%mG*bb?SBr z2r#ad5>Ck7-5~7qYLm&GwTMHQoSlFzl zj9=&x!(1Z#Hkf|fe|MJ|lhTtz5fv(xg&Ls4F46Wi8vsRH1jQdndX>|eO9;Kk!%8#1 zuM(+;n0xwtX<6@sJEHAgMm zsb_S9A*xzjk~5}v47zi9{nDa|%q`(eGY+e4#7h>|t;JlIz|H|6+tL=SjBL1U-#eOc zYA8W77Eb?pWQ;~Qk_$oO#%&kKAdfF0^`q4%Q^sdUiKc=BAGrE>*FoEx5GKByJq61% zIYT1EB=V}{W6Jv7H?g|aSZ;t%_D!O(`A3R-ZqYz-9&}yZ`uYs1sJlptVAF*sn_4rM z$aAQDRX~~5EAbd$kdKBvk5>i`*Uka4&4#ZGTZt@%6243COoKb(lV%Ka;IZRHd~rL6 zv4xK{seg(_I?EvlcReFO0WzG!1w&piA^s??LxzSqY}?Za?YFDSepPCo(!r4;J2;+x zV4shGc8YNn$3NkH7=dIhS*v6ChyU<@PBj(;`B77>qalFD#J`GtDptkO0(5F0u5%hw zQnpCV;tgu&{1Kh7VYB#c5rz&)l!X2393Xh?zKS@a*1l)Z_jr-ucaHYdKT2X>&l=*& z^)smL!H4F29(X}gc;%?xt;!E}n*>5%TDs$3g${Q-SC!%c1F9<}1&~Dlb1lvvV?b8SOILI0OIl*$?+% zRu^pnL)zE`wWDj8n$vje1r+zIT>=#@n1-ld_d)WSNJXfN+Z2dON48Y$F0l?hD1B=& zT6l!7YS-2_K_%V#_tR)*)-0w^m>KPL{7-BU1NH$yg3NPF=X5pkw=@#8rbr%66|=?% zDgytm613AF$vzDNgQpg;rl2285u@yIGDhB<_Hwj}E|dOpgtp(=-yiPbar$Xi+(Np5;s5n?5O~wPZOCc&eL1{s2nmQw zf?o0O_tFKYoZIqqI));VAPy!&a&*7ionPnwpJR?7t`1^<%+>eky6DJaHMA+=qFTrF zI-9pnqh-d%LcOd4;H06SqYhBDP)}X55U9gTKBk(h_Bed3Kt-N5CFRh{kuKv;tSr&b zkrlqIcx0P}I{W8qCD)^U5T^Jy8sPHxpozy~XU4_cG7pt=&?V8~&?3oUn2U;-Lbzwr zF7+V}BRJ8Hbi8?p-<4G;)F9@9vqzuN^np6Nd6>0{aY7DkDS4|Shumd?)Wlw#i7~3M zbVxnhuu78$4MJ6|fIK3|)lASaAE9}4@U#q#tX+~aThr#D2n*+J6CDj-7h zySeB71E?$i56nvmW$WU}#2ZV3Qhds`o6!{-I*AK1#_SHYC7b}mt@}T#Nv~h$1w0}D zx?n2ZETtC#2#IM&tdxvCJ^*~Vk}tSWPgFU|eq@K462reWPUfc=*ZVJj&h%PKufOiD z?e>z}GP`R-c^Sjm-&W^)!!KG1y^k<3Z=wnbrU^^4FCHImyYnApAMVygy*GW7--fY> zh5AR_e_Z6?N_f3%)BS7-Z?#RIguG~H8SH716?-ud<*`y+*UvJ_P5_Uq-Iq-t8!Bg) zmfY|6;G{lEN5~fp{=t5tGGp2|gLEzq>|w zMd=DUHg+{It7S#~>lvE%ud?#&2YMQRG6Q)dl@jTkjPj8+8iZ}NlH0%)nm@P#DkPRB z97;tfv+;%t|AX+R{yJbIx;iiRbv zVD<}FTxAT-i6@G`0HzWoL9=0yoL~3l69ow4N#>BR5JDJ!vI!-3GZQC{{ zb|%ThHfG|SeCPk`oT{$wdau^quIjbc^XxjbcS}gR>KP_H6i~*fE*iWyp>%I?I@`Ja zw#vqs>Vq(DnMNdXfU^!Rb@g7Tw-V`LawvnK(bO!(D`~}96!KNVEMIE^5&cC4hf;&3 zCG2e567jk=*EyY8YkV^`A=ki7->9ViX2S z&bBft9czg^6_*8js;Rdx6I}cQa3JufiF(`YB@=8IYc=h7T!m&<@s0;a+|j885Vqjq z=&v{e#af%ZKy~vat!P!^%hk~xTDt#Dw!uw?ZWyM((g(KzNqVAaE=N9^fD23|+gz@q z5ruE1#i_6wn7;QeYmF|Wues}`x>8P@j{~eWU!6>A0`2;F{X)*H5p>=Hy}KFCh3$iK z;~JzzeekSnuqtyFfo~}+M%XNV!21+p+8!Kdp(kT7D^MuhakJwh z`h2qqlY-*iS^EdJ?5%#-?S3MVs(+djnGlV22gSnQ+~`roicr8sJ`OssGACPhbP`S& z1dQM9yG)8`D65@zYQ~-2(kAb*Z`jBIrHkO;MxxotgxVnITBsI-;Nqpuq70IYJZlh$ z;GDt$z{@|2@?9oA@_a>J%tbzQJV#52o!ur>9K$1(yPVUtuLMt90d*NqOX=O4Z^fEI zoG?f~yN#~568Al_e3lgq3-o*HeBM3Fq^+zlyd@GUIK!+NKVxifjKY6l5kJY)ri1$_ zPkBvdGxrhEt|55JipZ)^Yk)CcFNML#VAC*AHDW)fCzREJV=@qaKwi%Pb*qe5S2lc; zl-REFv=l~QJxh*wXjPWxS=U84MDQr>`7G?rjr1L11-$(QAK&6)Wb%4ai8qcH>wRc; zt!b8TTcJb5b#fxy(><(58c+&5nVBEpWJFfPUsM=*@L9=9d=wGYA1MI4iV)0-H;n*% zr^d>J(`P+;<~ph8tJj1tOxab*J~uNfT~k{sH&@J^xWq`t78d(;FEEdB5Z#@G+4NXy z=2DYDcQG_0Vqs>Lv*krMlc?u(MDINfdLzazYV{HLoVQPPA5IRW3c1E23s5M8vAnO_ zLDu|l>&~97vnZYYmV1IZz6YwO3d1! zmAjl|j)TanIVGo{1ph$0Q95})E#>wV9y%67?IOZ|`EDBMv86}M^4|HC;cSOA`18P1>V7wy zlp9w|Yk!V%3by`l-Gw#qG$KfwgGr>%NywK-VYeX6-W*D!(+6#knr^V2ybhxx#zxpQ z9_nvKjbnt0iDuJ?>kw71)ZhB?>ini*kag(9=UP@^0@?q(>=~w&tkz))JVLv3^c#r6 z_i+s=UN$gLq4`F0i5+PhdJ6(6jZF3NB;FN0o_e|HGienvq!0vyJ+YV1w!&7)#Xcgm zi8WWcC|oy3o{h@;lm8e@)0_<9r6zl+m%IXDEu*!f*2~*{*E!parFrAD;jW7MmmNf2qUp_cSZ|ZpT;}9%iHzk0`aCl(4#j)12@HKoVNjC5vb0K@H^zH z$HD3n$M`n%xt&xIEv8K{uIFFZt6`TGjv-KiWCYYEtzlbMv>^jjj56%$A=1y(Nd;s+ z9OCz?D@vB{@HHP%J1_l|so#jX5ZqDf42ovV2~l%euyD zf-i@1{t)G`J)W=F<#_=<6?3jND%1l^w{*1s?)?Oc@8!P!KP+IhFtfnKPEY1H7!P)q zYog^dDwyPPnSLPz<_8{)-#HykD+>NXC{)VnX|E7Y&2(^C^7Kr^=b?nwz!zi&Yj5(a z1P>+&ptp$v?RnOoVl%ER$6-reOWQS_+ZyOJ0hd~gH}o4%GnNDn&wR|N_#kWeW^jID zHXNvXr#zgs44^#V$WzuT{`}UOgp!&SkviOe`QiS$lXbbcwKC;^r0~y>z;`tnx`RDa zL@9a8tHIB+-Dhq?!1M7(mnqc;)2oM2vAFQ|ec{~no@~#X%iXXc^b96_w{ET?Sj}+v z_f9q$Cj&ktsRDc;4&9$rh4h*`AfQA$hS9k}iS z?E+b*6C=7Z0xJ;aT-H{ObQhs}sQT)NgC!fiK#!IpOUP}+{rG)jpLAr+*ra=D#u<(K zw_g_Vj01kSY{tp~%|p-IC@e~$zZm{lBRndWtCZE2dpeQEK z^-7Ajq#bDRSOPaoCnY9IA@Mc4-9Klj2P0h*^!UeTe!GD-xw@6Wl=W;QE!Eq4LFnm8 zDukJmwNQR|e+TTM49{N_Lf0*Dy*tr4;S4QqeN@!1LJWxjbcNKnmWI4V;Z zg1C~R(v6S-+-P&P3&q@#;9Ke$rkx%PTRv=RFwnx^(JRk_1z|M2v)l4C2jH-f_9K6< znN+?3@3Eh_!9(+5vdRVFu8tI3iVaQk(E>ISd7Q+g8G+CNNLoKs9S42pq6s;QBx(8F zPqc(z22OVdmga!zfJ5qz#!!=A$N7u;J+C^=PhCsE`}yp|G*4^#~M>(jO6mF#IhSlsywNiYgqwZ!=qU%9@P zYa0Yq>n-_OYPr7J$$3L7P*b)}pDh19jd~yIDO$O@R`5Vp{2PY^FH7CB1m?(f|LNpv zAbOc|N6zh0d|$+|Zcz?$6<>OPK#%vgKm=zCqa@hUlC69NiAGSKHBH(lgcdQkvob|5 z;oVD)VI9tw{zPiVdJM)oq~HM5npN||aylE3p5(oEUE}49^pVb`dPd>n({ji1b`EMQ zCI;jJyXrO@z>8W2&2fVwe2U;5V{lwmvE@{BOE+53VV0$p+RJ8YC|wVZc} z>Ti&>o}lU9z_C6+X~n-@9vgy2Iz>I|{%|bYstbC5MP~*T0-4H78tbtwjX36O@Bw|= zk!kQPl*DOqEwalH$mFgA4lpBcx3U1+2p7lPB5D1p<`e4jk!uR5W8R z!E_O^@vuX7nVqfjS2$mD7h`3oZW2x`bm=xaS&(Qi&qSlV<8%2SVv7fAWn;N>b3zfV#*3o*C(8UHsIy`gUl)euS+RA4nD z?vZy*fLju9^1W&Y+W;AJ8~^>rnlah>8|ZQ=vO?f&H-0Y4b4GS9225DqF95#vM%^hG z)h}xq6KWn#%MuQWCCco`M8}-|L!M}sbSS>AeY8Gy#`tFk2^F81dvB~K-1rD-la?t~ zl$=rzZ^JmmIP-FjC{E=ckP81<`^|k&H)9?M2i77PTW|eES}U=# zZY-zEqSE)e$lRS9Y-Hn}iTRy&taoJal&@}kZzLH@rUr@pcH<0608Y;1I04MuA!U|+ z8?00H@>62F0$4FW1M6EY$^|9lT7~d)Y*7}C+c<|i&jq>lDpvd4N(0B1|Dh-A3JCRP z+foh5-%V@&2iF=^x(SBUZnynrQix<)!*|aS?xcvf={Oz8k^Z1j+%mzXi)V=9mF0#C zp{evMKKBz~_7~Kqo3h~|a2!N|%?yI#TGjcjeOdU7icJyQS7wwXe|*!yAU!%MS`ror9VOW6A9dT_DoWgC6#aFJyP!B; zaMKh9_JiyOMH%GYNFNg28;d5$YpHyWgeu+8+Q|3T06$vg&G;D>S^|2=_UW0BFn?bn zX8_cVlRNPtQJ9~+S4ybFn2T2H%!Xtq74*4+1_YwQ!2pq1@^N1sN{X=4D<@#EuAmZh z2}}wApC1jLl~#YdUKBYf;H*V1fGtx_d%?;tkjpe{+Fs>%cxuN-9=5Zefv{A_CmR&s>-aZgI*2; z^^A=dD}$X5w!V<02$_(T=w=qtH&e3Bhp5L<6J7WjBgLZx0Y^~;$Z=R%5@(_hTB$Q& z1ya_wAa^6;5j=L#=Swj0I_rj1=jf7=-j|N*JonP-T{7{yESb27&I-K45x__71 zjkh=8wd| z&oJGk`GlR$8&RBb=bNnZ1@^)6oKG^)B-~9ZsPP+94@-oAh|f4|d^Cc_r4!`3%;vex zr-2w#a%hUDt}Q5?&oZ83%hNX|u5D@TrnrFYWc;PJy;NUZH(mxzNFnMRGvX(1M2cmR z)1qb8aHzeW6n-h?E&Jc2!IiA6B6qWcDJy^QcSokq^0PnSW6}bHn>0VR7qvBjE69C8 z&lJ#bX~KG&4G%CEaAnX2{h*d|dyE@d<0cenmb<84UcH?H#i+&)$T-fvIb?ew$J%re zF4Md>v%sqON%pfTN;i)7~8o@Pw`p*6A#%r$gIm4X^%-7G%p7tM~_kE1( zpC}M4X9qk!6T~i_lE^n7@0h}2*9~ip4tb$q?R~8aSL((?nEWHT1E=Dy$RG6%Ltw>F z_Hy;I&|3$S0kpUtgL{+6rGMjF3|4P2LzwCU=~bbGp7&LSebSRFMjXABs*A# zcoUt2afp*0A}KYzqj5EBWf|vhy=ZqymIR6zcN3|kdn(`UBhXeuc~-r1Y?Li>#T@Um z)6Ky?OM}uQq4X|!*Rj%%tRRV zAjK4*?}W}Mxf06*Z%c!2zWaOL=r(%!pxYJ8fTKRV!oh!>mL-&kEF#6sWHUm%m%Ga6h)qc(-uzvk&BZbl7myxzv;8`@$KY6 ztdB~8<$e-NFBiT;_dhT}R;dU_u9z`J2yd%gMRtbWpN5e{iU$hQzNbK+xw2HOcm!t4 z{h8nGQ6gBETB&8r^yPwbS(vlqXf5FcSz_&12BmHw=SjYu{!f}bSeV0Kqfy>mwRAYs zSwAf^N?HJKiTdIxq9aZ$1}0#2rJ;*7^Nq-wQ`mQ1>}sy! zy89n+6|abIhi_#{g5Dh0%M6S(L7aMDO7s2>LIsi;Ig2)uT;X%Nw^#uta^E>oV9p6q zl}%8aEG7F$a+7gRvhe|Gt2t^)L4LhN10>7;U%^G!_1hFfUnV4kr#@kjDb^kYBl@Dm)OK&h&W;b_EVZ(9Gw4k5mOF*t;RYZ% zS=A-rWE_;3<{Rl&*CT^NMAH2ksb6KdCI^D6SEwQLa*@HpVuj5^@$AzLg6_`uaJVL` z5K8L|1Sj@ylml{p{tb8Pg4TVln)G%s%+b?T9PKks@n0Lx+XH4Y1K<$v(@O6Gw;F*iu$Az_W_o- zZZjOeJ~G==^%Rv8Y8aO3IxRe<`6siX9MT=FnK+`S#~qGz9V7Fib#8qc2ZPmdWcMoO z=%0tkXMV&RpcOUSL3B;DCzX&f`-~PvkAGOWDaIw!;;tIw`o~*j%7rAzW47FkCS1|? z(DKjUPS_KgKe@QO3JZMUTBr&A&cIBtyL1($)ZcbbY&1r#QbkBL=r`iI>+BIr$XVt4 zw-D9bLprto8SjmOf5Z20DzsKOA!F^~mAMk!F#y&htM}H}_>I80gJoY^7jBl_kAQt$mgjrK3R{Bh$NzF2 zeBi3q1-_~(9@azw7D>X41l@~RIfZy2x(M=R6w-kjWICTlgT2HCm2JiEq4Y&JDtst@`v)knqus6GH1ZPsMzDQ+p`T9i#(q93Y} zM#24x1KrrqSxnp7nGb95Ee1)soVt@Xd;o_qOJ=<)ie_&ubG(f-=d|*!*jl%P4dQ*x z{=KH}JDVBt+}4-X>AcZC!rA2lI^60!Vm|j`0?9}D0!~jo@zKPc@ZqlngHDU6*xgl! zcX5AtI1j{`TOsXCprhw8|J^<8SUF6q*O@6sGU74W8GKMmm%_r+7gbc z-(i?3e3gtj|063Zx;pzOTQ42&Ua=5h{VrplzS!wcdqTpEC*(82J6@EXin&#^ev z8e}=+clyV?N@YHwc}1(~pV3mXrkqg)u-9@tAvKu@yuzT5vIFxArsAhgeBMxFax6y% zE~xd(y=W?}6L$}F zuwD3PH}6oY!g8d@PSR`bjz`E?hT`jbhfgnLJ(d7aedJWlsN#1mhK5ByP!k>Yav`+svrrRvT~~y#&ZYxH z%))>3M#l@}5ZB55`F?t+r&{%*D9O-a_`Ql>rnZ%Eq z9trMfkQ3oZxoJ63_|`k@nwPOBO;?!@xDoWMF72VDm8I+$r89ua)3SP$M&eEhI|V^q zXIiiP{S;T{r?>uLJr$KBJBrqNFOph-$=<+9E-i6HyaZ@Ky_S``(&^8ZT6&nkXcYt7 zFx#G#AS(hu!OT*v^&~XM$=ITK4X36=-cBt)P*|^2QiXjV>50rw_M9gr0hok z_k8Uj@?yS?sU#p*l@2HQkZl20Y#{-%1S4c({G&MoyQs{iiJDdkPXZkhZ&%U^8=_;f z9xhXS4Rzfc`S$PR^{QsC<-~ae=&%i?3nt4ry^B{RE*nWGCa41FG$dTvcl|`qCUG5< z3D|OR_yE4gRwD$&guI zYMD%}xk}&fvNRIo=&}lbaecaM&4x|nBoaL{A@zKl2d*i28n|^_&B;!(c-qWV)cxPh ziaMx4<7lI0wDwhc{LwFv9myU2^$ALt3CY9VphWCBXjv?Hy>7fniLCZmYVf1ri~D+Dc_@_Btn(O!H4RSF-DOS{UcMGx*CAHeQ+0vmazeojU$r(>my2=%F2lE zZsaagJm-PQ2FlBDWZg6UmbnpsPzK4!m3crqQcoDu`M~&=E$sa%iBE*?z0mCz!_Mp1 z%-oAm9Ek)xl7>mHAugc3dnr_y%j0mrXzBN;i44!eLTjqSZHCJ+P+m5v`DL(}Y4xU~ z{t1Xb_Nk)zlhZ2C*C-?K(Q=l;4&hJ)vIWog+3^&knS3K4Xm-&bT@T2nvf_E zc)o>cUsdhF$UUY}xHQOpuXqqRTfXJFeX)@9Y6uSwEYYTCJ7BsaYfKY*nI zes^d|Nr511Yf|QZqPf=u19$QqBFs)5327xniOBO2rq7T|MCz3Ho8fYJD|Dj_McwVS za>0@!QD=jQ$ps)siOk9JW%oP2CO91 z2C)@Ao`UYOyDAWodRM0f>*RJ|Y3NUoC1sSoM9Y0GKjhR~^Ne$&h9>{LA|__@i7?}3 zStQcWxf`K z)uQkcfqOHLU!gulKLsGl4}fP2D7Tu81HR=}Fm|DkYho-DVeJEPg#)b?o)08EjqNsj zEl3s>g`o9S#ZR$&L1rYho6N?HQoY!$$-DL9D*dEd1^L!&w&x4vs0t9*<|ZTujF^5E zOa>x!DEA)kLcYUyn7(P{HG1*ofH}zO^akAm%pmp9-BQsKT@rC3G2pTPujrL0La(l? zwcL?^jH&vp#>aW~(CGBTE>g!-cN2!0=RQJUsCl!rOT97H*m2W(DL2ejk%eaVFS{)B z=`9@ahQ=jcWEbP3ABPV-Nr&pS@(QnKEN*D0XSv#&F2hM-vt9-;H)aV=V>=T;ADoWe z`&Tr3pghHn5H$uge*yQrYr=~}_e&&GS<+IR%x8ZCJD%6cp$CV`rkSwUTO(%)1>+?8 ziKE4KL;@woHoh6Z|3BURvNQrsvRMl^wqwDd+|!GuB`fY`0seHu77nGSY)k%mv8^5Q+h#N@*Zisspb+4c0n$>Bg>O=k1E;8DrsU4@B@0JK zGq>Lnp7L74{~LmQ{Pp~F^`O0=7T*j?PEN3z2>Z$XO?ur%jZ1ks%*t}m<=|H%wN2S6 zR6~bXeb@W%t)!Lt-2o5EKa$abL~>%-#?7rOmRp|q_evMMlFd^?xVqV?Etjk@~l)pbR zCZOp5cVgD#;F$B^IN+_DTrm9LIO6TAlR9>A9P*Z(nCCD)OZe;s3V>7cf*eBhaZ&!6 z|IClM21)vQ&O`ojo2fm2-?b}40J>Ic-cQ;%VIEr$Xh_g|eim)21^qSRu2*9#4AS`* ziqotVS|b}-nyn)RQz}u5NlLY#GfOkkh5Ab-FUjb*o zbX>C$@s&F~K`(h=lAZmDLZKqCY`qa+5QlbOAdJ*6vbm4IF;b|G$#3(PD_vc?MfbC< z#D@|#-yO%KcShRQ6|RTYLq5@R7i3VR@Yea^;Jnr(=v8ks8wej8x?NVN3}99F~P;Wd5FK>RKVceCYoCo|k9ca->i{!sGH?+DXSs2uJnNa(ZJh_i`EsI}7}nGm0^Puzr0Q`Pki7w)y8# z))h|mDjEKwuIWKit0f*T1LJNuJ0spW3fG>)hY4fPs92&DkX05$6niet2cBq?>}2CH@6*)&1z&|NA&)%ypD)#M9Y;-MmWXtIP=ak zeXbgKKD}8jayb%G5%`WaD-nnU1(%D+n+$(ZL$ehjS+xCl57>yLG~Y8g)Y)s;XZ}`t zMxp&4jpZObjX3|;|06f#{vT;KD)ON2J)v4c{RAZSfQ!X55#1e;3Zd=+C z)BN~8+|{w_X@K_|nGsM@_J-+&4CoOQU75euArYRMP+E=R#>KT_8tt!MGw^ZK8~6=D zT{)ea@t7k2n3fDW_xj9Xv(!YB55Jnp$pNOqrcKczWDdiubgB{D$!d3+W27S+x38{_ z3g}+eL!r52hAb5N`aoM@EaG-iEh$V|!9GKwl-Ig}6<}1LFQ$zJ>xCc+x0(LT<}GnO zIDj z2KPZ{00b)h*-N>2n?j1*MuP~YIXj>1qxEF4u-qJB*%_QR0uyB`k_=Tt(n2yYAF`pV zcm%r~^r^L@oc3l2uKYVcv8YjJH!=SqA7roTTD4|u4av5(C@qnB)*@K3uswBo1!Kze15a#7{9K}wfs3%~V5-+;=NPEv}y z#BXnk&ogDQgV8sCekce+tylPt7BXrn!>uc)a;q@UDasbAVvsAbnP3rZ94bN)hYrVPWvRf7?8MGWf04Noyn68mhGpT3}q}6@dKa7~I-d~olR#hKm z?MVU=Mmz){yfaF;kBu>U<$jLrrdjC9F27Gy1-f7B9nFmTIG}=7G;En`hxAMkPRA@1 z3{}OXbtWYit|71|OkxxaLzPSGK&sF1{p;Qv3_hw4;J(7}KMhrE6-V;H>?>>G0}2~Q z&Yq3`9vmXb1@#SGm|FX!w}=U#z9Mn+*sTqtLEIBSlQp0^u90;Vd6NW6lMpQ7xXUB` zp+!ZdQM@>CMlWO6!!#I&`;qpzx^~Tr%o+hYz3@V6TLpi}Rc*J+6B!qND<&TL>`C!Q zU2%#UCYZyF*u2DjlEuhsjDeJj4|v*)LgXL0oqQNC;ou(O@bD6D1*fY-qOT&0kcY5W z-wToE=)oSO=tR@IZLe@vNRn?TO(&^^9Px+{%bdFmZ_#a(TNJIfv4M@BTQa$kqW}4Z zQ30z$-^Z4^4?%`wByywa8~lT!UTz*&v&4pfLfBdC zBVdR(?3IV!+CB6%+aDN0ls7uGla4T~1Z*p{o>T?n_JuQ(?v*4|-6xV&?z_Fb67V~{ zoh@@KMW)KkbuNZaTIxNSfmt0ZNE9Kq;EKdQd$5G-kd$B0RQSiz9@1k;#AG3;RAne4 zEG({g$r4w-v<_3bu))#>{*gSQHL@%e$M~d12enTZgivv=7li|Gw#-w>eIX{2Z-PY=`U(9GXdmmq2vdVLa=7 z;ND&TT-6JSl!3XUG){R(>yahbpVV|lduP(HC8g&*cx&h~EfnErsKlB$WJ*9%Abcny zcf{-homx~x=}vr;j5^2$>zNWnuGro)t=ws%yG;EF0u}ffP+_##T2AEr>ykow3wOm5 zngF?z#*`1@j^=C2ABk`_j(SepOtRN!&E=&1=I_IHOOZcZAbDpK`&g_M|7DX%vbSs$ zH)SXUR<_iH-fxJAfZTyH`fHp|hy%SnwLYK^!r*bu?a6b6{}1T_@;4gLY(u#hJF|@- z+c~iLfYS9Hp!K`i@BCh(a}C`%kf@BlMX|nh-!%TWGo9Z!vJS=H=5AWW!0rg|*N$h6 z2SpVSBRDJhtt@m~%!|7>LwM0zWon&?(W51}hI&9RUcgi)I)0M;NU@js_KiORJ-Pcl zP_^K|S@(V~{KE_4UnE0A?|{;t+wO$`O#S`NqHy-#;V=Kh`;#>!yJ?mw#+Uz)%u`6+ z>|#RYbnBR8RTL)nb_+SMC=@=GXVk2w>PpS`Cw-NwW5EIhy<6M&P_~iKE%wZm?+>{U zH$nEOJV-O>|K3q>-|*VMcO|35o;NfUnYqy!M%8St_H)zaqisG>($N;_Ex7$VZ#4bh zE?IAbPYh6|(h3R~dCzbgVljNl!s+Pz2{kp=Mu)vjLThf4ziGfIRQc+M76i4@oFExE6a>IUHX`Nyih3n^s#Bka=17!tJ0Ilt9->b{v42{DLJD`tTiX6|wPaPri(GI3RPxo>w@#V2459lnQx@hVe9whT|1tP^Wi^X1?G zxLJB%D3PjUd4AN%u3vytM2(mRVBj2@o7_JtI|y{B#bh_MMf@boxd;~@Oww^jida*6 zwOshcV^wOAp2InfHDU2>!UFT+>suxzV3QZtTewYA;V)oHs`JF9>EX3fD$TY0-iyPH zR3kx_`Wu}#8z##ChpJ(%HKSX{ zHu;{~E6o5F2j^`?Pa+HK872*lEpjxN%fe)Kzge*gANNYn>dV1Q6eH`ghdxOlc-xJO zK24b5#J3!0u14$#aVBAI?(RE9c<#J}(qgu`0%egA+|_6ZCCdRs%>70ZPgUhw7)R0G zd4Lr1=Y^)MPX8i~*nVd=Sm;S{2IqKSikH%8N_HG8ku%dSYkw(b9)By&T}%cIu}@BJ zXaa^N7ZQmt>T0@^9FP(F?J*R{FdFO^F~l}<(V(lruxLgKR3IT!Z=&Cm8XV9qSW9az zu)|9jKhh~cC6|?^lh|JSf8@>owUvC`bkHjM$og{+i)`3vn^H1M^L;=8Tw#%5fw{&^ckIy#>M?GfrQztj7Jal%(&^$fQz zOnxZk1bUiLy+Z6&{s=TVIU*aQ$3kR}H2GdmqT?imBOZpPPCH@PC?Ip`S@d20Hm;T| z+n5REOgSBYBv_FAVSyo_k*4va>j0O`V`fhpd7k#Dc%z9xXY~LCK#lu1}K~xJy{`4;o1q`ggb?F!Vz$abVKRN51$#tH|KM z<_)`*rxQEFJDD`%-hii<8kE#{P2L^ML8)Z!O&aTq75}-G)J+PhqcJ#M@NArDjvu5h zTQk?XtjK~OMl}oL&|-^eONbV00;ZLD2v=oRT?8}(d-j3^Uz(3jb3&3Ul8C)KeWW$G zJIUD#?8W%r6b9>mn2!ue!&cKCHA-V$Q`=Gh z8QHp&-b<5J$to9T-II{39hhPz0a0(OQZ%XuM6atykC-N0wcA$KqRQh*kJTOA{myE_fn%ypi7bq1k7gEUhT?nkACcJ%hS$%FmJq@psnN(?~ zX3L3g#ggBM?dG#rwBp3ubdjL62=5>Z+9<0OG|E#D6O7MxN5OxE|J`Bzo9FGEji5q? zh~C$LY|IWC@4i&LO3JT}r)ChxsJdwfpxPTt38|wm&)8()|0+|I4>4`<*J|+S{gJxJ zjl~e1&X6Kj2E5N)4t9+b-0nI{`l_Lxvq}{0$0&Vi2l7iD4jPWTd<`ptCE^os<%Hzz zSzcg6o0ItO$=}hl*K$$ut^U$sft?`#IJQbJX#Knq!^SQ3HdRbF9W)*L0rl(#Anp}H ziNVy&YpMQ*m!f&n_F8HgE+P+F*ar{dgi)7jvGEsqx1_J|oPrt_Q=N^iGChXOjn|e< z!RX;-?EAW0rP!W{`mBTAf=mtg&i2NHG7x4rcTY-NOg>og+&q(T0CEOPgi1wr#+(H9m zhx4q9M+Cg9Yvs`V@NFQKEhvpNT5v6e+`ZehTld1_I3akkNJb_TVhQ1`AHaio3spq% z&;bwNh76<;ul%_q-tS@$dx$>lEBB@7AV+R zxegy|$y5kQc_!}hMLp2w0>eh>}oK=u?-Fki1CP9-M^f z5}!MRWWI48;}CoVgn4a1Oh&BP=JB_4B!1MU7IZ9Q50;Dm7~7fq6}CXaqe`WvYUnY~ zHMKS|e@`IB6_1gv-9tDSFZn$!R>$bbu!d>9fiQ&P-OuR4cE=O=QV)Q(-!2rKOx!^X zg^zR(7d}!&6bD5cXmwjT-AB>W+46Wu8Q12Ev?}qS>MP5%Rnt2x&_Buhn+z#Rc^GJS zh`m=9tgNUe9l!CoGEdftlTqIay~EA$82?brjtR#p_B)6lD)mq zf05;@nz6n?_FEjtkhz({DhZ+>o3`ay=RPyCSnb#*A*c#mqlgOV?*EKQOR149(V(zZ z#134s8+^YD)iE7Pge<3Kc~$QbX^HAmh`0Zz;nqQ9;GqlOe%~56;E;xLgSCuG8{)QF zd$S7v#-eR%lG7CG%`9b3_fN=9`va zROZ5{P32QEWbF~vV`-0LLStPE$Z}L9c?iu8#GK3}XxCj!#vPeLkr$i@5od+&aZq8& zUv#%%wi8O2z8X#CO1l*DNMzjunnl-PNfFs8BP9b$m5Q_$-D=m~m`iT-W|rbLmUFP1 zmH6t`A9{dm@!vy}0^0YTN)-bgey{uAXdP2U6dRL#Z%I51$x-x#S$C?)Lt6frfQ>?3bGi64lUGiDGqf7=YB8H*MK#LfxJ9}w@^4=)GndXTwB5BvqQ|y zE{EO3w$m~QLn^Am)RF88EWl7-|*K~B34t=LL`N@oVFxy@6o#JN|j_}hM&`{AIb+l6*ogx7u zT;&W{^i*^PZDqt=%?$S>VzUL?jUx;@E(#?nkeDQr#UzHB(>F1=!Bd6`$rN{_GTA5X zRm11pc{4Nx>C}Q#x4s=wsth)YPGna0Nb;#SB&sv?_U(p+kcuW}U_o#cKtWBd&;FeK z8dyZV2Orn;yzeJ1?iv4~AV|X_;ffKxiZ23!`TOJ08ifiL>V|BV^%yxzqI6_jbuKXagbPK__yb@O?v#wTqPd<^KUSq z)=>N%<#3Cnd~Ra3(8V!vZ&we430D9jKd5~8r&4)YtH0pJ9ESM{fg)rmlPhLcY+XJ- z;ro*z;J#5} zg@C0(3}#MN4rJf}3(x-bZh1T~&L$8N{3@w|?d%K2o{*&`Y2t$y0>@NRu`8 ztmuQ8EbU6syP9&+=ZH#eo`l1fxAmM8NqeBue*32ZP(qh5#z?luj_HqEk2RIrT!8}@ z%EovI{bOnjK_;@fiFSpx?6aGe4;CDTh-MC}puY>S{(6xuSD{ZBvP9FHEn|7Elh#^S z1pf%4Cs0%^Bsp0-YnNCMxdYDx7)FHceQk=k{dRA9G6SDR58fO4rTH_pS%c35VwS~b z!MS2*6IaLueL_NIYm9Emi3pMJ(@*hEU?Z)37J4s3MCy9kS=AFiXU8uhx)_**wSRbW zr&|3XF8Fqz?sG3;mh$K{_*J7E6X_eMjHyyXoE1P>(X=cLFSvZGM zG^&~>wBmZkT*hRLrW#2Y;8UFWnw!Nf$!-t2KoyP*hiZ(YO}QXvzKNbI;aehdi}rb* z)Q#-Vp|(|~s(jcOyK}8_V!_<~XHzXum5;LxyZ)^ih!*2+1(rHH}D4!Wi zMlaMYR26G#dY@W`jQ^RS_)WFYD;E`Dp6U(WhVY!Mqj%F_xdw|Z=QZR++e(uD9iLyS z?&fZJe&XZQ{`{n^YpmT)Hv?TbM*G$0ghx{IV_|S}A{t@aaDJf9<-6lb&oA;`T<8uT z8fTVfSrryyie(G{>0|HjB8u-MTbh=H$5dM_SJ6O6IQpqV_*9G&#Nrgj0hFy7#=k^9 z#XFLnIN@Y=*UY|f2WjDt^jh2z@pYu)`q7j3Zc_uqrGmeeO5V>wK16e9&eJNBu6qNo zp1-D)oM~Y6Sis34&6axl4@0Ego$pXj559%&lco_%6VRpt9}A=(=#~e5oTo6-|G@eK zdEG=y`!xu$AkJN&BM|W-5a}XX!H2=|BA_W@(pu-YTA+7(JdGGGMV?rjnr8%T`-kU- zh@9#;FLoqkp22KS51`%0TR94qz_G-&9dmNeah61O+c`TyR+~T}z@~IY+Cjn)uq1Hk17XM^b<VU-BWXiYx$#ba4)~f)HimW_%0Fqzotw$OX61-P zl*}+o^e4v%S$B4S_Vv&i+mmsw0A_{dGR1P4jXer026YFjczrUal2wzkGE0qI2N8Rj z_$&aQ>&4>7zkIxZF(foZUYU;@2Pl?o0Z?-woqZCl-{9$HJY6cNQrL|mQ_9P|3^VHrv~5Os!Ws|vRWuH-tkQw+ zxl``sap-r?gYGHQ8oZ4vh&#>4TuOMnkRFvC61=vAC>S?svj>kypAnnodJ-!JoK zFC;PRigxF>F^l^@U3~>qTV1m@BzS_m7A-DqDef-Cy|}v+FBY^o#ZQ6aQoJ}6cPRx5 zMM`lm6pDo6azo$u`|tg~Bs;U7dFGkD&(376taVQ2x%$ydyveC{Eqr5lvz#S|^uKfT_8yq-FXNb7U?}0ALC;9V$^YZBS0XPm6Q;_}(!e zTsDM!=1lFFrfJlvmU6~yzQ#Y%XGLjgMg63JYUufkgW`jP4<_ZbuZVNIq`nyr7@{Xm z7e?Igcj%a5w)-2-U&&F`kivYg*TzP=#5#p}Z zgxEY&P-0?Mr^B7jG1P|lhT>kN$&ge_>ve{UZk`A~T9U2#3rtU-4*ts>Vn+@oa zr}>aMUiv4}BTil=8?DS+$b+TWgwNy+#>|UHtQ{TKgs=9l&-zxDRsEFXNXu5_)CL!+ zFZ$diH`(iXESt$8JB7cIbYT}=ov_=9uG>bB)3u*Vir(y9cZU@lh3E3(WgnNRAHf9p zC~8gL<0`99T9+3R>ZRBXEVFGTpDqh&-9Dm7(8cadF_QA56f)lMMGm_b@|ThurtJvy zamV}Z!tZ)F#!C@y7S`hOd8a1Gg!pzm^o+fkLtb9F;o|)ltRu&97$?l{LMn)D@bzRQ zZXcZdT~3nP(r)TL^BUn*Q6j9zclL)ZODzelgr-+Q57Qt*ff6!4;1J|k-cpN9=E#Dd=7On^)sm;KqxU8HfQe~mrAc#fnO#|*#*D0@9}HH}B7zW+RRwDIcO35RJ~BEGdYta5UbHwqz!&Ib)I zxqZ~4@y1Eu+cSxY1%s3EPL7%`GtUY>3c_yO8g#@xhTQu@7wNwmbJ)PgLBTwE(eGXI zZjvZ^-))w2*t~$%G2}hd_2UxyILzG_ zfa&l=WfO~!x>Qj+N~%tx?P91Z*5Fkq>g|N7QJ>Y(qMvB)pN7Nfc^&_V*pyp}eTD{7 zvN@|x9fRufSgfUZRz9ZF>WC&j?2m|_QEr&E?|gy|2nHxWEbeqlyx^tNz7Q1XYew2( zpyPZo+(+}nq``_6Hp!y;vR;Qs*(nD*`mxBb7Bh0q*c#3xVk>k))N~Fx6d4q8KW5)6 zI3~oEdkVyB-gSW<6?G`1VFkU2XK%n>YHry+A3L~oX_HG^dGC}W9x^zie&VOL2bu=z z@spWg*)EMf<2b|UvO#LF5TI@Em&s-;$LFT@VYB1W zz9yoyfpCV)F7Km&SkHy|-?PsCJEx@ko!PA(3na`cQ9TqM>_C=bUH`O!1`DBT#(=3V z2OPBuogtIY9aOrjV=3)Vm^1NvTIkUAit~#Idf;ICf?1#1z@K z@3WlGB}2~R3QRc^n1VW~-WzqlK3NQus_n?@68a;#a~#3zJPhtZ{?^Ym_yADG?4@KH%y9Mx}jZ$6-> zb`2@qp}}~PXK-FbD0y#<`i9ULOno=QxpY;evGEQSqVF% zJUUsGmFWg(@r!Ox4z*26Ja2zJ#FydQ2l8CBZ*;9x4q5?^Bg}`rgYXF}4&&-#AE?RoWjYlOB`8?VC3-T2txomiALZ zV=_R6!j#a_ZRjhHN8ki5h$%b(W0g{tke@iXI(GMD#Nr9LWiKbLFBb`6=s4D3KsBndSRPS*CXty_CL#zXmu~#x4hH z$o`D@)1Mkxl}gC&PEwMdtJXxU5yJ=TH z`iRk|nlQ|eSBfny4a96Qj#B9|mqPILseKG8ad12nRuFOsH_Pg)a=#7tTrbh2quv=< zA<|0V7kTFm-?xrIZyjCMGTR9JrNmp=3ZybCdUXXc3InUt;`pn9iX6qjt>6tJ3H(8K zo9^yGw3^qckDNESDMUBid;^WX0EMz+r%9LFrk*bl>VSQTIWgevEp5`vBF0hA*9-{1YX$J`T^XfnN4D~73rEe5A)K4dK28& z;IdC!YZgdWl+5^_PBq{GwP6jUI6&e}fFQ;~n{u_$3t^SzVq6q%dYcJG`Cnn^u;_1+ zzHDhI118$z2DPYJ(Y=l*b06k5?|Yz4jf=a(5-5WG=xRM}^=eFBmVpJiVZYj2N4oZW zKDd!Nk`^YSRhWFI@_D?azy>B}U*~4krt8*Z{zY#h$Ht+rN}I|33@)QnvNe-W+E6Ps z(r(slE<)Hrc9JgoOWoE&>gM>{2bg%G^-4g}FK!S}(RFj2=l#Pu=kR%M_U+2X%C9@_ zg;NWiUlX+(K{{&*<^)b})gH~SfzYZw-yV2=VqZ#m))o5XFyRBwJ7$>)z&y_a@zQq`-zeR)p9V|Fzw6lOxCm9Kzws#~V|;?i>Cj1z zJl0Bv>d@JrqSKnxHNBYtZ+VQ1?O4UglWb=s{4Qn@eAygY^2`fkh4%bHUsdgV{OD06 zOM#)dkg`j6;l{xT90C90enopEKec zZF$+bvbeudB}X?Zx6gj*km1!tJiQBD_e5j$JQ2uVRFPkDQIkWR%=DAB)lWyQqBmZW z_!KEo2|u<19VcN}yNTTX-j#kwH7=jRWFdR`zH%Ad!k*@n>k?V5L0h*=LKB(uZj{5>8Necf=m^b^B7cMp)X2NM&_fCY} z;$$-eUu&VhklFlE?`0A;Y^%#Q_J^lCDxpn^8WB>a3<{npZvU^4Duu#~@0+LU9=qc4LNp@<~<92bMT7M~90U8}vC& z?!vBSkB`ih7!S=8Q!CbZG+#Nuv#($*w&0LI1#>JcPv}CMyQsXpc22BCR_}B0=Ue}X zPaRlc{++w?HtLhZ2OM2(nR9^Q$C4!Y%BaQex&LA(6=>Qzm-#0x(P9qm4<6HJ>Lh-i z7Nj3?tV1P&{Z>5qsbLCz-nddhw%<=S>xmaA39Dj)ZtBDH(Yt{Oe4FJhNqp*^NK)DP5Qc#xITEmGu7>)a{^WzxR}5FD)QwJwF8w; zk+TdVQ;DO1GefhozTL^l3kTfaz4MH6B_Zf7dr6ZE>Lz>xqYBCKB;PC}UmVi?u`zDCf`>5Iaz;hhv zxN?Z( zkA$$9s*>fWfz9&B4l|x%{hptMjRih)@eN(tJ>BgyI<8gbwIhEX!>1C4bNNPhO|FFR z9hhEZ>Uf)iA|}00B7;>O=q(T}Jm`*Vnxnm+=`lct9XgZ2P0+L$ssbRjnr?`W4uz1u z$MhaOh1U+M%5OhD=8KCCk1tbE?Q>6rmQ)q%f_kP;(KRWjEMYz z{{h=v2cr$9j_vr`3v!^3c`Ukza{lm{7=d|H>Srb+WB$g(FCGA1-E2y)dxw4TJN~3K zBYyT4X1FH%BA9A=NmZ<62z-HdjC3MGj4DxLG<0q{ztVrPu9%{q5{>Dj##@i#W|(FA zLN1-UGb~OaOS?2VGxdtON}GyeXGu*~s*K)QvrNh1CuW5@)!VXSi}h0r19#nO%@e)g z-Mkl1#sa@b;K^D%$v|{vs5LkD$Gz_M?Sy=fgoR7w{_#uJ7_d>tchWAZuumbESs@|n zb$i+-3v65-6wtB<_-K5=WeLr!X;E7vOL+M+kAF_{8Tq3UZ(b5*j!h$}P5LTlY1^!6 zX)XxZnMbEBq?T7h?^eujdI*y+dcnaIcE7(dsdiw{=42|q*MvOE=e^8K$?ZP9C%zfm z2%~rQ62Z4AmhFznu9YsWkmtnECUCA&$57X}(;<2Lw7@!XaN+JNDha!uoBQ26nrzdr z6ZSPs9V3+-1$xBW4C9Pvo7rGtp6|+DqUk#nr%^qO{U$@vOp>DN)OxeC#O<>In*@FE z2sMF9Dezq5u{2KXkMbrrKEpJQrON#@4%kZN;&GPsoFxTcVctZgC5eCr;cWl7srwxF zh^UfdUQF`0ySYCy?Og`RzVuF*#+m2FbnG4bLmhZhYN)soo?Pv!=;jjZyb zwtnIrz0xm35>u`=RthzuXE54K|LG4r4eZq#Q>Gv8R-z%2jLFJWXZ3efDOa$liiQ!h zR`Px7-N-fDGTJ@*R%6V|=EY|HQc3s32}`YDvjpp+p*b%HHzuz++_jLc!>h&U8?L$( zE_SI@>EV6pduwgNAnP{#$;ugK=P`qgogx84{Fmn0xrZ#>RU!OR`fEcfd5doAf>P)S z_(H>+Es;}E5xq&I;7R|4MAZ3s`b1b@{LW}WeM<)YPg>X8Femi8gYL(~SUb#v zbV!1#4lQKl4Iq)7*YZRtGf-w?r*P~U{7lq*xj3PLqe}};DB^Tk@lS)CH#!lbn}tC$ z*=bhi8832j<}}MF=K6Cf4wAL?N5B!E`dWX z`S)y5@k?g;5wv!e>`Iq6p_i8^$(Z#1$>Bu>R2J?sIOVz^n=*Z@ddCV)EdB^XHG+ya z9p0kt5VbU{S6H?pN$9U;HJ-pUd_R|^7TVPiPRFQmzZ`3IIBqZ{!kLQYuNxGIDosxo zQ~Kt)Zg7%UO%#*n@0bOt4yk#E<-oBGT=) z?rRmBRYge?NJCX!gTZo>(W1OAO7CKI6tv&`ZlDu}X^}wGts2jm zRv0|G?ss?^b|?~ap0G@CqxFx-Ux;GtAYR#_Zl5_DivX1FGRh>TFjt}GBU7fM657*C zj=WSU!WuDZk8pc>pSSRgrHz2Z^(|&YBILJoDUm-syO^zQuYzDy7D=go+g# z)pOj0s_2T*irzFf8%-FaVxL3u)f}ct48?a$(RHsKY3Bo))6-u_qD+K0h7N!+`LVXu z63Sv(leRDxc;CcR+%5aK)rS8mb~~Y+dzVS{OWsiT6V)ejDFEi z6j<|>3300QLtP}yGprJABVTFD&f|@v+(M_nB3yU)Th(u=UWR#8jm%FP93vC z8Ao#+L0GJ6l5J-{+XmwoI+LK8I`(AO7qDD&Cde)|+!_GImK0Mg*$#~JG- zM#@Bri72I}x~si6UjY5aG{b#Y?^W9KbB67p{(wRJjj-rJ_s%3q2DVq?-oJ;&spF|P zzUT?Um-|N=c3||ux67~cqA$_1Eu)lUN$$<2M>Ti_^e6|?SBaIvi@BGn3)1uLr9qj| zYpXdzypM0%3h&)Ra2ax|+d)y)F`q_VoS440ed{A=I&vUno-K{p=_?N{+^k{~(eJ>9 z;MEb5(EHBHJG6%-T8e0XYGKSpCMj_)?{>XCtq8nIUxKZ}DWBak*MPN|xr7+@tlq`b zcNlwooeG+RLY{xVSA*hl@Mcp}J-2(7mX;zr7@B{-np8639A^f4^ts`=5B?-j}!82w-X3tfuq~dws$3C8VvJza8O* z$PlaqhAiWxXyh8I^-_tGskld=xlI#zaGB0|KPrHYzMl_cV_k?U>(B2ZOMr~to2m*@ z@bmoXhA@92;BI^qp7njnvWN>lT_0H2aM7dKe-JjDRKpL)(teU71@w90?IW6}KRejU zcRpsSUcMRi{hp;#L-XC$&M{%^HcTVEU9|A&w*v1|EK(bsiKTZ+W0{JZCb7c8`qZ&~ z8#98i_`nhz5}obN?+H!4F5bo2P7%iYnZn;wmn!Hgxyw323HtQXz|GW}Q;+qS>Idb{ z4F&d;f2_`D33o(*D6;{@HsO1Y@U-VcX1)=cXziU6m?yt^bi#r{Q}wzSHHKSd{QiVl zW!KnFFAP_(M_H*;@iWI%N^v3XXAIhy@|i2(Y+_|5@GQXkgmpl>Lp^h=WKg*nO}xWq z;F_T+h)D~(1FI{8QHYQp9xklS4wT?TH;@Y}a1#sWL)d2CI55eR2gi#J@Kc114DJ9U z1`1-JHV1d0mEJr!*;>&N0|PNI5d*84tremv`k)FrhX_8{Pv;Qf2YYr75qhvsFCa1x z_8J7G0#Yv^j~_t$1w{P8-d;e&a2#3DjlUf|PW~f@gf#jEP`rdFKX7Z;5Gqi-0Lr33`pZ$IEGb|Bv{w+U2NA~=gbEo9Bwj%%agF{<^pFr)=z(K@ z2z|(b?D;noaJhq^0oPX$=zk_guK7z4n7VuYcbdOb>;Z1q5SIVU2%=L0UR^`T@f`m< z`0qjJ|HGy@E1?6AZvI&)`wijc1P@rag+PJ$TL=z_p$JI2LQsN5ME{SX0Oc(|cxh5vmkIyz3fw^`z_jGZ zDX$bC0V3qc|GE)*D8Ttz4{mZ`><%J{Ob6iJLkvKA!vNkrgdA`hMkap9K!wNsw|@Tf z77t3JgHiy?_Xx>g8ZFce`8q8GDn@6A_(-2KB7X)aAR(c$pddZ|kKnK6tpx#3YC+5o z8L{XesuA7z{{~S0Ux4H<4g?@WsR4ClC>yBZFA#ay_*P^n6&Sk)3}DBBsR5@82o~*s zCnEm;63Ny8v>>P?*!Ld|B>)3Kr9d8^AH1Z%4-oXxL)wQr1skt{fus&F>3`RqI2b`D z{X;SVVPGf=coKmoJHTAPA{hF&eD)AAAmL@e90ke=JV!w+I{YuN1qG}DlBiG-pt%Db z7l=cJLIFBdC=ICU^ug@~dVBw%c>=7uVPF8qs8C*TUlij1O+gp}fEgN86kL<~FZzxK zRRZT1{6$iLEjpA9ELid{2aCZ#2fm?0xxn((|LY2|6#vy!Ctwl)B3Ott#B=|;K42lzaDuUb zH7sZ#sF)8hgFt=35kgr1D9E8uX>gLsU!($5LJ?9e?|(T`Y^VrWt`rM+5{V@ZWMCtX z_DRKGr1=mD1-NjaG+?S~tcMNYL0nt}6#_ffAjBPD24DpTY79DQ1jKNmk|2w1AmqVH s{Q>%Mp~6gbU?`Hki-)JVlM|P(vy-|qD%wLC$RM6MWJpNylu!iv9~@GMrvLx| delta 31345 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 BE))O& diff --git a/upload/admin/controller/module/img_opti.php b/upload/admin/controller/module/img_opti.php index e5551bf..b2785f7 100644 --- a/upload/admin/controller/module/img_opti.php +++ b/upload/admin/controller/module/img_opti.php @@ -2081,8 +2081,10 @@ class ControllerModuleImgOpti extends Controller { $this->config->set('module_img_opti_wm_corner_radius', isset($this->request->post['module_img_opti_wm_corner_radius']) ? $this->request->post['module_img_opti_wm_corner_radius'] : 0); $this->config->set('module_img_opti_wm_size_type', isset($this->request->post['module_img_opti_wm_size_type']) ? $this->request->post['module_img_opti_wm_size_type'] : 'original'); $this->config->set('module_img_opti_wm_size_percent', isset($this->request->post['module_img_opti_wm_size_percent']) ? $this->request->post['module_img_opti_wm_size_percent'] : 20); + $this->config->set('module_img_opti_wm_size_px', isset($this->request->post['module_img_opti_wm_size_px']) ? $this->request->post['module_img_opti_wm_size_px'] : 200); $this->config->set('module_img_opti_wm_text_color', isset($this->request->post['module_img_opti_wm_text_color']) ? $this->request->post['module_img_opti_wm_text_color'] : '#ffffff'); $this->config->set('module_img_opti_wm_text_font', isset($this->request->post['module_img_opti_wm_text_font']) ? $this->request->post['module_img_opti_wm_text_font'] : ''); + $this->config->set('module_img_opti_wm_text_size', isset($this->request->post['module_img_opti_wm_text_size']) ? $this->request->post['module_img_opti_wm_text_size'] : 24); $this->config->set('module_img_opti_wm_target_product', 1); $this->config->set('module_img_opti_wm_target_category', 0); diff --git a/upload/admin/model/module/img_opti.php b/upload/admin/model/module/img_opti.php index 71fda6a..f49df59 100644 --- a/upload/admin/model/module/img_opti.php +++ b/upload/admin/model/module/img_opti.php @@ -4,6 +4,57 @@ if (!defined('DIR_APPLICATION')) { exit; } class ModelModuleImgOpti extends Model { private $translations = array(); + private function paginateScanResult($rows, $data, $headers, $extra = array()) { + $start = isset($data['start']) ? max(0, (int)$data['start']) : 0; + $limit = isset($data['limit']) ? max(0, (int)$data['limit']) : 0; + $total = count($rows); + + if ($limit > 0) { + $sliced_rows = array_slice($rows, $start, $limit); + } else { + $sliced_rows = $rows; + } + + $result = array( + 'success' => true, + 'total' => $total, + 'start' => $start, + 'limit' => $limit, + 'headers' => $headers, + 'rows' => $sliced_rows + ); + + return array_merge($result, $extra); + } + + private function formatFixResult($processed = 0, $total = 0) { + return array( + 'success' => true, + 'processed' => (int)$processed, + 'total' => (int)$total + ); + } + + public function get_cached_grid($data) { + $this->loadLanguageSafe(); + $tool = isset($data['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$data['tool']) : ''; + if ($tool === 'broken') $tool = 'broken_db'; + $cache_file = DIR_CACHE . 'img_opti_' . $tool . '_grid.json'; + if (!file_exists($cache_file)) { + return array('success' => false, 'rows' => array(), 'total' => 0); + } + $rows = json_decode(file_get_contents($cache_file), true); + if (!is_array($rows)) $rows = array(); + + return $this->paginateScanResult($rows, $data, array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + )); + } + public function backupCatalogTables($tool_name) { $tables = array( DB_PREFIX . 'product', @@ -196,6 +247,157 @@ class ModelModuleImgOpti extends Model { return $entities; } + public function checkAndFixFakeJpgOnTheFly($filename) { + if (!$this->config->get('module_img_opti_status') || !$this->config->get('module_img_opti_auto_fake_jpg_guard')) { + return $filename; + } + + $cleanFile = str_replace(array('../', '..\\', "\0"), '', (string)$filename); + $ext = strtolower(pathinfo($cleanFile, PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg'))) { + return $filename; + } + + $fullPath = DIR_IMAGE . $cleanFile; + if (!is_file($fullPath)) { + return $filename; + } + + $header = @file_get_contents($fullPath, false, null, 0, 8); + if ($header !== "\x89PNG\r\n\x1a\n") { + return $filename; + } + + $mode = $this->config->get('module_img_opti_fake_jpg_mode'); + if ($mode === 'png') { + $newRelPath = preg_replace('/\.(jpg|jpeg)$/i', '.png', $cleanFile); + $newFullPath = DIR_IMAGE . $newRelPath; + if (@copy($fullPath, $newFullPath)) { + $this->updateImagePathInDb($cleanFile, $newRelPath); + $this->deleteImageCache($cleanFile); + @unlink($fullPath); + return $newRelPath; + } + } else { + if ($this->convertPngToJpgClean($fullPath, 100)) { + $this->deleteImageCache($cleanFile); + } + } + + return $filename; + } + + public function getGlobalStats() { + $stats = $this->config->get('module_img_opti_global_stats'); + if (is_string($stats)) { + $stats = json_decode($stats, true); + } + if (!is_array($stats)) { + $stats = array( + 'total_files' => 0, + 'total_bytes_saved' => 0, + 'total_original_bytes' => 0, + 'last_updated' => 0 + ); + } + + $total_files = isset($stats['total_files']) ? (int)$stats['total_files'] : 0; + $total_saved = isset($stats['total_bytes_saved']) ? (float)$stats['total_bytes_saved'] : 0.0; + $total_orig = isset($stats['total_original_bytes']) ? (float)$stats['total_original_bytes'] : 0.0; + + $mb_saved = round($total_saved / (1024 * 1024), 2); + $avg_pct = ($total_orig > 0) ? round(($total_saved / $total_orig) * 100, 1) : 0.0; + + return array( + 'total_files' => $total_files, + 'total_bytes_saved' => $total_saved, + 'total_original_bytes' => $total_orig, + 'saved_mb' => $mb_saved, + 'avg_pct' => $avg_pct + ); + } + + public function recordGlobalStats($filesCount, $bytesSaved, $originalBytes = 0, $source = 'opti') { + if ($filesCount <= 0 && $bytesSaved <= 0) return; + + $stats = $this->config->get('module_img_opti_global_stats'); + if (is_string($stats)) { + $stats = json_decode($stats, true); + } + if (!is_array($stats)) { + $stats = array( + 'total_files' => 0, + 'total_bytes_saved' => 0, + 'total_original_bytes' => 0, + 'last_updated' => 0 + ); + } + + $stats['total_files'] += (int)$filesCount; + $stats['total_bytes_saved'] += (float)$bytesSaved; + if ($originalBytes > 0) { + $stats['total_original_bytes'] += (float)$originalBytes; + } else { + $stats['total_original_bytes'] += (float)$bytesSaved; + } + $stats['last_updated'] = time(); + + $this->load->model('setting/setting'); + $this->model_setting_setting->editSettingValue('module_img_opti', 'module_img_opti_global_stats', json_encode($stats)); + } + + public function resetGlobalStats() { + $stats = array( + 'total_files' => 0, + 'total_bytes_saved' => 0, + 'total_original_bytes' => 0, + 'last_updated' => time() + ); + $this->load->model('setting/setting'); + $this->model_setting_setting->editSettingValue('module_img_opti', 'module_img_opti_global_stats', json_encode($stats)); + return $this->getGlobalStats(); + } + + private function updateImagePathInDb($oldPath, $newPath) { + $escapedOld = $this->db->escape((string)$oldPath); + $escapedNew = $this->db->escape((string)$newPath); + $updated = 0; + + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $this->db->query("UPDATE `" . DB_PREFIX . "banner_image` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + } + + $blog_tables = array('oct_blog_article', 'simple_blog_article', 'newsblog_article', 'information'); + foreach ($blog_tables as $table) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE 'image'"); + if ($column_query->num_rows) { + try { + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'"); + $updated += $this->db->countAffected(); + } catch (Exception $e) {} + } + } + } + + return $updated; + } + public function scan_broken_db($data = array()) { $this->loadLanguageSafe(); $rows = array(); @@ -323,20 +525,17 @@ class ModelModuleImgOpti extends Model { file_put_contents(DIR_CACHE . 'img_opti_broken_db_grid.json', json_encode($rows)); - return array( - 'success' => true, - 'headers' => array( - $this->language->get('text_th_preview'), - $this->language->get('text_th_entity'), - $this->language->get('text_th_before'), - $this->language->get('text_th_after'), - $this->language->get('text_th_action') - ), - 'rows' => $rows, + return $this->paginateScanResult($rows, $data, array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), array( 'backup_available' => true, 'quarantine_available' => false, 'description' => $this->language->get('text_description_broken') - ); + )); } public function fix_broken_db($data) { @@ -355,6 +554,7 @@ class ModelModuleImgOpti extends Model { $ph_category = (string)$this->config->get('module_img_opti_ph_category'); $ph_brand = (string)$this->config->get('module_img_opti_ph_brand'); + $processed = 0; foreach ($selected as $row_id => $act) { if ($act === 'ignore') continue; $parts = explode('-', (string)$row_id, 2); @@ -393,9 +593,10 @@ class ModelModuleImgOpti extends Model { $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $this->db->escape($ph_brand) . "' WHERE manufacturer_id = '" . $id . "'"); } } + $processed++; } @unlink(DIR_CACHE . 'img_opti_broken_db_grid.json'); - return array('success' => true); + return $this->formatFixResult($processed, count($selected)); } public function scan_broken_files($data = array()) { @@ -470,20 +671,17 @@ class ModelModuleImgOpti extends Model { file_put_contents(DIR_CACHE . 'img_opti_broken_files_grid.json', json_encode($rows)); - return array( - 'success' => true, - 'headers' => array( - $this->language->get('text_th_preview'), - $this->language->get('text_th_entity'), - $this->language->get('text_th_before'), - $this->language->get('text_th_after'), - $this->language->get('text_th_action') - ), - 'rows' => $rows, + return $this->paginateScanResult($rows, $data, array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), array( 'backup_available' => false, 'quarantine_available' => true, 'description' => $this->language->get('text_description_broken_files') - ); + )); } public function fix_broken_files($data) { @@ -819,25 +1017,14 @@ class ModelModuleImgOpti extends Model { if (!$jpgDir || strpos(str_replace('\\', '/', $jpgDir), str_replace('\\', '/', $realImageDir)) !== 0) continue; $relJpg = ltrim(str_replace(str_replace('\\', '/', $realImageDir), '', str_replace('\\', '/', $jpgPathStr)), '/'); - $img = @imagecreatefrompng($fullPath); - if ($img) { - if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { - imagepalettetotruecolor($img); - } - $bg = imagecreatetruecolor(imagesx($img), imagesy($img)); - imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); - imagecopy($bg, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); - if (imagejpeg($bg, $jpgPathStr, 85)) { - $dbOld = $this->db->escape($pngClean); - $dbNew = $this->db->escape($relJpg); - $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); - $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); - $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); - $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); - $this->quarantineFile($pngClean, 'png_jpg'); - } - imagedestroy($img); - imagedestroy($bg); + if ($this->convertPngToJpgClean($fullPath, $jpgPathStr, 85)) { + $dbOld = $this->db->escape($pngClean); + $dbNew = $this->db->escape($relJpg); + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->quarantineFile($pngClean, 'png_jpg'); } } @unlink(DIR_CACHE . 'img_opti_png_jpg_grid.json'); @@ -1738,7 +1925,9 @@ class ModelModuleImgOpti extends Model { public function applyDynamicWatermark($cached_file, $original_file, $is_preview = false) { $realCachedFile = realpath($cached_file); $realImageDir = realpath(DIR_IMAGE); - if (!$realCachedFile || strpos(str_replace('\\', '/', $realCachedFile), str_replace('\\', '/', $realImageDir)) !== 0) return false; + $cleanCached = mb_strtolower(str_replace('\\', '/', $realCachedFile ?: ''), 'UTF-8'); + $cleanImageDir = mb_strtolower(str_replace('\\', '/', $realImageDir ?: ''), 'UTF-8'); + if (!$realCachedFile || strpos($cleanCached, $cleanImageDir) !== 0) return false; $escaped_original = $this->db->escape((string)$original_file); $matched_rules = array(); @@ -1755,8 +1944,10 @@ class ModelModuleImgOpti extends Model { 'corner_radius' => $this->config->get('module_img_opti_wm_corner_radius'), 'size_type' => $this->config->get('module_img_opti_wm_size_type'), 'size_percent' => $this->config->get('module_img_opti_wm_size_percent'), + 'size_px' => $this->config->get('module_img_opti_wm_size_px'), 'text_color' => $this->config->get('module_img_opti_wm_text_color'), - 'text_font' => $this->config->get('module_img_opti_wm_text_font') + 'text_font' => $this->config->get('module_img_opti_wm_text_font'), + 'text_size' => $this->config->get('module_img_opti_wm_text_size') ); } else { $rules_json = $this->config->get('module_img_opti_wm_rules'); @@ -1940,7 +2131,8 @@ class ModelModuleImgOpti extends Model { $type = preg_replace('/[^a-z]/', '', $rule['type']); $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); $imagePath = realpath(DIR_IMAGE . $imagePathStr); - if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $cleanImagePath = mb_strtolower(str_replace('\\', '/', $imagePath ?: ''), 'UTF-8'); + if ($imagePath && strpos($cleanImagePath, $cleanImageDir) !== 0) { $imagePath = false; } $textVal = (string)$rule['text_val']; @@ -1948,6 +2140,8 @@ class ModelModuleImgOpti extends Model { $opacity = (int)$rule['opacity']; $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $size_px = isset($rule['size_px']) ? (int)$rule['size_px'] : 200; + $text_size = isset($rule['text_size']) ? (int)$rule['text_size'] : 24; $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; @@ -1980,7 +2174,13 @@ class ModelModuleImgOpti extends Model { } } - if ($size_type === 'percent' && $size_percent > 0) { + if ($size_type === 'px' && $size_px > 0) { + $targetW = min($imgW, $size_px); + $targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $wm->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1); + } + } elseif ($size_type === 'percent' && $size_percent > 0) { $targetW = intval(($imgW * $size_percent) / 100); $targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW); if ($targetW > 0 && $targetH > 0) { @@ -2016,7 +2216,7 @@ class ModelModuleImgOpti extends Model { if (is_file($fontFile)) { $draw->setFont($fontFile); } - $fontSize = max(12, $imgW * 0.04); + $fontSize = ($text_size > 0) ? $text_size : 24; $draw->setFontSize($fontSize); $colorHex = $text_color; list($r, $g, $b) = $this->hex2rgb($colorHex); @@ -2070,7 +2270,8 @@ class ModelModuleImgOpti extends Model { $type = preg_replace('/[^a-z]/', '', $rule['type']); $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); $imagePath = realpath(DIR_IMAGE . $imagePathStr); - if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $cleanImagePath = mb_strtolower(str_replace('\\', '/', $imagePath ?: ''), 'UTF-8'); + if ($imagePath && strpos($cleanImagePath, $cleanImageDir) !== 0) { $imagePath = false; } $textVal = (string)$rule['text_val']; @@ -2078,6 +2279,8 @@ class ModelModuleImgOpti extends Model { $opacity = (int)$rule['opacity']; $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $size_px = isset($rule['size_px']) ? (int)$rule['size_px'] : 200; + $text_size = isset($rule['text_size']) ? (int)$rule['text_size'] : 24; $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; @@ -2090,6 +2293,12 @@ class ModelModuleImgOpti extends Model { if ($type === 'image') { $wm = @imagecreatefrompng($imagePath); + if (!$wm) { + $wm = @imagecreatefromjpeg($imagePath); + } + if (!$wm && function_exists('imagecreatefromwebp')) { + $wm = @imagecreatefromwebp($imagePath); + } if ($wm) { if (function_exists('imagepalettetotruecolor') && !imageistruecolor($wm)) { imagepalettetotruecolor($wm); @@ -2122,7 +2331,22 @@ class ModelModuleImgOpti extends Model { $wmH = $new_wm_h; } } - if ($size_type === 'percent' && $size_percent > 0) { + if ($size_type === 'px' && $size_px > 0) { + $targetW = min($imgW, $size_px); + $targetH = intval(($wmH / $wmW) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $resized_wm = imagecreatetruecolor($targetW, $targetH); + imagealphablending($resized_wm, false); + imagesavealpha($resized_wm, true); + $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127); + imagefill($resized_wm, 0, 0, $transparent); + imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH); + imagedestroy($wm); + $wm = $resized_wm; + $wmW = $targetW; + $wmH = $targetH; + } + } elseif ($size_type === 'percent' && $size_percent > 0) { $targetW = intval(($imgW * $size_percent) / 100); $targetH = intval(($wmH / $wmW) * $targetW); if ($targetW > 0 && $targetH > 0) { @@ -2211,10 +2435,18 @@ class ModelModuleImgOpti extends Model { } } + $userFontSize = ($text_size > 0) ? $text_size : 24; + if (!is_file($fontFile)) { - $fontSize = max(3, intval($imgW / 50)); - $fontWidth = imagefontwidth($fontSize) * strlen($textVal); - $fontHeight = imagefontheight($fontSize); + $gdFont = 3; + if ($userFontSize <= 14) $gdFont = 1; + elseif ($userFontSize <= 20) $gdFont = 2; + elseif ($userFontSize <= 28) $gdFont = 3; + elseif ($userFontSize <= 38) $gdFont = 4; + else $gdFont = 5; + + $fontWidth = imagefontwidth($gdFont) * strlen($textVal); + $fontHeight = imagefontheight($gdFont); $posX = 0; $posY = 0; $pad = 10; if (in_array($position, array(1, 4, 7))) $posX = $pad; elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $fontWidth) / 2; @@ -2226,9 +2458,9 @@ class ModelModuleImgOpti extends Model { $alpha = intval(127 - (127 * ($opacity / 100))); $color = imagecolorallocatealpha($img, 255, 255, 255, $alpha); - imagestring($img, $fontSize, $posX, $posY, $textVal, $color); + imagestring($img, $gdFont, $posX, $posY, $textVal, $color); } else { - $fontSize = max(10, intval($imgW / 25)); + $fontSize = $userFontSize; $colorHex = $text_color; list($r, $g, $b) = $this->hex2rgb($colorHex); $alpha = intval(127 - (127 * ($opacity / 100))); @@ -2497,9 +2729,19 @@ private function roundCornersGD($im, $radius) { 'before' => $this->language->get('text_grid_missing_html') . ': ' . htmlspecialchars($cleanRelPath, ENT_QUOTES, 'UTF-8'), 'after' => $this->language->get('text_grid_html_after'), 'actions' => array( + 'remove' => $this->translate('text_action_remove_tag', 'Remove Tag'), + 'placeholder' => $this->language->get('text_action_placeholder'), 'ignore' => $this->language->get('text_action_ignore') ), - 'selected_action' => 'ignore' + 'selected_action' => 'remove', + 'meta' => array( + 'table' => $table, + 'id_field' => $cfg['id'], + 'id' => (int)$row[$cfg['id']], + 'field' => $cfg['field'], + 'src' => $src, + 'rel_path' => $cleanRelPath + ) ); } } @@ -2508,6 +2750,8 @@ private function roundCornersGD($im, $radius) { } } + file_put_contents(DIR_CACHE . 'img_opti_html_broken_grid.json', json_encode($rows)); + return array( 'success' => true, 'headers' => array( @@ -2524,6 +2768,57 @@ private function roundCornersGD($im, $radius) { ); } + public function fix_html_broken($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $cache_path = DIR_CACHE . 'img_opti_html_broken_grid.json'; + if (!file_exists($cache_path)) { + return array('success' => true); + } + + $grid = json_decode(file_get_contents($cache_path), true); + if (!$grid) return array('success' => true); + + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $ph_product = (string)$this->config->get('module_img_opti_ph_product'); + if (!$ph_product) $ph_product = 'no_image.png'; + + foreach ($selected as $row_id => $act) { + if ($act === 'ignore' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $meta = $row['meta']; + $table = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['table']); + $id_field = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['id_field']); + $field = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['field']); + $id = (int)$meta['id']; + $src = $meta['src']; + + $query = $this->db->query("SELECT `" . $field . "` FROM `" . DB_PREFIX . $table . "` WHERE `" . $id_field . "` = '" . $id . "'"); + if ($query->num_rows) { + $desc = html_entity_decode($query->row[$field], ENT_QUOTES, 'UTF-8'); + if ($act === 'remove') { + $desc = preg_replace('/]+src=(?:"|\')' . preg_quote($src, '/') . '(?:"|\')[^>]*>/i', '', $desc); + } elseif ($act === 'placeholder') { + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + $new_src = $catalog_url . 'image/' . $ph_product; + $desc = str_replace($src, $new_src, $desc); + } + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $field . "` = '" . $this->db->escape($desc) . "' WHERE `" . $id_field . "` = '" . $id . "'"); + } + } + + @unlink($cache_path); + return array('success' => true); + } + public function scan_folder_tree($data) { $this->loadLanguageSafe(); $dir = realpath(DIR_IMAGE . 'catalog'); @@ -3231,8 +3526,8 @@ private function roundCornersGD($im, $radius) { private function loadLanguageSafe() { $data = array(); $en_paths = array( - DIR_LANGUAGE . 'en-gb/module/img_opti.php', - DIR_LANGUAGE . 'english/module/img_opti.php' + DIR_LANGUAGE . 'en-gb/extension/module/img_opti.php', + DIR_LANGUAGE . 'english/extension/module/img_opti.php' ); foreach ($en_paths as $path) { if (is_file($path)) { @@ -3255,8 +3550,8 @@ private function roundCornersGD($im, $radius) { $lang_lower = strtolower((string)$lang); if ($lang_lower && $lang_lower !== 'en-gb' && $lang_lower !== 'english') { $paths = array( - DIR_LANGUAGE . $lang . '/module/img_opti.php', - DIR_LANGUAGE . $lang_lower . '/module/img_opti.php' + DIR_LANGUAGE . $lang . '/extension/module/img_opti.php', + DIR_LANGUAGE . $lang_lower . '/extension/module/img_opti.php' ); if (isset($this->db)) { @@ -3265,25 +3560,25 @@ private function roundCornersGD($im, $radius) { $query_lang = $this->db->query("SELECT directory FROM `" . DB_PREFIX . "language` WHERE code = '" . $escaped_lang . "' OR directory = '" . $escaped_lang . "' LIMIT 1"); if ($query_lang && $query_lang->num_rows && !empty($query_lang->row['directory'])) { $db_dir = $query_lang->row['directory']; - $paths[] = DIR_LANGUAGE . $db_dir . '/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . strtolower($db_dir) . '/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . $db_dir . '/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . strtolower($db_dir) . '/extension/module/img_opti.php'; } } catch (Exception $e) {} } if (strpos($lang_lower, 'ru') !== false || strpos($lang_lower, 'rus') !== false) { - $paths[] = DIR_LANGUAGE . 'ru-ru/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'russian/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'ru-RU/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'ru/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ru-ru/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'russian/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ru-RU/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ru/extension/module/img_opti.php'; } elseif (strpos($lang_lower, 'uk') !== false || strpos($lang_lower, 'ua') !== false || strpos($lang_lower, 'ukr') !== false) { - $paths[] = DIR_LANGUAGE . 'uk-ua/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'ukrainian/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'uk-UA/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'uk/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'ua/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'uk_ua/module/img_opti.php'; - $paths[] = DIR_LANGUAGE . 'uk_UA/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'uk-ua/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ukrainian/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'uk-UA/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'uk/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ua/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'uk_ua/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'uk_UA/extension/module/img_opti.php'; } foreach ($paths as $path) { if (is_file($path)) { @@ -3415,16 +3710,8 @@ private function roundCornersGD($im, $radius) { $fullPath = realpath(DIR_IMAGE . $relPath); if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; - if (extension_loaded('imagick')) { - try { - $img = new Imagick($fullPath); - if ($img->getImageColorspace() == Imagick::COLORSPACE_CMYK) { - $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); - $img->writeImage($fullPath); - $this->deleteImageCache($relPath); - } - $img->clear(); $img->destroy(); - } catch (Exception $e) {} + if ($this->normalizeCmykToRgbJpeg($fullPath, 90)) { + $this->deleteImageCache($relPath); } } @unlink(DIR_CACHE . 'img_opti_cmyk_grid.json'); @@ -3573,24 +3860,26 @@ private function roundCornersGD($im, $radius) { private function deleteImageCache($relPath) { $info = $this->mb_pathinfo($relPath); - if (!isset($info['extension']) || !isset($info['dirname'])) return; - $dir = DIR_IMAGE . 'cache/' . $info['dirname']; - if (!is_dir($dir)) return; + if (!isset($info['filename'])) return; $filename = $info['filename']; - $ext = $info['extension']; - - $files = glob($dir . '/' . $filename . '-*x*.' . $ext); - if ($files) { - foreach ($files as $file) { - @unlink($file); + $cache_dir = realpath(DIR_IMAGE . 'cache'); + if (!$cache_dir || !is_dir($cache_dir)) return; + + try { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($cache_dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $fn = $file->getFilename(); + if (strpos($fn, $filename . '-') === 0 || strpos($fn, $filename . '.') === 0 || strpos($fn, $filename . '_') === 0) { + @unlink($file->getPathname()); + } + } } - } - $files_webp = glob($dir . '/' . $filename . '-*x*.webp'); - if ($files_webp) { - foreach ($files_webp as $file) { - @unlink($file); - } - } + } catch (\Exception $e) {} } private function mb_pathinfo($path, $options = null) { @@ -3662,4 +3951,1084 @@ private function roundCornersGD($im, $radius) { $rel = ltrim($rel, '/'); return $this->safe_utf8($rel); } + + public function autoRotateImage($filePath, $im = null) { + if (!function_exists('exif_read_data')) { + return $im; + } + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg', 'tiff'))) { + return $im; + } + $exif = @exif_read_data($filePath); + if (!$exif || !isset($exif['Orientation'])) { + return $im; + } + $orientation = (int)$exif['Orientation']; + $angle = 0; + switch ($orientation) { + case 3: + $angle = 180; + break; + case 6: + $angle = -90; + break; + case 8: + $angle = 90; + break; + } + if ($angle !== 0) { + if ($im !== null) { + $im = @imagerotate($im, $angle, 0); + } else { + $img = @imagecreatefromjpeg($filePath); + if ($img) { + $img = @imagerotate($img, $angle, 0); + @imagejpeg($img, $filePath, 95); + @imagedestroy($img); + } + } + } + return $im; + } + + public function convertPngToJpgClean($sourcePngPath, $targetJpgPath, $quality = 100, $bgColorHex = '#ffffff') { + if (extension_loaded('imagick')) { + try { + $im = new \Imagick($sourcePngPath); + $bg = new \Imagick(); + $fillColor = new \ImagickPixel($bgColorHex); + $bg->newImage($im->getImageWidth(), $im->getImageHeight(), $fillColor); + $bg->compositeImage($im, \Imagick::COMPOSITE_OVER, 0, 0); + $bg->setImageFormat('jpg'); + $bg->setImageCompressionQuality($quality); + $bg->writeImage($targetJpgPath); + $bg->clear(); $bg->destroy(); + $im->clear(); $im->destroy(); + return true; + } catch (\Exception $e) {} + } + + $img = @imagecreatefrompng($sourcePngPath); + if (!$img) return false; + + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + + $width = imagesx($img); + $height = imagesy($img); + + $canvas = imagecreatetruecolor($width, $height); + + $hex = ltrim($bgColorHex, '#'); + if (strlen($hex) === 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + + $bgColor = imagecolorallocate($canvas, $r, $g, $b); + imagefill($canvas, 0, 0, $bgColor); + imagealphablending($canvas, true); + + imagecopy($canvas, $img, 0, 0, 0, 0, $width, $height); + + $success = @imagejpeg($canvas, $targetJpgPath, $quality); + + imagedestroy($img); + imagedestroy($canvas); + + return $success; + } + + public function convertPngToWebpClean($sourcePngPath, $targetWebpPath, $quality = 80) { + if (!function_exists('imagewebp')) return false; + $img = @imagecreatefrompng($sourcePngPath); + if (!$img) return false; + + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + + imagealphablending($img, false); + imagesavealpha($img, true); + + $success = @imagewebp($img, $targetWebpPath, $quality); + imagedestroy($img); + + return $success; + } + + public function normalizePngPalette($img) { + if ($img && function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + return $img; + } + + public function normalizeCmykToRgbJpeg($fullPath, $quality = 90) { + if (!is_file($fullPath)) return false; + + if (extension_loaded('imagick')) { + try { + $img = new \Imagick($fullPath); + if ($img->getImageColorspace() == \Imagick::COLORSPACE_CMYK) { + $img->transformImageColorspace(\Imagick::COLORSPACE_SRGB); + $img->setImageFormat('jpeg'); + $img->writeImage($fullPath); + $img->clear(); + $img->destroy(); + return true; + } + $img->clear(); + $img->destroy(); + } catch (\Exception $e) {} + } + + $info = @getimagesize($fullPath); + if (isset($info['channels']) && $info['channels'] == 4) { + $img = @imagecreatefromjpeg($fullPath); + if ($img) { + $width = imagesx($img); + $height = imagesy($img); + $rgbCanvas = imagecreatetruecolor($width, $height); + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $pixel = imagecolorat($img, $x, $y); + $c = ($pixel >> 16) & 0xFF; + $m = ($pixel >> 8) & 0xFF; + $y_val = $pixel & 0xFF; + + $r = (int)(($c * $y_val) / 255); + $g = (int)(($m * $y_val) / 255); + $b = (int)($y_val); + + $color = imagecolorallocate($rgbCanvas, min(255, max(0, $r)), min(255, max(0, $g)), min(255, max(0, $b))); + imagesetpixel($rgbCanvas, $x, $y, $color); + } + } + @imagejpeg($rgbCanvas, $fullPath, $quality); + imagedestroy($img); + imagedestroy($rgbCanvas); + return true; + } + } + return false; + } + + public function sanitizeSvgFile($filePath) { + if (!is_file($filePath)) { + return false; + } + $content = @file_get_contents($filePath); + if ($content === false) { + return false; + } + $clean = $this->sanitizeSvgContent($content); + return (@file_put_contents($filePath, $clean) !== false); + } + + public function sanitizeSvgContent($content) { + if (empty($content)) { + return $content; + } + + $content = preg_replace('/]*\[.*?\]>/si', '', $content); + $content = preg_replace('/]*>/i', '', $content); + + $previous_libxml_state = false; + if (PHP_VERSION_ID < 80000 && function_exists('libxml_disable_entity_loader')) { + $previous_libxml_state = @libxml_disable_entity_loader(true); + } + + $dom = new \DOMDocument(); + $dom->formatOutput = true; + $libxml_flags = LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR; + if (defined('LIBXML_NOENT')) { + $libxml_flags |= LIBXML_NOENT; + } + + $loaded = @$dom->loadXML($content, $libxml_flags); + + if ($previous_libxml_state !== false && PHP_VERSION_ID < 80000 && function_exists('libxml_disable_entity_loader')) { + @libxml_disable_entity_loader($previous_libxml_state); + } + + if ($loaded) { + $disallowed_tags = array('script', 'foreignobject', 'object', 'embed', 'iframe', 'applet', 'meta', 'link'); + foreach ($disallowed_tags as $tag) { + $nodes = $dom->getElementsByTagName($tag); + while ($nodes->length > 0) { + $item = $nodes->item(0); + if ($item && $item->parentNode) { + $item->parentNode->removeChild($item); + } + } + } + + $xpath = new \DOMXPath($dom); + + $nodes = $xpath->query('//@*[starts-with(name(), "on") or starts-with(name(), "ON")]'); + if ($nodes) { + $attrs_to_remove = array(); + foreach ($nodes as $attr) { + $attrs_to_remove[] = $attr; + } + foreach ($attrs_to_remove as $attr) { + if ($attr->ownerElement) { + $attr->ownerElement->removeAttribute($attr->name); + } + } + } + + $nodes = $xpath->query('//@*[name()="href" or name()="xlink:href" or name()="src" or name()="action" or name()="formaction" or name()="data" or name()="values"]'); + if ($nodes) { + foreach ($nodes as $attr) { + $val = strtolower(trim($attr->value)); + if (strpos($val, 'javascript:') !== false || strpos($val, 'data:text/html') !== false || strpos($val, 'data:application/javascript') !== false) { + if ($attr->ownerElement) { + $attr->ownerElement->removeAttribute($attr->name); + } + } + } + } + + $styles = $dom->getElementsByTagName('style'); + foreach ($styles as $styleNode) { + $styleText = $styleNode->nodeValue; + if ($styleText) { + $styleText = preg_replace('/expression\s*\(/i', 'blocked(', $styleText); + $styleText = preg_replace('/url\s*\(\s*["\']?\s*javascript:[^"\']*\s*["\']?\s*\)/i', 'none', $styleText); + $styleText = preg_replace('/url\s*\(\s*["\']?\s*data:text\/html[^"\']*\s*["\']?\s*\)/i', 'none', $styleText); + $styleNode->nodeValue = $styleText; + } + } + + $clean_content = $dom->saveXML(); + if ($clean_content !== false) { + return $clean_content; + } + } + + $content = preg_replace('/]*>(.*?)<\/script>/is', '', $content); + $content = preg_replace('/\bon\w+\s*=\s*(?:"[^"]*"|\'[^\']*\')/i', '', $content); + $content = preg_replace('/(?:href|xlink:href|src)\s*=\s*["\']\s*javascript:[^"\']*["\']/i', '', $content); + $content = preg_replace('/<\s*(?:foreignObject|object|embed|iframe|applet)\b[^>]*>.*?<\/\s*\1\s*>/is', '', $content); + $content = preg_replace('/<\s*(?:foreignObject|object|embed|iframe|applet)\b[^>]*\/?\s*>/i', '', $content); + $content = preg_replace('/expression\s*\(/i', 'blocked(', $content); + + return $content; + } + + public function detectSvgThreats($content) { + $threats = array(); + if (empty($content)) return $threats; + + if (preg_match('/]*\[.*?\]>/si', $content) || preg_match('/]*>/i', $content)) { + $threats[] = 'script_tag'; + } + if (preg_match('/\bon[a-z]+\s*=/i', $content)) { + $threats[] = 'event_handler'; + } + if (preg_match('/(?:href|xlink:href|src|action|data)\s*=\s*["\']\s*javascript:/i', $content)) { + $threats[] = 'javascript_uri'; + } + if (preg_match('/<\s*foreignObject\b/i', $content)) { + $threats[] = 'foreign_object'; + } + if (preg_match('/<\s*(?:object|embed|iframe|applet)\b/i', $content)) { + $threats[] = 'embed_object'; + } + if (preg_match('/expression\s*\(|url\s*\(\s*["\']?\s*javascript:/i', $content)) { + $threats[] = 'css_injection'; + } + return $threats; + } + + public function scan_svg_security($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_svg_security_limit']) ? (int)$data['module_img_opti_svg_security_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); + if ($ext === 'svg') { + $content = @file_get_contents($realPath); + if ($content !== false) { + $threats = $this->detectSvgThreats($content); + if (!empty($threats)) { + $relPath = $this->getRelativeImagePath($realPath); + $filename = $this->mb_basename($relPath); + + $threat_badges = array(); + foreach ($threats as $t) { + $lang_key = 'text_svg_threat_' . $t; + $label = $this->language->get($lang_key); + if ($label === $lang_key) $label = $t; + $threat_badges[] = '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; + } + + $preview_url = '../image/' . str_replace('\\', '/', $relPath); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'svg_security-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
' . $entities_html, + 'before' => '' . sprintf($this->language->get('text_svg_threats_found'), count($threats)) . '
' . implode(' ', $threat_badges), + 'after' => ' ' . $this->language->get('text_svg_clean_success') . '', + 'actions' => array( + 'sanitize' => $this->language->get('text_action_apply_changes'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'sanitize', + 'meta' => array( + 'path' => $relPath + ) + ); + + if (count($rows) >= $limit) { + break; + } + } + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_svg_security_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_svg_security') + ); + } + + public function fix_svg_security($data) { + $this->backupCatalogTables('svg_security'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $cache_file = DIR_CACHE . 'img_opti_svg_security_grid.json'; + if (!is_file($cache_file)) { + return $this->formatFixResult(0, 0); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return $this->formatFixResult(0, 0); + } + + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + $processed = 0; + + foreach ($selected as $row_id => $act) { + if ($act !== 'sanitize' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = (string)$row['meta']['path']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) continue; + $realFullPath = realpath($fullPath); + if (!$realFullPath || strpos(str_replace('\\', '/', $realFullPath), str_replace('\\', '/', $realImageDir)) !== 0) continue; + + $svgContent = @file_get_contents($fullPath); + if ($svgContent === false) continue; + + $this->quarantineFile($relPath, 'svg_security'); + + $cleanSvg = $this->sanitizeSvgContent($svgContent); + if (@file_put_contents($fullPath, $cleanSvg) !== false) { + $processed++; + } + } + + return $this->formatFixResult($processed, count($selected)); + } + + public function replaceBlackBackground($img, $bgColorHex = '#FFFFFF', $darkness = 30) { + return $this->replaceBlackBackgroundAdaptive($img, $bgColorHex, $darkness, 'edges_only'); + } + + public function replaceBlackBackgroundAdaptive($img, $bgColorHex = '#FFFFFF', $darkness = 30, $mode = 'edges_only') { + if (!$img) return $img; + + $width = imagesx($img); + $height = imagesy($img); + if ($width <= 0 || $height <= 0) return $img; + + $r_target = 255; $g_target = 255; $b_target = 255; + if (preg_match('/^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i', $bgColorHex, $m)) { + $r_target = hexdec($m[1]); + $g_target = hexdec($m[2]); + $b_target = hexdec($m[3]); + } + + $sample_points = array(); + for ($i = 0; $i < 20; $i++) { + $sample_points[] = array((int)($i * ($width - 1) / 19), 0); + $sample_points[] = array((int)($i * ($width - 1) / 19), $height - 1); + $sample_points[] = array(0, (int)($i * ($height - 1) / 19)); + $sample_points[] = array($width - 1, (int)($i * ($height - 1) / 19)); + } + + $sum_r = 0; $sum_g = 0; $sum_b = 0; $bg_samples = 0; + foreach ($sample_points as $p) { + $rgb = imagecolorat($img, $p[0], $p[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + if ($alpha <= 10) { + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($r <= $darkness + 35 && $g <= $darkness + 35 && $b <= $darkness + 35) { + $sum_r += $r; + $sum_g += $g; + $sum_b += $b; + $bg_samples++; + } + } + } + + $base_r = ($bg_samples > 0) ? (int)($sum_r / $bg_samples) : 0; + $base_g = ($bg_samples > 0) ? (int)($sum_g / $bg_samples) : 0; + $base_b = ($bg_samples > 0) ? (int)($sum_b / $bg_samples) : 0; + + $tolerance_inner = ($mode === 'all_smooth') ? max(45.0, (float)$darkness * 1.3) : max(30.0, (float)$darkness * 0.9); + $tolerance_outer = ($mode === 'all_smooth') ? ($tolerance_inner + 45.0) : ($tolerance_inner + 25.0); // Feathering range for anti-aliasing + + $fillColor = imagecolorallocate($img, $r_target, $g_target, $b_target); + + if ($mode === 'all' || $mode === 'all_smooth') { + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $rgb = imagecolorat($img, $x, $y); + $alpha = ($rgb & 0x7F000000) >> 24; + if ($alpha > 10) continue; + + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + + if ($dist <= $tolerance_inner) { + imagesetpixel($img, $x, $y, $fillColor); + } elseif ($dist <= $tolerance_outer) { + $blendFactor = ($dist - $tolerance_inner) / ($tolerance_outer - $tolerance_inner); + $r_new = (int)($r * $blendFactor + $r_target * (1.0 - $blendFactor)); + $g_new = (int)($g * $blendFactor + $g_target * (1.0 - $blendFactor)); + $b_new = (int)($b * $blendFactor + $b_target * (1.0 - $blendFactor)); + $blendedColor = imagecolorallocate($img, $r_new, $g_new, $b_new); + imagesetpixel($img, $x, $y, $blendedColor); + } + } + } + } else { + $seeds = array(); + for ($x = 0; $x < $width; $x += 10) { + $seeds[] = array($x, 0); + $seeds[] = array($x, $height - 1); + } + for ($y = 0; $y < $height; $y += 10) { + $seeds[] = array(0, $y); + $seeds[] = array($width - 1, $y); + } + $seeds[] = array($width - 1, $height - 1); + + $queue = array(); + $visited = array_fill(0, $height, array_fill(0, $width, false)); + + foreach ($seeds as $c) { + $x = $c[0]; $y = $c[1]; + $rgb = imagecolorat($img, $x, $y); + $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + if ($dist <= $tolerance_outer && !$visited[$y][$x]) { + $queue[] = array($x, $y); + $visited[$y][$x] = true; + } + } + + while (!empty($queue)) { + $curr = array_pop($queue); + $cx = $curr[0]; $cy = $curr[1]; + + $rgb = imagecolorat($img, $cx, $cy); + $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + + if ($dist <= $tolerance_inner) { + imagesetpixel($img, $cx, $cy, $fillColor); + } else { + $blendFactor = ($dist - $tolerance_inner) / ($tolerance_outer - $tolerance_inner); + $r_new = (int)($r * $blendFactor + $r_target * (1.0 - $blendFactor)); + $g_new = (int)($g * $blendFactor + $g_target * (1.0 - $blendFactor)); + $b_new = (int)($b * $blendFactor + $b_target * (1.0 - $blendFactor)); + $blendedColor = imagecolorallocate($img, $r_new, $g_new, $b_new); + imagesetpixel($img, $cx, $cy, $blendedColor); + } + + $neighbors = array( + array($cx + 1, $cy), array($cx - 1, $cy), + array($cx, $cy + 1), array($cx, $cy - 1) + ); + + foreach ($neighbors as $n) { + $nx = $n[0]; $ny = $n[1]; + if ($nx >= 0 && $nx < $width && $ny >= 0 && $ny < $height && !$visited[$ny][$nx]) { + $visited[$ny][$nx] = true; + $nrgb = imagecolorat($img, $nx, $ny); + $nr = ($nrgb >> 16) & 0xFF; $ng = ($nrgb >> 8) & 0xFF; $nb = $nrgb & 0xFF; + $ndist = sqrt(pow($nr - $base_r, 2) + pow($ng - $base_g, 2) + pow($nb - $base_b, 2)); + if ($ndist <= $tolerance_outer) { + $queue[] = array($nx, $ny); + } + } + } + } + } + + return $img; + } + + public function replaceBlackBackgroundImagick($filePath, $bgColorHex = '#FFFFFF', $darkness = 30, $mode = 'all_smooth') { + if (!extension_loaded('imagick') || !is_file($filePath)) { + return false; + } + + try { + $im = new \Imagick($filePath); + + $r_target = 255; $g_target = 255; $b_target = 255; + if (preg_match('/^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i', $bgColorHex, $m)) { + $r_target = hexdec($m[1]); + $g_target = hexdec($m[2]); + $b_target = hexdec($m[3]); + } + + $fillColor = new \ImagickPixel("rgb($r_target, $g_target, $b_target)"); + $fuzz = (\Imagick::getQuantum() * (max(30, $darkness * 1.3) / 255)); + + if ($mode === 'edges_only') { + $w = $im->getImageWidth(); + $h = $im->getImageHeight(); + $seeds = array(); + for ($x = 0; $x < $w; $x += 15) { + $seeds[] = array($x, 0); + $seeds[] = array($x, $h - 1); + } + for ($y = 0; $y < $h; $y += 15) { + $seeds[] = array(0, $y); + $seeds[] = array($w - 1, $y); + } + $seeds[] = array($w - 1, $h - 1); + + foreach ($seeds as $c) { + $borderPixel = $im->getImagePixelColor($c[0], $c[1]); + $colorArray = $borderPixel->getColor(); + if ($colorArray['r'] <= ($darkness + 40) && $colorArray['g'] <= ($darkness + 40) && $colorArray['b'] <= ($darkness + 40)) { + @$im->floodfillPaintImage($fillColor, $fuzz, $borderPixel, $c[0], $c[1], false); + } + } + } else { + $im->transparentPaintImage(new \ImagickPixel('rgb(0,0,0)'), 0.0, $fuzz, false); + $bg = new \Imagick(); + $bg->newImage($im->getImageWidth(), $im->getImageHeight(), $fillColor); + $bg->compositeImage($im, \Imagick::COMPOSITE_OVER, 0, 0); + $im = $bg; + } + + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + $im->setImageFormat('png'); + } else { + $im->setImageFormat('jpeg'); + $im->setImageCompression(\Imagick::COMPRESSION_JPEG); + $im->setImageCompressionQuality(100); + $im->setOption('jpeg:sampling-factor', '4:4:4'); + } + $im->writeImage($filePath); + $im->clear(); + $im->destroy(); + return true; + } catch (\Exception $e) { + return false; + } + } + + public function scan_black_bg($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_bb_limit']) ? (int)$data['module_img_opti_bb_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + $threshold = isset($data['module_img_opti_bb_threshold']) ? (int)$data['module_img_opti_bb_threshold'] : 60; + $scan_fake = isset($data['module_img_opti_bb_scan_fake']) ? (int)$data['module_img_opti_bb_scan_fake'] : 1; + $scan_black = isset($data['module_img_opti_bb_scan_black']) ? (int)$data['module_img_opti_bb_scan_black'] : 1; + + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg'))) continue; + + $relPath = $this->getRelativeImagePath($realPath); + if (preg_match('/(wp-content|elementor|thumbs|templates?)/i', $relPath)) continue; + + $is_fake = false; + $is_black = false; + + if ($scan_fake && in_array($ext, array('jpg', 'jpeg'))) { + $header = @file_get_contents($realPath, false, null, 0, 8); + if ($header === "\x89PNG\r\n\x1a\n") { + $is_fake = true; + } + } + + if ($scan_black && !$is_fake) { + $img = @imagecreatefromstring(@file_get_contents($realPath)); + if ($img) { + $w = imagesx($img); + $h = imagesy($img); + if ($w >= 20 && $h >= 20) { + $corners = array( + array(0, 0), array($w - 1, 0), + array(0, $h - 1), array($w - 1, $h - 1) + ); + $black_corners = 0; + foreach ($corners as $c) { + $rgb = imagecolorat($img, $c[0], $c[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($alpha <= 10 && $r <= ($darkness + 15) && $g <= ($darkness + 15) && $b <= ($darkness + 15)) { + $black_corners++; + } + } + + if ($black_corners >= 3) { + $sample_points = array(); + for ($i = 0; $i < 20; $i++) { + $sample_points[] = array((int)($i * ($w - 1) / 19), 0); + $sample_points[] = array((int)($i * ($w - 1) / 19), $h - 1); + $sample_points[] = array(0, (int)($i * ($h - 1) / 19)); + $sample_points[] = array($w - 1, (int)($i * ($h - 1) / 19)); + } + $black_count = 0; + foreach ($sample_points as $p) { + $rgb = imagecolorat($img, $p[0], $p[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($alpha <= 10 && $r <= $darkness && $g <= $darkness && $b <= $darkness) { + $black_count++; + } + } + $pct = ($black_count / count($sample_points)) * 100; + if ($pct >= $threshold) { + $is_black = true; + } + } + } + imagedestroy($img); + } + } + + if ($is_fake || $is_black) { + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $issue_type = $is_fake ? 'fake_jpg' : 'black_bg'; + $issue_title = $is_fake ? $this->language->get('text_bb_fake_jpg') : $this->language->get('text_bb_black_bg'); + $before_desc = $is_fake ? '' . $this->language->get('text_bb_type_fake') . '' : '' . $this->language->get('text_bb_type_black') . ''; + + if ($is_fake) { + $after_desc = ' ' . $this->language->get('text_bb_fix_flatten_white') . ''; + $actions = array( + 'fix_flatten_white' => $this->language->get('text_bb_fix_flatten_white'), + 'fix_convert_png' => $this->language->get('text_bb_fix_convert_png'), + 'ignore' => $this->language->get('text_action_ignore') + ); + $default_action = 'fix_flatten_white'; + } else { + $after_desc = ' ' . $this->language->get('text_bb_fix_white') . ''; + $actions = array( + 'fix_white' => $this->language->get('text_bb_fix_white'), + 'fix_white_all' => $this->language->get('text_bb_fix_white_all'), + 'fix_white_smooth' => $this->language->get('text_bb_fix_white_smooth') + ); + if (extension_loaded('imagick')) { + $actions['fix_white_imagick'] = $this->language->get('text_bb_fix_white_imagick'); + $actions['fix_white_all_imagick'] = $this->language->get('text_bb_fix_white_all_imagick'); + $actions['fix_white_smooth_imagick'] = $this->language->get('text_bb_fix_white_smooth_imagick'); + } + $actions['ignore'] = $this->language->get('text_action_ignore'); + $default_action = extension_loaded('imagick') ? 'fix_white_smooth_imagick' : 'fix_white_smooth'; + } + + $rows[] = array( + 'id' => 'black_bg-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
' . $entities_html, + 'before' => $issue_title . '
' . $before_desc, + 'after' => $after_desc, + 'actions' => $actions, + 'selected_action' => $default_action, + 'meta' => array( + 'path' => $relPath, + 'type' => $issue_type + ) + ); + + if (count($rows) >= $limit) { + break; + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_black_bg_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_black_bg') + ); + } + + public function fix_black_bg($data) { + $this->backupCatalogTables('black_bg'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + + $cache_file = DIR_CACHE . 'img_opti_black_bg_grid.json'; + if (!is_file($cache_file)) { + return $this->formatFixResult(0, 0); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return $this->formatFixResult(0, 0); + } + + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + $processed = 0; + $allowed_actions = array('fix_flatten_white', 'fix_convert_png', 'fix_white', 'fix_white_all', 'fix_white_smooth', 'fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'); + + foreach ($selected as $row_id => $act) { + if (!in_array($act, $allowed_actions) || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = (string)$row['meta']['path']; + $issueType = (string)$row['meta']['type']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) continue; + $realFullPath = realpath($fullPath); + if (!$realFullPath || strpos(str_replace('\\', '/', $realFullPath), str_replace('\\', '/', $realImageDir)) !== 0) continue; + + $imgData = @file_get_contents($fullPath); + if ($imgData === false) continue; + + $this->quarantineFile($relPath, 'black_bg'); + + if ($act === 'fix_flatten_white') { + $pngImg = @imagecreatefromstring($imgData); + if ($pngImg) { + $w = imagesx($pngImg); + $h = imagesy($pngImg); + $jpgCanvas = imagecreatetruecolor($w, $h); + imagealphablending($jpgCanvas, true); + $white = imagecolorallocate($jpgCanvas, 255, 255, 255); + imagefilledrectangle($jpgCanvas, 0, 0, $w - 1, $h - 1, $white); + imagealphablending($pngImg, true); + imagesavealpha($pngImg, true); + imagecopy($jpgCanvas, $pngImg, 0, 0, 0, 0, $w, $h); + $jpgCanvas = $this->replaceBlackBackgroundAdaptive($jpgCanvas, '#FFFFFF', $darkness, 'edges_only'); + @imagejpeg($jpgCanvas, $fullPath, 100); + imagedestroy($pngImg); + imagedestroy($jpgCanvas); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_convert_png') { + $pngImg = @imagecreatefromstring($imgData); + if ($pngImg) { + imagesavealpha($pngImg, true); + imagealphablending($pngImg, false); + $newRelPath = preg_replace('/\.(jpe?g)$/i', '.png', $relPath); + $newFullPath = DIR_IMAGE . str_replace(array('../', '..\\', "\0"), '', $newRelPath); + $newDir = dirname($newFullPath); + if (!is_dir($newDir)) { + @mkdir($newDir, 0755, true); + } + @imagepng($pngImg, $newFullPath, 9); + imagedestroy($pngImg); + if (is_file($fullPath)) { + @unlink($fullPath); + } + $this->updateImagePathInDb($relPath, $newRelPath); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($newRelPath, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'edges_only'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white_all') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'all'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white_smooth') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'all_smooth'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif (in_array($act, array('fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'))) { + $imagick_mode = 'all_smooth'; + if ($act === 'fix_white_imagick') { + $imagick_mode = 'edges_only'; + } elseif ($act === 'fix_white_all_imagick') { + $imagick_mode = 'all'; + } + if ($this->replaceBlackBackgroundImagick($fullPath, '#FFFFFF', $darkness, $imagick_mode)) { + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } else { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $imagick_mode); + @imagejpeg($img, $fullPath, 100); + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } + } + } + + return $this->formatFixResult($processed, count($selected)); + } + + public function preview_black_bg_row($data) { + $row_id = isset($data['row_id']) ? (string)$data['row_id'] : ''; + $act = isset($data['action_type']) ? (string)$data['action_type'] : 'fix_white'; + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + + $cache_file = DIR_CACHE . 'img_opti_black_bg_grid.json'; + if (!is_file($cache_file)) { + return array('error' => 'Cache file not found'); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return array('error' => 'Invalid grid cache'); + } + + $targetRow = null; + foreach ($grid as $row) { + if ($row['id'] === $row_id) { + $targetRow = $row; + break; + } + } + + if (!$targetRow) { + return array('error' => 'Row not found'); + } + + $relPath = (string)$targetRow['meta']['path']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) { + return array('error' => 'Original image file not found on disk'); + } + + $imgData = @file_get_contents($fullPath); + if ($imgData === false) { + return array('error' => 'Failed to read image file'); + } + + $this->load->model('tool/image'); + $thumbBefore = $this->model_tool_image->resize($cleanBadFile, 350, 350); + + $cache_dir = DIR_IMAGE . 'cache'; + if (!is_dir($cache_dir)) { + @mkdir($cache_dir, 0755, true); + } + + $old_previews = glob($cache_dir . '/img_opti_preview_*.jpg'); + if ($old_previews) { + $now = time(); + foreach ($old_previews as $old_file) { + if (($now - filemtime($old_file)) > 300) { + @unlink($old_file); + } + } + } + + $tempFilename = 'img_opti_preview_' . md5($relPath . '_' . $act . '_' . microtime(true)) . '.jpg'; + $tempRelPath = 'cache/' . $tempFilename; + $tempFullPath = DIR_IMAGE . $tempRelPath; + + if ($act === 'ignore') { + $thumbAfter = $thumbBefore; + } elseif (in_array($act, array('fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'))) { + $imagick_mode = 'all_smooth'; + if ($act === 'fix_white_imagick') { + $imagick_mode = 'edges_only'; + } elseif ($act === 'fix_white_all_imagick') { + $imagick_mode = 'all'; + } + @copy($fullPath, $tempFullPath); + if (!$this->replaceBlackBackgroundImagick($tempFullPath, '#FFFFFF', $darkness, $imagick_mode)) { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $imagick_mode); + @imagejpeg($img, $tempFullPath, 100); + @imagedestroy($img); + } + } + $thumbAfter = $this->model_tool_image->resize($tempRelPath, 350, 350); + } else { + $gd_mode = 'edges_only'; + if ($act === 'fix_white_all') { + $gd_mode = 'all'; + } elseif ($act === 'fix_white_smooth') { + $gd_mode = 'all_smooth'; + } + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $gd_mode); + @imagejpeg($img, $tempFullPath, 100); + @imagedestroy($img); + } + $thumbAfter = $this->model_tool_image->resize($tempRelPath, 350, 350); + } + + return array( + 'success' => true, + 'title' => $relPath, + 'thumb_before' => $thumbBefore, + 'thumb_after' => $thumbAfter + ); + } }