pax_global_header00006660000000000000000000000064147453644170014531gustar00rootroot0000000000000052 comment=b50ed8a801b36bf8d108d8137ee25adf8c23236d swayimg-3.8/000077500000000000000000000000001474536441700130635ustar00rootroot00000000000000swayimg-3.8/.clang-format000066400000000000000000000013401474536441700154340ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: WebKit ColumnLimit: 80 AlignAfterOpenBracket: Align AlignArrayOfStructures: Left AlignConsecutiveMacros: true AlignEscapedNewlines: Left AlignTrailingComments: true AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: Inline AllowShortLambdasOnASingleLine: Empty ForEachMacros: [list_for_each] SpaceBeforeParens: ControlStatementsExceptForEachMacros BreakBeforeBinaryOperators: None IndentCaseLabels: true InsertBraces: true SortIncludes: true IncludeBlocks: Regroup IncludeCategories: - Regex: '^".*\.h"' Priority: 1 - Regex: '^<.*\.h>' Priority: 2 - Regex: '^<.*' Priority: 3 - Regex: '.*' Priority: 4 swayimg-3.8/.clang-tidy000066400000000000000000000001221474536441700151120ustar00rootroot00000000000000Checks: 'clang-diagnostic-*,clang-analyzer-*,performance-*' WarningsAsErrors: '*' swayimg-3.8/.github/000077500000000000000000000000001474536441700144235ustar00rootroot00000000000000swayimg-3.8/.github/gallery.png000066400000000000000000004354661474536441700166120ustar00rootroot00000000000000PNG  IHDRvX.|tgAMA asRGB, cHRMz&u0`:pQ< IDATx[dy߻UrM2 $ %qփ[ A ?وd9p HB'XaLCR$Ù!G3C\vԽy8VthtڵvK~gT-UI/t88PJ7ov;@a;@a;@a;xTW]٢'"}</6j%|<]k=&usVGb}X_/@v;8Ttyp.)J~V{?tB$teךI{ٵZe?c,=ɟGʧ%%iRX_/b}-9a T4]xD;8GJ>wJ>e+xq hcu+?=Ա3G"/ɊRX)ޗb}X_'l}=a; `u<(4ꃏY=#ڎ=eNS9-wE5"jK^˗]Aye?+E%Q6Yb}X_/kX9; aR}(i^Wڪ8v33t\YXD#rvkXĪ%ZY3iG:QȖԑYܬ/b}=kkm7; asIqhD8B,AiYgQDzg"QlG:}dtZЭ=풟݉uiU>,>&8M3#ucb}X_/kX9; ar~b" Y,VG!\e;H"9h}vaזOHE˾FJpPG +sb} vE,J/ Q.ZkӒ_ʖke= ۑEn,et^=2%;H#][2{#6#Ɩuu˩Ŝ:p8 vJܼib øYX9)Y>]ҌɊ>M*م),OkK*|'QʓsHKz)E"kGז@$z6#}6=8"^Uy.IO*#׎[:X_3I£m(>ȭ$yE_0X9; asKwȦyHey.^\Qflݧ钲ϑ)bHw둗,ywy$q,ih1oǯuJ/nԚ b#ڈ*a;@ a+X"(1rHɧ^\I?*sUXʬ%Ec4#^Ӓe\2rՌc+v$W'RG;bGX?yjwY;\~`}~}Lrjx6K/$_*N"2%HLkUK%%_[ɘXR~G>psv;@ID,Nq-2Q1c!+yfqbŮT:%$޲o DHK%Q)s("7"oFUYRhgh~--~JeWUQ>GKXM9k4ťohM,ZX&R5z2~\ oF,vvxRyb%˗]KO1GbB\^Yy7R~dOU{9_݌WҏěW{dcb/O3z)/^Z,rY)W<ގسbDFrnDVR)~hWsQ}uVNw)==\xFJ~ X9; asIhŪ,>Lw%ԟ~ J6coI3bK~J}Ke+p #<͖y^<ʣ࠿l/~J0ҧ []6g#: X/MRgբV龪YCկoU[3g17~zWs$IIweckŵohMlbpA a;'8ŵXX:ƑXħH%uD_^;Kx5Z}ۑ-V؏ 8v4rIPEs@wfYi$JWeI"-A{_;KߍDE*[.I=kYeiAuיjYե(b^gY_;Rz̺sa;@<dE匄pqeJw>T؋8^I?YHL"-<22ER~^ш[U,)YFڴ$g%+{=[viՍkZXT#ߌJr\O+>3Z_zCa T^TXx˲Q1Kl_{"߉G_DfHD^j|42Y#G(Nn5I39ۥ20|wx7#?\ʖ\hɵTyTud+xd;oZ2j_^{C=[_oD.l..~sZz}F'R7ɋk/>z`8 v%GS6dIeS* OѾ~ſ~fag;*>`k}}qES&XZlIvuUW2`}*їKwcF_~.\ti9a vp.>K~8s&7H(~#/iFV" u HPHHٴd$9{iibV9ŏa]k>#bۑ>cnwJac^9Guj}s+_8Z[/cS-~ˑv4#(OV֎z`8 vËY,_b/KX4R喇# "QK"㌢UO"OK=G&/n`Euu HPq,BT$*fuL#IE˳99OVTZe7呹5DK/}R姹4uhb}t}=Rʶ+X_v}}??,~V]^Z+׃9a TMS\"Kb\N"Q&ͼ,$Ґi'pS%EU~FblKNd4򬤲"IĢe"c%uq&y~X;D,[=?Nƒ_UlYrN3Csb}=_/U[*ZY3; axDKGbhY$4H|ICb/;lI;WDH;4jY?eKb&%_I?Rd)Xq{VEeGu:ʓזD][_KKE(-.no㱾6D=d}/-|:<v@ΞG/A{"qjqGĞS,#_ɳRIk_Ib=iHRG'I7pIG%d%Z<+Y.I'%GK]F:zVdjDʵ$:+*_-:v-E:ٽXW1.\S,=*#b}X_'o}=ܪa; `uT͛ǓHI׊FLtU>Y22..OOQGYEyV,Qr4+.Kbe6,Y^_RG#R,֒:%iNg"rYg8Xɀ'e}X_/Sv;@I屈9,R>Zl3]l_vd*)j2-|hx))UQg%L1)Qz{%UE4F4[,3l;ˮ-g}X_/I\_ pA a;'(W>N++:Y9ŊUIV6()ư[E4KB~%'֦e_J/[ӒTOQGj%OsOdk/\VD>?lmSb}X_'q}\p@ a;K|Β!iLk&H*OU4@rw7"gaY}PIɎ[֤XKꨔ#-eʖWO5 먭|ٵᭋ%u ۥY,b}Ubz9a T~8*-:{ĢSL,R^IJ>:*+k*_ljv kE#X_/DrC vxRyQ1J^x`8V6ز*ʆ_=M!>4u']aVu|aeqb}X_p@ a;'D|= VNJWb27b}X_ ;>?|ap=ihz4M'EQ( U*E<ϕ$Փejz]jUJEyn~UU}{ӭ['yjc3H{wd~7oEƋ*8voZ }D~}兪^4΋j繦i\%DfEσZ4M5NUTfĭZ=^0͋3>qGьj5>gcaѮ[:};̺gyˮZ8Nu]ݼyJxͥmhYt: ݘRMh4fIj͈IjAXcihwp8 м. H-v^Lz+Ns= o?^dyh4q>r<j͈-hEя /Bg10ZM PZ̻qڻλp3wy˙we4歖/X6޺uKn/b~iݻwO/X~"܏prh4M5s]M4[ٜYCoٟ֟ƍPNؙH7 `V&6vċ /4өy7/ժA6ƶ2aάBvVoLmMxqe8ͻԙVi8jXͻoTFoaF K;g-7g ޚ-rVfc>B~^X]^TW* l<X05!>jysO罽=}[RۍWpFd`0we4Y#,d2q3d"S^86yW>̛P4agD\r?01UL :&6ac IDAT]͕o~]+nsEo&cĒo}^{GR ACJFFAzm0^фN_F ƒ  4Ml6c]fz&FaXzz_~xbpFx˓mRy h&lkW`&W6^XDGv Ͱ>|0pL&t:}gϞiͬ^&| sF,gN냈Xh<./<}jp֦hN/}E"Zj>NZh49w`esNhWR Zjk$?q[NIԻS6뭷>wU5wy|||,IsN5 's0k|n+ylΜ2K2[Q2d[5}h>GYEv6˻ͦ$Cef7( -ou[}<>g4fs kf3K=˿3K=Ьe0h8Μ@k*cuvͻĚeЏEQ)W\QZٳ^P n,SG5kY|-Bb9"-$3uU`)@@)l= ?UYբkZ-5xF;6M Ù*-Y1If.\ \i~CoFAHXDcbY0N&j(7HJE~_NGpF|};'фӮ7 mllh?*/Dd>ҩ]/N%/}VBk"d3<NG ;3ƣ( 50W_휨Vz/Kuht_bw@:ge[l hEQ{}"H ]KBj{m{fذyN6֋5;ϲ@,=ϟ3+E`7eʭs=3up^A^y1df>|o/T*E1ͲL`FY|@{_{&ѾXbyq"VZl6uU=}áﳺ|By[?nWooW^~H18cpF؆6bfA,`>J۟zh*,"zˎJؿLG+!-Zd2Q՚q5g߂jwʲLfs&%&8oQ'{F#>^1G,; Z֯~iukaKfb> }'.~.>Ltxx_37+W66n6l[[瓩4\~]ׯ_իWX ~>Y0ޒejsOi/BLyOY`.fw0aچGVl>ߙN()&xܿ%͟-Hkkk as3)ԋ WUGtX>ՄYުhwR3b>O}bsp"-vo5qb:vu=gl;#O}JYݻ3͟B%nۺu^x%,ojṓ,xɻNݿOʓ$u= pFM0//>L܋@~sFăk,&L9*{G&m"g>Јwg|;&p$QmV2/h=JПmADL_ܼEź'I] ֗FC;;;?٘еN&_ {zZIv wVbDk6"ՔKRxzLXU笎z{[lđh}]>Z)0IRb]>Otť-s{> =,.oa3hylnu{J_2>3QiEQ%ki/f~pHs|z>8{Y }l6o߾-IzW\07vkA VckY`vP6!db'7D|&̅Ѣ(z0D44;C5:.~O-Ϳ哳!^.9l# }{%074cWkӼ>Pl/o]>K` ޕw=>]D["Ynb}O׮] i«VڵkODFAZBz=ݺuK7g扏jsz$gE4eBΟu20LB2o-A756>djZ!R&fy&G4YL<͟`& LjmT*jj43ϱzi~ֿYҬS紳%K `㭒s"VU5e5ϟg9ox!nc008tGGGzz/.{9=s kcdsmZJD7ooR@\k43S-Aev3xbpFF5xۻ9sQ<@L[LxGj~kY1oMI u}{wJ T,>07?s81⭎&GLN;Gb׋<^Y_[}yqg}2l6&]}޲eccgV}3U?f+92煨m[Y_$kgggsmm-<ͤ0+o~3cemT*AiCI \}c /̪ɲL^HvG46r^$`fA7v[]li^(^\y {hZf҉… :jqD4,Iw55 K&,]3LΗyAd϶/{,:h}rGXńO[sZLv-IQ}; B֟NRz"T*!&|Mv &ҲFG ._vԱ],_6sbU:Ʀ\KirIlG}^n Mͪmm&V8X'ۻ861Q{ /M%z=͋/Bͭ[Ƽ[f|VƆ<3<F*B/^T }r= C(ImookmmM{{{֍7^z  2hVO&5 |sciϴsbk"1oL۹94pŻ-ڳ|9v E{WIzSOݗKk׮[áZVsu֌;Qt¹FF:Zf#Z-vxW>,cN!)Y,ȈmM`09->ټ2Km Ũ :Ihmm-X3(\|N7䶱7e7v[V>hvvvl6výpBj6AHU*]z5]Vutt_~9nFꭘ&_7w4koejf-ҩg&Fkcy^3>=܉mwbh]]xqk_$IO|\ׯ_g5_.I׿>#eĬR R?_l>8<.ju&:^N2!] s4W>o'fXv;Sx !f[̒f-$f1ȉp&o${v.kss3X^jávwwF|Yi)2hww7p 35e~pǴpG "ăϿYE  ϸzWP\bצiZ`yaEcs$^ݞq3>(v:F#MS+MS]|9nܸCc5އJDv[z]V8c烼ۡ we//|!$ضY__t*j8Gq+v͓9`>b`؛@&-W| K/a~MP׃v[}H//ψJnimMT 6~4$ gWL3F "v >M" k1K?T:,z7oNzsY}O?MR76Lt]tIL&:<K-6^~F445`g#h;w:}֍7fy迬_BT`ѻp̼p&'J,vO/aqj$Zf]OsOqu,kG ?ລ4!)9IFє,R>=$A515g,[wUp8O?\& ĄnB`vu:^[qx5"imQvU3ϨnzAZݣ(D\[[S 8K;H.^L#I]SS^>}2J]&*~Jǒ~ʲ,T0Ktb!5 YA}bp#ޚjQ]{1\am.8З6! 6 нW_y}c V{+xR>Ҩt4vx&M 4z^i}u`}ɶf'n>=m͊e9Nm[Yп. ~᜞<6!`f M Jnԧ^yLz>s,YJ%F˗gOC޺zJܔ$mllm3·-ښ$3^z%ٟYp94AdԻŚ̟Y|v͟&]ֿfcm_ҋDV|0Y}$ShcaʢZ__e"qwwWm( mllzi"}:(lnngM@Y+á~v&zݞ b=6:7;hwM$ & cAaL ZaggGHwܑpЙƬv&RXX^Z4y=]z54QnuEݻwO\3bmm>z}kҟU4fb[HMg+Z;D^ٵ4 6wZ-Kh4qb|A f4N[#outty>8PR e"ڹIi 8`8ٺdL&P3\ŻbŬ^ĭѬqY[jh",zs,hcof%4K|E>lpӴ8-5N,L^O;;;esggG@҉%immM3Z[[ uZ[[S^WL2r';WhVv5}֗%|h>񺏐im7S[aRqyϳ1<ٷ^Ez/bf?-+y` e:W_Տ؏I:o/R8gs[+V+XAՃ_y%Ih4gz^x/im_O=T[jk2h{{;`At jsykmm-X,F3sjiӟtxkcG)ƌsw&j8"[`yd >A&|ڃ vЬ6Ooτ?hx?? {wQz^Y;NֆJM֭[ʲL??z] f2Lؙ6~?X8ϋ=i`"P0q`BHRfb6YxFuu]]|9WBvҠh &j{SKjzٟ$nݺ??o#?ά-뼀2QSYxjea-Vp,_]wqhfoXvn; Dt: 9 "@EQhggGԧ`ìf@3KSxqgV煟 hἰ3qg!d7vNkނb0MpDI̸Y۷oX@Y3l>C -3~^`t:ZVC}քt:LN… ADHf!rJ8 4G>}կ֭[xbjRRؘ裚zWMsQQ6wwwCXN7҄7XZXzhe۳[F=} _+MF q6Vv[{{{Z[[IF}Y=zꩧ;(2p8Խ{fBۦڧ] IDAT\|q;77n8XL|؂hx5/(-S/R,,tMYdV7fi6A\sZ-_Zy  o988b`<kgg'F[Z__vtݻ!O^>3Ss'AX(1˛?hϴVOacŒwe?}}}=kb/}`d`(횉n7xCov&V.Dөq8gkCRH oeڄ'?I}C//$~ L z=ݻw/f[,kmҟC&&L|i63Q0kOB?sYLÙv NG8lv8fĻ< xITnWkkk AݼySC@͛!睝L&Tŋg,ZCp`0#ԭ{z饗k5zkyo&Luf0hoo/ŵ#{+u&Ƚ˯c'N|֟韪h`5j5EvNRj ժ׃62mmm?q[ؤdv ~&*RQUN'zg~Ĺ6nWGGG?#5R[5]jT&ʋGoQmsۻz]Hg9TiUʋĕJg,?9zZ}L?j穋ڭ(KsRrQ鸧< ^F^Ч4MըfVQ5_QZvX^_@~Gƪol)5jVBp\ FB2mLi6R̘OjʋLz 7ue5.k:mj'}޼MzVR-hrOEezn*N58dA7Q>NZSJFt@Gz{W赗&*rVk?~tkvF\:::I[`mSll6&,̾͝}Mȼ mx>@ }>8C߻wϙK)\vwwk?Xu]=S] 5ݻ8Un0詧 A]4Յ tpnG~G={DmsZ63l6gΞU*()w]n-wv}ӟYvj4Z__b]}T.EqV:88K/f۷o/>xݾ/EOaJժz:aV8g)>ቓM*%|x<֠ystD/sKñ&SeYME$iNSE8_b,1w'/][oQO7Q6j ^?ڛM*ՊDʒDJ*tgH_|NШ_0";Vi<*/RM&cM#%TT剦ӱXX} U'6j4Z*R)M4L|t,ϔerNǪUGC*#YEI@G={R-+ZWӟ@oouo2=Jc^v>E;;Jh4^oNf|>lK?0Y$DŸL@\t.iTk|=ͲI*޽;s'βLkkkʲ,$߶6Yh͐̈́D YⷱZ6XŒOSmnn }֏ȏhn71KE%MS]tI?s?'I?`T >pkkk}v fwLy dcD >MgeWW^yE@.] _y>gԒYfm6{9Iҕ+WyMmnn>?+Re ;4Iw#"ܹ7xScݺ5xah4JEMӬpPfQzuUI s9g׼P^Q VH=mdm yKҤ*'{iZL~)ՄnVdJDP^_}C_FfR^-4ORxT%yv#QQW5uqJ+áuqulk^{C򗾫}wOWͿ5*2%Hi]*=d)UBD3t9eN˩P IׂVL߷LrLl4`xz%QK<$;RVk4v}ָP"H'4 +eʉ`guL*וGS%^nd6 =5JxaܛEnWbM*kazPuU;U{'A!Θw`֎y7ن6&r4 EsϾ7ah.y&laq fG<-ߘymMH'FFMwRQVmB(fYiHN(&R/]w͛v$vvvBLKCa3MSh̜=Ny @|VI } _ /~ ލ lj_ydz^o.[VpKjs玚fpQ1ξ rؘW\?#j :x<0z=} bsksss\~_nN E6IJb:(N]5JӪڭ uMMlʲLQ<ꥯ]|*ꤩr7kj6&#UT['V }]rIYݻz?t"幔zKO]jB*#Uk-m\TS}h}}Xdj O$ebbϣ<W |3gJpE'ony2s9kUhd|lhgT(Ʀoo+J&CӟsF5j*TIȕf'ck٬5U+ɻZQV|Ҵ:,DyV}YRKI\ָjN,iZRhʉ$S'*Lն2CZWv5::|C; S>~Mj4J )44ʊ\͓<9t4W{XK֪}s{7$ERhˤdYY I$ $yK1@^Đa`S&<1l3I"Ѳ%2/j۹u]kCa7EdK6yz_VZU.aeB$<(,z=8Pj_~~);mGЄ &LHZuvj\_@ugYF"%߳z\}Zjٷ:`3NI~Ic6ٶ VO!㘵*pPR}:Ҙ8Uo82z~`PlFۭ/,[9bt8<<9S!!W=RWܹs_V;ͪq<j2uqIa:y׫_Pa囹jօz?f qӁe~5l6|Eٴ(cRnz%uN5ֶ8ûMڀB)~Y~0$iJ;iΡB+Ѡށ*HA(pE ."،nrO9{@Lgc2(gPtށ.0*/ ՚)I&P$y㛊yu4`qΝg}}cHz/9v [>0 ~w{r?>1i;m'Z^6ϰi|iyPh0JfMAyA^$XyCJfHZ_p_hS8EpLkѦ A@Vv:Ҏ'l};pspnIGGW|`؅ dB>mYNh](Ey8qt=+mFlFiވ$7Y0Kyn޾(F?ݸ@` p*X5vw[*'S+9+ZO%vn9%Lۻޣ!0•2-*Ä2%(,Fil`쌦yՐV?Ccϙs>xWJ) KAHS8,;Erk0+s8_ŏ.-X}K_u骧惞W?[B@X D&_:S&suzl_zcHh=M(M^{_~_kJ$1abXYY،}auvLS0d2aZO=1t:ܾ}OW$z<ϗ-RJq~ie:UgT޻Re7ԵIMʜYٗ-ʶ^~evvv XEv$5XRfŦqI?֘L&u$۾tRl6UμTyA2G` bDՠPṣ 6úr<1p1+10"m<%]P_2-8b#WBjy^ڢmEzs9ku4^tt@# I HbF 0@D4'IrN , fKK;ok /<+yBweIbUuյZZ NcDS\sWpBe-ePƁ.zO4()p$ZoR6-(e3+bA0捪i\=lOzb)ư3+AhY(\iUwGwplu ':پ/x?lA̫S`wN MRM{ޒzܕ痀^$/~_1W8P"a_ t:y^1:u&r>seܹùsF  /kww,g?[|p8@h4ZCj{{^RsU}q6~®i(J`ȉA?/ٌ$I*ƬQI:pԼ_aܼy0 +AYz1}UR_jA(R ' OGį/\DQT nx8w IDATGjAxhXx5#1^SZ,Ďl u5`zq,h`~L" Zx헬KH_췦j<$`]Ƌ^kט[sgpbo:v^CGs.|$:&ajX&"XȌ x u^(1i M@F)q2;'.nCMp>üv{1‡!7vf|L'Da&qUoXF-LT9@Hi7 h6Ȃ* !Tzy@kL) ڂ |uJdØwH.Z)  Df$Ɍ,9G+|$2҄4[!*(Hqd֡hԷp>A#B/^sRȲyu Ni kb,  ",4:"$(FA:xoXm15ʕ+DQx<^|^ՙ,*pTgi׉2&m>6Id2a8VL^tkkk C>U$ n[ܹS ԥi&DQ"IVVV[OɲSPAW 0b^7ŕTzrR ;R[#aXYO@8ɓ~,˖So5oϗ' ~JihirM+ORoqt,#I.]hŐ4R1s'ԗ~M-C9p(0N-$hN=s4% xu@U#43孕śy{o5=l)2q㈳6HwoW#")S]M8W3 89.Op4^k1ΡFYv|!SQu?;"T~i[2tFt0!<#v)P4S*6r%aw0,dxg"NX㵵c,A`mShUf=ܝzoT1{oa]_v7}2|;e_>ӰS:V9iZ zHGQQsrèNɗ@UܺDժpG7"%O]z"!bׯ_WUzP%۴߯)Zɤ2C666*&hpy.\PH0{UgΜS'dԷIWVVD#N[ݏNνU @?Kit:C_w( */LbEA_j՗7o$ ᰲ# HӨ .pƍ%K+}} ,'j<H7a)*URZP,|B|ŵ2^?՗_! :g}ǬscCaKH<]6#=.=h|vDd:^R6V6c4ynA){\ +߸å>A8amtưn3b}4qE—V봚ƴfooÝFKC;PsG.Mqw 5pX[֭V]Y3H+=mdt0!Qi-:(c^x8L}Cr6@ŘhD*ӱb<tv9x[驅J*FCo6h=ܝ:Y as='> ?ꔱ;mSJqE ZRƘ&:(ho: uYAф,\2D[`x {(|3Dh\cw/OyV(cQi׿Mmrh7vƜ9׾{EhUh>/ `kp\nv6b:ez;DŽY<#P9](rVv߹s(M)BCsY;bcwBcp3yCl<2-Pa&nS9:(,uZh=F;(gz0S訔VhT|氮mBn(l,Fa"ƒ<.h AlX\bVB(|)D3s9Nkw(Wւ"a]:r1jFU@c뀞ornL($Z%ipc9VW;"e0d̓>V,Gb\F3Rٌsg&cVptpVЋZ`:Oۣc:: [$_/׾+_?7W26*R/v3 Tlq1wn9Mf;r9+ W*qI#nS 녽C0 :Ain5hmp -2XyjPZVFoXsf7cwk° ȡ}^{ٴ[Z!t[#U<p#]4@ .ֽOռ:v=&A& EYyzp~2mMWI=0 lKNE&7)8y.]ZJ48+:N+uI[E腤FvW^ҥKDQ$!yNgg( ar||\QeY!ϟh4qM& H:Zk+^GNJ֭NRu{u%x:VZΓO>xu]'-sL@'|7x=i4͊!IqWAy]SX}\RժId<W.\[d1YYYt:~0Lb8ynw%R\e _]7oZJ-s]ET?[V&ꢌYp(@L>K$<;wI9iU'E|dOO?s?ꔱ;mz:X٤jU(* $rՃ{ +dR 6Jn,.FQD^2 U=~%Prttkkk$IREj+g:bakkJzJ]S̿{9._\}3*pmmbflllT.Y__O9_:NZgR@5M+AtHjqe FĂ@m~})T^2(œ )b3 gm>O˵kxǸtRu.en;)_I~W~?bPȲJ[ ՙ+WC-5"@󠮂)ۺV|"b,-HH9M,fb &bo$^~+xoGh҆nӊ,|ΛhVCfK g>/~Ü=lvxsˈ36zL2tQdXG݃1V; SZ݈jHg(]“BC8rƹ\0.4zY0NW/U*4J2& iGajfs`;fsG<4[`UxꬅHMatl9>$YD|U-u^}``U>O;mWX)ysʕ 9mΟ?Ĝ0 Ð$I55p!EQp <\xFQ)1:*v`qL2geSad.|ӟnoWor.Pl6M}곝x'*s:)*ŒtWOQ 󼚃qeY5~Wh@I:-cRmkOX-(Yj)O҂@) G<" h(k6(UbN o92wSv){,~Ti$r4ܜrD4Dod*K ׾ ߟ2=ū|?C9Ys~ZaNgoqO=$.]Z "˙'$7>v_n Xd?`.7oq&+)IzKLK+Kiu@py™xڠl *t[v[[[qoWJޥK%0WyJ)vww. C&Iunp׮]ŋ29shTy fm6x K5^,SKꡀf믿p8dkkkJA59ik!@ZkO?SO=Uyۉܾ-1qw~Ð35O<[w]Eʣmgtuq5ш`d $qe5SOcU uOBZ[~}P+(@F`!Ä ! 0`4"kB&^h.CQbJCo(TgyAjofo3̦ky^)Zbl4>@+Fb01XY҈FS6||P2 ,+&I+,+x"y ֆL :ms7p8|M95%42z~@sN̡tdhtBfn(UdGQm丢р֣S)QeI<>fz>7jGjԑ^[cu( b!#Rlq//~m݀w N2;m_(+yu Ni{@M؎K9*pF51%`XWaD褾_'Y8I[WJK@Qj׫?ae+++2˥/{{{dYF֭[ձRps0\2ϲ2Cq6U3͘N;wnFUטi(Յi|ͪ0az}XݮիW@GEQmʯ O>$Apڵ߭V*`@}.2I!c x)\}!c IDATj :ntVvw.{R݂S0mJ%LKi(%9Y>e^U #Zk,#ostbi+,6ye.O>/߾,C\4dnw-~ f61<3ی hnٛ$\Zsf0oup6{ڈ33bG3~&;ZZ2K<*kC 9@J9K&-Fa,lcFra $wC>, }~9 8'lܳơ>&0;_x;{x_G&gS4 xƟ;M|?Y/}K]O~~s}ٺͫIO p: Ni;m{wŊLP*KЇ-UUM)2M<> ( U*fxZc?8:!OIfcw̧1y2K#l1Z77StV!N;;ë関)7<:{,U4`@l6ٳwoDAPId2D)tmm 8::ZJcFT1_U[=;;;RjJjOe:2z %Efg qz8Iun:f9Oo(f. bA׾*ξ [S <={p{\OpKDƶΈ Z)Η@c&U Y (.I,$x| |5TV)ͨAݦny/sM@=.}6Csem@)Q2x7)F;9w_?{e mSfG}6 6~{WD=f"X_m1pyFh hG?W虐y6f@YrΜoyΰͅ3]89#ށJٜ~ͽmΜd]Lmqßq@Ɩ)x+os1C(*IDtJ42V`QxE@aWWy̢3кcC[d`=dCYଥ DcL mDdIdf?9xU>yGڬY>, m8dg)گZN++K-?w_*=iig9^x^T^5NU1Oi{@.B!lGQ$!'Sp/<[ eY!v"f!u?k @Ch42cLU&j4ٳ;w,iɶKz#O-j^lGGG Ae^;>>lff3>~vvvXYYR89nl2NFq5 9f{{bAW2*czv… \x/Viz?zMhM/2y[i֣g.(gOuxI4x} Ch-Ft|Iblqޫw5h]L%ln4yyKܼs^B6T<|y yC'>Z4Oa},/ #}ڵ v seVWW}JiZR7'l'5R 8՘q7hAN`XZ^amE6fZHHgc|PLxکY5 \2xdIڹ#M=] Xg-Hɦ!#¨Gl1lAL3}˜s5 [rN|i"+ R^AETl>~<ՓO>ךgoZvw%VkIAR$M:'0:2I_T 0QO@(za$ y駉ޓ}4 Jc6668>>-}4#={˗/3uN TƺO y,czR@EΙ+ eNYauuW&>Ij=USPKQYSNaAiB,SOe>'wMo TjA{`x^9W J,|Ɲ;wsnh4h5ܸqB'Gxse D&h;Ua[:0D|" 3hlMi7;>O=yW^Ae09*2r-wr}8$'t{C {DŽ*H q̦SB@j ˷s͜لx$Y=Z{\ge.+( jޛڒeZ+=}s<3#Ma1t Pj?XHYhT u[PPJ -JHP*e&v83;i;ƵG'Cͼ9Oں!vĊ;w Rߛ?vP(墄IrC ucQ.}^{wii/SP.ԫpdeciy' )>RrR!= =\#u:s ƽC!_;4N0ڐWG s$ϵ.fư:qa@jw%;̷;EcB+wWB UWѨfFN^YR<މ'R +yHjH2HU4@h"c4CZVQשT|Z-ʹTCeoڽ_o+ |gqx׻g>qq-w,巕ŋo{oWw^8z(?xkܽcۏx–YPc6=weC4M OV.u^%ePh~A`0(J-,[ ZaVZ8)$ \91=߸yQ]0>}d•+Wr Vv]A@i=ZgJ QvIӴ`?@Yٲe<)%Jezaa  ς2x/2KX^{{f(".\p#̓2W]T2P|7_-1c)76SmTvL 8Ұ`ayZyGӻ14k-c؞ш, au fAR8?0."QQ׸pu^?$N2ip8*Ԅ܄%o N]wS!S4xRhLS\vC tq`D3(Cw4i4P"$#+ӇI\W9 GC##[8nu'2C䬜һO |sU8v:k:J'Q)w|wBcnu{O~jÇvn8; ȼߕ+wAo{Ǐsڵ׽Z>cG BL JJi69ՖfZwزX@Oٻ:WRLA$d+( Pzڣh4b ֨) Yin.e1Gpyyy s`eBp`ɀVl7p V@i?^&%R $g[7sO}j%@Mp`J0ƤVX2i ÄvtظPq*&s @x,l(G߾Ϳ:qfB%P-%"pҭ`X\CMC3L"3p^{{^ϩB E(OzB5 >+0:*&a.X v[E!R2pqIGP)K8am ɸK,#bLܶȲ|F@mmO;5.N`A~A~3|$}[\[WNg&c&Y:{/thGM++WzV*JMUH!"?ïY&9[[[E_^o^gAL l7{^c_w{3 qm5ﲭ=~Z.7~ڃuz| ݯv˷ZkjZQ*hqrY,oE9ϗ1m^D&{.F+ IDATn`{,f/^z@{+++xbH %ui/OL#q#DajuvB!KHF^2s%3 M!Y(,1mIPiA,T/8HGI/WU$49ʤD&bʡP$~n$& 28yZo DERZhɁP1&)qĉޣGiݥKfcȸ?[)ߓ~zzzwZ2s|#'xS؏(,J=D-I-Mmk3VP" ÙYP^/+Id XF,R﷢&֨w:ϫhfv 1|c&/]YZZ*ܿ0,UqgΜ̙3j5 Ðm*Jaa fǎ7 ۛgJk^nAMZ%Œ'$ Br4҂hD̙3;vcǎqE666>Ți`02^I#9S;V}k,,,*HXwZf2lrтմs*9X<(eEؽ%W='jc*LOJ`1Ӟ.-ry] 5q`Ra ]J"UzZ)r炖)#5d~=J E+\37h B1'TKx$Fa !DžXgDBj<$ $cI:Fmoˤ$:Ĉ4# /=O|+_̡#O rQ㇎0NqF4 EʱWVɴ$6१YoPA$Aj(J'.L14LhYjr4V 箆*qOL5d0 D!6tV8;[ !\@&ːi8&0pELf^ Τ*R,|`P{6pȐ̂L@X=T -PILeDe`.t88NdPCQSm2Hנ~gݣ_OԃZϨTͶ2{_~7~@Ryߌ1\x<6 PwL+B۽g^{ K1c?ޢeeriAP۲I}mod9yL(f2TP(FIݯ(h6\z fXEkm*KZ),//Eg>kkk\~}F -c$ f .]vlvqLZudyy8`P3T*%/fFQlgaa`%-̗'}pXX<1z!V?~~_,--qȂ;&e-) Z{^~ej1OZlZeˌz+ZV^שV\+[s C+ۉK>{b{ט9spvAHq=2j^ĎkPxiF4w׺dzadZ ɄRd;f##~gYl5W~k_au.^K-Nƀr"mRkd786| Id fh4"vRR41HkT1Y|W!RA {aą DsZrh\í-30)DḨק}u # s,Ng,=R3TU#M&5"۰0t7 [W$@VhC6t$Z Cyy hV+:p8J'!xk| Mב*'Ջyc3k8d^߹x=w/_~ ]ݾ}xȏHʯbkn~¶/[ >ۏ6hT$69-8UJWl+QrHpisMmM-+'ۅW\q6779vXqj( 4M 0077Gq$&E ɓ' ܼyq8tP1neV5ӧeZg 'f'IQzT(;nnnV`0^b<0,Rz=acc8P8wVYt:9L&EbWv~~M*${ΞWE./̉'HӔt:oL2`V5xq/V%8E_^y児rhۮ~~s'(@ `ތFJv-%t&P3fcOk,x 'J @\bzB`ߥ!hdcMUg#D$MH;1a̹_2{>x<[|K_wj8K0C .ˁV`8fxmqpH*N ?@&(zhRSRvZJҏ sM̸ j !D䊔B1*U)yWkq%fPmCP(YJ0-Ce H0zBuM13č 9葟g=qm *YlBkoQj. G6L hISh 91dRP$B  xuM{Bֽ2۷WY_Hsީվcz=c 2Xnmm=y䕯v߮ae|Y$I 4ᲇMMX+ٲrQٌ2&eef}Mj籹ɑ#Gyf0m^Y__aɲu:^(\__/<8@+|666 +q rUT`ee=+aHߧ^$I2Y!KKKm6ȱ)XXgEF1 ð}kkx)JB-ܕ^t{EpU>|}Ξ= KKKc/0cY2vv]8P>ynwwjWj-~uEER335~}YִM$xBJJM KGLEUJ O~_u@bCjBR[&[.i[XثdxrPi},8 o8$j]i~IED-TRK.jHoL!%+H 6-shaǁ6n*"Xm ae kUO՚.dxzq␎!vLbKJ{EyӀ@8:o& zނi2 I*f$(_;ƑC]\cw@Ԑ%#&C6'LBIzxQSpٽ c={T[F@;!X<2G?吏z}w]yjf~~~<%])$C"7&A zLFFhРɐS%p\EnaZF&xLhe|P'&w>4"7N4ZDxN@F B#bag0&O!Dv]#(T&Htp2Rbb=A }[@'pG_/eCMDSNI']$1:aiyWn[l/aan[XI,V47Dhcxa"K)q.@;Ny`A*#t|#Q>M 5 da\p(jY킿0&v` I ɄgFʉ5V1?w_GL@bF+:Kn ,4] IZY lv+J)'?@u?*ԕK/mŶ{D+W1*ErXr6ۻhBx=uw^}K_*~>O}3gμy.W<@lp]_{(Vջ7]2ﶾ{u߻;y0z;;y]˘YshgLr^pXarj8Ki2 3k[g)R~c˗/t>/^dqqup8,L_ٟms'wb%߯~|I F,y#N_xu2 7 LǤ"7^oI*hT\lLgn[.[ [ G#d&8~d =a3 MLAR9q: i] "MNgӱ<'JqUl*8RxEKpyĠ1DrVf0#R %5;qdqvZE\U!tBfb4fwdBP2??ϩ)ׯdBi @Q |gp|ڽ;ūe;MA=Wi)z/yU6~~|3?pMJYrrbk,#jrI- yȲeoXZ >!… 3I:f}}cǎsϱ6#a- =oT*:tx\(hZ>sƘW^)C \v7o}h(찹d2pk۬.\VUӥiٳgGj1Lt>lq11t]:2W\)^_^^l,RڳsP\k{3*<իz=.^ /0;?A>??v|u`0hpԩbOcfDo옔e{vy  dv {qhAܾ@XӺvޘ{:AJ+$sWnKTZjW qm9T% 2H! E/T[5~_?D-WD0` (V~) "SlmE ư.ϟǔX0n<_c4 SM H(0ddViTև]\UA*GjtHsyw}& Akh@ U )ͶބX1Iqtjo8J⪀"s5ylLL{ic2ߣ-bX{;̐@jcnclH$|{o5{3@ݫ$P7/{/ >|K.F 47Ft,& AxYjJV+BDž`0G񊋢z^e߸qo78fvM^/~0/yORp =s,//WWŘ:t9VWWyg}vkyQh쓝c{{2C˦~ŋ e\eqqj< P V$'JaN+yb>srd{e%-K)ݖL`_jp.R~ |_QTh4 04IL:_b *%&Fk(‘fB3Uw0\DBq'u_Zzbx<mTOztī.^=w-pe7ρwxQo>)/x9O%ۃ+Øx)LFݩcPࡇ vXY5clqFɅ xzX^>HMTT[ujf4 T#7d"K Zg #\i$V1q( \_gHIǡ*C$/0TM8ǁͯ';ɐ& jnfcGqTeʲNmcDH JchI6- ټAXUIi% ހ*|LLPw7pgAN\βlf9>_t8bV2vRٱEE/@Ru]z)>!@WPZse1lnn277W|U%׮]+@J)&_W*J@"-<{lam'~'y&O?4(,u)i{A e!sf0p% /=wɤXhb<>4M1|E)UWlinַz׭6l1NH vʪq[;nvnɜN9C<W4.oŔIwT˽k_|Zx'azxlOho~C Ō]Fm9t+6AM&Ǥ(pY_1)7@aWo~PJc&',@/3nlwy\\έ %! &!m'glmm88%LFFθK1qdFFn|M*3[idǐL)2-qQ WTk M CS0抗Il 㡃:qiV~a;Ɛ1Q=ٰe-_*)3rekݗ(f1ł]+9ȁޞaQ Cbn @L<^|#s nD\Cg0DxF2gOo%̵C*+,>/ Qŕc֖w1u*m gjTɔʯ"bg:M3\!ID s>O,|"ÑF(,K.ՐazZ~7OD\>&v{ ?OcL](J߿Szv.14Mww:woռvFwͿwݣr۪pjΝ=_w~r[;n =El[S6/'{r_N- *3n(ro/h/\7TnscKf{{Mq iJ٤V>tRJ0OЭLۥO˺; 2L y~:ZbL\{/ihofM'y衇 f~2Lì4l흄NZٴlm?9o^RK){w:|ȑ#^+2 2twld Sh%b)tZ]֭a=^xy|(JNBpjt3/\de++\tlETCORaAx}w*중eLqA j:F 39 t2"Ą]VH:R-q9Q )Y:UUVRRh4JB & қL-2Fz;![!8J+'q5R Dھ[m?v%){}SNI^~_gc?c?c?`Sc$Z .T+*< hDGҌqB/N`|:DpAbǿGc= D!1UH֐f4LcFV' E_Y'*6 n1cRR:Ȋ$) .d6ϲ@868H-˜Ng_ +lM\Cc4Iyjju F ¸`"ؼaVVd_ZkɄh1qF'""ͿPe BH[pPSAC4]^ )3;D40z!TD[. 1שM-)?ɟdmmm潯uy=Q.x]Ax; F㎂TѣoYj~ J+q^Z RR Mj`{UYIY–Y p-{6QRLe/|3aHEqƍjqVTyfQi{:q0VӖm0,X0 9BŊqLSfjJ8xd23gYZZ<쳜r)- 뺌F^+w0ؕWjQiM^Z$~i8rgϞŋ=j<Y1Ă[[.B/~j,..,.X𤔢VqU~qJRC :m-}Ԛ?ôZ\-p(-C@oR>(ŏl9E]f43(w)9!o|iHsU,cnۖ^V2R,? i-$LԐ&~d^fjA@V >VVV(.%v{/(0,cQ7:kmoe IG6i6!e#r8[fe{l2ͱ=XXL UVcVe0h 4 v~^'"ZVBVVV>믿*Ρ155Eߡiɓ={ui6EQ [WE7̏txTE/<+p{= `8vXp>ߎvӲfe: fffqܾvxeaq-* .,?Zd2;W@]vn)vFwڻoM^E1g7=e ʝqf,.} ԚT>0kj?)| R9?|?wҬWqyg ~||,]^jNkt09MPo3spDT`reu1e4q[YR lwp 8.~(uRΜ_ jfit`HbAq%>Z2i򖷼\A>>wy'<.LNNrwԎ),uzYvׂr魽6eǎԩS]buuqh5}I㠵Vl6 \QZc4jŜs*})U \݉bGivao;1=AD #wK޳hPJ? | 3\ LjXGu(DrerMuc:61Qm, .qv XX #ko{zW MJIV<0 iZ48v5 S,- 3!^lA~gGCb4@q*7zɉ9V.lҜNNR]!lK6F6;6OB( @^ % #Ѻm]ȥ6 g4icO<ų\>4dh9D+7"MoI5S.QH5E=_("opw^Gy%[v[_<2([_{Sy˼~ܶhrt}2&g ~^1>>ɥMHʢ(*&`I8++nq\Y̾fc Zmd4Mdmm( o4[VzN:z^S:CEq\C;>J):N;+jrr:8qDg 0y^:Yl62L NԬ/h0䅃UYr1 JbUzUz݄D;F)"sklF(Ae39rLO`b}u?pήmJ8ި'CzCv̸O2D%/ EhbBo}D `5LIn *Q3UkskĦRm%i$뒩!Lx3TNFPLט4KUv,}'Q6~jㅨ/T@w=WgWo%;ʋ&؋kX'1Zq!QN^-+24 l2]6 e n0en w2??h4H-˷FV#M/neeqA~8hu,--c}z^4??O,,,>sv+i}s+Lď9B^/@FDCpj0-x ࢝V@h >vff09/Wv]W1n }8^0?|E4b_eI_8 @,czz8 3rgh'ѲvY9!1)[r- qH)><H" GiH eKtJ{ f «cd2hܘ !ApEup6&d5z&q''0T 7zښpBdDU&Q~W0X32X0M&Yj#S.CwtZc0(JQzl.|g{qm6Vg6$?r1nO,=ǰ]w~6]*. Hd BBZ$5Tj:ZŒ ~]ٹTU H=%ƽX!g'#"7 I& V 0rZGGTGuBzB0Yq{n/2pX"ܬt٠WlʞmӖ7VlieIébCǏ3?3Z-R{Vc:{޾tD Τ:u8Ř:tk IDAT}ceuu$Ih4u]EU,RFiݶeE(qΗp4q$I ִs5װ\tiJE8E>d - J0ߖ-APxZc=w/G:{WQ;?ˌ]y8K?ڽ]Rm&! ;"ea,+ۜ]X&ހNoD4^5@ ʟe,$ Z\/<ֻC6I>nB!ISHzm-83=;H1Jcv5߿y\ "d#,v3i32\BKL p&;['qk@1T*6ZGu:"N&BRj( sWSUpehD(<%avDI J+q߫*FIſkZi&$Si:E;D `ϭaHQiHރyU^u6(**B{1y;wnn^^^J1Eg'$Mݶ/2L{[Ks&|G3]vyoFp=$T? P2_WyK5f0F+aTY:Qx>4C,g&?'s罷 u>HԨjTB: ]z $uuxaɐ0HdHGm25Bk=fWF|ĠZ 2!HBW0imo Ks$ۼ$#=zIN<@;϶Y&ԫ5yc8Wpbi5$FFڕA> _V c9l 1s+=y)#\0F7.㐠\dԤ;2)Jp5<ڸcTC%Qs\gyЛu" xJٳgw, }/U3q̙c xgϞ-Եu/911QT ؋xq",xRfʥefgY8]+'@!a?S>vʬ+e,ʯ V`Sۇ|3Y*K+a^Wbel6\eiiJRMOO_YY!IіC̮R9{,׿ujZA {ml) ̲l-NRa{{ed߁b^9l:w?-a?;jY9jΫ/O'D"zʫtx> 9+a˜V*[ H#`NLhr*G9v]qrP$ f]&C-F9HuO)Ѝ0wí:h$pMq\VbHb精33nhDKwCePiV[  Ԥ@a$FT& .Gྻg8ttg6yMaӧOtXZZbP;pq]p:$Z!KƕGV5*W,zNMh$%FdvNҀV4󤇑G c4dh4r)iJ(D;J%/A3&׾pr 9x1^qT;b/RF_W , }h[t1);[[nqEnin=[Vg{V B:VӧOm{wzg u mZavM,(XZZ*@N.:l6wxW$F,h.DNl9|ͅ\V+ymmmqa^WswlA=N;GC.ia*U mϤ-9=>|{޶?O:G?’Rr]岶2ZP[[3MP^h@\"\}pqmf&e1י?sD\uQH@7pbR9V#0=ȑBt(0LdT)&K \'O|'x-Cl;l hhyic |eZ^p.'_}3 븮`rFM19ȯ_EU:iH#TQ)FZqx^ ˆi@:ݍ!A4'4a qbH3( R8,FYt<Ǚӧ>;q߾WWXڦ1pfQ撥>nXA ^&J q$R  6&Z)q1Z0Bbb043 )]z -5,8GH@dH-qD h2#BeGڣUj 7.Ȳ\flTX=+lnn/&:"v{{ݨ,lnM-Vfzl"[V 6G$ vhz*ٿ, qX hyw}po|#?pᡷ`PL:GŃyssqX__/Xв=n_n qaY\\ѿfNS fffð`677m:t0a/vjZt *ZYe+cYrN< y^G$|+֚'NF$;3-pveO^g8Nʰz=14YPUsuEЕ'{qb=>997m9cg+9Mz9yɣ3Tx0xHBף UJ%hYAA߫2#BcQ`F(!!gN/1"E{.‘luYpd.|' }{T\/x?!GG?Q|~w9pO}Sz?m\xqtع{ <.= hbdZ0<׶ly1K~9>+y ,貉U~J,Vʂٶ2Fb,SV,3HaHxˆB+%ˬYGz?xQZh(-PJ84 9R07'NcV [h4 ;ѣJ P@k5 8~xq^vO?4]w7 4e0,Regff Cx Z.%oyEy$Nvͧ?i~~`P, XYڢLeu!r6 `{uFٲƘBjZyXV^|`Ҏݯt++pYm܊ZQ}͓@a"(!3c1wH.*{zG/i_ayu<&g݋YXМrvGi0 !.%}*B]5R#Yc~HSTP-*$e]Xa}<:8<'N<42rNRoVU4IR~呅b|dz|]aHtn0 [T(`Rӷ*c'|q^;#t/$0ɗE8$.~k^y3~ͭO299`Hvx&& 42Hx:C 1.ME,a(t RR'!%l5C%:3\quPRᡍ#YPM A-E8l!KEz(="N {+sj>>я>Zl6i=.Z~}/v{^|M-Uz6Z(;ۭeK/@>69w(]ɯUq[P]Ellll76ZNnjF#~a֊z ϲ4΂cǎsXpe"5SSSr-?뮻JB[v\,tVWܨܑ;W;eiɹrΣ`F C Bh@b"Y pO'8}4Dt;1CD0#ED2LsM^F9rFL>7P $#?Oߏ.r*]{ 8s{: +>:8AdVL$5G᩵sȭ802sVh~ 띭N:.YjʰAmm͹mmYel'|35`(WoGsg9ē\u 0:!Z 3JprpGdx < Nn$פxf&!xӏx$?@]k0 BЛ$H|\k+(0 [t/gzvc^+7x#O=Tq/Y<kƇ>!~~ۂ8~ߵr+!p\۸ {n/ؕo@LɬMB7vZ| *Zؒ:ۧgBT]6p\VWOSSSl6ԩSq=0991,WE婧~SO= 7P0GE-> CR B߂"^Grݧ-5jjjy'OÅg8w[oJ;G=jp[puVefffk3˂16)9_*/:{oRxeF(\Jي&z\P>#k.0;;Kj&#.JD:-+UZCWDIZqpp@II13<,!@8 '<1H,ltO׿'ۦVgp3='+VN=tc'5Y!!%\\//E iۈ15HqC:$Bjzzp%Ji*:eOo3Y[,8ݓ(=??dL&YdCX#UKz/ీ@dD ?#LQqĴu0`qcQF " z(|QYq@i< uLFdjzniiWA>3tl][{p_(cp9P/|Cgv[[2v{{{߳!cC\s0:w )!F HxDX|,ODD/o'*@:iM:aኅKk4 %=Hϵiw6Zd\;.RDB Cr7="R4 RLRBa-Ϳ@kUcGWfW=:?$Ӭ}nkH:*I #qTGX̤f| ʏǧ@Kde22BIbC92$W{_qA%44<6cu M{%/Oo vHi($\,x 4ӫ29 B QLFA:Bv"M\B h4Z48wx*RaHU"Ebm[ HG=(^:_%7~.`DSn&78F7Xm vv_b/RXyz8 nϖL ,cj)V!Ҿ^J-Y6y,3xe6ξo,-XVܴyw/KIE ڎ1=U333loo>r˂4MT*E57ܲ`}}"Eӧ rY`}}p_BŞkӡhq!2x9[A8.rIePˌT!1f,e@mմL좵9C۱_;e֢rRnr˲B^FQZ6;hKP]+ =;wfsE%hWw 1'WDt¡s?_njebF?̷M/PτĈ\f-A *KoY@E$W4NQGA@ %# t p 0H%7~7Yc7?[бy1$B ˑ=K6#TcHjbF!J -x~n1BÚ3(I02dg8b~jQ6"74F2 @?015f %,)C׹ti&JY\a(]Ûos ~P*qn+MgdL a9#⁣IMaBuMp$Cօ%TK ^a R|v)9^M￟__޵Huiq'N<~hQi~fsse v؋JBъ=l)Y9q-=K m_[&qB VӖI) )z[i[('Ip뭷rԩ ۂ8.ƩSJ=ZVŅ ~y6199IE9E9fْm>L. 4ess(t:E٦89{,sO-;&!jQVwX!ky^Q2Y.)hPe `[^Vȴ^sTl $ vu{yߌT۳iw:S\?;6fYsss(_ZV3Zg,B^N\Ӗ>W.t=0">2cDy.jȾ:s?+=$K'q/#%QcqϜ#1Cͺ4vfc >uaeЅp6d2YcG$u JIQ>"UHc\^hp%;oɯD?VzوjPa* 26Vαȹs􆻐SS?3N<V 4iEEO>dō-&7! Jcsu<|zBlwXR9:O7coei8h R:H!M%B7Og C?d/.,jVQ IDATlze&^orʘ߉r>ؕ+r?X|__|^|)A\Rurk=eeT&纱_Rs ^f[_?zԆN]F/U NpeAQ{|,f˶M@) G<;P(+AY"c0ٿ[rRi٫nIN>]XqxLA^[ا$ɻnwO~~QǼZe{^U ˨faZY7&eV w:.B0,JB,PY|cq8wEn&8Yㅧ۱c7Q+qk `t*QVVV kmm0 i6,,,y;\`aff¯Ç9{lb>|Fo|>Ge`߾}.fs _و۾gx%Iab|Ylm}AFZv]bڞyfgg ִ,STh4 eR\;?mGj'֓.Xgr#[Y1*%2p݉VԂ<i^ 12DÜ+Lʍ1d3%[Ҩ , (ErQ3&0Jg߷4 N<(G\+^ \!*Tr 'CQ#Pē#~r<?{bsmH^_Wyf4gmZk sx0}掐83eo=@8pO $2p Q!}:Ä:M0ЌyRDQT,!x>FBg@DJ5Z&W:.i;VpE{ۿ݋/&٥ȑ#E['?+c|ٳ\s5+v󖷼Ey؋>XŲA~erϗMmR]NhAq\$6.X۾nÇ L쟣шzT*AE^KKK[˓2W([T*byWmVrV^ k~حze~?cfgg5>FQT|مX{VkM$;XJ;^eV/y)"ٙܕ\>(17B1K]Ր ȍ&PȯiZ,&#Kdid1C>DaCP0Q0wm|Ʒ=MV 0- U Ԣ AxflDFBĵf21VgM5Wo<3vKC- ZOg4ηu> |p4!f4Aҫ0pa# <&kLaz6c3lmneA.oR|4!AA7fc+qaQ &J @i\.H}#UV1@^4J~3'jr]! KnO!2 e+a:#"e@ڧoq azңVKCU+g2f..,0(\I\{(/* xnn Թ S^֗x3u{n/*G͂ݒJYrݥn6qRXDye&6|Ϯ,o{l?\=q\i0,X4Mi6]wU,\ߘ79hMm0??_(6e@fk-'.Y+%CymR})eqiɛnjB`Me|ggy ;ςrssюnm{{ gˢ80 e[8CZ- Ð[nW?~Pdzz8 ղ^+ǞCٟѣ8p`\( jYվoi?ˌe/W\p8ܱHaE(Tbi64&MlY5tu}zA/:Y:ȸ`d Y&-@h:0c{[gH̀2IxdĽgpw2+dN '\4j&A'AFt 8K2((E Ajh%8#C&<ͯsje nQH_eH;Q'NuGڭ!-O1٬'y<&j tZ;f zgzj y!2c֨:>7k:U\?C|oFHFF#])/@":682$d4?1$dɀצaptĠ2MDރ2**g>J8zj'///|y`~...>'[r=۽؋؋F]rh 0dqhcVޛ[rwsryo6UPZ 0R7ؘ؀;hp#3wG0:lLn1ؖ E$CT^ow7s揼TWJUR0;z/|b#bQ VzZWHLC] Y٪%pH тTI. T_)t&$Cx$QJh BR$a߲;nI1QB(Ԍ8sxCuc)2T& 0r0/$U)JQik$(%( ÐlBwg^X?'i,4kD:+sԗ=s|ӜoG-wkfZ33#:o¡K ٱe;å FVPZGA#/L_y\ gem_:KT5Rfv4>,6IOճņ z|ISJYH`uv~{)ri311֚Y:v0^pI2뛺8Tq(yo'^wRJ A]8|cg㮻… t:~ sbH<\ץnHTb˖-4a}.δ͌@>iYz}3鈆! Z!DZpttZGbhvvLjklr(D@*M%6lƝ9 4 3h45pQ0fۆ  fcF#A(( IX">4AlVb&0ȇ"$ٱ(zRh|TQh8J"tӥ3n0Ayfs!mAԉ3 n&ƙ%%xT!n/xK)(R~>CZpN/4汃'Govo.s-ЉrR\9Bo}/m%MY_d-8|1#e N=Tܴa1#g,.ܐ4I:YKPY>fZCh%b A0&UL"e_E3/vQǏ޻i vPhTi#Q@*z<+ì'hH5r5@︌%82V`ӤCXH-E]2tqw |;7w7tx׻5zn(4b|ПW4A)&-\ƼquD 87 $_g9򪙅B^1 sF(ĽMoԩSv&sǎm{Qٗ qLRرc FcӦMtMlA,|;2Enx>,t: رc>ndž9G410k&5Ĥ60_=4kGͤ<4ZRZwQXTѺo=Pbǂ+ bnR%AV*ۭ/}G-5hR혤\䙧Hiq, N|6TF,& !4ZCdTঙ̹( DIe Y# ^/ALL( qdJ K>JFD*{p/_*Tyb <|>^qZgV9&Ku=ϼe7xETܪ5h$$ 鐔.==G UT98B:&7@85ΞBp}Hd^K.{U3ﵟWMhOO>|`qaaSO177wMɓnɲ7onh4ft]+a睦)v ضm>,ONN7~)<ϳ6@V FU!4رcSSSV<$D`&_3g뜷0(ϳHѰ" :jjt]Z\FP%ό&ȃSΙ9|yo|-^8k kJ\ܿB[`'kKs\|E>Gh"f Wng={u?)ܶ}_EKA\vA.fpWh$s\RQ6ٳ}{_7_~@GH 8E(G]] Zl)22ka8ܱ88O,f4CP@h@Jj=ŊbU31}ڵr1)2e~+mVV^$rzfpDCLT@'JN5'mIю{ʡ8ntcmR}j'{t62 ѡDtSI޺ݵ4! WU[~ヨ{[o}O|W/+xwz[o7ڰ{|ג56E>57'DQdlܛϒ$J% mf$I,{gks썍qiﳴdFGG[xIpN:ũSchhӧOm۶&HA!"2SSSLMMXT*ŢUgΝJ%8`8bG>N8 S)  ئaNNNLv]ofv0E??̣>C=c= +++V~\0 mjc94?V'NcǎkcIy5aHwi-0dƍ,//[[SPgp=cttQ63) :h"*&4j>9hW FP(6"0f$IxFU vܕcq^)zpf]T*Xrm]98 UP:fߝ1??G?W@d.% ~kt.J%- NP&I4X镩G ,UpQ"U̞mޝ3  eaK):1(ϲWQO 8F5tfܯf{U6$QG?A$ ]BKczaژ+$% )QA.{܋Ic7OC '(;5lƍ9(W~W^|mq۴ik:c jAX.T$MU d>[+y4}(Giwt]RȥR0z=f`?={jeA3 IDATZ E_,5nzzwXdQ%J*\Gt8ZcRvVosrxoF%fI"tT$$ c\n#REӮtA$L*(&]CMXJŊqF~ӣzqrA,E؉GC" uZ*8j4:c4SۤiKJ)$z]R6O\]j.T*|3 zu|#F~^>o]>я''6]wj71~:_6e.1z-v#nk{\˳5-/}eߦL)eٯ4Mmyn2k&^ V+^11~t}XEA`7@ԾSAtlz[1/r935޹s'kfyfz)^xZJ9+yfnٳgbtt7!{6nV.۷oСC6^2.\h|ЦA@Zl6Z244>9:ngt(,//ۺ¡!|Iǹ[ivmvYFFFx0:d;{,RV9v@ЀΝ/rLm+c:dSODZހkl6T*Z-a׮]v7"8f&-8/=iv=Z(Hp`<1dJ\>1K`xRP)|/"DB# I )"\#fjHvIE&5)j'pw\'%Ndg>W=WڇvZⳟ,gyݵXDOu~g~U;NbÍ#z[omv4k sZ_ TBP+].7fZÞZSw'qu؟uW$QN !I/ejj^Mh1ViCW:̀v^g||y<R4a`+A&2~q8؍WD :E jAPJS&P8ߥVo331r%K<ʄddƾJ$Z' W4]0K/f˘g)힝Ϋu]3)o  |3~Zk+pG_N3ʉWyZv]?99oo^ >OD5fvXsKt}|S8 W}FjYulحF1!UTJYEE3,F>iW^`?A.5VlI%IԲ[y[n޽{ٲe a>];diiwRaiiRDX0UPw~w,j$V)V8aaa{w p a򪘆5,'I|9w֒(ahEͶ{bq@uڎ30*0)M Os\4=Z/t#!5O.5/Cs>FJuNB %DWOwɚu$a7u ᗋ$Z?;K!Il9{ƾCH1>:© i4=P JCO- ^(T6fmd|\em4ЉK|K͂ Dtm)$ZOr_eN1vg3Ah\=w;DJ_&k&ueQzT'%kgiZh퐦4Np >I"SOO&/\n-g3kX={p!>OO|_=ΗjE~WuMYV_qzm]s0B!VVRnKӱ-Jy ,h0 xu]g>{әڮ0 i6l޼nwݜ:u~f'?O|ӟ4M(brr^|+_affJÇٵkUp4fׇ oxMc5ʕJ)y;iK0 9yyſ.Rɾ ( GGvʕvT*T*wpM7TGyP.)I2ZXX,Z877Mgy͛7333cL$֚ 2gTT*1<<<`@nmz>00b=&פgT.f/w3O^//baӤ5^N$%dܜ-Rf꟫ \ +_Fz.<~xPHB%(G J%=L5(͉癭A/T8z.h%O36>Im3?&v4CFit㊀+%uH,2^bM(-q\/e_2:02'Oi)}DucZK@]^,ЇֶfݴpJ~L~A` ˿|YUf֚={X{[gzAeL#E2^`5 b@^>b2UNɘfm5f$?g&۠F)Ez= Rs=y /| T*@u:Ο?7l!m6,..8'N`Æ 6RX󩧞bӦMٳ|=<={}066fj(&g,jժnP0'$ jl߾/| Z-$_}1}yUS#`_&iRTٻw/.\[X5P(W3u=56y_I x 2uO''߶iXLА}L]a~1ۊմXRfB'JSH"ͮO*Tj?_mXѤn RiMhG"pHVLq%] I,@1qS[=؏8JPJ\<Ke.%ҨwCV3)}P@j4\L);&$x.i+5# uܳ P0DH} Aq, jq!۳O?$(g $Fh72XVhC$BEp$ CQG!)e)%Z g)s'w_j0O^xH{Gh1SܸF5P=E*BG+A " R;!/QMN_̉ѹ핲LWZ5H[l]k0ekZСCoohu`z{ųP&Τ ,%u9yv$a~I3B1YCf駟f˖-,,,P.-3tVctt)%CCC>|vbtt|3']TVt:.\`Rp.j{9zI0::J>ݻmc СC{<կ~5ﳸhYo,Y^^u]߿=yLNNk`T򌫑B7wڗJ%|.jbbYktؽ{n1j^ςʖNVEvJ%0 4A;'^~|i&7aʏՓkkM3Jf=mcamjZ){vuE.k4$4NW/]qыuT\"R=b+*?[g*S\Au ^+Ի)iV_R Ylty .hVVVq8{,S 'Xj(\KTd¦!lDXittJ2X(5D|'/NU*ҹK/֤"R?leȁ᦭3tB;W\*! ).r/HuJRLfee^GZq0d߿gR.djj=gΜaff{/77֘{˖-۷SVYZZbiiLء)Z->-b%wΝ;ٳgǏjsh\=fX(XYYAJĄÌYuKfjf0)bqٌLV?L*WmTb3vL:f׳ r^Le7 D+RHWs[_&_\6CZ_:2xV\q|<:uyMǤ\1\B k]u׺/\L&5} &T3a؊g,{1ZWWo;nswm)_8oݺG}u .vm\axxx,(*J߿߮k ލϝa*JQFGG[@V044D(?~Ǐ055EXzZGyĂV]wũShZ{O쵀,qn6$Hy{eዋlٲŦq~`xxsα{nw4gݰO-fzk]~ ٓ]"qd1]HHW8>nG J϶7^8Z" 8Jܦ8 ||Y!җ˼ցZGY8q=j0(xMvz[omāI&NN2N 1HMq|Y#V8RD>dr+D 8A GfHv<}L UD{Z*:%љAvYSڥ!NHH(կgzDx0E+$:$E"BpGP =ORbqS_;2>Lq|X>$TF<{w*FE\d]O@]H(G̰bP5# :LBד 2ww/Yjyg?_պ.\Xsɓ'/i&6_+mnԌ}>ʘ6uҰRJ[d#d%n0U^xX լJǜ8qekow͖-[8}4y{,3Gy7r :in0ϟ)6mF۷[kmq{E;wRd.4dVVT* yP(wg|ӧO[qDZc=fӍ7;[7|3 e˖:68wZ͚jVA IDATDDco3&c-Ka pg 3FӬkƼ ̏|@O4̳Ic6ףT*144DP.mW7c,tf\9}w)H"WI ]) ũUnqZ!g_런]=\HΗm&biN[N)9ä* G(q/LvtfUTT_TeRNM9|z ВJ!%nLoaqN7:az$eRwբ(a$gfIbI|U@& z_B^#ZH a )6ޜ q Z!A*v@xh{eF*3lءLP*:  (WϯOثگ|;Wb+n~zmkmcOlw:[o5S eJBٴ\yE@S __[F(5= 0Nd֭|E8vozӛ,*4 R!w 8ggazzznU,e rǏnFgdd%N Ko1~pFÈ@Vog (9|b͛ umI13033c7>>n-LٳǪyJ)9r4M6gdÄaJ)%jGj~7R9s 9r sssVf۾[ᔕ|gbb^fI\feeeJYV4.?`(_qlA,{ i&55&ҀGuٰar٦}e*gq"J!MRR)(RPr,9βta_PnY h๤iB s+HY7H s1Pi4z>$QQ(9(jH3!i'DϽrNDur2}O;R_RHgaxyv}2*bk7A)y@(M7qy9?]h  ӄ8!P@\\D$e:D; K<@G%DL$>*T0w-dqqq=Yc9ׁz[o7lQ@JIݦ4Wow^dхBn3djowNcZf#Gp4>}jʷ->Sy {uQo###Z-{{eaau2U)EPV8N[Ö38|0O?4 &RDZrq v7mDEȟw:Ţ0"!\gK^Ɯ$Fň={Rd}ڸq#'OSSSYgLj5T}v ҌLXf˙ (y`g6s/:S7YVqknZk6uR+ 9ox @RJ h?BO#P )&8%:^ā) }3[$$(Zh)Z h7zQv€B,# (z t&SԪɑke4EJ)##eW1v<OǏ&SSS? |LLL0:::pNh_׳ ;(}[pq&&&@>P(paz!FFFgfF${= lZ0TU5T7Ƹ5Q (F2K^ ^Ϧqq֚e+c|ާ}+uY\\LRR40s6)V  sc2l뺔e*ju`4ыx&D8ti1p(}Պftd/P7 Mm`=Ʈ];9㤩ꢅ/EfU6~vR(.z y} +IS}AZ|˕x+sۿ{ YzhS jT1Z'1Y=`}̘BQm#.ԥ#/M5>>ƿC4LJKˋ On/ eNH6Z2KQEh *(\4$IH'"6<hy/(KT3ײ)SXv^bie^m׸5\1]~/v%)ݘ}/^*fՕP봼p-<f &J}oL0ka M=FQ4`E`}"RJΜ9c͞}YVVV۬K3i###;vnА-T>ussst]T￟#G055w_*oy[xg@g\L?>{e Z_ٰajժ~6i{@da̿###0X.pe%琦MH&^yu cL̤4vB*U4+++v\q̦M( t:K @Ϝ@V{L:v&5Lyfʞk3"6VZ!T)hFAE(ǽ)b#9N):A)P@)A^*HbJH zAIe"(HPэS̶C azj຤!:NMQ/j/m";$N/HI$$!ɐ:u'"AQQR+p<A ] $> (JDl-2TcT&2i8(dEãG*nI%tR 8H =BGxA87qDSB!.!!qqք{tCn@O$Y/_dR'X˰.3THT>Z&:d9褉NTC4*JEE?A+$r@F)Eѡfv(*٥u4ZݷFKr&h5װΤEAXP`X/ώͺ&΀|I}BC2KgyCf ͛?̮][YYqv{߿ .pIxR&? h뭷jύ$IS ooa rrWSgI (KLPg@OR9I@#bJ^U*3N|߷";\J}ߦͮ4cĀJ3zMie'XC3MZ)`0!Bs\6ǗO5 c3z_FÒ``ttbhjk1cZ_=%ؙv"Y2rWdSRJkaiROi .*NRF](:JhTZh|V%NF% u(xF8QJ1pQ̏0f*lz&.7~ART'*DG(iJ2B)\W>ZH嬿.zgt^s FהnP3 Ml@ղmySq^ї7 l1g6iImBVs.'N-oy Vqxx~ghhA;wRי^ě& bY6hcnV h4طooxxcصk56>vy&I {7|ex۶mcxx؂Q@\=m,Mf#Ϡ4t&nI \6q-IzR˻iJX KdN󕴌qT RTߗ <݄ .^h@( {<{uCBI:8ZK5*l3<1L,C.B& %(U/P12T<B) /pPH!J*>Ed' F.w`5ciS܎8zlc相}!ǯ0ebQF.xNQ#Dg?Ă|}r~(϶O ;$ JH1b.+eJ2"T!aKbiENJq=Bd=N޻uZrݸH DeűK)qdƉ=_7rUJDϖISaRaLIBm&Ժ2@A5}s5͑hM0:65AJ߰݁Ǖ2UJ@܍ՀI{}4j}x\*f9dVݧX|lŒrSҍSwa[S %cB?$I6[[I1I !5d ?˂_|| sр,Բ@NO|bɛi/^~j$ovc^MbP'y,A<:|W= |D2~Ng֚~_nR>t0-//swyK,*6 Μ9CZu,z=9u7y'>}fffX__w,VavMΫ8JWb C9،rԜq>Dw$LMM=tG緵vlOHk r^yYMm$& 񘙫݈ \13]cmcZNfmM"5`$"MYe>> Н B33[f<Ρ DF$BѡT{NgIVO?K/2_g/x!SLsa5,_0yGM1`e0D!͘MfPmLUo24ei5cyycZL-eRE{AlQU1*ȅS5 0Ũot ){3eh$Mi|Y7v1lll8h;z^kI]Tr5W1Љ˒)!Ebo8Y[[c0 ,--q}yLLLgϟgrr .#!gpHR^y|A/RDt&ᳳs= ΋N'B _v2_>|RɁʢaT:j:k4c%%R>/ֵ *9!yvIY W.dnn΍FiJ3(/V+8p`L,FRXwbbмN]4MݵRL>T*۷I7EI@Nd]V~|jُkk^#"*nLA&˪(ON8I:bl˫K-jY@{EIlx4U0%J,K3v3T<c  NcRzUYr3cs4~"*4#wPOAoJŴn8o;q`~? B+4/ِG30]Z|[OCdi·4`hXR٢H (RQf~*2`fLEDQ(QeIF1i<) ͤJcTTm!Oi<?lz3w>z׿ڼ>k|yzޛv!?t͌Vhp޿gmǛX}H$ȔV"66E` })ujgqq1KP)J9aG6q48qu9|0?8v gySN/]@>9r.]RםH$" ࡇJ%'MߧV}yI_І)@BW,)p^@V YTƊ~0EO,QKƕRίO9ۦRNnu V$I&K!(s+I+ȶ8w~85~a!?8?8RIz)ǸeX[[sLZuJ16>,Zk=vMݦsra ?`ϰ i2==?g̱rJ'[\v@I^i2 \_Jb/¢(rl[ e|DF^},ȢdeN'>RNΣryw@kM\v~uY%-W"K\^|űVpR mC`I1:#%!Jc=t7<;Ҙ4;CevYR4 I8g06&!ی|bzAcjrpv;G .-5 IDAT|[Xl(ZEۥ$}I$hQSv)aЛ1W*Bcj?Ͱi7*zr]oZ|eV^oך ך1v{mݦ&^lZ t%]yRO$gϞٳN]seez'N#M\faa* z=QJ/;Qyyyo~;v41>8vM,+++\|٩ʶ8+ؙ+!ES%cX NuCwc4I8{:t E =qwHZ"O`iyffye+-eϪC3/|aï{LtvsP,G+@}V8-|.$YRPhBjM੫gedFS RBo76{^k5e~~XnT \!IgPy} hwkKYC$@TUb-ԩS4 'xijZE~/\|zȱ%~qNLLʊy1N1m&fC1 8!ժJƒ><$U^S ZzȚNLur㌒9.SFQ4V%c&`݀Gc0$ R4(鱒vYӄa莩9,Looo͝'b=F@}іRV^Z^5EƬxycZ,(4(/LxZ;˲0Ӥic/'eXQhy>Wd41xXkH2l\fҀ(]ioN^ }XsrZN̓ﳴRtL z1Va@bx$`S>TB^aAl೴_r5!P0׃,a0J.ܷ;]Ą5l}J&X<?$VBEgjdXWCJ#^^xω{?;O+dGLC>4IϐW'WEA2RF-Stږίsye }m~WV!TM5-k l e1A:EN%i K4%[4DVJ=CUH{,{pk{^{5IY$2'-n1X`3Mbl M8p~|鰲ĉ'xǥKt'Nk_$O`Їަv ~OeΫ/2*j9=ɓ.};OOw䤫OOk1;;)0گ^c0+`PXfq5EaR(r"]a𤖭򊬓QFUZNi-q'bQYSRY;qrrI}*NgrAp9@]cۭMRR4 sC"حjhz3NXLNW:+d^&윿iywʙ 25ξѣG}~g}OQ/8ZNg1WRUFBK+RCwV7 k[ۻϤ4C0% Q $;(Y\_?7XN*AL7BRxU?w] VAuΝ}W^z^F^~k뛜9sJ?Uvݣ4 3yjh_mLSEW\de:{}BsPUv&&BS<$5fMOWt#}3T|#Mn%ymΎ-zC{p;UF,,65=`^k{m5-X`hΐ\U1%SKX1`[&udEo1a%mT~7Fd>/q,Νr<{? &-2<& ,eW׆yG|ֺ /^fuE@? G6#ﻺx:dusĂ_*eμvNKJH}=>$~ xb}MmIX`c ]F#E<:рInʦx@ʭ:M~IN3ϩC4'`,OAKX<*?3oj,6u;m,]X}5fߒ+Iӈ4ڐe1blO)!/i*Iݛ'L^½lGq zo< ?O7_z+AݩSsxI䛥~y㊕*V˼*qпic~aXHW* /icy]]4b2iW،w.{KV Ði:DsgΜq9s 'XZZҥKNRğ,I:/alDرc,--qi*Zͭ5M=J?wqcGqǭvb'K $~q'l0_)A(OV*CȅQDrI>IK ^Sy/`RGu/~ ˘ܿOK QQ*Ĩƒ%xMǿ?>JPaHRakkrQc `<8*4[q&3CRQ0ų6VB\49N$IJ^Z!(i#?;֠tx|9:i3ڤ6/!Q!IHVkɀ`O]wwW΂J2QJ.rcŗcuyRSSc CRMXaG-o0ňSxloo/87lT*NaR&aIs{+ `+֒ 􈽅0wR/&cR*yqjDSb}*n ʥVO怰n]}MNN~β)#NNN:T XLAHsB/XG>d:!b)&MjKPAH <<$=,҄V9zxeXdxU.rA?_ 3v.jkjD  Of"J$y睬|geeɅ t^qNO+ppv wˡChZc?MMM9Uɢ_H*_EӳL_nKfrrrI_n;8vxnu~t2Eoٳgo[N-̢1 g l6pj57Z(X 0\/ETޯjJ_j5& [W\(5u;-YjLD6ye hc>#?qB <-ZAxx$KKvgyYW 0 Yߴp<> plreyT!I4d H$˓V.W_3?Ow>LN1c{{? .b"FA\0 l@W*ԩSq MnK'"&''9uc 7b~~SN/|WߧRO}̱7$NEb3}$IpO>?Xkv5_ᐍ Ƙ)+;/_rU,"ijfffhLSsmmuch4Wy !}&b-@Z܆XHP$E@Vܮs*ٙ\4zjZVGb2mI*%:VRͳEpDvf#3hk\a~PkJaERT*% miz?WtLĦOQ,DQLE(3`WxOpYa)ty/У")ՠN{Sn5V(|thye^~i~E|=\X\FS]P5dxCHOy,} v/uY/EP]N@*g5s,?lBϷ_Wo@{ BZAiCfTJq|~江y6 Blnn(Y/_׷ R<{9f֐Yd?I?!1 Ư%3b0 ,#V]K}~Ïry._Z(|?$4-I)|anr gcFHXV' k2 cynQ1R*R3_4s=T[p.A ^x^."T*`TF)C/*k<42kXhW$!S0LT[Z^E }x6Gz`G0\eADBJJFFJgТʪGkv'VFnZ?ڣEŽ;{myyy{3fΫm`w]u_K{q6׳.gݷwQ/]ky}Z}5Q1Ę{?Bțuٶj$A@^l:vYTܩ>R lnn>Dn355EBkMZ临D`ss'O:huu9={Gy/_]:̙3T;3}j$nJ//;z4osvK)ERnfnnΝSRKV"V,t:ؤ߅9I g*2sssNDj,4 Ǧ VLslNh`ECq%Ο?<ȑ#LMM)|,ȱm0!EbZxB_"#WL8߮χ7Jw3̪}Ϝf`5I&<ÄzF3d =&eʹ % |aDr3d16] IDAT켓i[07v^;[ iC.Iiu^[螾9H\QmU5h2=5A?MJ%LAQg(3-[*(yD|arx NK6zCEkaG]j1==M6$9ko=|.j5 ,ryen4(^͸wN3QRXë LD2[#9yb TtN۳JAoW@.]ej1iWu{Y36ي*.=CxS$DOo۷]5nk{mn4+EibP1x mגntb*H\%%TӧC}~~g299p8tSSS?s裏277VUIӔO}SPɲ5~/}$ w}LMMqY?:R{{TTRT*- 8HNHVř#j'})aRVT*:,333lnn:0Ykz;* 6;8 pmmm{zzMfgg9{,KyLr (J?S_EX/*0MSW۷7Ŕ"^l,h4edƚc Z)|_y i9+6R.%wjj*X,sa2~)vd1ztqV=s%T2p8Ѭ~=ŵr0hQ`9GijaE4}#M'SCphybY>O>DVNB6T\j) <Edss ܵak/xj 3Nr~g~{;@8Q"њ (S jc|S!Z 6k2dֲK7‚.O< ^־>OԨT̝я~/}K7;{Uv…[U@nyy4`auH+LA$(b- Ԙ$ַ\_xrc=K/s=Ǿ}tc0t`LjxWw@<&''c??gaajw~;33þ}xWrfff_>񏻾5a9?U5XI3"*EO5aoI+&b-.5MSWoXVr )`N/crhV^zi S)l6o{FvM\xѭĊ򹹹X.9/aՄՔ<~qVիryA+ZnǶFG@db4zMD IrUV$+5 [[[u]<}$%FK//7،d8KB&f&RVRE7\Pvju:A.,B^<褶pcc\a;2H|B"2RxڢLD%@!0Yc_h @ӨtcN)ŕ|WX2m 'fJX3v@+Kǧ\OR vk~g0 T<O+op O5~ٿMEzkp-V/Swe-l:!Fvϫ ^kI)z}qav쬍oS'NǩO;v~ډWm*?ٟX[[SҥK,//x衇 (b^|#GyyWHӔSN9`桇l~89v@wVΰ[P@@AV0cɄeMJU۩XdYlh]LfVƭYd0&''9p/^Mdc#.\wGKzbqIvcTJquo8'~"TST)v@"+kg2:˜2bt ޕQf,0˲+_YzRge, 'C$$:fg@d2S DGמ]rCIR$%V0#8\׷ЁŽ̒{yZ}ZۭM/>AF)Zu05vMP!Pehؽq+$kuٜ[| zZ}VWnS+(aJ'b@l J@J`8RTFѣG$2r|y}K_rgNY)wGСC>}= hZqʊKl6<3uN<ԔT*Op<#|S@& Uߧ%ӣ(rl>> 0[ƠfȽ,Ee3 C*R }ά\鸺/ cxG^Wh4.f~~{j5b*}s H>X.ۖHmُ|.EM9L@+;cu9sGeXSus^LMѱrL#lD0,{ _(4@!CÄ!F{Bjɬd \ShmFl[.]Hx)}DBHu(7B$(2/?P?tSYҐt:Nɢ!)0LSiD T}TVqL\bhbNZi4y*e.T< azNpHhdu7@ LϔiZ*>{yqqkGҏcl#5*0gS, XS) @) 1FF0bn][6tc:GS(hE襄i2hԩZUPFVJs]AxnwԽ]of^2yӏC^-MꞄ@_guu[ڊAXVLIX"brj 1j?8?8A8RPk-N h6133<.\… DQ4fcǎM\fii% s=| 癟gjj~j`vv~c}tMXz=ZKq$(QL+wIEsgY6V/'i;kWL9I E"'iGm^ρpRWw~YqLI͔> c\ryl1DCƵY.ao n=oFӲ4cRL3WEэh(4yc[7oPΈﮊ9j\3y3nk< 9 ~cҊ@H !d1ߊ*֚8yY]]ua9rW:}vɲ%jNZcssu]'O̙3KlŠ0R ̜z?ɆEde~~er>>5?^A0V{SWU0ءZM ZGe :jobĵlvRg'  SvW.#|K^kj&>_+|Jc31۠19W^E%/raȹsZS+hc-M-R@ا<#ujiqž,֜^{s cKZؔvˠ Y߶$"vX$I:_?C-N w՛Uz@VJjҤM{جE: a1 4e5 ` ̀b~~ Uuο]1w#9v?}KWcmjnױvJedVEtR8f(^.Rˤ W/o۬>۷o/^$cǖ䌟ckK2j#á(v*i *iǪI\.)"W/~t)&+Og~Q)yML⳯0;d~jJMn})EQPkhM. bl46X%} Q{VC)P`c, XH37 Y_ZNō\\(Fp= &5svw;1mD$% e?3uM0 F[ϼ/-qq8nby,0 cҴM(ʺB)iܦVV~J8Dؤj$$P5(Jg>~w~߹ fyeW}9.=ʪM Z@T I mن16v'txm!p=c" b&6C&nh7 ’ڗګ2r{]qyY*TUqŋR.:yD_CXXXX\_N_]NZ3+_ܜnovuZ^;'q'keb\][ϰÕI[/&U3333 UVWWz}yG]CE'4ɮ}R2uP񸵹W8.7s'ݫXKٞR'|szغC{-cl*2vaaCJiSAٴ&e;Y#?ȃV֭[ym?h48q^{-[wIq|駩jt]b1fy:O=o{xGґewG7[RT{}{f׮]= W册w:*N$Ih66OoӡlZ&zv0 08q~XY*Lmfgg-[NNNh4l2YQ8vvi3όLq Ddnn-[DZ3gX-'R7===̘" s#F\1,R<}Okmas FE7bf<A8\@’ (T l'&j3У0.S Rg jH#μr <6 +>gd0$QBTi&zF.~z YcrLz]gR rxV+"KM{yS'9:g@fRj'ǐQy|ᰱJdt\U*aO M9q1?飥ss\ jj7O5~ZCSG S~~䢯UY[; pwu.nwرcڶ{6uku~95.F,ȷ˕:=6ͶٮR bJ1 ,s"5(.:- ["Va~dNV*^^|E7h"0duu;E{9k"j,Sl6g||'&&,wy'wy'Ayt:v>33C׳_drr(;n[)c502j^GCkmY7cN5wQIP,ܹs߿裏r!˄-,,pYN<ɓ'yx9t~8|0ѣ۬A Å#GX0$J;D+`]r%Or0Pزe #J2"t ȶ7OKƪq(E8c=KװC'7FA J@~)MUְilW b ퟵH\7D/ M^ 8|&& ^YfJȵG<: 4g5MXĬ$) KQJGQ>0xYA+]I{aB:3~i,fb;p=2&3cU^O C Dw> {E*K%UL5j7kT" 49&O(Tʌw\3 /"G<_6,-pN$t֭&(NYTk8{=77g JTBZeqq"v Q133ٳgmm{ K̩>u02cf\ɻQZR͝@~+]`YFǩu]{/VPAgu:I3=s~S:+wtñunPQ } frAPtVVNQ}v[$pI|2Oy|[)2VN!`r*o5/<;|㐴Pcw S j#wݐ/IP9ne ԇxѐ*CG!59ʙ x9+b)U4NʑótWʨg?{pwRPT)L>J&}oXVdŕe.u»{|}ݱ+C( ^hulyn_*#tffWw`wYM)flWINA`Ho4iJV9Z&ZɆHْ$a߾}:uǏInɓv,}+e:8FNömzPXSNq=ĵ^KKKvOrmqAyj2޽}c&N9]1/q#!YU];s.2Ir)Q>/e||(VÄ2xlZfp۶mahc:=9n vKܱ#r8G P\ȽQK})Mkze>5h/ 0977ji>9W֡vkOVE(S+0&0МB!,r<"2> |)r0=;RZhCz^~qBf2=IH/_^0ƋK9$@ FD%;64 (0ʚ`X⌄2?2a(\* zZ Y;|{S{ P UI: (Q +iJ0cZ{؅Ng D(45h~re`|s 'hZ֝QdN×%þ}l(^aDb)L~fiii<ϭy$IbM@IV*8)sWJqQtz%PGٴ$I׿n-l.߄aȹsܭKk6#.k,Zc&=v'nv <11v"풽(_[̿#Nq ;j(+)њ!(٬jx~`eZkB7+ߒ+ZV `☱]d *ǎ-j{}K{E_Zz' !5׷.ͳBp-vV+/4 3OE_A>x=WM(ӪcK]sYa5+L2Ht|d1/<`V~iLNQ 5 Oh1Zp؋Tjo!@taB+7Eg>ßɟlN . t:vp-?cWO}S|#Y;w@]qկ~__@{mͶ6fl4Ok|ߣD Zi+( $iApY)/r4[lЋ]͌1ne/=74jK9f c!{93R+*\!ƎON?jx&Z< >' J ƅHӫ^*^ {c^u<=ٳ.$}kk[J&Bq 6fJMwf(RGQYlҹi /PÐ7,//v6I077G$Z 7`mqq]vY`pI;iv-oy(rq>ǭo~3+++<|_nȏy 9??o%~u]ݮ&D7M^jH$ ){D($IFzN۵&"P^zv|8qqyވSԯ\>qʔ֠3(3)A'kpj &/3q鿍mŖ<# WnG4N2fj?E)17EI ZR}.J)O{*+Pn/ֹRS^{yC,,?.]COWtt_?Oyۜd wȑ;R?yoͿ𺎾>OO|ׅܼW/%~r.6{|9N9+/4wd^sm~h84y$؉{b֎ߝ NH :[;m6yks]y6K\}YzKKK\wu$Ibs/++eqqZȉ z|["2[($I&F"x6͗699ɉ'úeVVVsss˲pqfͭQT,*BtkVVVFlnj67 @ٯ|V\I z@,l؈Iٴ~.MƤkZ,fXv['t'. [׷-.[dh W@u= vEcP#Q )MНHאeEZKEro?4{uъ(ȧߏ)F>qaECjjHf4+qӊʋ Y1:D);wn𙳘bc-fQ {JT= 4EE9&Mv"A'`r*(G)zOtCܩӛj%P>OJKpBnFJ=ЮH}ռO^s,+'9?{ם<8|+Ud߾7Tkru(t:|#ٰjoP{Ͷ6Wnx;iwD֝ 95bXN,O>mݶZOX nɓ'ٷo dzz^4vuŔ'jH9)nV7ϵw0cҧ^vm?:BP;vmcm:W&QJ)*HmL| pY {C"ѣLNN$G[n,,,t]6jÇn-kv5א)]w?ö-c&&&(U昘^n__N&ɾS8l=7O$X}Ϟ=v2. \$IQr\)5"3'`5s4#MCm~f0Ai5E>H.xW󶞉1dlW(>qqlÃ,< 9EZ@+1J~ˆswpx~AC)s BQEBՇ$ztYr M)6d QZ1d;UNw7'e+Թ w\̌o-E{~yUWa6flj3%R4S5QqFQDbx L@r23 Q\$*#9: /f`4IfxyZ֓Re4ӭ5EZLLC@mCEDTP/eG?X4QaЉ~O*;ǔbH~)c4^f.Bx425 cϖ[b{5a\7s&`Lktd+7(h~ş~+Kk `"h{9UN*0j+=X\])9Uj]@ah?@l+DJ(rxãRf^ƙYJ I3vTas?mӓ^@Ԯȋvv!kI+]lܿoؿw1lVտzݍM`6Uj.3!6ojXDZ‰$n`Zik8qlC*uY8>}2xxh޲v8TU*?0< 6Q{n+Ӝu|zcǎo~[nŞ<\7 e}Kl0: syxG7I,..2;;,O⨙9~~O$3K9ny.l{e"o~533362RXihVr:??oF E*Saa%rAƏ.>c C'ۖ,u|"۔<뗦)QQF wn={!w*Aa0,,3i (Q# ʺ4| FQj`?<8D{/qj>c[aqp(Ͳ TJ|?zjMfǨS%{~rYQrR(tz=⼠@[3/34h|<]fͧV+xSvS;Ï&ϟ7_xzƮm-¬-7N21ضuv Y  {N09(`Y!|7{L-[߸HxED]'x ҌV*sD2 q/ۯ{%[gìˮ?͉fh馛F~߽{/O}jײmJ17fʫCH9 Fjt] ܺ'wr ȐU"Cl=`HZVY|WE5QIJ-WVVj IDATu"@ݵߗk'RG^R'râ(( {Y FSJrA,WLY Ð`@Ery^Z vʄzȍQƮ{Z y\W*PIF]JpF1*s<㟿$s%LU6Z+TV{y)z}MB>u~vX3!ύ  5XYNr-[1#tNAE`T&dPGEQޫᣠ]kf,_]o.4%}{ɷ)Z1^o Aj/ů2>>_I: fkkܴ~?MQo~90!1)JO3 cҼ@a, J9xx3tl@0ae"]] !7p__aoe3$/w?yF&\l\-cY+_}{帮渺<.} 饦MF 6x~K죻dCk/ez6zmxQfRlx&Ja}XduyyvmA]RnMz&6%L;wSO8=޽Zȝ;w2??o-%ٳ޽믿:<߿vyΒ$ 333-//[3vmC ؐ4aF~ K[&*O:Edjjj@7[+RJLǖp)wnwcA.H2/IZZ͂cPEQPVgR+€u: [ǵv^75sǧ\[(h4]SRqk="9z=<ۘ(0EQfMBs_Μ) g.ۜJ}<( 5 ϯG$5?*[έd2G'(;DA7S4Bt[&}Rrϻon[`J}ye,Ǎ@AQDD@MC-v jUBx;ݏ|/|?[[6fŐ@ <M|oQ5=lbV^ix^8Ķcb|iR2tX٧|vwu3/>IXe\J}(.3s5pAC=dY {9 Oݶf2>>,r=fiCy\`*qnn6MFBfFQݽ~ҷZkzH@k1b|"KB cj:"sjָD"/uTJ`. "S@J=%n:n߾}$}ak|wD'\caaon轔: 7Ki]}5?CZ[/ wRϹ+ Yj)."m[xG' c CLbH)Ŷmۨ./0 6WO[7_au 2~7~(ddf5IFne*FԢ jXi*ZctP(<B2p+JUi!T`)0W( Z-M P䃴tDFN|1Da̻p][)0YɊ#˨ S[sT+Urz 8 F :f/W4fh{zߧ?inŸvm]&67@Y}05lk'2amn(e>j0X Y+] Ð~ϖ-[LŢZر^>}۷c> yyܹ5sssLMMq5%;w/}KVB7A`e$17\d4[&,ז-[u)cAh6B0m.ײOqy U༄T]&yNѰ!,+t:u0^, + \լQ"l?R.u# ^W`ygs^RHƽH)NdlYǏ3gΰ<*Az(04Tɳm)qUATJT".'B{T*3 kʴݕP߇dŗ&QB@jyL_,9{* 01 DԊeMV1(Nlx:$=)cA95/ 3 p.f/[&fȊ EZ)|E~y+GSs9șգjT(SQԩT[Y"F\nq0 Ƚ5U6.9hoUEyIS3A Հ,+1ae%9Tbhݬ@:;E>!1{'Ї~"ԯ6v14W|,..2>>@FM~_lՌW>}u16flW5Dq'e2Aꪵ]DD-v ~??cv+VJ\yL…I9u=|k_VvygVp}^me[V|SNH$H{s{}a@vQJe˖!m 4Skm>¬fll̂;wRT*M7DRѣ=zk<\m >'Vx HJZ-ir90SٯDgH^eʳ,czzZfsݬ;w1Dݍ7`0瞳VOf}2/izuY3#͇n,r_}\AZ&]_"^mKv}2ՄJs.j?fz|)0ʐ 9EI-V.>TM=P_W0Ph` D)s#mSun~FZ4= 7֜&UI  \hKsFJ1 `iA'+o 1=۷cKl< [Vz)8faas111 s!nnF;箻b֭v&&&b=sZ|P&V;wβRZn+cj6N5u;FLL\}T O_]Za- 鰲bb٧cVlZfPhĐ<ޚŔ fW&+EG?Q kD>*7rE(x z_rS5#IPm ?SF>%5jh_e{)U]^{J+*` d1iQX>›n ?)YRXVZ9,lMR)4|=9Α3˅ < LDP^bߐe~dX%t_Ve`ʉ\$+r~[ȇA{n=ʻAҦ lUM +F;EGvIɎ!d  UH!Uug W\ T2Q/CM+yUPU=Lҥ] ]^& z)Qr@c :'U)/yW14.?^²r_2=b^[KPm)DeK|K'KL~zΛf /W,ny6?ʊzf~eeVe 0{>۹9x ccc([ho|۷s)~ڵGy]vG?Q>O_$!"ǭԗq7  l%pͷLd|;v('ry>b2mɌL`]fK%Y"QT*zLWU?f_2'NSp c$KPV81fkrr'Nh6֥PXJartr\Ѝٖ+ӕf 92vG,8A0dpZ /HP Ea`fX!k&ǡRt)fD>_/_#1Ur(!ԭmKYA%P&O}mYQ̈Mx!Ɇur?f1`qBH <x Ba Wn7$[7>>NN8{Wb-2jN]{Y^k4|%JɞHgeb:yke"gXyݭC?A<ȟzL}jHӔnHI{MSj%EV+PEQ$ ,v| 0vCĥ_]@,1`y5![^^'ge~~j5[(q 6B958]T̎))rRw79~/0aMU+uC N)E&ZK/5# (+ \*JC4 $NAL0i)w5_JL)r)(sYΝ[i7,ZyW!T z֢_Edaa)Bgcj:OAT ~Dk9zﱁVT&1i򂜂Ph t. Z,+Gfȶ~ 3=uzLkl+Zv U;'񆛹;tNh5 Qh(#V^NjIܨMP.׸qunmn8fuu9zf&~E(jEXݻ9o%;n~#̙3t]7?P ÐIz;v]w7M,crr-ӓZ7pQ1gΜG?<wugۧyͲsa:ҭY)n`RDRҍYbÛ[/2vnZ0/;r-ovpq>OqQ^ab$abbvGY ZP`V%o%x]>+`cGBaL%5F,`N^KǏ~* z~wx(2놵e=o O(S"E^?O9I~yj)xrH1Mg{}Q<5Ye]*x)/d ;$Qu]h S?{o#y_ {oݥn-]KM$%jeI%Gg</ p8A`Al#1$(dɖlYI)nd]W%V+cZn{~s";8bznnZ yHdR,!N#J//[[x/nv7Fz/z~2WjuEJ(XZ[a)@RE}qrkHQÌ F N0n0}C@#tagbg37ô-yYLI\Hݤ ^yn q‹W:yXng~R06yCϫ+;WTD}Ƚ9`ji (P/^ yvy7J205Pr̢(BZyسgϐ$4u C:h8 ?C¬m!ydd>b||\J)3 ~i,//c߾}<Ν"sw]-JNСCO~0* HFC䵱vQ.q!HCN\A v-$X,e~$I${#V_$o;VT 3==6KdbP1 IDAT^f)\ԱMZ=G/7e*{\.RUy,% Bs|ccǎ0b;Dp0~GqAm8X[_ŭ[:/lۆ뺛8amrL&|#xdc3Ϝ^l&!քn؅mZ4XM348Ba{);wa_![R tA\vԃc dSPͿ"ӟO7?=D^ Ŋx#cW:xWW{^ib&l6 ufeQ]ױ.ew\d\a;C´o_W8ϑ|=cx׻ޅ|2yP*pQLMMx;)'OP(u]9R2ɹ%?Fb 1!\#1( b G&<͇7q ڕ I>fY֥^e!횰(#fl Ih"ě~(4!ci$Q^5Ķ)tuQtF`Fp*:bl=:݁ii/J٩j?yK@p@`P7S}_9wQD. 1 $%Y&~}ApJS*U8"h=l=$سXlE; 6mIH 6c۝AƱpf_EJ! 5R\z"k c̥1a`Nua)̣b#iЂa@7!rcK#r3=rQF 8I+FqzͰ\}+ u(%*a/*)j!^ w71}%jͫ76_E]6٪&p9w:]W8FK-nnq?Wr.UG+9o^MrmΕPj#10J.0rՐjB/^(WB Sy6bch1+"J ࠅ>VC<}LOL# ]뺘e4=t/^aT\; )!{͇`YW-[ZCPs:ae B $IKz6lD I ,A僓`3FI+T9j@304}3K\Vݟ_ӅPH* N^O|=տyU[8E\ׅ<P(`jj `.05!@J|ǣ>G}_=2ACXD.CP@P8۶y岔OhH࣏>|> $뺸P(h4t0==i8^xhZ>*4{޽{|FL/ك/}KЇ>$=4tf⍋f{J|P( /fyyt:m[.VWWQ.Q.z,Fe}Q ԲMF0J.Mx/8}gĀiعs'>OY Lq8dxe߇oO-v-C+"7:68s,.\ *aLÁ])t}Ӆ>zU왚tI=w  0ŵ, ._  XtCu#xy໏^ XaEك\.~~.uqcllLQŎ&< ~o3xvi[}(d"J^#L*(}9nkrTH@&Ō7PW6n Ž]1RH@*e0/qTBrٶ3gȓz gϞŲ_FrVD!y#o.=J`}}]Hɍ7ވX^^*݋g}Vδ_XXfy0PI|GǡCr LC=;vԩSm[z J}:O~/4P,Ip`Y9NOOSpZADJj4V$bjUMUSho6n, tK T@57z3A}񳁟{K154ְfN#*jyސ)\P!RU"-"}nWԅ7һwF߇8hZb>!~%؄3TQܼQQ%AR`}}]JU2ȲY`r^f)zd_?r?7,V eHJ(V ~@s޴= J 1-iV V<67U$INQo405d2?vhvh{s;p2 " ~c!J2pNOgǏ;~bs_)N4:6jU#[qc޽ky~,QC@>Gݿ2Yzr` dc ܔq< "lcrz 7K7XGx/[c)88|emԵM~0͌FK_-JOHAWr MU-jqs_ׯe|.nۑzu&:R9۶mKT:4Zn;TF<̒$e۷c-0D>GRǏ^ t1TU\xRoy=ؿ8q%U8zTs߾}8z+c… BN|+_~9.{j`/ϣV$h40>>.c@,$9C&#Zȿ ˽OKul$ԈPSjFl۶ Pcbbab}}LFHN߇m}_rkVϙsM}*h*&cx P\lZC<Pe5 C3v'DR `-!?bE.d_1C?6KjnK/`B2z.CsS=߻>?('wa5'$q]NvwAv՝ϫAngw]]%\sͫ}[?NCC'`Z VkK]&, JU)A.LR8<~ERfQ(/"(yVKD.&zo.ac~8u,*Ν;`5MLLLu]* 4tZ"8<6a4^CCCCC]V44L7@Q#~PŠږdĈ&%LgYdw} 9bjNEՙ00Vz6HwilllСCk`6VVVs(DbO'Ja׮]x"J> 曱 ClÙ3gdĐS5eY؀md2:­RFp<Հs*@a-;0}bT]:~E 2<_8T)Y^;֙~=g`T}v)T=vo0TYDZbu߬CihhΈX.zr11nz؛n n.ZYo>/f0 CFNtZjVհ $Rn'yZѿػw/$AP@*2٬\>}x;o>\pIԲ,d2? e2)m[+3Xi5\# mϟT.dqܷj&bH_.7"!}r4QQ޽QDRH|=ϟعs琙j䢖HX=qV{<كY)T]^OwY\\‚7;#%49d2BP{[544444,ХWYz^'e\Ђ{TeFuFT\Ⱥ5<^T*k$ ŋ@a(fSl8.DP(BW̶rVp]~wabbz˸7.9Wf24d1{ccCԐn $ќ^OoXTmI>yչF*DQuueY(JCcccOPITJb.ͷZ-LMMabbBPoSEt{DHĽpQeƛ ZиJ Q`rAXBwn7MVkd4Āw]YW*Qͪ*,׾5|C0p{}cjx衇WWWEyt]V T-qcffF΋ب8Pyvj~*$<&0PHFYS'Wύn{T8 u~G &Y۶133\.t:=}u~aN#;}g١15ѠF%ة/E!\.`1-~>1-~V_O;~}ٯ1+x -m.a(b*1#XQR{~3Ֆ=JaeeE|>/jtd' IDAT0胣 NqAl߾weMlvvp))=fXT4.{N8BiQAT(  rJGc=g"Y.=CVm۰, \NɪJZVSj6{x>9u6_OnsTY%@/~=xy1 ^5}TM-$WϛPUX*hϕjpDZ"TZyU>P-Udihhl qSK.(\$j&x,kLpN("Q|߇X^^L۶nB$Iz ~;`ii # C1Ÿw\NHHkŏ~#ض'NJ\ÇSO{^;Ejm&qtӴ,KK=VVVpz=r-rlOg2vmXZZm]ݻ~ٳBO$ڋ/Z&:11<(?THCy "V,,aR%p$RhɣJbH UV̄$JTxlUr~_%M͡}_zQFA0:z j9)[zj(*9=ahZQ8Hh4SCCCCMmV44XFFb}}sss> }AC_Z%M#, nQc<gEx\RXV,VT*<&I"aT'''OuC%,?SNs~q]zocǎpy1pǬ:XDC5XɒS0:{BհDSQFU5iRUmԒN۶%M5Q)=Vխu]Q ϋrs嶜j1:)GL>;Ut:X^^Py'UL57:3x^Feh "Q5444444Ԝ..^ K#Ȃ \] n߶m)53 517j']1 Yq%Jr9h4099)P`n&,--T*%?!u]>E#< ʾY"ZTpAj5?~\Ɗ%۶mCP8g# P8F*ɡ I WT"l5?csQ>|c)mpGSIU2H^$jI:X:q:GYr$n2ڡnc}}}:YK# ^c^t: u}Ū=ؽ,^/˹eClI3~ j}RVeǰ^_+ܭ:~]2ZvدŞ%.IRŷ8Ȍ* # Q.9J%YTs=ڿs4 7uE9H \Ҹg@@:FXDR4m݆Ç{‚eLOO… ;NSO=4dbӒ <#k͂ e& C!*y{cݖ~77 ɐECU9z*I!q!u} />Tys1F;vvT6}4vK)]͕#icoj3jpBŹsFDE!wBnihhl FB^-U{|9\̫aت88;RənvFchqp̙3R~:1̭jRGn=˲kkk(J׿@ǩSsss(8rpwb޽QO ZfYr9vfHӰ, H(1T*LLLȘd[j,AՊPMF8X9F<UGUSɕ>1mJ%,}uaZ&^La^@^ Jٔa,j4M1yT*n;cƂhhhhhhhbe hJEuAOZݫU1s|ΝBzرch6HgjS]S c[[V:tHߏU<3x{ߋz.'9!q;wC???Aؘl>ƐyC%TJBSnQ133#cnWT5,Q%R$V$'&Q#_-|9m999R4^ Ðrߣ,qyAAEt]!jdtR# @ryVBfרFkhhhhhhb%0M]j5 fгeYbDu6$+j06MRZ211!le2QC;&NӒaV(x_ض-ɉ8cp r97ވ{b~~^z~x;//(J@nTKd=J^f\.'}tZ MyZdڥZj'9ڣ*^m˘pGS˙J%8pIŋ i䊄J-GKIz:&&&`Y\VzN3Tbk-t'2 :8F6ER *LUY,S5cy8q/ǎ;p 4M#<dBF0=hت1z4Q!yvfSOyfT*%pqSd% KNAWCCCCCcUP蘹U+~Wqyƿ^ͱt68Ϸk[& U5%iRIšT*accCΨQ!G]YjDHRٶ=Fծ!a}}R Jɛ4Q!Nŋ|=73hrQiy .H`Ɓ@ [Tk%'N*=B .>qcuujvUJwH%N"㰏N-嶣Yw܎cƜ;2XZ,[ZZB&mۘ?)xxajǙM-S HyZFIkRrT滍fe'j l}hZt:CaV ff I4}rl6}_nw^.ͦ[h r9z=d9NR{444444xUPh8(* l7si^ٽ`eeEzHЩ|^O EnqqQ&mF8Ο?x".cffӸ;%}رwƉ'*QΝC:2jJj8y'T>EQCaaa۶m2P/FsG*j.܎S sSIG{)i>2*TF{y^Ν;qQfxt;5@\y[V4efe`qqQJy [B u]彡 $9Ɩ:wt3dU .iAAۓ`=s[PmjU \rMc3\oll ɠ#auuU%}ǃ>__ɓ'q8e?jPtfEJVL&# BqtNqYannNW)U*nOBfQ1ȍ$1$$jضy?T'Ouj(?\:WEGR88j}JR 7 :rAדjX]]<* Ðr`CW۶eWsx޼kkkC!ihhl ,;rC6$v"9a܀v .ع=-=>>. z.vTŢO{(_V*04m6" Cq p7bqq]w9ؾ};Ν;r/ٶuTڶ-jjyKH9~_Ç.P,EE#9(72%<LD*ec*n,r^}}j^Jp@J*U'xj)c H$qf)ęDecCWc"pu4 K;OIXkT+^*60R/4444444xŘE6*c$0Pa$ %J= pPᣊn q055nd)LOOU!K,-MRڵ ǎCxq 7`rr{rf|n\{ v# CJ%!{fqa⩧00^޽{Q,V'ci.q;U]UU"bjJ&0$#J!yuq,@&v#Ӆo.,/ƛq NbDq a*7l&ihh,.W hhшON#VuZVjS f Ki_:^0 111^'J4MZ-QY5TDZQdd>GZu]`fYmLNNceeEnac޽]i=oT{K=R>fgg166&; 5U3ް$R5GQgy^jT*S%*rQJVn.g_=qjzT87U۶%΢tO5 VKLt9f .gcccfRVy9ẮԜ; 竆&v[\ i)WT8ƪm;j)JTA4NXjRX 6fgg.*Ɖ 0 czzSSS`gIh^G:Fӧ7l\.4M_4I`/ 0 le!}qK$i6x'97ȹ9d2)v2S%IEDZ({IR:NΧܞZ%Dߗ{ŒQkuN'yiJnfrLHz4`4Z<ڵKr 0đ#GΞ=;UHRRr,NNnjU\cΙ~n ۶{ćihhl nWzǑxBUTWo穦̖GC.l[(uk\tsA㩋 v[T=..цao}+~)ssshۨVOB9iرCB-6xQ DQu!DZm6cũc8z56jU-Tj(9l~*GbJ7LLn4N^uJ%t]Z-qf%Y.ѡ,EȂ~>fU4 ?9rkfq{fe2<TUHLM444444xUT*0MlVT"pIRzv\.~?d$QE\pA!8qacc.\^ `+s0 ,7pn<BB h^o.l߾mT*ɵ1/رch4dD e"U5<ĉ* \H >U C:fթďի<6K3UL>.wihhlnWJIF,I48u]%j8jzѲ*G$( bĬ<`iiIHЮZjk0(3,طoΜ9#( G?z(2pѣhۢ---o}gΜB!s^gٳgaPMOOĄ28PʩoTgdpF%KTT$KݷQs*m,TOyN$ܗ:W *ym߾iG{dr(sqqQnfStl䤜GFG:F>m2+`y!LjUpS)&v[mX[[4jAb R$}\Njf[R/޸ZM[HD>< zEonn/"Ulvv|ihZ VWW%n#I\pAģ>)u]xKVVV犥$" OR(J&R{FMCRIcQaSM:֍'+%M5O=* U3sIJrU}3SF5iIӸ94hlرcB6 4Mض^T*5T;ɑ0,w}}]JYJ\.r3۶e[*|jƛ :GCCCCCCCCCCC ih\%$IL&Vj*JjF BbTJ훣 5vȒ(28!a̱Khn~7wq|=طo:e{8}!LMMannwpm y~*+JIDATWrrb(st Uu}Zw܉gybBK{PytZZ>Ѳ<0q,USRJլ cS#h㏒ RݶI JՒF-5sy ǤP(jI9Z~ϲM DF'<y2j(v:$FC+j/ ',%<=NG666`ECCCCCC; WEh.zP\A&{w4155%n\RP%=]4ET\mL$V^fs1cYHgY7j@R*_Q 0Mo|3g`mm r VWW6Yt:-$J,fp*() xj9Fw~FU9o0_؅qGMsH-  `r@8NL&#sps4T(1nZmH};wD^Z-1iCSo; ::#F)NGIY3Ëeq\xB-* Ν;Et:IR.J y9y$&&&$lbbΝ<0 e\sarr333>,}Y,,,0 8pabbB{ZML1>~Jf$I066&F &' CUTƜ@;F5\UTEjh =D*)SH8T5UU`9l;PRP?KIűP[h4đ!*ƱTj8^$tZK}pRr.ecccjCP2Femmt:; 7bq1JԜ:պ f=u\S90=$ D?V,7dFrjhhhhhhb%z=Y|/cbI.L;.ŪJdyO eϛ޽{ψġnu]elllby׮]뮻cavvȶ"!N[p뭷^Esvb(}sm6?~+++8x Μ9۷>(BVxTk0M%T@rI1Ic$7J ROS)SyHGsixCC3j90 >>lT9Jpu!㵱44>:'I"XʌG:HnY|H`ynzj*N\bq(\CCCCCBO.54^O;U=?1͗n򞆆O>kM^11~7NoI3p, kuXJe Wٹ<[__?=1u]M;ۻ ;wo&a $Hf$EQDжRKkX[[;VfGgvڑZǖ)ZNb+i鍱蚀MH\m!\,s޳~>36{;ys/{~4iiERt<:痿TL7UNC=UjZ\d(ɮQ6v5;`W *k{#YyNsUs8Sy1 5N:fS+F v:iܧUdרw2k4ސX{I[@ZvnHkTNjs{0V +v um/&HX)k]RRi1U_ 59 uIΊhn3@8khδxLUNb+3UK{V`|՘+5uGݢDYωXh\u4F][KTGqcI|h]16c%FBשRp7:k zMmh L(|4ʊh7N?ջ}4FjtywNU2]T8F_';+vv@+cݳSXsHhM0VfkD'yنW-|4 Mv/FJ |Sђw5Pw4P`WVo'H2kYbv0sg]wpx;Qja+]Wm $՘55⍊f{Y@b@@@tTCLXIy/X;f)]1P#0V3tLs+v`,+jdjư7}vtޝg\pA* HSO{c38xE*VSv&)6΁,\؟?_}Y:4w/;wq mzxS*}sŝiu_sFIo7TkW蛹g=yE5kko*.D}w'ɟ&I~ߕ{ vq} D3:6OϙeW5O]uU.~{;4tݝ͛3uk\3֯ȪUy?SuСHj3ϼ,Νkfͯd:'o<Mv۵9q [*[eeIǒ?c}6W27]pSv}>'rվrv8[3'J% zEo;zHjZlhv_w]۲gӦL$I{}}Yqc:=7_\/EcJ3];$]/8ؑkMYٵ+پ=9thzҥe%^b&IW׾\qş70,;JU;K`?e"=?f+8ۖ`{jIju|GtiP,{{+L;mhiI߿?E{}T4OH2bE&&'Iw}z궷zedÏ+%Xŋ릛R7/GFGe̺ux{ېG3<<,^ܗOeɒ9tPϤ(R<{`j \sM]]ۗglcݙy墋ʫS9xLL\{kO:;ST244O=OW]uTCi)s+fwIۓ::1j@sܙ .<6/w^rPK6SJsϭg|-;v|-= ~ERc^ݫu;٪/?O}ۏ|:\2CS<`;;+jxf_`s+fY{Ԙs5Ċ``` 0Kiƀptovm~IENDB`swayimg-3.8/.github/viewer.png000066400000000000000000011354511474536441700164440ustar00rootroot00000000000000PNG  IHDRvX.|tgAMA asRGB, cHRMz&u0`:pQ< IDATxwՕ9n{;B HB``cc{iyg֚3ocm`c3fpcrB!I bչ:GnHVޮu뜽gﳷsp8ph/qWp8p8NAmn+]p88qp8p8>N(ԟp8|e}?yL/uɽ߿'vwS L>j]lyckn't$[vv7~=Y:{hN꟯?%aHsB 5nC̘8Zٖh}bA0r_67GsaWϴ1ot郃a Xbْ3t^z6/tS8҅}EkO8nqp8Sտs'f~=vEVm`]h.nşIvnhhj18h.Xږ__G`|;fp8Q{'Ѽ0Aҹ9\H@x/YQ>ﴌqp8xqqb7rij=KO𛥻 C&\M K7J ~cxO96c 2fYLGxk}nmnA 2i)w?1h ~x>SQ{xgx셷G>̆}NHw#ae31a\ǥR@F1~s/LƟ5SNch}-;xkՋ<0AV1r|Ν>@˞lUEtz^9ؕwLyw8n8c*S_*>W\3}a~rŵrFmu}+yR>o3 >+)a˛Ҳl8j4O4*yFV.Y²M>؅#Y!F>sgeX]uѴm+/m}>csnl:a'Ĕ)}I}:K]fSLHФڑL v>&GIOOϤvҸk/fL>s ێCr8ǘ )6eI>12ƲAt5_6ˤOgis߽s%mݤ'Q͘3g1gܿq<~F6ZˤAOoxqkM [c 2%L*CchmYD.R+i&lԏ;ӧ9# e sټNP}lF}6>ygh:>TCgsȽ w??X8J 9LJu5sg`],.ĭu ^ ;zvl)K;2/_0S1ݭʹ5F09\|e>;~d-}Q͕_NRNW3K1f'C.ͪ%~&f)lgv A-]Ιg͜_ƿ.ꯝuڬ:q.b]wswod!_j|?Ƌ<_AtORdq3ٳڳt50ͳ.#g.'˺-*(sGSEoV&Cf{-|qf;wYyn=z!_xͿ~3m(.+xjI޳1|S6{mtĀ?o2j[Ck4P0|l]O~[ۺ{_SyϦ{Z>VF-u 6]~/^p%7}.i ?G'k៹-}7tw aį_yK3`hoG7~DZп XaUw-كwf3>7#K_G#8 U~_X+I*ssևb̳o7ph}l<'q>\ǹ{1؅Y=1XͽWtn,:毹-v_׭_P?la?]} ҆dfuxp-g \oF3|i\Ţ5ĩb+1p.W=8^vُsq2la = mcj}]Wv~"ʻYJZp&Ͼ#RsqocuqΘ:g)5Q+ >㧌!c$gh|r@ iDtj^\Ci܇j03gD4~Ə8&e׏ 0/iva-5Kag'J,"2tEg}'F9ig Ѹmx:LTէ8\?Bli{qc;526Sx /3MrW:Zᱥ"ERb8HDΤmz[K>nK)=$Z:׎ik ksD5>i.?ES [K̞>Yq w|RHlnPڗXա3}*}ި23/V8n8c*v43}X X{sqm'm9.Q uoc*smÞC̣F6o~]=.q5In'fˇuST@>w{prp|]F-Z?z_.I3Q!v=5ł[yR||. _w{86bU(!iHGSP\ϲm̟?y!31q68n8c窓Dg}y'm>w'[鈴۶vvvG7[؟c YqԸ+5(ulo[?IYG:v">-MVh=E˞t7WnO5?^hYQ;Rjfӆ2caȨAo>_O !뗯syF O/jA9s vC,i|{ ;w&Sǎ)Ù4=lh{Y/o6(mi;ҹ)rNQV.W#|7|+r|ﮤx\󍿜Oi yk aW ;6M-(rØWyd[&>{ܱ=mM/q)<탧s ˦߯d9~q8c檓ILG`a4a,նG\i&P+@qÅ#gbخ_>OC1x.1T6?,"w},Y,]wX,y`׸mwwLԻ݋t'vc̉wnfMɊ-% Rz 7ruCx/%Ocz l9[惍ǍqL:(Yid5r*~:f\Wl5 ";_z-r43ǵoCc:?X8dsşug"}LT KЧhEoP#gr`}3g* -[؟؅جx7Pç2A,`.qńCcGlsu:q>G0 uq%̖̽ 9d_>u|}v^0,[?~>E%뚡1^ks wG\a'p5quPl+-Ϲ+/٥Z!4-O~?|WN`˴F#5F<鐫M*UiӸC-%5Cظb{89 F HGKԱW7ylG4AZVk'>w,k98᩟Iײp\1YT&nvLOpˣ_# s%2_7ގ;š/57_3s/1^澟sų9ol +oOLk/~= v7~DZMG:W$e~0 `fU e'Yv@qpc%nμ*=0f/-]*o~ ^9TZ)Ѳy~xp\[?nc'̇}n:sjNӕj҆6:=G.l+˪OilDs\_?^vU>5""v f:BW\ħ{+6o *ƞ'CU<*.A!:e*G]u@FtVxCRm-=?njgQ'-mt]_3UPQ7`2=j_S:gtZ<u;6D \QG?R;CY)vZ<N@"MUV"j:3MR.냂jMW;d\h~d#AS(yƓp8n81̮zMJQV8jOsk=v?$ڬj:aN1/p̻W4-,ŝ<$ŎmYȭy]tF.~3qy5Un$#]VrXPJUJqhcC&=Cy.Qǩ#@Y^ I,cX۸J.}>7Nh{Z=G,LgK3%px^!Z(J#D"Byh)%ZK|Ck(c QFeJ2qKQF!`0`Lt8d|?8N=v' J(;)c(:G1LU)%B*PC "$B=L :kYD+I*F)iYZ($"ʥ0(˔J!a,( 1㨌(.!qls8GN=v' CX>LOOȠxJA!5B)^EiP(+MV)l1o$2'z\E!D"|_JxZ JI'hJd.:)!eBb'®Jg1WDQ]>$K#w8G,N9>xH{t%G)>B$"N :sB^'dwl =!DEyR ZINJRTqLh"eLw$"gc6Ƙ(ڥB\)Eep8 ;q *@Vt Hi<$RjKLU"P ;zw<>QJi'SH)zO*XB"UDE`IIN2Zt\x=Ѽ$ѻ(,eb<:p8 ;><)}Ht"|/J%_R&)*)^W`S\_N"̒ț^:u5CO`me˶B|HwI f%C}u*]ˠ*fڊD}JB! DqD b0VǺқNG:qE0VD!̪榊06͈C+ 0Aҹ9\HLGtp`oC4B*L#D ʖ=QHU|%QP*y"⒦`2 j">R24z^n99J>΄upN>)SJJtۥfSLHФڑL];FBdž~'ƧgRo;iܵP3`\&Mςo{mf?,Ymo|KH4ӥ8b$' ǟdiHFsW>UMlJwOys{-fGO1JIL `1F`¢"DVA)9#)S,E iپߦHB 6J"/ *i֍96>ygh:`xpYdW)0hd:Rw6SSx{g_|%.=BnZɬk`uweqJ+ 2HBh$uҏS 0L޷]] 5Q?fg; /=K7e2572D{Xa1PkM [s;) 3r3:LKQP3nϘW6G^PE>zn>_ ?K-/xs78EEv8=K~ϫ='^><2>rr~v:u㥝E,#.z f7U"ňnђ\Gk՜qWrWᒶG/k4ޢ%͕_^Ňq*㚉ܻiYN/̂c;Xbu|ݯMl =z!_xͿ~3m(.+xj薱NSGA GJdPLT*]^LUl9 Cavv T(\J0R UԦ dDlPF$&m(KxJ`(wJl!3f_D/[I.8NR.rRtT2/OVJ-tMO%LI.[EuqQwBrll2%D"BxZy4U5B];Q*`l cU gHpRs/V(JDLE9Ttc8؎xa5L:x(1)f^X82Ә(v0x9L||Yӆl0a Y"@*PG$b'%R HSJBcAngsMńةc3} CֽM[ B͠ ;Ć/ԍGʓJ.KϜ̀KObkyE6p0:_hkW".UrUŘg`EDZ{@nu|;6&0=,l9fþ[ʣww-KjjN{rO߉ jcceg1 ^ſO'vmч^M1n\;(9#,6W*jyZMwj,'eS?~̝I#&j;=UIM*Pkx8 A+|_V)_{ x( J .XIDhq[P CR9X*S.GJBDR,)#Jd iOY A@Xc6a#NJWAȤ=2iL#Z(JKWHG*m%Z+wXE*@O R5~5Jn9]uڋkR#9)~GP 匙yռTFB`(n:;VȲ͔ %:!^d鶐lmy?&z l`PChPO]]-5kjsd3dӤSd3r>A#HgtrHdV@PM<E6* UY2_YJYvQZ<*_`tTdTd |Tj`}Vj4W}: ~s}>P4,Ed@Ey,uq;V??;ײ{pUÈ"EUJ뷲:XdRS}ř~b;_cZ42Yts]48;kQ[G쟎6⭥~7''N{?:DV"t]R'`W"t)Guu\!vqHT7硵DDy80/X@*I*@ ,qZ$%EȗH%0I$ت˭8NE Qس(L"z*b&Ȼi]@ؤUBA'J%NW+4GP] :-H C l6VUFII,eO(@bHEv1pG׽̞5KiPbج"J nt59JeDlrZi?_Ku LaV !${ Sh/%ZheGAdaJJ[,Bئ@AHDY4 YD B2RPTJQ*7P_ۍGSuħtP#.:D]l'mgދZؼ;)+*@>'D~89Q<*^ïtOhRf9? hj: qW7RHأoCljR* "@t%2ic=,=>"P[[C}]55y,\<5U>iAi] 1tuP$BRy:I,2i".E"b ľrB`Eɘ86c*.J T`J+DEߌ uwŦ=~IAaX{(L PLU?(#( ^1XbE$))B`m)gWj¨7ùc5L;kw'[鈴۶vvv^v'j2OseUT>3=G=`˴H;ΘM,_0M1-]K?AO%GELe]R2>.4A.O]m 5TWg"ːͦIR"6!m%=&EK%-HeҢ'&-]z[RRUR,!*OT"d""ljPM{cL%JT&r$/);&HƋ}^7\RAMʂҪO/)Yn+S%XEl%JkLStUHH`=QJEgLJ5>ndOLi y*v=qgXjxuL4`[&_S ϑ,VjsIGJ R#*I$Bl6M&Ud~JIIZ{ٺt:~u@.i3z65c Pd$Q"J!R!:PxyJe5!&5Tarx̦ eÐQH?ObhZz'>ΝԱ#8mp&MO? op8aw'\gGoW-vr|ﮤꔺ3mWCNH!e%ST^A#,y6Ouu594O*}\#%AD%)+Ja1(8, ZyxJ:Qf1lQT\6IsފҬb"L\LC%m UoOt.9EyIlK @Sh-4x(KR@'NVuQ|˔IJheA& ,I@c4i#OE!R&{ރ Q(+pSgC*6Oa &)34jV(/c"3-Z):d 逴H)";)$NJ51@lt#QDT6Mn0F[ؒDFm7E'[u-+&iCJSS@GtD$MG*EDX"*ŕB+QM]ɶ|~M|rcL%} ܲT@MF {gg]?vARmxl>/Lnΐ}l!r9ͬ|0UGr;]b I1x.1T6?,\D4Rf2vR"V0 Dꪨ>"h":^"ꄨ0(D (+R.Dl$53,1J4$2p$)BR*ı! b)ahK~u=U0MlL.b&}a)$Ox(Bʡ:QީP:q|42iӢ8de4-rQlk}2NWʤW;CҤF6~ͭX6)܂QX|7#VMk@xE۶3oLމk|]$KULA.@h 2|T'i2"[9w#T֕w陣`%cb{QEPB .Ϣ3d_4i%2+@9([dQ2elNuqr}:I~FVEƇq_ ;ңJLͼ@e⤡UvcVj5KwչLͦK3fsy`Ky5d>2+oCgJSO Y11Uh6OMu O: |M&#I*ިIJ2\Rh!MU(dJŚ2D e_Q}"K'Q8 ˆrVtR2 r3q"8N\d{AD|J)A@. CCg1q+Ih~ R (E9c-rr^3IuMJjL |P@NWEQjCRO#$0KTVX8nɣEzL6MU& | M;i5a5#Ddj2;yXUA<>]YkO-El| W|Z2knzToEqgLW_S/_AϾ]*C\`]+i]:dm~6]ʍo_ZdACPk7:Q#/Y$J&p%iU(B}QK^T*e JdR__E}]45)LFIK@%T%AJ"e"쬍1VP.UȑT$ZCJb$=QctM!-4Ou>:Cd<% 3!RDEd7/\OUq! Nt\}-rEiо$hbC7g/"IĠ*=L"B(ڤ2O˘0+Q' ĔB,I8A{T dVLPaQS]OUСzJ9UYOy)vØH{eZvUZ^yO1^ݝo)/ 4 >j(-!le;ggs( Y̲QjwLOT1,Uljs;ˀ+pesmxLUUAQuq}vs~h(& &9f5hZQ`:en$<)BShkœ|Oj@TXDb{/ܧ9|iGa , [['mZIٺ](bwG\Ez$GeɢcS7rLSb j\~sIl5/?鉕~ IDAT3ʫ>xj8"}#.ǝ<=}ϥ CjʵM6tk􃇩X+={%kNWkSj)_x];FK-ӭ-_z&,eӗzB8n2e}^;gmEY&TՄSc+vڈCYVt] xO%w|AFrTȂ#S,LkրsR~^T6",eo,z`sȁc\8ЖKghibޱ0_&<lc C]KH B;=ELAMqQ!0v㲚̭ҥB:\B l(  뚢,(GeYJvShBPgQWRʩhqbl̝ ߦR$=>yB tŎ&4,Z<]L95bK*s8kԕ)-NQA<4h|HJoMO&Hl#~ m/,!t2 Lgid:ML6ZXd$..Ce/LHPL>,θ/!m‹7й7OCJ "}CSrA>{liN[c u] ;*R2AYh-& f5iHcg/]}KWuE*F :U9礤ܹZz2[&G]+0hchd(Ksnz@մf]u󹀹]l Evv:|X,!NhI)uZġwkmiێ٬;#~ `0ޠmM#`x>OdRd6h@]$(6j1U,bLX[VX|0tY}V(2K嗪e 庤_#[La4卡Db@tR&O^ )tA $93b<{anEj>-tNFyAaFur>.H,asBja7p`5Yլ;#m&;KG*1,njFk e5 4.PzYaG),]M#s`Jd>^}x{ iXrK2c^eM =hkbfQIY:\Jhmry 5>YsъEISLY!>=KgRX:X>b2QM ,vd)F[IPҕmWNTpCLZ⃧ mh|G;|c4N;umܦ>#M_ݙ,zzz|d4 %!5;3̡n12Ry?EuA\o`,Aد36<(e.Y7xdaduY1Ԇbе ]+T }f7&\vKf -MhhY7go4(ȍ#jWŊTT0eUB`IK๓ç$, 8RҔҔ25M66ǮlK ]+ Fh;ao`N{s 63(ElAr`cui;O[-^A1Emh璸լfVon:(U5  H(3֩RL(\Y:uSXk߁Mك& } j.Ģ18燠{,]b1-ϩ'F]':{bE -pܑ @8T"H9ODefr}I@rgEd0u_dhQ}QCM1'cRXOR,r̴4aP@7t{%&A4ef(G@L2M#DjV8< +DzzOr T/t ]h\mѕFՙ3i)$ *\xbE3>H\~n3 bhY7cL1j;/z@'1bZ^ ֐ʲO9X*[aڙybc$da Սj-J)::W$K 3hV<mzf=g>]I3W$SƠBX)f5guk(=B!=urB+ڑtEїB ŋu}ut//)5E_ϲ2> Ύ.[=r=`}IT:aL" Ȝ@"#Zg 3am HSm0,iϷ"I!^KjIfږ˚y~F֐V|rypZ|y\K(-d˾iZ( 2s]} cB$ل ^U$FrqfT Ĵ#_C`uQuŒ cJHEH.K$= M$c/@*!%D; I  -Mh6Sv=v]b:}Ė@X$=̥Rp>UQQڒV Ha \ۇXc,Ф(=kjq ReTւHHU((Q`r8e*3Lgn:\j"=7xW)\qU\gOrvX)Ua'qGy^.)J #^C\t5Фj@vΎqP SQWH-**@HT(P9$b<4\0vNޞ9 [KUfyFBLeʠ]/sE) /E3ZU2̤OT* QJ  UJiάS5ĤS@e])BkSw&,Rv޿_1(ѲevDc<ε) OJSfb.VHjV5Z1|>;{x饗^Jl\J/"Rʈ*NAm1b>o a!x(!)mlYikfg_h)]) E7q@.!ϧ)%A]dx>t1aٜvTr#WSg(($h+:%9S 8ќ'/%u ITAi-Z˾io&;DT޵ mLf5PQ*7/u4]l~'_ȣ7̾ͤk_9b7!~s{ch=(.<J!c2:;:)\aP1rU@ϊu^պ.7-~H쥄]h2kY,|> ׬}(;)DRޱ9 ER"J9SM:u48P R!$b#`/uh} .[{)i qxkRneq(,cFxv l_ +J>4f1m'To1\j0z15u!bTV6yӞ |߃>/ܗvك; щ"lQ+DR' J$HAL4"}G7|bGDftEʼnVh1kX%EBSzO])"|<* fj%)))]SZk:I@`!ʠ.iIEQKߞg,ӹ,N՛oh+jV-|տϽq;Ș y+_铼_o?^LrS+P ק^֙Iڥ*Q&v+QzTT1clL*l/pHt]\a1{E/,k_)0}Għ3xk3!ḁ Fb I0EO5(5T4B@cȮb`_a< ,I=JgHEK';93 S@]~tʕtZg^^bӴ%WW?,Z/z cMGJ!D:%̭좉Ĵ+jFY$Jm@8E4֩2$c\g8e:ohnsi7(BD39bKU_zA2c$DV$`j:iU%KQ1Ap^8}M۵t#,- q8aS+9>!|JZ039*C.>Gg'eyJ@`)aBeF;TAhIjnoVoa`ݓ'zy>}ݭDz0e]Ϋy֎mbiUUT+Rimƨ :1Z1=KOJ*   s罢"mN[ڶ? 'R`#iHS>mxBPX[u`cKBh0抃>\gdc\ {8,>sj!&":0FT$TI. L\9f)weR]\IyX0/p뜣 }q:*L] 9SCI t,4fb;j3!#ݲqrddkjWSR`hMR{@"υD#@\TԌԈ6,܂KKZOi^: fLHZIj Fغ(VYT4.UDKR k:(AUv`C&dcjJwYͷ3\Yu&K>vr&gW],^ԇa9\؏=q_JG)yb|=pYs7Ļ?t;g#9Ae]˳`;?g>>^ֿ9ss<.;r5[oCPZ@zLh 5FM!m:Lzٞ( 5"$Z,1Τ@] R\@j+T5Ί(*|(vx2*h<(ɡL}ZvxUw1"I- 1/,[1*WӐ"khZ3 ΁u&tTLd2 闽P%nd2*4*;{ gyme0"Yڇ1HB b1Ɠy@@w;IH->W[c<媋9o'9{YoM.{\wwq %S_3wEȒa}77|&<{_“x[/z {ԈyivyS.}M6FT!:]sQ]mP׉Ns}wsO[OT1n Us έ֙X; HHN IDAT6R–OkNMQ4a#Ruˉz pI$edFTm%dԍ\50rZkJ]! Xdbu{CIi2k7Ti)`Fi6XI*U$(CR}ٺW ux4MV*լ ^W?R{M} q啗 SJ9p3-w9׎S4J~GPؽ',\~W\n~?Ӭ\|yxg~zS' νmoT=><_^wAZF)+^ ?Rw \q5O?|͝q[p>p9|%篽7~h {To qs8recwN"xz[. 4޸~Y^G"f2.;eF@]1:&|)(Q2$48[3ZQ)ܷGݳβ"!tTT"@RW:l`wuw+k:y=Q(m1(9%,5 BzҘbGWPVkPNrÇװI7UYVՌʓ{bvce}AbwezD"`R+̩uq ܺ_7l\D?nwq?O`ƏGa߆epImBȋxc^|/7S<۟.i8=&yS8ćY(?+ቇ[v</oyvP?ogy^l~k/_l{P|O4/}x޻o/ho$ ȝ,U17'^v>+II:`o4$|7t{%YKaDf9ũb زf}R@m>W dGzdK: :6ǔ4NYxAnZu⻰"O 쐭\ ,ffؑ'DM~CM3x-c[sp]3?q7 e ֌72vv JzN-YKzb+%jTy=ƈYhvO5m i)9-ƛ9:$} !—jnÕGV?OJ. ndfC t>WHj vISCc"q\Ä zP~Ie.暅Ѕ(MhEA_s1*n٥p [,+rG]a K[+'t= 3Hbso@]??N*jb 4#9Y1wY7} (u-֛x}缹_ q_t @s:6|3K>w7W=\6mv-w-SNp>G\zu N)n}x߽rN.{#8thsKJt@9U8r '?Ag,P*]IԈ^J@$.B6@O/]$!!&ՔUIY$⢣K mMί49_Sv/kNP0*kBO"l/:vi280`'nkګw8O^{.{x4}.kh?~#kR[q}}W?{'{I'y?S z_ثEnb?r?5W^J|Q.X>şke:qFoeHO6zP&#Qh|K.\#hc[l)رI0EX|.>=%)Pc,+qS"8P@yIzFj|Nڈ-&Nɻ"Wwq́J1 iAH$(4,V^^ t>,]Nl o9n)TeK<(*j[K]W7.I1 ]1 -mhCG:ͣӷsL>#c%@F'}IGp*e5H\լf2xͫ>vf/q3C|tZ*sYcο|ƟweOi ??sGiuM~? 'b׉w}gX|?_5/~Gxc7_ͻ=&~T@h՜LrhԢ͒jrI]ST]ZQJjnQQ4 s[8<.?S_];K\\]@`}jz]$4%6DdַS6vu6p`11RirH죑b“zO]dF᳟ 194*LEJŞ/svYLpVô\p&=l*GR7hVJt|i@ۆ f%RvN 1lZC][( kɾH\Lrl\lI^2ɀڎXk(5͜CiSG\q$om|.EcNd*F1%W=޹}>3m|>Z+bR3+PCmGII$`$@z|my܁ 09>:p w|*~cǎɭ{5ȍٿ"K_Wt:vﳇo` G/mvҤ QN5&@.R:qu|A1$UFPdc*r$SQw(->.)R>sGaΛD 67Yْ k'־TQVe,bZ!Ǖ^@E+"J19RG@B[::^>(jHJLqtlVSWQw5e#}vb>BapJ]iJZwmi\,;O쏏C%RI,!N]}H)!)T褰ɠHdPI6Ȝ:&f5+`7i}@G#@WnN:@:@|yC̿BX) y Jcli2cq&ѩDR e(L]f3+T 2e}eJ;"63v(-q nKmr9$ZMEO+)JITT,Ai٠~i_z'}Ip1(Q).eif: 1NWu:>}͜|ӸݧH\9Eӟv-W=" YJ|+Z9gooyē.PKxӰ$И@,Eؘ(1r\M1*}ҒhLAi3`-F[6u2 B [6T80d^gXt%pW7RL°E?Th --:v]+@ IL!G%)ͬ AU Du.K#%3_"v)G4Ɩ{g򙭣uPe E}ݧRCണ0RTi*tV+cVܩxg_};aY}?7M^,6Ύк&֗h%u0B)bXvR݌SwoŽ7^XJv,̮ iK `k jQX+ɏjT_ƝSԨԥ Q~q[06>z~ @&j/햐D3oPR8O}S꥓}=qscN%KRi`{Y"!ɑdȄk*.}U*ų!Wt/wtIf1ht u)y#ƖIO^{%^vλI|n澳;?0o0%K/7u/Q=}tf׿=O;翂W+-}"AfAcղj&LHW:9nyKxs@5?7O_PWP{k&X;AZ]o𡔢qd\ѐR650@0طX©gRhM}RI D/բYEQXa wF/@py6(N}DM4K8F[))$bY:BR$tA):9ǎE@Xؙ5@'(Y X *(<3`U"4a]B U)0?k ]$/˼$rqG@JcxRs=1)qF|wݷssw_ -Xgq+}w ZjU=!1[,WGiZX^1-n38}|/;_嵟|wc?P%( U;ړJ"` ;[4ӆ@UIUXR4X]RƈҒ.i+(U~L_pٸyr۶vDTN@ݤ0-L -)ʖ$lSG@'R J_%6@JA@JLJBƞ K "Qtu揹,ӎzSҧ`d20v 5V 5릵%E|fiI'ƿ$],ZAUJΧ (CW$JHt=j^C$ ybS؃8^ˆea"j!N퍟d)sgd>db,ei1P6M"dyvRf)ϩ4ٮ(R(T䡋K)QA#tl1Jz )Ez {{0?[ I16":0AGfJq߰{/孼~s Mꈿ'>Gk1_|^?(Ox]_ͩw<#X_3Wr>H2V($(< .'L!yb(&YJ= 'H9n(AHѬ@"N92} =S3q5U1.j&i5vu)=v[7DaBjNzI1rcLin^zꢊmp v4,ک" P0m5V1c@" %`2WkbJTn͏KQ> =vZ*zsˢQ$2*U:]’o[sx e8wg9Wm~Ts[o.9n= u/p.-hkG],0x̬p7L Bj g,j(J9y1=Kخ)OtȬIa]΄%,rjLJ:@ЕdK.ؾ[o*$t IDATՅyP4;9PC)L~*=m(4l]pڛV U"DtQ(D~7N)k}M*t0:CUYPU hF|t3hv@֖* zH:k;\hH#ƎM =[[xn_B3xS>g=%o>p7;L*vog>󸵻|'ޝooz%Oz{Ѓb'} ;ϲo_Z7{|jn{_73K[>N>krף sw˿v;G||~]̬H1- "I*O\I=UW9{-&e!H_ i?mDwKC[yzkhʆ6)겦.*.ITfK9pED-+挄 w E"=I[`.C",|ӆ5:b . ݕ*thC~޺tM 5=[߲K Џ)}D6{N)E `&K""9y)eD m~f5:$JI sTHZH7n36A韇oK0 Rw~=5oݓ~، ˠn 6X#NEP/5˾canMIc$@ٝ0a-ۧ9>C9$9Ēӧw:uFg{7;qcL\ܝ+ Z!;pp OڢN7b:uT9_2ޝS!%K,bCdwdR%0ajH;_HBxʣ)$iΜ)ݵL&riyO>Ҷcd7\__vp}=?g|7|*=:s~5(N.rW޵?{xCo~%Xғ5-8K(IAQ!>(S MLr^>co*ƥ_bᶭ0-T%lEUV4N:ꢦvA>싋R7%6{; @Nk1EJcPJ%mhY 9}SJH:B4$%r{S* ]ХNBFW%Y?J}/$0> [ ൡZr 4`1D"۱aqXJz< *C8 6'xh3ՄH"l(،͸CKH Rn[Gf811[;g?«=Ӝ:]˜OWݴs|ڀƺgX;!%uC۟{';>nla P{H |R.O"k*kKUYMLN4aμSt]bFem=yKؓR'-wyR$"c4{z)6`?9TL%UeqW 4!H=Cb c kY#!HEI_>x$07J%rtc{`ehJ}Ju2XDfǞCŋ?X ̹ڱu m{ȲH]`mzv\ܿĬOpj9,µ\~83Ԟニ\)NT Hp,*Kv؞N&KYKarpgI|\)Hwz(r|Dz_2oKK<0{r眽z*gg9}ӓ;6L 6$F [z=]-G#c. //r~ 3"!^$)˰P嗴%DOJTUԮiYJOE\>ܭ8Sturrz,:mKZ\/rq]?Mx貢HtR,* SPJ>GP> y ]RPDZ@ qD?Zf/-6,7h—@]AYlS[="tm46_!*8uQVPU⭫0Ri` M?j0E9Rv=>HrR!J ۔S!%{+Iq\s"XH 4;;;;Mrt$pr@YҤ)I_mSgK ;&GQh 'I]P7Y:+/FxU+mR&k=Z$1L\O-1tpM?HtG-݂HWУY?c/s"mTIaa6^U>xI]~d_SROcQy?t)-ߐnL7j=;>ͣGċ?n+b8`VI0zD2s5Pӷ;$6SC5tNHr?#@S(S(6;rKPaH\%o}^@O#} "B{C\6i7rlseׇ.}QjvJ,YtN=_˟+0jdݤn}-Zٛ*N.u9Sdb+C]k3ɉ>XDP=E1;):! $jCJf1; 7!ƎΉq3yڌˮe[̍.,2 sgU ޹t.JH;:ダw,cL"S h$'"t9+ vC ̚oXcJ-’<^҄>@r ) ܴZY7T%Ci :DbGTQwPsK&nBa 4KnƑ?b>'iq1_֑RϬ~yMKq"WB&\!ʥ`U"(K=VXm&A|36qzՖ)ݸq|qvm)Nqn 4h]3+Qɠ&(Je7~QA)%vV8#MFn@kIX4Fa*KFpgL̏dׇH`]zu)9޷a[\DxR9J33@#ݒD,FN 6x"}{\;Ux%Pp >;57s:e-NRϣ0v2QU"<-QFGǖ_FXJ5J9HzLꉦg|?A*T%Ti?ڄ.t)XL.Y9XjU#} >ŎA/3#BLvknZ#cR"!|$Pwb)n:b'ʰ`}2}'cZf()ѯMsUs[V<?sy{k:q2w \< # 90Mܧ~F {G@^4(Uw$h!8E S$yDf *N|?G7)#`iujPL82c'?@g<7xk9+<ٗpIM̲L) *z9!mN3S}㇡*q6Nv1M˷b͸ly , GYq.ҠaXcZ {fSُ~7E^B}]hDe ԭL#^إ6ΒLB$)L On:-iS'@k*Og-u+jc ۴8ڬ-%dOvKck1ZөcgGSR"&EEst: \ ǩ=q' :597 ePaL-%z OT6 Q*RdP}w("쪃-ƈA>NFr*e􄐽u9]M1va/)EYQ>jƲR8s>qcc<,b.s@]: ~ -_RYN,n(mO)a% y7[OxJ뎅ZtUfI  ZA^c;xHգŭe*h:ߍT29%w\N`\Ty}Uido@i @B\4*Jo=nXflmfIس Ka",F <,*=ԭelBYWZrgM :j=赧=8,x Y$J@l1@+ZkxAam@iF!z)*SKkKZC4}W*7R#bb]q|4|v7c36n3>֎5nR5F(5aIH ceYPW4eSg I9 sdR^L#[]0J\*?+⃤`>е^ؠ^U1+fP%D'ۭzBqr)fx{ 1w,-0ue!闃/c+E t&)}$HVtl<4iPU4k_I@٪7Mbt̛@]kSVls= CXբRSK-ֶXw ԕ5d RT@̟%R5{FsM$RfF.,ɘnq7Rw ؐ,e1j))ͽycBG,ŕCQR:yn eH-_Rq1=,ؒBǒV +L-6ZOK̠O=OiQ*<3N 1H(1$F%e0+ 3Tq 3NUAKR0uD!rʟ،OSƠBd}\71n;wmIʥnI% Rq7Ra Rʴ]'.̀ndFå8OVj:N`1,Dy|y|[$ቱ'>^P*@* iBH# W(J\ VBZCUTZ =urŘ=t:kuþXUAp4j̉@Q7V@\e땧Z5&^>{CE*X=ۖs5}g8%J(zdZHbIl9%s3>A]A]F?0x䩈Rwtb{@zvmI&_a O̅}쥘ַ{u~vB~>P/~a{K_ɾut}/b6JZ_Js=G!맑+K//ud`5KP]2ʠgRK4I'Y2N; Ofs' Bt0bib=tсJhH:ZփS*݁?8Ot:gX;EcDny2r j*?u-L]QHxQX%zb Z>9=zܵNLJ̉! !xԩ"ɘ'R \P}?3@} 侩 63­$ƈTBNq AͥW/d@5`)߇PoMqa{$jm)^]6 v@)Ab:5%X)RnUYdˁ]Jx(DYF*Rg\fXU&|UJg֮C%Y*p|?PdWBRAr**4<$>z5_"sxDjЛnٷ,Rz2&nV"C=m/̚~`(!_;]/xoYt ~1_OzB3f\XstuP7,Euw8 vnڥ9wgP:D1y )R肒r,O!t #^*<ﬠȶ)҆,:p@L^yEq!KpA Yfdd[H⧓kML  ;`{ocqcޢT UUG4.St IDATw5!?c‹>͝/J^x9xvӧ_r%oU-׻?݀?(܄ƚuP'!)Q7t|y2Lg픂JGYZ).,Α'COY1uC9z;ɘQC8c"dd_KC@:zHrL PYޏ;0u$ʲ4U(R$wVICIP霋q=DPQ8l0S'7O[L:UݣrԱ`dyJ:4C8x'ufge}{W(6FQ|hu֎_szxR%1V(աzV%Tnɠn(9B*KQ`JM*"z‰^[ӇVDf  B+ -9ꎲkܩ,;Tʉݒ X[KxRYr sY+.'%Bwr'u%tG]URgΰ;8$hT$^ܴcuAL-E*l%y ə)& 3+PheLs:> 0H3  QBPDH0Ppj5HڧFU8έg}0x-UWr)3qcݒʿտ]\QI~5w~Gtfn)aӹaz3]jk&8t3x&"~O,%r`$1DU 1.q–5,r0I# Sl/Fas`̐ir= yr9:J( ֵSy}cql͞Ajv!䠅 ROj~? g4G-:,3`rNN<}qZy暀(A:j $jʼo]gm(}tPwbf/$U`LIJ ѴTB}&*jHeue+ 2uE8TW[) 4ַbN "bGe*I$دꃡ䱍->3>PL܄Ut&_vHwc@Aoivu1nɬq3zqw" 5.$NC]Ov@-FP}0t9(u<,NH-נ@Xx>Ufވy(\iPThe$!(0b@EER~쫋Q;Vуj *U"4JaJ@eByI A:0vfͧ泽; _I|ʛ>N8:G| K׾?g.Uy Wla$L@]&c)-rN1 @{kq-4J&Nn7F֪QW", 2tm{s5e8>жMF@WWR&̙ljб*arLk3[JE^ +2UQI׊>;(^$^Z3;b1.PԵdEm*kc t(UR1-1Ր:JgIgSCAQEe(ꂲ) W;j42Rm_ŜxY,%w8% .3?aiCˬ7_)% Z.1Wk !ʰ&@bҷ,}KGC1'!7 -ˮ0΄қSt1ŶvxrYڌ͸N`g˞z9K?F~m7~s1)z~}KW"LpyOjS<9oݺhuU7?B*'>yq6vq{[yumQ-\1p)SAR PSHU%q?F9RP%.uN4V %C_c턑xe)7t[3Tf2c~U+Zũe708R`9o9:<9F;abvдpE_AA,1uX7JDBY;J7 ȡ@$aLc]VCUv~q(P$3Z3+0~̨1|Uyƚ!(ķ7]Wrgc -L:ZbL5lg +AWUN W9iI]WMuV]+L5)CIrYwB.^;-IyZY u-/Eغ– ) jAKTXc1+bʵpGs%]jTG!c T1?MÙ;L+Km0b1IG6r`kg$Q'Ni"Eչ VS=)%n br]uj+uE}JkE۪^E5טN~vckono7c3)t|].8nů큇~y3?OT S*oFs4'O{%#_m=-;; O4{ZM{~>1b+y tv 32nM8Q.EOO~.O[y[OsJv|y5F>qپ}ط͜}>/ /I88,Qt}ćq♳ܲx7dvQl[}˳ywo{چ+ RN-3. Xt#{` :a8 'N::Rs5Ea>#KƢʹ\j-R*58m8 )hZKFgr:tզ(s;cIfvŖB,I:cQ"1t13fEL3ELK3GɸBۃ\~@fvi%,vs/y UIRU&A9̐LԪ'VҕHEs IB\^҈S̒ o%*-.1 WwZP5-hk6Es [we=\G$f&qQEZ+cWMrz:"aw66(,hʅm\҆v4 A9u )J RJ+t 1BKrCPRT,-flƵֶ? k/㍯///3uJ};fx=K{x?w1ztmkzGxᅦC/s>guυ<7<[xN>x> Gٶ< 9X8?~7x7u=piq/ob~}ԺNQQvil5E1Źm 7Ź-]HS 1Nӹn-%MC5W2̮ #@ǜ(,sS{OWU&(į\:MQRO83nJ|r58ҌRA699M$5$hQ 3 ſ}pۖ, D&+S77\GnpmU]:FYlkki0ƚڢ(vmRmiP)lNTu7q֦=h*T$eϐl)`.Pp`mOǵ.3M&%go9{Π`ob2@'e`BH UHm$%Ck^@M8E$DIb7%4Dd4+n`zј> d$|G K2Ks̰O*s63j.ĕgEdˌZt?ZCh3E-4? P7)jt!(0՘*>2)TA;"q]!r;))/d4W86;nҔ \Y"lCB/Jh>a!/W PFj;rs( "cg6kLevy\c<3rs=>tt0؍3Dv=Ar n<7FѦ)#basL)62p{sc} 14JmSןzwpDii)>}H8먌Š`B'"pЩ,R !f.,qsmmcT4afg4af,Y5cVϨr")ŎW$SqMH2V[a<-pBG %HeИZ$̠I^*sNoNց-B{:G%w#'Fy_O}%U}amssꄩ}* ^rIIO{2rIz*2Trym)"L%"pLm@-x+fSZ쌳G⦟nlJ:J`dLjN1]~Z 5ɴ(ׄ!Vs^s̝VbSpј- 7*j[c!:M67tTvc7L;5_Pz#+=uF3Z[¤geTw璂ٶv*#7FB7(OVgg*^>w::Siq&?HYCFKdKb\JG h-DB$RQ5Fu 9v}aord Ydu]`VuF`}u)@Nu_6Jĥ$+)/S:RLi.|D1=L1IUM3_BJU8ؽG5s\|^P7Fcn580`x`[1 [7uY974NSVXK9CMe桸sv"LbBe"SGω<׸^N7y Uր㹀;E &9qO4V[j[Qk.> Vu ^l즤%6Զf^b9O&^y~3Fkq$ֵUKcںYG"Q X2`|M9Sd"4w$&9Gpt*1Ackk-ap6ȵ+,twB؍x9)V~fo m~ӟ1_|#$5{相N?=Xt>1oA~z6핷~;o :.Zu+_ė?>Zs&xWP:_V^cSm3g6c>bO]υTFϰw)MBTRT}2DV&/|h{;,q.򢃃Wbƕ+-\2Ac&P79'lX>V.x?2Mٙt` :Ͱ  z^Xf$lJ`LHbIӌJ .̺bV;By-u(c+?7X.V'ܙ{ cҳ^U"pzpPUK7߹|2ggR0Fkx#DžCκ;SUlʤuy\xMjRnfx& cjt.%ܗݸuCꪙ/W/u#=`2fR{&UI KA<B`w|~Ľ/ *Kc*]-f 0Q'`ܑV[R㧓SC|@$S!.8[րуPnW2?3}?}x׿U(*-5UsX9Qi$S#퓧=>8",:ieH7a霳!65ihv RAAjLa~Ns!2A9:nB+~,)d6v`0 c5B190aO7ˁnN /?`  ~5?s'3kn_wg~Sۓ5OM[]>_NO2PyG#w?z} S{%sk4U_lAjrSQ? >th}S vE2=IFɥ<)9z )5dBTQF@r%B)!D|dNЫW+88{ i()֌ntSc|kf<k|~06Fe- 2lI#h0icn4:GRJF#oZcE{3bMM:La(YsI369 ӧTΈ4s$!h\rnH8C6%%t\JU騰jC8Cx X%fg;`v?a/}&q4K#?s𖳯_om72IKM>[?Os* 2|Ϸ/Ag?4{Evūț_] kfo>yh>{G><%qf-Prs)!9\Ս2YHcb(Mk˅{oޞc>7,2 疺Pk*옄 H.γZ D8[&LAigk~=i*>bJiy T(2(# ƠC-t+BM[;\Q:ҭVrY%O(%}\ca49UG/}v''|`>T@]Id#Ù:sbz]XX:ٙYsv&Uk_pJt@ƘM=)SrsƇmjiкAQH2qUCvKm󊋗/T4XFR̢+i5h >30'Qz}־c58w-37[*%5vJe )L1J)Vam9TijF@x9.i,wu23W<•E+};帿ȕFCOy6x!L$TPX-zHhB XcP]F5nB/RR@XS*?؞$h'pWMnS?Go(;:JgαH3U6Vc&V Dziz)s4hAx;Ansz_y7~t`Š'7cq 7wZd^_ ?005~#s?sO. `W7~_lGy-{{.4c>Fclk2ff!͇NN{.`0|~$Wy>6[^޺Kb! ZIo$D{>LK)_q,ptkn9> , moRR.5"2T,J* ozVOLbJˤ]T)ASx2ĚYk|GZ\qJ-.\PW3dԈ1LGzւsrYOV x\3 y1 kÚ_s>?x8y81s3լH 'ԕFJZʓ?@$k-fH>ϩ0pM|VڶnY[Ө>plMU;T³YmZ_ac^cC]7UCjWQ9,RVDLzB y (_)'Vbs>e2z쫸:ʕ̮peqKU3CX5+x}QwIwaӫMH;vTڢȂqhU{"/:Н;𧁰 'IDŽaϾX޼s\^h[8ֽ46jY5*Pf!CL:z0Å1/h9m;fTUs-0g-Z%pF}$B˿yw-i$aWJE񞒬)_$1ӝysQa7<1泚0@UCmVU H[0>*Zˡ0;F\y϶gG}YP/%U4i,Ι*9/N]72vUJ;S*?9vh]aL%P81 kбc^ rh5p\p[YlmJO TY%st$i!G<B 0L2et8ԟ)s;cg6/M+I~*hH*nB=GӎKEֱ f`#x.޴pUipOgYٗ{j;N3!o EQxSœhF+@+:%JGl6w8O7=C0> ;oSLy3p- u/ař_N:t*,QM:$L%x0FK%H.He7vo^-g0M|~퉴K d~_}~oޓݲ˼/J5^-3fZꪕZ:qFFe)|^TʮE${[9a` I(L!^^+%/rIw<}mÞ9&FI7m31HkI)ך __ˎOl|سM[w"_j!pFZ6z-M&9g^ucj9>89,zRyJ4nwR 6+fN"RJ KGciѺ"EuVqEY#I#kl\WJ&P@c,Is͊!H@zVaͱ?`x13.4̫mn6;`G@ JohlHr"@6KJYg^h|k3{(`Ϩ, ORJ0Id60'<`*BGq Ws:W + +຋C")0e ؈39\=Ê>dI6/^+- Fӕ.ܼ8 q7rIL`X c7v?a5gmOjK~)?rw|w[ {kɯ}]Zػ@ݴT8kk0%IPG,~0@gVT~ް/ z99PwvdzPg, 3wnyη7 Zm 뉽SBlL.^;;%v0kQNa+S @WlOLq[$ŗR N ! OrWxpgtng\l/ԺqL)ݞRױ-Y ,'I,Jɓux㖏r.> [)"K=7|˂M #m?As7֟p+n_2M$OS밦 CG:zSJٟoCョ=až8N9 Oƴe#;ڤK?Ҩ!D0T#Գn݃}'~?ۭ?χ~Oj\P/w qtmҵd*hPaTj*mp:mnK8;6 S/Sm;؀T:/yV5e@I% = C(jt%q( Rr-*"FzCUbks N1pOXȺp~?>džMDS CzsK:FOj ?^Gz^}eYC:CMF3vչ}[У?iJ%zrhU 3ngpFJdb:ػ04rr:&\>/+E:;K9$BߖO^R̹F\n.1ĺj K,v7+IJEU(#a*aBc9*x8on^@4% $sZ6JgM;)"\&n"90waq3זMȖc…p",SrX7Ծr.H""uѸ#Z}ճk__q3A.mU w)RLy"5l3YgNl*C09hWۍm?#/ӝ58nal.Ȅ:mۗ|;発`W.c,WRQU cRak!Hsz62I]T3$=i145JiG@ԕU؏'?Ak#L?._:x0 wɉg6`4̴0 s H1Y[M5tO(1vvvrIcykrvڑsCZ.I_QU]"r3f#Lpyp'<1rndq*2G0* 0TIMXkWSuۋ3Ri7j$T~I@DV ̓"̥l?u[@]Rnyi A{K1H҆lHğgP$&V Wr|X{1Mw uWs)0Ua+[e^Ykc}}D?'VU?“,rmņ\/t&m]"*Ӵ{͎ۍmx֚]]^_ cg>$:/:}+%nL..2: Df |rJ2aC_/=i2u4T,5UHI^'aLNn;$SB^QIiw ~P#@mx2yd)gL(՗m%" 0RTC(Ybt $a2I_Sʥq|.o1, *jӑsmҰ;Q=En愥 dI0lF cL,(-Ii,p}pS+j=Zj{nE3uUBS0Sm)'aJ} P)Hd<̳$t% !I=A.1% hƺ)bJ8ȪM^EFinlm.D$N#ks?fۧ iN٪HkGW.6̪a͙9n]p&W'T 1 Q^O*2Ӥ~&u[Xl3Ҙ,~K܍ۍxB[#eMFtwIђ&iWq Q)byL_@N?%-މ9zA&(E.i*]J{r躁q8'7 wH7%NJAlVX0z{ItRS(%f.`&8QZ{>=+l};=F&WU@Jdnu C,KFuJSasCgl圦Ͷʤ4J2-Jgj׮=x5"Cs2:qK`*酹ucxJkrU;jj4ShiO{Z#ުJWheŵtcV83s3089&ΨbA$ryX'6Gf'$9 IDATIep1}l0ӥ$3zLE$1+3:g ,QLr[p'Ej\3'avE._I6|'JdV x?5N6LY`kLCSamjZК =!y[8VrdLT$p}tѕ5 ;n7vn7v4I"FElJcx#)^ KS7 boXݒFgIZbF.Nl]"!x<7nzeRfb$S@\D ڨ*鞫jQ{X. 2Wg-8+-5 8}=MTn ^*<ϓ rQra*c 7#%3RRȬt NN]0^Q箂\3UDhnFL{'EЍhLhU"HPA $]$RbiMKjj]ѨֵX-˜wF80؁հƩ! F:7pT*|Y%} )Lp$W~ AP}:I/P7+.U!`'MM5Vу6(5S\ 59α\[\ !'ƁtaT~ϸݠҝ}k9PZTw}[ѸF>SњV572$W·9CdJT;RdI_J*qm]Unݝ'ƠbsmviZGA:-(3/b.ɽt},Sdž؞[@fb>2lvgehKCaB񪉏ny޳^͛MkERijXcaTx1fYf4PWIH.Ѝ [7n 0u]7vya)<wʥ|8Y{=VRt/ )MP_03"|v򆑓cJe!aYc5c|7&vAQK]1$h$($E`!'@e4JWen4ZiyQ%!990>vTj#TY32dOm+40"&nLE<|ez4S>s;U  y~}~c'/V(S%$C7d\_&&jN.{^2I/d;N2LBgybxbLWдhZ몰mқfmrΖ lT/N ˥c]av%0%U ^$w*#, GQ2݌37sn̙)f*+LL JJ uQuZnKKz*)Loӗzч>H:tsM绹݂+Z WFǜX +jSaW}aUJ^TF$R@P LeH:$V{6zqpRGN+ϓHtVکAwTY$Ul>z|؄e[hZhui<#Ye|h0$|N\5P:F7(HK;Vn"44kEKtn;CG^j4{+ ~&7S_F{[eGKZ􀷵Җ )k|V(MƘ5cdAe_naKR Up]7#S-wwi步㷆lXGI/-EUkfmE8r,u-+U%eU%ɕ9+VQdRK1"l687_#]M譓 za6-=';Iqp{3'؝O4Mr~ݔ2ੜ0$[9J67yx57*qaL>xokPJ5Qwb͌OwWX:c5B À/N {V~*)V*IJ_ZpEd2' ݦ(|u#a*XVr4FEhULl SpShJ,EK0N[6 6ȫȩ?ícH?Fr&D8 )Ѕ" ;J4FBXTPXǎs9&fD6/)t$Jɯ(p!1!}疻/ gc>p,ļ;^,Y×%%n.֩V*E$73š)8u֭fus3EA5\z]صxv,ةK]>7w$=`>snlWRN3X[C)@7bot}qqbv,~14N4MA|v"cjL챮%;=%N\*3,FJMxE,s3@Ww݌ֶ&\IKU-*+TRzHe%E 9 oD=!EB-磧=}B7>[esd5gtedNA5jRq6DSj*d%V%cxJ2 $;%ZcaR:l8Enǁ6 )l\ p?vD1s$cT.!RB)Ֆr%CH;owoFiY$LVkSLH&0ģ$؍?Yӛ7qQ9Տ]Cd?z9lxnCk2FYj먪 ER- ,#y)fB9[~NՆdU 3gQB%P ZrHC.|6XX_4BYVLԘ 8FOwd@uQbkM @Do2JOWaXX0DamEgae t^Q9>A*l%P i@!1O^mp} P>-nRF&RX{` 7ӢuEJuP6 $+uZ54.,;Ǣ^03+IK+-n=QE3+cdR8M'=}BO֬eXn7JOIɣ.A,d&ЗQJVXT 1'nJKv)Tlh3d2F[)=pJ|p&L5w}lӫ_]a>y㌓_8ȹ+I~:N2%% Ns #RDžO峡ɲ2j Qѕ,:ƟW`gx7|}mR_Г6|WB1 |7=QGWU?Fo7r~?V]_y_Io7-'e~z`\ ^w5o"^>uZs'̟iWV?|Oox_/fذg]b}};oxl}h-8ms-u5%DCb® Kބ8gڔ#̚Y6( V7#ԍϭρ5KGM Em iVq8.?qܦnL6wwF'KX[O :8U˾e:% g0 *b)ufJ/0F2k鬋&2(Yu2Q764E5g,ܜY23Pz["x:ݡuqdܢ v6&{ ! ʗ#S g?ŘC|<>˄]X5˰("4phtC5Zi ] =bWb/2<ɓ'GfG\{.+ڋpu!eZ, }D|TͥG05{7x#l:k=OkbGprcκ@*)CKЎ`#U,@% JcYMN=]YCUE:I3DuFM<,CӲ0vt* <6% SH 2 a0087>z|誢aFIfts.^hPc"=u \ϞYRQ3<V'oJ`*Ml5Wne|&K{C׿Qyn޿w~s|795y-FE>w;jG׹4p=哿9ҕxB#?-$*9'|񻾕=)?kܟjNEoop:́7G/w9vV;ڦk꺥 +b " RIm,šLHBaGֆXTNjBo% ʄ|,.Vli7Vϗ `*Gim)ll"@vH@ry3qfXXEk-MUmq0G@WC/TjڅfX](DY?fnuCuꖶWֶS;J'ʬ6w ݹ3B‡ c:f@a "7[!xOcwL(30oe"5)(59KA" 1] B׉ SY i# L&K)ѤxhKL|uv1"tti)ה3Ad 1Ǟ&HF]SHkrv'\b P@]:Y50Js2nO}hҋg.#},2aiT 8y[L\أ>Wy[ z^oYd9<1?8jS9zUZObO's^s pՇ''W>;渺•,?uXQ gU `q';IFdjbpUo@k&:`J qEXs7-=~ߴgn"dI6 ,Hl8)U)TRv10!F<(d(ZCO=L{Xk}Cx5sݷڵϹ^k@R &\1}&+*+ jWd>/{Gy%gO_< \Gc_'TJ<_q4}+~_%o;/_|_U|ˇc#pi9xӟ;;|Mwϯ3^·Lbx׽=x|7|=oO ѷ\-؃o~U~#˲peYQ@I %^fzHN@S:^uvNQ_VL~u!DԵiedM[1P$J@~Uf`S5=׹Rى7l&qr",ݭ[HJ_p;@#)">9@QNwXh54mvl ڟلݻdn&݂\u5!dq{][urmqݝuΌF)3)b4}^62hm Kﳩuފ"SJb,zm3!r >lB,9$Ӡ"exLR\\a^ΙŴUE%nŤ3`06#JMԄ"a$9dž1ĀS$aO MҦ:E~VY<-vi6|`@h|M욪]”k5YV^-v(e8{}~EU}8|#,Z~Mtn8ᙓcnĊ3qVU3ݰ:Ԭ%)fܷiPr {eCVA)jNW;,ٗv*t<婛xB:)RSKge>/lښ>ʨ'?~k?wO c;W~y>rㅿReEQJsRL*bo"%P׊M-'m0ZzTgxlto}gʮc[Ɔܿz1YYJrlArdfî_Gh2lõ6}@ gHI,sz*}CϹ}r >(pVQǘWtea ֜B6>&C={ƽ78io| 5'+vQjc vA:}nlBw!$෎Y1ۗ˜\bL|ǶMt=RDAVrh- c'pƥʋL9PVKj /Τ~h4WWTvM)+sS&NpZ,16ZM N;9/"X3N84쐙C)pZq an$?-)&j#Wv]a a [dՃ fҮ)%gQQ髼xsbwtRO~֍cgfFG<ԙ\ǓyI9OB &N>P쾮@kIlMHJH{< ] IDATRp!dō5z3N#}fņ sfSKKVmh7̬`o8s'9ܤOCْagQrtrU07|5l9ESliik;r\F/9fC4|/|{ɧ~0mXo8Ņwr:LY{ M|)ߦ<7_w`?/.qÛ܊jR+\I\Q2ڒA!Fף)KpE]/}LGԱiY8E#E3xt"b!7MO'1cPƨ2J4m.Ё;g;i4To9-m A RNO$XIۃeЍ8%fb~>O#g@]7TvZm  zYs񋚂'BNgP9vs,Ha_yTixƘO-;{H9N$4 1ޥr@yZ笋H=zUvZ+,E$>AiG*%q l[aj3ZX1˽^ ͅRu&} ɅVw,sd꞉9Ĭ$ZNN"''rY.=1nS)F+躛=uܬ;h}*5mUYwyz"#SZ=kwn%r !hNIok[a(X:V 0FVQS`t3ꦹ,u^a-[`$ՠR`łT}]* [PZ+8JKLr\jDE[Vq%f+YfciN~+ eVD#;C\]`WILägBxj[ %Lx%OdfGO=-Y4E]^cb |Ev+ `3B!4AGY aǟ'D%IuKZfoX{.3X6v'<>((zOK5A5kT `C$Oޱau۝3wje39+6>f;ԺФ]$tMKR\q >-`ƍO]}#Nלm"lF~{z Q<(}wL}ٰdru.lZm3(e M*4Ύ:\N(Eq9 p[Yȱ MFo°ՙ)L007/`"+#,=gh{rqdZu@T#vYQFJ[̑0@=,FX mË>)ww <0F? ޹kv'W}di}>76Ү@]tz rTu-Dr bp}`R`/ǽu>Y[8]9(\Aa-(lA*buqU^xBi@u؟`EU&)cou\\_erbtY1cb&'՘ &1 MP͏u35.8- %i}g9J&r! '7p[9c$Zo|S\65%&sOy*OHϛ]_WZ͒L kց੍ƤHL c8i A3hY{,%'Zy !g8ȦleB"m~;4vܽvr\4m#o;[xO~u]~-t_?^V*aBهvQg`QlcUdlq Ԩp%罩 ;;H2΅r`.hr3*vٰyEa!zҨxW${\ uKbu4pFˍmPw~"5bvSqH@w .9S?PFy^33( d^*&+ 7>m^ 9c}‡@%d!c0D3dI1hui|YF]HߓM$zЎN[ ]ʾGȽ54Q `N8pW#*bيg,ѣ8SL3J3;G9?gotE`RL UYj{`Te݈Cg*QcXtɭ;eb1͵|%v"M>U],6rYGLbvr] %RYmh僗LtVz00~_z|j)j'RKV!4$3aOkM)J+T؊Μog+XXFGDjqHڕ]r\K`vUdUfIRdTPw3+7=H$Wo옥DNaG Zef'n]A;  -5X6cUo"&1a!GH Nj)}w!j[2袏 (, c]qjru+pF0fٟK9ڮ1(; iWxu Q}*9&>#=1>[ XlIw߶sv&[NNYtޖ9vˮ}58J٣Uu՟x>cp,:13a+F_|{=v&;& a܄i9%ʟI mnrC儔>GO%SQ<pj}9|α_s(C茗}o]Z9S:2-BbOcmh%<\y9JB@eEY4`#OVR9$]z65994ƕs [ܿ8;kߟ1'fi30D6XM*!a[MVu9.g2Ƕ< -o*xG>]'l1}=oߵWuM8vj~VJJR) _|j޿/{YDnض&eW+q]'Qmb+l[ MNR2;׹dZ+]t4CuHX4RؙWti\T"15Me/R(,n\tm`.ZtvbA#R=2@i#8 0[7mD&yVM3Kқ+( }{yV ]ʼyً\޻@Rqᐛ(&$ƏU0!8BX QhxXuXm1u]y8H89v@% qC+OD7W{uZ> i鏒p;bMTQ"l/2L|{@bdw}McTV`A,e2`zdb`gìI^%{Ԫa;KBy>xf&l|53vg l8ic{OlyL{7HTseregA4>W'@4e:N5mTMب'zzͱ\-$I: bu- -9S`s|ߋiH E֖,3fVK0rb>W֢0&rHQMϹ'i!BXhY0 6YE~d9f W/xHM)y m+_y _q >C${ _Ͼwx̮=7} =Қ8=oO?+A)|}xG/soePE~wߤoX57?A~wlWO׿?|3Z3ڣqe~?]ܪ =b\ QG=pEs 1ހD(e`]qTʬ :gr1阶m/sN!d.ā>,7 ӷz89IשnszL EiجXL~Nd}+&W:0Wn@峾*$28-,|+fU؉%m.Yq ^?} u_3O~KR1[}!|*N?a~f|)b/mېLuox~L];[{͟c//5>1Z|xS#w/0v `J(,E(Kɴ/DaRqŝOO4E4:>@E$ IԒa r2)oios<q`׆AfP'X#3s8q~G͹v?;jʼSSv9U)ut ۦ)lnK ]PXjZu>⛢y4'0=>t9-P N%ݽ/LrhuI1°0e_:C㧟䮖Bcr0Uݕ6X0-Yr\Q7”EO \c}NĶC)qU}fY=P@:e[I(Ôkc3Zb0F\rT -vCҁjWpY=8*$v|b*v\'eDl.Yr$}Va3n8τa*,p 76/u#xwwuw4Jb RS׊5=3j2<2hcpEp"ZGKXrZȶzڠ7ղxC݆p2t <3ym *x|L #CлTPBƒĸLu{&Hr~964Zfzw(uDLgxBxoocV#6r{Wb0sD 5SF$\Nmy1ūyd۹L8pevb`^Ιs&Pn{f^ڬ8ݜr:hu[[yz4Ϝ=>GJ"K@#3kyh!\*}Ղ.;N+QJ]^=oiۖfŲ^r:>-6G\puȍ ?{.c-"i@ 33|=ewtMa?*fMaݮY7NNSWos~CWy>gbg|l2eZ-XPĖUK%,3 :uRT%ҕCVkNen$mh}M65vêYs:Ƨ+g'n#Cb#ߥ"mTB/9 HXG24G/#/k/9l4O,< ܴ(pԄL. wc@N{au۾ӟ֚`2)NJbRUeAYTrEo8uFzo|VVEؾ:NhmEvْ:wmľ8-ar^3yi(,e(zX!;+^\YC$J@]gY #us{QD#sRܫaJ9gcR$qAYk>xMޖ}2R*K1syfؽP|]Gvct>$ ZjE"%COzY|1zq^lס mݾ5){HbH؞>6a=ULݔfnRN a*'` qP_ĀQsSIT$HȬ`U-t+ŤLuٳJz.WK $|m8r^r~~ݫKRLBPMB;X&.xO33|9}Ot/~+g kOqkWx[?pe9pf2qLDYUYi+` {69Mӎp D:Cڃ7!6M] C7B]~'I qJuls 'Z+70fEa&,Z4zwuZ /L5RSD{`=HXЇC(߻ %>۬{[_سs.ż0ص#M}m/nSW~U6O19*0ƉKfExc2a*HZ2>h Q IDAT)FYPF?#qc g4wnnشa톓͙:$;LkC3S`'lN9M2.L(4uAj@jƽ콚r¡㌝ɂi1ͽj]06alv;wD>Y?YE#<W\_`l|tF]Rϓ,ZL>9LELZڼӫ[.HPcKTA2, 80c%fc6 GAnPAM$ewV2m'$\h\;Q;<TA](let]K`woX̘g<6n(z?wg}WotlSX|~˱wEQɤt_W.;hHtǮKFBq9HČ0H t!Rm@tԥȰƨ3MXq^m+ 9CQ$&ib5Gr'^p&|e Io`7m~:A؋R^,Ubv2StbrK$Ddu`6mK GLl^a~d9?дirdA"H~.:b-U(%)Zr(;C -1^fڽ@w:t7=vl 繳Lصv"e]o}E}[Q,~M~MZ߲kVU`s*Ŭ dIVF\@?h@\7?N.^{fƛb_\ea&vάrUbFe*)q͙' f& ^o|H"q|eS&ߏsA(%ĝ4DoBügj'6{@HͱMlpa-&t2]c) #a80*RY n=ᙀB܌GхrJL Z.'Aפ"kWYtr\{|׏i?A*J36ЄFkLY;6Qq:nH-RaIۓ0uN)́Œ6@PGΓۮwr!?wce i yfӃ(=o"rvY !s nmc 2C^\>uuWB޿0P= `b:5.  'njr<Ɛ[rfYWM$䫥yLFsvMTܜJغP?MZdRSPs>nևܨorGꝆ &E;{8%~ae9eBz^X{i@mﭵb>w\Z+n2T(%)u2q,2ÚZBAسtF1) {ش䙷F@z!Dݖ}~=AZC. S%]dRxSC:x$ul>`rM(6VmCcKnSoXk튕_n~ʯXKVe{Y{asگGLE=C\smyBϚb؍I1zRGe`w˼K^1cVM &s@,f*5a]lšI[qPsyD~u,)Rb N8J[❗\(4}yuQ"F4F+l R]F=]R( ]3mZG&JlFz*%f.7OM?t"L?_NTA%ZOi}MY7.s ԍO餘!Y?z_blIٔ8#!j_c;}ֳnwn٬Xk6q/Yg,gg풣_UΎ̠䟭6f09QZey~y68D|䲁OL9Npevi5cQ-$3+gT;LI;"$ڶ{k|Ec)L_+備 lهsPb`4HAN^tTGԪf唃>>yZ=Gi * L%iJ ne0gU(ηgSX4V; sJĔhC+͘ O榼'^LPV颏Qvr^aTK1 B=bV$)1 2}.Z@J,RЗ5XAX.%]$B}n7ی8ת< Ղ1uSŔdάE/ ARo=e(iP c͔ؕz'sFo_R"?DZK k)|A*RR[rcs_<IzPth":鑄Xtnd;ZFmT1X!L+v8S;%΀jKBOIi&žN[ N H>4evr>@V@c_dYKv[Ex; ܺ!j?fn{s`dOAT%1kc6]181&ZIm2v  (f11S$P=$] VFw=rql98p9 s|e) {mg.?{{GG%DWBȠKsc\!2Z&bҤd pƫgP;򼘡UBx/t,s,Pz剩 JQTE T.#eta@$MgG ܨo3Ǵ f}/YsV jZ64ka.9m2;>==Ƨ:Z_=v[P rۧ€i)ѿks#žO,$h@pڞ|yB|kX &Tb I>-&L g\>-7b3 L+@@JUg,7s_gnsIܖ:m]d[0ECUT =}j_szhrho9Uk_˶!6$z.&d0,4fŌJW8\K2|sҖ8[88{U1K>}$P@qV7s1.Y cl^ ܸpd`2?3:5a()IS 1l6ĝ:7Hp6Ul94ގq/Ft5>\$d#{Z;z.AuCyET Ĵ\>'/%a+'Ok kqVyȦ)AQhvw {}epg1:JQ:WkL5Qסy!`D؄ޣ\h}7u~@J6Ku=:ltcDEcX`c`ƣ2SgYhݠT%J@AJUUlc H ;)iqԚ˸F}WK:65ԩ[͚U=!(}uV ہ#3Gۋ"RKLG#7"n; k"W"uHQZij_s3JAjW;Cl#TWI.Kr܁_ nw}]dʩ{YS|>o~3$D_Mob^yj{W_̏'y&szw~'/I7YvIE)fSJ97L~Z@]& M84g fr ggҥ o& ZrQD5f8H\>&fp(Ne{n^ 4k-Ô5z)]0׉Q8q+ >U1|{=ޞˠΰXfT39q6!"3%Y׮Uܸffz%k)RcK7s D]'z:i?>Bʰ/.r:ag(\(+rNQc'x鵀;UҶdzs jVyޗø(JW p/|; IKA|ڞ2kXc*Ѳ &qծ7^!_K_qڞqsXr+H 5Vws8saHJl7~#nQmmu9K st}5el}M>;}6kc@J}Rj ͋9S+2I1( =WH_QR ԅxqA gRCb9.Gq|P%%ms7{UFaFq!s0Xe'Pg sթBvTQ+30>څۺ윍dzn}_^',LA*vJa *[QH z;?#Q@SB<"A;LSN6rZP*S>Wb9o[3XC?_ws3*.#_~k//G`75_o_]y>Rm)d,䜹@b'Fac3cP(Bل$4L&) F)]Tc ts6b®;:.0r>YSg%q^,qP\ u2LKJcJF1Q.^|קByiUi)K w(ջv(]ޞcwaqfNYiG"0yhHFvw ]-8:4d*Ug&oS_+ҘheM\5̗-wh,|%1}0VLؙ;41H9婣JeQPe~Oùe W㴣Ѕ;bCbL2X֧@ԵaZLIxOаn,%'17CN P !IVk|@1]3 o̶|Iܗi1 Qz+3{Z&4Yի4k͒[[w?S"_&rXz)^9reQ{sב3v[75Ǎϕ)e1ܷe7f,6$'-cG<{u|PVUgQS&Ml%OV2kc^6 )P71q25AR&YKJ3e)tTENJ vC6$gƷ9AMlb}nfKv> [(ü?^>ϏsY[/˘C|_{,眻K/jII d%`0qjf!LHf\5LTe&v8'e&qbbX 0 I-޳<{ιvj-x򞮷޷{=|5pTWIc_Y(#}xD 6P ]*%3sW[pE^X%}XgV+GjY4Zk6F&"T+޺%AR.% w2I2Vς^P'WU9^ޓX0lnX66_7ڜjVȄF0uudR]]qR- sÂqzUz(83 m9HQ+^Biz/O´0'Җ¤q-RVJ+)rt8V3=^,'[N1,Meefٴ &`0TbĖ=O&5Աf|_ ][~E\(p"EPRs0HPcҵ$e܍Y Fv4Hjּ nA9s7.s]ìp wOhh2KgKʾwOC5޽JWAzGXS|=rև(VYxyqbOHq-s Ri` ,˜I =| +A*oB]&aZwZ'gXG=w R"ˀ+[Iٽ;^)c1i{{R?Ǿ?{^.IJHF<٘KqsXvF3[|Y8 Okr;xڰcc?uL rlC@4>$5S`Kݠ(t~S,F Mcp`)QZ1y. I^0ʬ%w.XxUrYx=p]]&]\z hK]_^ H *|O=&x<U,I?bQ!+԰i٥/K&FAUٵIjxz=lD]wL͍:A#NO\۳oyE¥_.BIbTkϯ^KϏ>@ [(OaQl3L %!X+R51{[\~/D)(m!?VK5(1N $"]8$ҩ15#4Hv:RNmhcÓP%}$R K4e}uTߧFdFu3fj[۰0 gU9XzE dI[0υu+qJUì=;12!Ԥgi[x۹ .%\rWIv,[2t$ٳ%}(IHaxϭ@5${Zek;_ݽfOCSI!fkG< IDAT.(`1ظ/KhuaB=2!.tYa]]= $ Y܎,EY:w<.oO`gx͛mXR\|7ŏ=zn7/7, ox#yϫV#;5>O~ߞ^:^gs *<ݳ~\1PLo}9osw[OYDf~|ǯoM/Mn^|ė?;|pT!7&^Ss/çZ,%N^};y &\̯2/&^r~?gU}3wY|b vsrJ\nq*^8}n0I|ˉ]>woiDUPǘQ `E!42H0\*#U,SVlli&5mq-YHC#5Ǒ,꙱D$=X"SYb'kA/ѵZKɔz0[ 6Nm13.P`\v~ެ;Օe4`c\RXMa$HL-( -3&*KƆxStjxlLb2I[/*Iu^h0Y66$cNnp[ 2ñ֒9ōA3@, pt..{*3K@Iާ~ 9` =\[}5 J[ XPk9dg7yȟ~>{_fWAvBX^+!RDN )/EbU g3~,usx0?%]&9왜:rٯ8jH7^2E,+b;5mC̛9vμkpqQp9F~ LYދhӧX]ͤQ(- e/^T mbS9*Z/P.Ű(C\x%r m9 ,%TE,10 A{0c^ڙ%k#@>‚F9/|Zg킄ǥ6_}慄 x\ӱ ٩BB.#eu 쎷 Slz'/z]8Kf'W{􊛯=q/z)H9y;/Gm4OoFw qǘÏ1%-ϟ(gLs%?/tqK8 -wu7ox?ᡫ\o_Yq{w;_KOK̟7.yk^ͼO#랖C?»^{0K~xg_bN,wP<⟞'-^13<,DeGӖ8a넡ygbT{ZxlO Y;BRV&6ҶhRWU"{rZ9&u!uWKN~e_i1\*Fgsq9#G/=e딡L Ո37nQ!IPVcFL8qKE "+F[<"-Ǯ(++0 ,hn*hdL$a WW]kr;*NKƣ(OLƧ tc\Sb}AP*b'PTSXԎ@B[Qa"(m1VQXCa<1AzlH2k$!&>Oԍ|'7pSN滸Ηg?L44ӥz_W1;4ڪ Q$rHb%)G $w.vl0%uL<]e0}PItkifX\arHR_+]o1.,cURi(>z\熀y3Yڎ.Ypvvr-8`ʼn[y۔xტin&pZ ġ._,k }cP֡f5jKe^ {TD Ua!|ǤYe'kD e,J"TQ{t͇b+X1\1Ѿ0ȡ:dn0SܳyG( >3"{ m^?ďOܳ/\|_Ή '_Roq}_=|OO̯;<t8F~G?vk'?/]/0o~i'_Ļ{x~wg9-15n>?yWevyo#?0R}'|Ͽ߾@|˛B˽x`ZN]IO"ϼ>;.(n|]v~̈́jZGAGBj[$qx/ GEJ%OAFS&MsS3(F FzWJ$]\5U%lHE[DZ^[.ᝡmmiGGLtR`r^]t~_Fc(˄"4順>DfYp'%t9Džy'@`:<~9S(7O]& \8h)*˦S`s:!U0+˂jF\&3e( ϹMUdaʪ`:oQhNR{aW}Y;?MKRiK)"zGzZ[m Aز H R@1ĤDPJX:vk#JYY튇O]QVOtǛy??{Pg?#|[>y~龧mR/XPYR䜰ڮa1Eqrc ZtDD]Rջ$k9q|;dlAZ҅"`*MhM5=Ox7߈~i}>泯Eϡ34G.}O:_ |ßm/37|J&Ѷ|,2̓>˛^ ~'.N7pKoF_]>%_x1w9>q<~aJ_7.>fԩd4R%7W~51_7J^zsϡ>^L?p3;{'?1[{/3lg)R?ٝ,K2&ɓLCP- (bE;٤VCR&;zIdz@YJE U$!DaUNEOxf|j4FX3,u.c{M@Y*꺣iVeˈ|Ez^\RCYYtOV ٣ز{)Tbq{+7 #, xR?d%)c,FE 0FYQ3:'P բ0#+䳭*THVm .(Sn:8qq1jBt8 xXЪBn2M5 5d q^*$꾋#)d+0#Ɔq^.A^8u`7-XХk׃:("bAa뒔Űu;i=Qοbe1&4Nv ͕mW>A|qlL˩ n*ஓ%hC R'h}hk7fz. 2f֮P}m'4s;j]O??G&mCb](^ua7*7~'%hUK׶0 C@%s4xONT9CSJFVM:FTbcjiFRYe)|VDU)c*))י1hm̿"X(ޮT#xLzPƠ45VXru=(KOlDR2lm0ʽhf¼ P98%N)gCVA]~X YO.epRBeEa:*7ĵ}ʨ0s͵@#dKPV$+`c:bs+ع"}^{hJ<:)9nK2d KA =skIrΰšH5X鹝!<.CbՔdB_œW~{ݨGhxd\ 23P&I:άs$f䢒Jq )K%BgZ/[Tucf"b +}1?DB$fI$};ad+If4{҅miY3cحM%ԴUxMeaA{y4#X%dZ JWW]!o5! -K^:mV4{_\JSs Y1K:Q訇KkEkc +(PuN>_k;9;6c7aݜe ˍҖ=U oҴ mv=v&3 x;v_MX#qxQ4ӷgaZ8u+<8:S?y/v0#Nh` kyRM_:OI# 9tC ) 8麀D9ҹ^elE(㤄% Vj !a#JyuRdH VJt#%'&sRR7%iP|GXd{}Dk*f˯+XӃe 5xze蜄EA%QOCW@X{1،,ZfW"_`F cʿ ʹKtn5b{H1҇Y+w0ƀC9mʟ,0Ds!`jS2o(Z 3ՐCB!XBH+^|.\2wtZxHd(%6{B "LC.@BUx<:*Ze.Pkb凌hZ)TP\-y\m/uO)ȹUmҸbDi$]O=Soi]˼/A RAmsqg9` h>1I% ED8?E+λ @D!e뙺и.5ݜfͅf\)j PKkJI޽Geٯ-{q"iglU@`XB =30c=O*ukvAmcgZLURHn\k= %2*F8|Ǥ0-4eô12L3 o*N"Ip{65o~~m8V&׉ '?y*eٗ]Kyv)BYA&80Um+u=hz+%2Y+e&Q5U!(B!*ҦZ5ܫ\똻dkrCٖ>ڮC@j%4F*M$w%ߕy0WJؼ PP08'R R y" $'=9/]ȟ6kV*q@#`iXX]]sQ f/ )%r(e@ɉdEy30U6601Fj\PHL14$agu>1_nɔYp|P>(?Ǹ"< dIf i`nrI=zJTe Bk2[XE^{5=u, OP2&j%ɇ+2Psbq'WdMd2-TEKZRw5݌zGmbFGt^"Upzox8PGώנ%qښQQј&:CiKu62;h[C}åf06# #=F+c%*acqH'@Χp i;R?ƖB Te%nA[T h CL)Q>6}ERT~TE؏iʆ0/挪1)[Mvwit+`-,]4#XHex;vil_}pE47zOForOF2:G4#E3 Z"bHchd/KV[*{02ޣrزF0ڞ-Pj鞣Qb4Ry@!0HJb蔲89N2`; egMe^^-.P|Oj`8Z@loUJ׮J ,PĈ-0/DN?iZ顋8EF {G+nsBE5,єy;ƾL=A^8),],څ2 mK-;EșfؠbjE\P*b9'u>xȠN2.I sgP6VφbX]clml<7?Ǎ3-PW(W\hIّQQ1.LFc&1zA肀9@'l9ގ]jO+$jm9/g?pvo'#CM7P ^V+?Oq y,'r+~GymR'?]]GĤB@a%ܛ+)$c IDATՓse5:KM{eמHZ(G3pkr師$mH'97;}rCS cr ۸w)E]4 YH:DZ&%|tu 8po7 e`xOylp > c!-+DLb-^4:)b* */tC0K$5 &GU(ХuN])_? hEMcm貔8 4ͨC[x3f2i)uJT1Ŧ /uB;mVTFmA} ɥ&Ks`WWyÉ̲ifݜws݂@K-8ls^σּu#S1-X]PfRVL$ ;_F!eӐ~* 0V}>!eOLj%@iiѥPH yGR*'Ty>N(TaXmyXJC"50BX&eVw_/To9`yɫxt;S>'I'yq}yV&#W/c[Ǫ wRn`ܷwEÅVi?}Ga9żż[k#xkk<pl1/y9~7SW3/|ueyza^b:}}qhw㆗}?_:'?f1 '76%R̮KhӢT@&F?E&41h2bfb'g:0asȼme4ֻ<2weiZe**[b-NdR#F6^ƅc2㦤rEZv؅(A+G}x/"WIa3bX )5Y.1Tm&>6kWȅg:%$D}71(M),Li H2i} k> YT}x/U>/g }#g/armկ)~*wzLsG#"}/{=S>p q|KNq緾r Gm<'2oo1|q6ps)~˷?wo07p3p掗7g9}*&Y _nq7o~֗Oy/rOy/<ٿЛ).{'?>x^98ЮO"@j@4,!Tev§. f!/+I^ujb=4>KT3fğ`(?)u1ÊG'F"K}|<'Lɼnq!T)_[ ',i'[lxʊ4T[HoDZ]}]@f(C`Ã:歀m9Ȃ n8]Ӟ_Ir{Wa(KUY&! + 2RMQ4MGL:NX!8.]jYxV~#_!Ule]`Cq 8qM0j+ʮ dzNl.z'A-]OTap[e|PQ~W1$sGg0х,*sq!<\mp5_.QGk5wлf!ɰ: eEHJX%!`N@VZa* *tṛv ?|~[y{}_\o~]OGRw7=/Wя9o^Zy7>_׼$.~y{x xn'C@wo?Y~#7ز{A>~'Q˻獼-k2{_}?\VkA~_Z]oT >|}o⛀C͟5s(S;FG1q zs2H&&YƯAO )'ƛ5cfaw u6'6F6WEXE N@JQpڌ40^t?o|;O]Fp1ܲ}-j2(~9R`g'J'ߪGNIOՕU~c*ƣksҢ9M tq |!m Ϻg߱MYs2!{3#>x-9}9w\ur%+w iӨ£R)Cǀ : *9R8U/02=79}SiCS b,Rc g2_0v׸|bzO ' š=fRf=ԫW[n> @%iQn'f]'R;&n¸`ܲ: O9t4iHƕ(Mx 2hxZKX_bo&$fd!sWVZ sɊA6.DŽV,kE*Oţn)2s`M;VT^ c f}Z,Bl<vݦ~ ĉ7GڞRiloZW1;t8s?khm1f&a#10 Ub.4MX ΅b^[F ^XX W WSkhT0ݐR*-Ue=+NlmY-'NeJ5+RMLİ{$q"usmC3q. O<)ӓS[cF'F+iIUbbTPbsRt5u|!>9Ce+J3e=3tp!rZ;\P0xTH9'e7௏;j,<%]V#A^qyҫ͓S i&l؜l2.$as]NݬsP_?̎"TI 09F1d20e9*+2gc-eUd5 Z=׸f:u}{}Rf@3X}cKY;ˀuڨޙnۺ33l99>vhjq9$λ9=vxt;q@G"g098>c'LŌ!}4?y;Oi~zM8y Jwڶj!8B J b4ywJ%>² dd%ˠ}Z6>2 oYSӐ'Q:/_Y Kc6/tc=KMB EN$mVVUDF#j_hPa2%(0-?6{b)xΔ)sBb_ѐVt)+MWs D6ٶ<:$,]hە i,`OJ33+4%L\mttVgvMWBȾ%KrJŔa>) AI>JRߺ,c>kb8u#ܸvYjTiKƣȯ&%LR(t%At%b,d "Lve]G2'a3C%a$*26khG=f跾'z JjWr^{95:ɬ9ddGh4wS{}qݹ*)h#{41GNmE[R1C*ѯ7A2={SCx:Z.xj4Q~; <@!....բ'Ŵ^F9d Aͦ1)K`J&Fx|\$egҥ2a}&裀ZCYqrvuR49S@LzOuְ93π.}y&?ER75eSP61RKB ʄ2*be!FH&$Ӵ1&PrUi%*,T$C"ILA,]c'ǬAxq6|<֦>k1R(%nĉ 6 ńҊ5>ʐDz:C骫/^YE^Ǫggo])py=76BP^?$J-a#̖?X$uU |`*e.ˇx]$r$@/{]6ATJ s[Yf݌6Cg_MB1oA_gL[}C-!8qYفHL_h[V؇%+dRn0[]mq=S^ߛ>^S2ޛۖ}wXwQFMi lk(!)E*+ d*J>)U.*!rLq`¢Hϴ;]k}}o[mIg{^C@D?' 097[qJen~h|]g eaӘ}12 !R{+܃PY$*%3gkrߗ{`D1ipf xIho@vc#4aras{S8#mX./ +yPqL"4fau~(3N9:-SKP|I(2Ork$L#* ˛B9z-U@hIw zWpkyw4eU)uQDzg]t܊y7 ,LvFцh^:u:cvvI6N鈱ͲKR2Frf-Gft s)0c~J 0* E|"[-a T23udMA],M/`VT4\Pz݈0Ud$r4a:֨/1 IDAT }e`)GSD8'RVƀs ɘv)պMلF~gFf3O];RkHzƳXlё/Yq"WfCz*V8II06)AY|sbOv چx3,ȕ0k1gA<[rj*Ua*D(MI0Fλ0,Ôь*`+Q\`RĚHL6Z#lrܝye_tW A]Zi&fuz>?c>c4ٍ`JjRLdT#Fafň'$0A )N騇81suzby~P@eTQMiTzj7iUX12Xb3b`DbӨߦ%;5c\cau΁:_mX.L-ϚI5$" li)~XY/v.;"M`VzJpМ1Eq"Q 588RhV]3 c,&Tt(cFSX6QQ%9~ s3\z0^{VkWOP_U崅]3q*aI9{T^$/l> kj?1{ vҺC 6If]oySftvN ,GVLFCVzp6G3A+͉;!N1и.v>}"ߓãr轁e\ Oo!Ax6;[6>1)K-c" y>B"HslEk,X̀2>1eIqȨ 1 z/ ç$sWeOy}=PN7SH>H%o㡱οw{ЗΛV)cPWqޜXYm/-o.Ǝ{֮%"2IЛHH: N5]+M#@j4br<* ,uv hAlK#G,gjW*.ʲDI1!lPuv5ۻjF6}`zku:Al]-!fS0~&V+b*M]O Uejj"WKr)֭8)x1ԲwؘFNU&|-t(є?nd P*4',qd.NybZpHAi0J\CWhHQU.EάVj t?)n.hauШ,SbK"=5<E} ku EAbG l}\A:Ӆ (\=#0Z({C0q}x>dcH~d)j ?0ԞB/= C4՚}S2lGmiC8roީX4{~1ϙ*-:__kR,>OO"SN՗y_;gf=!1|f2o?Zj5E5SSGJ1F"lL CH5#1]gƠ%%躼&4<{A5;A0'KCL9r^a*;kiM7#A 9ḀJ㌿a}uqI^a Z;lx I0u9cgZlT)݂_z8Lu&.(꽬qlJϟ 88A rڪL&;9.2Y.=@1egZ)`zcdjw&("i3.N~k.Лdľ#ɯr8gTDke5RM) N2$v_Q.[{kXBYsX'NgÔszȒ3& `rZʬ^~Dۥrl9KN,N &!"svCnq1\nhW =dƓ +K5l]SVo3XHu=ipԃ0Cj0p>(p7UݭM]t-g<׷SQ:@1 $.8E" tN <8Fkj%*!lJ… `*C(;awoN:qݽ^22Z!̣ek*T35IMY8Bh E떘:b,Q#B'F8a 1 9^ٺi0ʙi k {gupD&VFg4IBX#cpVkFu|9|28|Y T`~j])aGok5Ω% [Xs_dz&8ܔ_A]..D%@z#y,;uux/bNuսL7@DҦ F4F^ÜLdZ>䆁4 sFO:J^[ۇ[PQDA,$ƀsw_g?KE-.1kN{Zb5pR쳖k YvK tđ/"&9"={13u}a)w& `] ]TX/o]CXA2O["P) B ıg]Ð\̆@BkF+빔 LEcJX+w)l qZf5/ '\L%# ؐM d)<ЃedRXe%܍ulr|ar^y.MW2QrZ=2cKЍC۵Rb`Tf.{rQEqDVIbQf#S"<.HmҶ-wˮa1|5LDTk:qHO^~k`TTT0H1 1Z#!e3Ρ8oH# ug<(ۣ:!D`gxs[Z?{Dw}mc%P|ا<,6oo0__^Zя|O?]'9/E#cZ)9rJND28Yz(&L5h"]bQ؈Sj:*,V@ihs}ʌZL'zhy SrΠF+]um+L̘ F.uqm<[ ʁ"ƚ,L)D_㩋HYٌY]bA.,{#61g뜃yfko, wcQlMKr΢s8,lOKQ;\"[T=xv+;'ܾ$+3(dY ;Yz3fP'xvsYH.8Pg X H)g\Dž݀ ~|w^˻vy~Yo=ƅ`x>ƟȒ̔| txqrKG.,[L.o]S|͹>ݧOPNvwؙm3Q>t(7b9e)L0$:*f;[Lwn߹cǙbn]֤0%qkwY>ÀzKw٪-:t,r`{O^~)zk ER3\h9m(tAa-FaN,$l"qu,8\GZiy9!x\gqLU?$g'fU*4vZUԺ)TE,RLa}c"$ az`Z\p){a.zPbnf:x=7ub牧yYMJO\{O3`,= EW_4낮~ &<1j|~7f _1y{C\Gܾ1y佼 o)~'?ecf;7܅ɬͬݙEUQۛazkw]2ݮ(SF`VA E5k1G+?0R%5`ل.*RZvybAؔPTiT4lXJ1gi0[5:44m@uBQuɜ@XLFl1VR F(45\T(L~=bػzRA] Ռz2c{v['\|iygGش\x:̈́ӢaKM)ٹE[naM-E4"v!)lt#n< b29$3is7LY3fjC\E9)`ZjX4dHK/4csBUv*ZdfSOMٝ,qE{G31?^\b>={~[oy?ۿ+\s1͹yr] ď_%ާ{'<3WT(/iGZn/? .LBy]|ٙ.!jvnKg.#TV,-*)ΊVF|+1jK3ROݻĮ-SxHo-w^7pₓ-o}R}G ,(KA`k*o{"e8v؝ӷ@$F!:dI PVEX mPB~Xm)}"it ) )!5]˪]h/NCWպ*&h">8(-ҹaJTZXJSa!!e&:pT}LB$NY␍:̼W+$*JY$fe1W"|K>SkwG? ٷ}+-?ƏyPo7{sݗj% U5 Ck;b(UGQغ{;e$]1Ve{2c:bvV HL<_KfL#VQYsV}\id:p}1ѫuKl*A:Y9!vKT UT,O,[NE6ܿuݳM=cJʢ|GGj` %JE{[U>w㫰 yZ\ D>ƿ?_e(^I=|[ӽ4̷{?,'LOO]Go?o<-IH(ٽb牯;^湻$޷>;C}:U"/5$m؟]GR]bҜA L4uVr΢u!zZSM30^´B5 T ;WgTe^Ys9Vw[8~ \;q6̮v/ U5۪ @ WzW|k^ S5 _CH{Z+PB@)(B;k)RdfK$ Vb/-A x'F]h}s7=4뙭>Tڬ}>i0lLZ:UR$`h1!NQkÙ~(3!J$8V[o=ox>/ߵXul,&*'IIy!'+g@  J -)&/v,-QYB@ZEr-K7)ϥp .q赙W@̡ Q ]he -thKsp.}(uPz;1'4bn @R()KCQO4XsA@ YD IDAT%E5 nlIk iPw1prްGqqosgyZ 6A8,8Ea"mI (OWuFAqj~hHӜ~KVsM#s5ABٝ>3Ol}o+G'Q*[SϠ`ʠB[S x`AszNL(%e$yR.,8>axN) ,b'`>bₓ.`LSa~qz¦hK\QpNp/G' t"jǼxWxO|smhPl")t {E L;_v&ّQI\p f ;V⨙h|^3xMHBkE׈e )Pv.ە"ٚY9cfw؞P@M"l"pQQ ;HەL}`:Y['pl:ʝ]T_-++L9Y#MY, DErat=9|SȽ4eWopKJZߓ@T!߭(+S2+TE-.VXf WWyTH~N`4#sxyjj0_7l#ٺwTy"dE)έ19{ʎk_o15{neRz2h|~lCYNÚ*B(1&&/@O ,LܸMJ@b=z pvfQ`6eF{0)3VywZ mv5DQdc~S`E!ˆ㦱E]!G-xo;S<lw['rDΊ0mIUZ^Ϣa<[ &lFy6;imuEa-(J&'z0ƀ/Dr^a73 7i kڄ֞$yˣ߾ƥ{\ N}-|S٩FElƫ k!""9qYQk"&{'h\K_4ޔxc0:J딨l͞ByYTlf´QNo&yTc5уI1'wKH[u}*1oݛJ'3Nfx3C)Hvjʬ&\R-3u1g@ A\0dvϹu9t G1!`^9! ]v #fi-4bb`V?wMN%(LcVsQQl"fIs 2+#0K%b\C:vykLN?w9~@XUXSmI3 `ܘfĥ{Y>"Xbs/L*R".YuThb6,z\vRsRQ)ipjMPy,u!H3ƣTm#.[◥=L=$K$*Ttt>gYK\ޭ3$w?|gŭta)ssV"4%X. R0ȟap3]Ms\]?ǔT>ի_|*G'FGJ^:|GUv1y!8~'{\4|BxԷ6|UE;tKbm8.%Q>|Qf3M) hK ΈPيń_JJ)){{..X 5cjgń)QDrH=+h,C[ꮰװ$`:|F7<6zia=e(2c1h%=2A9O(RK:ֵ̻MX{ܳd / ܅ N݋.F0@+ɬJ02 91ti`|6VwQ" ^zB.x|_|?Ěot.%|Jt)H#n۲߶4KuCKDBHT)9IP(THbz˛7K'͒vf+bÒۋhSpav~^^˶*zNmͤR",I_DA9l ie-)ow4tZyjml }㮷ُ9΢M %$1 K>zJErB6Ƶ4]üsk 2/H%@k=˻dRߠ,)7.q*$|ryWu)JFLR'~dЗq 17kpLP{sFt:vZVL(xSC|}R/_mJ~/;,'X[C mvlI"i1Stɨœp?agȀ̬%NIִtcJ(h<+EQM(˂vf_yZ4T[+.K?σ |p Gz@Jav.bĀWp.x1qbL^ r0ε3+馎6N9㈃M9{nK-_.bo?ܳG,o|7o x?po'γafjkɬ]ٺk*d>lݖY:Nv0ڑeӪ f.1$vfv ?o -E^?"@*oXb*l]圦e2.Ak`nŋ4kYt% rNrH%1SF` 3uT(ɪKI=17#@9S}a'\E4JM̓~1dnGRE.[Ҵ  њ*.fLʂYU]ʒԆ0,-HZF(RBCa};T_8)c(_ѼvTZ1M}p%ѽ+lm%.^{009{§\gqf巽|i..>#[]l8vPjlCfI)kϕrOƖ5cVn̾Effvw)ڎ.IWS7/-2dsBBiMYԓ%d`K" Q\Uv;w%5SK,, nl8wkSL:"ic(MZN5I:/]v>BXtilB;OW' \8iw/O))Wef6 $ >:AN蘂/LJx:NJӫ 3AEMέDaO:v]ȏ?̷}_ývl9x7_[Ajw>VAXo7ί?9n~l\5+&FJ5ڥ$v¬ 2.V֔j*nEj|l3|񡧚L%դAcx5VKAb BQ2ϼ]`5VرXv9,[1_Nf*I7ˏl fދ!4(ӊzj'ЮZZdKI $ڹ(4E6 킃#Wg~~z{eCb8b.U\p0!wN<{ˎ|7n…mv '4]dwgi\N:$ hKUp-PT5CFdy:~NeR6\IEPΤ T*fWdUbؒi#R[EG eRv4͂|d(Fm4Ӣ`*ٲZkZ1њV$HLb`CMb<єZSE9G7\,hb"E@P>)[7e ZE.[i1{M[`oK EA< ] =9bۑcqpf4:.Vb&5l__7aa"q1/-5 (E98H=֌&$[t]mN.ST`RnQ53SSuUSRb! d)%EEM0I=2@vUfw+7 J;\}B=Wn G>j-ͮ/1xD1IɊ17XLS EazgDQlWf-+[V9mh=S?a"siyPٕ<PS3abRLLIUBrT)GU9pPCXեN=^шڤ0ʐ:^eNLSBBs" Z'?Dׇ,{\؝P!Cu].O9K'hirq0POeCawz;(*P%wݜG-@K8R%H+Rgg,5XhLF+hDzCwF3ĮEhQkM6{~89T!;?{ok[wְ3WUWUs;D2D" #@"_DbA@d)HdBpB "ێcL#lwkx&>}CuWW;{}?um-Ujճ^Ċ1ُԱSM Hg ]ڔ?T*m+B*w>W!$BiHIm{uRZ* EgLE*qd&b Ht|ZK;Oz˞anhL۽yaHPW:HJsq.2B _!9S\;qvv;w^RF.W A3!(DJ*)b*%pJI(h!Ȕ[n[iB9G-fRWLnV51p+K҄9?h!k][qQ);esARJO MiJ: DzeD9+ˣ8 cE PB%~=sk\\xzy9gZLZKu5fɲ[r^ o>CN'z?'.Vj:s{_.c%y!\OoømSԟlo{>i#u 2zu$crcѶܪIpQֹ,.0;ӘY wύ;IS} -ۀ.*+)tAYI 2B Tq~vh:ZӲZ"c1 IDAT#P| &tEF+@jSǹN];ů q8b x)Iq$ZTQ^b"运)scSXD9pN> my|Ĺ=z7.:AU(xmT QlFX*Mqax1DոAC_m6V%u쵳Br"#PB|oP Ub='ZI@*9lgI~[jE_غoA]dxDtCB#Ӛ 2uIv {\v>nnQv@ %ubc5J)|`ȒElxQz)e 6fd=!X_#&9\Q@ L9->o!Q؉h%Zk!5%]`I0"+;z@`BJv5ո89ye%8\sRi&q{KS4|_moOa.wkd@4QI4 "*ƪ[g0HDw:ғ_."Ht;k``"PGВE*t&:mEZ #QDؕao\6l%d:CXaee~В,}vqdYuR"^X3`T8>v C. яePdY.#Z/Rc{ha ޯ~MoV ] rJ8VLF ' "0*!(Cޚ!.*)_~9ʛ7)ЇU ˢ4z|1q/%Çir) %vPS !9#J]ecmb(@&o8^eֈ"GyO҇*V%(gRv**PnmYՎpgbxy]ZrTJJ],23YNG]%-Rj?!D9P9dAfq̘_skxо3@ΗRD&H M ZK8@cwtEbaiXo]YLD]o,vp Եw]S:ׁCerzƤ(tI yo),Ei5x-#ȍ48J]r.l hE>34V-St 1tA8$\>P^[BuW ]񮌮kY2lVک}+ ⊻Oacɞ$ږE2E  qN>aX1ޟ>tźwM(OkU@oIny;~'I6޺K ww†+uFgdi{2c&s|mc ܦp<l8",u >PSM("R\p޷xb*ܒ;g:Y "z&2L,rS`"`N ;C@Ϫ~ f|(u*KdQ d6T*)FF;{Ŝ/8,ggjb y6ɑO[֔ENY %V(&`?oıPD%fDB@},{TS>it2x~ya˳7=L l\!@ U%f79.rXI'Zڦla{=^9}{$_6tD)`!"+Y\e2ٺEX]Wj\wmܽ&ɜڵHS1)7n`B슷}lV`t??b QA{Ecd H1}6\x a':ǃ1dq$}R$ #vtj.b/lFbw` V9y'Aq[dYluxĴ%1C$ubTw_p?3V@ʶJ T^f9鹣.~2ZST*8);-!4Xږ4~IӮXW8߼-07Qby+7B`FeީKvά8x^Dq&EslȲDyEJKd O&ȾGS2qb豒 uyN9Pf z:jMstc=حLsɓ#I~励P@^p`+ŵc`J)T(Y:'l@w RU:{NC*2HPUN 4[ޱR.# oT%B19z٧=ҳ/:grOQ@Fu-+K2U^&@e7 Irk >A$Eq?R.* X!Tu:@"6\_+`w5ƻ;sJ]Rvh|Bt(Y%Jf_{Vn\-a3) 2umVx)! DZ/r!EB iamu^eӉXM*hʟH~1!ƈgbtmZo`w۾$L.d0h,#ˊI<nEN'n`0#[=tCAK%)dHːXlM4{m*%)qf z&dIkTU!lP  7\, i߶ ^C",`]z|rYF=RRL'Ud "J*RSHI&O!a4oa,UJ aoY? d| 3UnFI@V#Rg #$_lG*XoK,1%-ԪkT2nAzBDd# >* TH,V*:GJ;S9٫ДqvG[vkcmX:B( cط7d<6 CfnʠeSA$GIV O"lI#{u``<^&-+NR Ef9m'=,3\^8Vlo!nӎXҬA\0m9"n K'ul]r[(BmR£>%#̻z]QBwD-uQQȘ8u#BD 9zĖg7Za2C^rX;zxT']܉Z1Or۠͘nˆT`JIOžVҏbse=>  + xp]ոvWj@-EV96+Dژ)s-1(]nMucغG Y ȐRs)2D>Ɇ HӮz2lKӗ:i('F9$P)Za_dq~|sݵ: $ !2zL_&?[H~ Al:u`=֐:}RM`΍RFkL SwbO b=[ɛ{!tx_`l1 cWES!IqeZ1sU\R[!O"4׾3>aϿ57iL|">x~[,0`xWi9q]R#R&7=<>>(3BX6U ؅=p#X.,Wtgg.c0,R GA )wu `XA'ZM "+z1Rɝx~#K{dYӿKlAa#,d;I]bdR!!f%]b_DFPלu9[ K>bG;T M )p]2-TP~5@a:9Y;%[H) B<.,N5[ 0c*_xRܼV7q~d îrI 'VBH]ְK [oSIez- P\ (UAqD@!J SI9/ &{q'i@y57QȤO{? ArSݕ*.]ɗ{Y>@cXJ˘cN+YeKf߇ZKg;3\Jg&.7VS|]ݕj|W''[;Qc>mY>k_}~s3>O|χ_;磕7ޟ,/~_orc<,k*pSC(tE !=[mO*w8QZRq_ҺKsV/xD-@]z"eY}3 ۖ:$U>#k4g>ϩڒ_F[VжЬi`H<@˱[":s{ 9jYuK!?#eY60uez䉑uSh7=ζX` ӯ]V~Eݷt3tL3'p r("0E8emao9/(`?GC?G?ϴ\vd~1mKwrBߢ{ W^aկ>|#V*vxy{#+ ]Wd]U) >z匡oZ,8c}o\?=‹ᰊaށ|]Nb[c,cb^̙9EV!JH#0KlB1ԙ-4^dPS#VppFw>gwBlم˭pIe>&el]EUd* C Nm;)e2suaꍍK6-jYgiLҮ@I?y=6EN$^G8Kε]V`(3 !!BV/x{n<j:eyzW7xk:n4|CTn!9˹w=m\GgSmǿ-͒ԥKE~)|ƴDa%.ƀ-cPF.$ 7bW✋!m2]ƼYऽ85}@?ꦤhL⛴J)6PJm@_w>|8Zk`q>&|}ˢYЛ< ɷlQB]7Yd,*%߭勿ݜq<֨O<`5ӣ*N9>Ww(* 2 IDATO#j}NUXS`tk\Ze;a"a(ް0n?m cI8!ޫhO0`ĉ|uRtdt^mtD W$/^&3,S<)G Ha.J2)P~8=Hے]{s* sf3MUe[i5JH)2 ';noa\BGL_z?|7y+Unܧ?Pu=3Cg]7t-k#k= #d6~/yMdFWnuJX@Xx:-Yw14(= b`DiS)Dx:CW1x/A~.B*Y+=ÌJuLZ>LaE>2uG1bzw}Gwsx@n9Q&`ޑ\m[u^Mw+aNxnHW߻G<4z^g5s[A􄺞evXۡrȑ2/ %lev[2$`TJ01|$>N6)r*.= 4|P-K<P'Mܰ"0)RXP .~¤In?-s^ڵk9ytF#=T,=Eڧ9jX~J- Dd鴆#C j%aFY.#bmcIH@/nL1TJR7#{ƞVgXB+fA6!-pA=OۇAsXt֍̗C"}61}ݣ{od)2ϑZ#lY"eяfK9vKj;;/|Yų)4Dn7n09??mWx^g/[)͘)C*-̎gvt|>vlI6uؕguQXTU,UJG{mǘ؉K:XejϜp<$Iy@.M+2)]8=_!YaAHC["|PܭLC閔IvhF~vͺĖyaI@YeZR ic 闿B~pk[/Hqx*X6_Hkm_Ke21vR:l5 Uqk*KLӠ瘶E5kq]PU9Xg:jjanD6=HsG!!ː2I TUᵆ$ y1f79}xz3C8vO8:R jņ`@%B)\gg=lڇya!7?9}&8Azi;ZDΤmX>ȼ?=J(i{Nyǝ=Gk.(1z .&&|:޶-Mp֜јZF@:¸;pk7]uW /~O~R(*g#߹p:Ї/O~$;)Y.sDry? ɛz8C?S|ߏկ L0',?c>_{>4}?/;_?&p'>/srӟQOu|c_ήVkwtNhӒ\E? (/T1lBeQ%*t MEQ̨ٗ $yYSYxQT>*]P8/zP=R.葡RdH%Fa,Lg:XR盐zZQ**ɭ//_ m@Y, XƐkk<#X&La,v!DV3M!d¤k9Jn< kpXM{;\_r4WԵ,U OQ#Y٭.PV2VNĩF!J;t!I?[:c< AU̹v'AkD!u2nX:dyj'6ozz&!D˷!LGX6 ZTCt%%rd~\1OwA.g%e$RQypeNREQ!i4 m [Cce1ߚ#)zB]獻+|؎wYܾ}2e4~x:>AAM佱 ]1`:2t'fOȂG!`]Q cͅ_"l$o5 u.zZr>̜E80 !{ ]qWW Y _;yr-kܻoK_O|T/WYšGGԟ+O 3s_?_^s3q5E?W_U,}ԟK?mvs〉^7(7s(YjJd*ɊY޶t.NUuͤw,;sQ ۜqį"[,A$٥)<']dMJޘ- "K%A(& Dv7H$kV}9ǰjJV+zcu$ c-C^-]،B+5u)Es/Aҷ֞ɤ ݀:duMl]sn^ osw@3f7nr}~K9G53M] JPzvY.偮 ghۀp!B%*#tmX腱?Z^Ha4Ov1gx68<>(fGL&׷Y݄?{1%z&vԯ[Q3<@;ǝU1Rm]$)!2dyNQרa$۷Yu;.lB)tYfU.KdY" eokỎ~?=;=E Sn~q6NhݲȌ5s#\bld/6 Tr+4o@ߺbɃvkG`YD__ fĝ}GZ nMzoY%~y?W;q-)(m|ps"Xk=Yf3ܸ֫78%R!w\e)j%*$Jh!Vn6^$s[MkڱXrp=} iLˢ_D+/YyXR\K:%zs'iŇ@7hEK%Od'Aލl]6#2uG]ob, iTO^ "c] OЁ,6ګvΧ?u yow1׿OW}W/} |ÝW<|~}{?9>W9zw ˇtX*(4"I\kX5v3)a".#WPc bcAVCVL+bPcpJn] RN**&KV]@᷋>}ncAw@k1qB-mkS6x45n:3Ϸ9(Ĝ. ;_/I쫛rEU &ӂ$gDGiQ8mhC-YMNE6ɘəraXU8>?:&q񻯌 ^衿sc+tWSRy]c%"^`Z ,{^9Ypgݟsڝq֞r-h q \Q՗Cov']r=`< Llvc`0C]H7ϧ!WG9| \Y:=uާ>- ;V̊ucq @:ۀ@&i%צ3i !ut:NΩɀimfD^5BxTYiA CGy>\CtlOgZq֝vxfP'.yFD`J H;读W ؽn\߼ýK [o9r47?}kɯ|=zȉUmc#jJKjt-(x#E!iQ9T@sU)5+R(>6S3>D_wRDCHͤ -ХP uO6+Hr>ѰܲLa;s 5c}oGSgЮY)";7+sO\g%(kl^RL2ɤ`6Ե3&M]g@5#X}uIYu('*WFpe>2%O kvR&^T!wN0 13+aN!+faEe|דlb\JaTRp[, c~zONe1E)3tQCGrcqHw1\ <&WzcB,r6Ȟ+߿cZ`xڞj2<:"*-WL?Q^'TnQݸA>2E,8LOdWv;}-O_b%g-Ws\~N1AHe{,B jy &S'^byq,!s,SNNStX&4Jn/-LCh;17ؠ Mb>&RJd4`(^ Yp2J7OȴąKs \Dz[,rh'-1e1"#}Pv+\:jֶ6-[A*qc`[+v)MRұWoKmfaSn!~jt5ݻ;4C</̪P}ݟ}Q^Byur`:(J*1KlDŽLuD@q@ dN6WAl_#cgzEVJ'MLF^>vButm{U4Uea21H,^yR/Z8+i@ǩznpD[:ɜIn事dEL8DͰb[6d *wFѝCpKo^%\ۭb RJAbZtSw#tiDK & ,)0N֕tMC{>;=>>&ݽ9=_MKLlɋhBp5(hRS5jhbLp61eי +k^T7*dw]G{p@󵯱×joMtЪ$zٓ'<|bBt< =xöK+u g?x^x]vS6sP @F"O1(g|4'?ϐKZBS %=D盬 |H 53|L)}-}ZIMb$Y(4/]bnB@"(v3_ؿ96SXẄ́KƱL{B]|zT$ާw&5%D#; R0mKԮ mJ2t@Aήqy=݇5[>|Nwdz 'GTZ[[\/dAHyaWɤh!PZN6A NTZH_7|5KD_<-ndrm6ggo %Z"y%q,LCIؘqKR闿ֈSO#VyZafo)Dڭ/f~ŭT6:nK4t=$,2rW(RHEq!X?| K ulÆֵ:˪YK;7Dԏ'B$A{($Lw I! Je(tt!ymphRM]\sR*I(4uцnd! Z(dX3BfZXO!+n5oi^Bf2;׳u^ :z\e_ezYֶ,geV ZWHYbE Pa~_ 7S@ptc>HJ'?"^qbb+ADw&Zb~.grwR\b-̓ʏ; $0]0w.w}~:ޗ<*:hEQ$u3FS.I 7JcdrLkFM=9ネɐR2jBPUCW4FgPa͞7aRR =ѥ)oEҾg*2|u 4ɔ*FyKb%m`'1s*@SD\F OWd`K.v-9ZRUv 6ځ)C&Ŷ$i* SqʰS]n3R#FzD LS_Dq )Х.I|3k[r؉ NmɝXo0C۾] 8rk I KgCG9ySWu@\zƮlu-u1#ri #ˋ7^{=ĤOiS/3 w>3|P7rЇ9NNEs-9HFvQ@^'>CO&뉽D)J$D`  RFkI |b䒜2d fb,BI׮oM|5Ĩ&Ll8?_xV5fҝax- oe.Q\8e2`c Zo4\v1Zˎ1c}j4hS!D J0(LwhQ7ٺ͉T9+ #b:3T@/RNn\`Q& &uZϫq-qH[̙3;Ǩ+ 6[W5Dz}8I44,M&tU) ,e!LJ" R'YK{lU B*1Czfb92E0ŀ}LUϩۖzi[V!X.;n(9+Sc(:uCLK |uns~ݏQ\\U3,ΰO8W9W9WX[g3B%[,'FSktE H^~ᓄj0[2BCF[NrdvV;U TY`{)2(2RX pl!4Q%y`%/ -ZT4mYއDž)WLT.rz\z=z) KmrcRm1cVx`+*UQBtf6Tg@gCfM@-Pޡ:{O3V!l% &@!_ mVeuE078߭H%ݵz\o'E?/z?Gyء~/c~oy3?,;2؅cW7;Q u݇8e>?hU`u%R(CBd`QFKcAtN"&if1bu1F̨BH'Rg 'eU \ ]!sp Đ3Aؠ4],S6zEVKj3c4ѝ-mh]egRKkmN"TEK%]UiS.j]-iwvO*b}L4ia<- @ S(R9$vJ0D`DȂɴDFQg.3ĤGFGtL1xRZn7bo)cNqJNd6bϙ? ֵ,KɄ|~9t]]SFf3-*Jc:/ p𖁙 b; =ygh@F± 1"-ޝB8Y2E;"54!U`9?暃r-+^= Ą8.NOV2 S3agr[zQ0%*)TV- 2WD\X"`ZLt@o`RUVe3)UA׵eA_uhA\_y0:]k`!/9 ?^`՘(9xu|7?w<~/cOol~.;w~/ 8ⷾz }Կǯן9.-X)S5d1%ڔ8ߠ}5^ ܅KbY!"`t1d2BR)!0Zu@HrƆt""A S()JT2˱_ۜ'o׺&ft[P ^4I|eg^ &swYYJBoSLMQv:େR]Ͳl-#a+nvyV\D(mv'"X4hQJo0n=<><"4MFcLV4!(nbz6-LYbf>guxfYέx}4t6;J6i? Fpk{lXtc|k@n33mJmNOii?fλ, [1mr1)c99U5vF t> JHP!C DN -p-|cTsڠ(3Gop}Xm`1_s{ݻT+Z/0Irqw- F %u{)mj.׶{ Z/)&3'?z[R su -~`]LV}jy ɭ4 B)tfcYQ(t)R\/rrIu]ͨ`"tJcrRH`<]Lq.Z oh6Zu'SR=_rUfy]>Kg:%}0/o:߫wSrO;wvqIh__4#_"zį8 ~^ /W6_q;Nv94 'kwKK:C9Uc.Nn;R+PjNB<ɑR&`tHG?MjOQhFcˮIeLqn-_H B&P/}KƸ,eW(`4câѵ,jV5aTx4?Fa}&8gϺI51RSժ0uJ9Z)S@k{wnm?"9vz+roai I}[>a΄уo9~trZ8hQvG7oss%5s?~yanowsWg<~n݂ UbA7+Zpcwd@K8G$YJ] ")>E("1ꤔdfaZL1jbR:R()d:F㱡6[WHP&dx/,4QS,c=ND6 y{Ե\^::Zy=!ӅF;m0cyzi>>5bE7(1[ou/(*^\^sxnCW>vwUMUmSmRU۔[;o8;!iʼ2ػϤyʃʰyq}&>-ýW'ܽ7 7+GܼY[SPU[ Jd8?;saӧgFO\'0lˀ'"]n=cqH)6~?DP/}{liW+\](fd2a43͘RأߧؿIuz{LDQJs]:غ;?}D߆-1,[(cs,9{=NrZq\6=>@﹖@Y~u^beWSxo0h3Hm^s~$587$Qzk鎎X=z}uUUs3ÅPb<(˴(Hqf}bsuXň˷&-yBh>ySO]gJxvy#kYnPSix rbTC']6s>@}yE- t|!02]%TRh%amK MTAS"- )J"( T٨'H [! nV7*fL̄26cFz2(9HX~Jԩ&4q C7͌):ڮskY%E>hnMV.-ON4"%l_3Y[sDVe}K*^b֯oN]>뮋jػ)TX. Ru8~_{4X6A*BHt۫A݅ے_Z&34v}4`7aLs^Q'WL#fᘩ믳=}=.z2N\/_A>* hTwnޮSZa|7e:V{ӣcNUZ\u9a=OLmy,pۿf3tYtJZJ̦lڇg^kcsZ#dB9FUM9C g! 铼O;͠{O>Ime6["8k/,dmJ `ՎVe!Lc@.L@!1Tƍ`:#Bp`/Kئ}?80S\]bT*P2k=!/%'CHA*]m&z،J QP2R)!UDkJT4O,x/VPSG]ĹU*YBz\Ƥߵ^"Ik=Υ޵%duhI5͘L]I׭V6^{9G((Uif@cƕ=9`~YP B2_\j1Sg;Ht"}ޡcxB< e,d~ow=UA(C]ZlH35cr[$ۯ9v?1fwPlSf$вLqgeyե9hXsȮC{ao-)G'n^&='_@*8k9::+)$\B%=Srͮ+Wm>@7]z.1+mg:EYjP2*GJ]9g/'SDH@TfBxˡ轗F~W$dbؓ =Hנ>,(x>\QNrL`Pu򉑼Z1FhgO2'{W(ww1 b#Tdm̤2 !oE>DMH3 IDATj^yjo/yږ)'O89Y՜j]7/F륌i1UI#wsv`<QU$j0lSkd>Мi?=9}$AOƔ{{x(}eb)PAkCTSJpn"&P Eĕn[>9^)xW1s@q!k TOz!/-aB亂mƋŗ!L7Pz 6!f$t=aZLc=҉tR2 (Ez.鈈QD 8 E@ Æ!Xߦ{(/y`ycP>~P70uiAvߡyW{_x<~glx=2(Q2Ki@SEbBQqN!eD!1q}I-ʘaWD.KR>1`(,,}A顳 =J Vfc.v=QK=38gᯋrѵ\/d&c$\)=|UN#e舥L] !U\qX H}ۃdT5r !<=};NIuW|=w~L>1&A]sW~acH/ ,ݺCg̗KN떓ai3~EU9fUyFF3rX~1'1ȲD{=9y&T((f4C6$끨B^ ч݄@bxNd&1>='@+D:@eyfw~λ8]A"ś;-6엸̰ ?D<^`te/bX8m7XMA/00Re.m \Ptw#}R֔dbfV*bȌXqP"$0{gB\\tH%Qh.| `C|Am2+ր oi[SȌu9.{Avz|m8=;(J6(D!Lbl+iRxp#8H/ȉ uT0Klc-a`Ku-+2cKOH&,Hbc&$J&o͹_kg/]_K#u_Z0cu-c/2'[BgOTf Vh@ׁ 9Ƽ=2ڐ/q_zPC@^PL|qM{6;)5ʘ4SRȏp'Uv~ӌ_{}Klq_Y[.O@Xi|8?Tѭ[iX8=t⸮9]hvm5py56ޱԆiY0њVTHkhֺ6(P10HH Q(CDBJ$P @09 (к=!_w, 9Lv:hJ 7*-12=EzHS3np&k}}:- 4e^uJ\28Iueu[f2JUL2L4F'g{ov$λ$7)ZRTBJ}N'l|5ߍ碧pJO]Kt%vzih]"F"3v)ZReIfbp%=E]l\D:iO!4::O O !#]MYc#f~@X2}KtgdBU`d6ވ\FMS~ư?iv~^{Ѿ^ooz|ѝ>xߠ޽O8##DmKXpZqִ5g6<!E6;:u2WiY0UVFp1|KnޤLRf/8hH֬ ~AeIr)Nw=ca~8 }`4{|bL@Qa\ 747f锦ԉR˔r=J(DJ !ʈC'%wDRrʦ%ϯĨJmYz_]b9wO <>rf=%r^pA]_ne-YJ t=W>ɵנz\q=ǏPU...H2HJ!]* .l %9 $cDf6CGRRI*Y|[1+idm<|I/Cf60P"'/噽 41X^4Ƿ^_oe)1E.\7*?fTR` IF <1Xϗ2dǘ/PTi.~j֠NIQ Bm$@]Kx:41Ե(B-xj}umkGz.1)#`[lzNYb1wpOf{cd:1͘Gʒ(A@'K4I\Brb$vqu}eu=x4H|c?sa(!sSQoR?x@:b!P{b]YKf_ueJmg}Z%(ot\?}2>ıTPjm=p?uE))KIUד@bʇVmOfcAf;CJO'2ŐSACQјSU4M`Gf`2zLmd>wlmyV+ba%U2jZ0Ȝ0zSXʲXtԵ~_.-y/zq@qq.ۜNdƪ$ɘhDUc֫![[ tE\5np%xTUB Ƥ6`؅]9XY(Jc2E(I&u0s.!ޣ}]i>)C. 1ZrYNږ.K/P8Ȭe<K6ى,gٶZ361RJtY)oUuO-u믽Dž$"E ůFƔ9[!=%wvytizxwq᩾y-لV\SҕY"̑,Els>I3Ũ^~7鳗2I8LU{Ml%IgVIe"+L?)$T4 UQRISٕH!r]h|Kkj[r5.9 Ǯq ]C. L}HK85/{ew x_`'B|+z|/ǘ21s:3t6u]s).a CwR 2$ Y(>q1|K2^;?h2(9e]ރ,=f>N}26j$( AY(<J?kG%u)C&^PHW*:dXѨmdft%)f%VdlvfD\Z52a^3+r:UjX.e1hģc'Ǐ)N!>y-]\}-K&ɄxĨwvP1jkz27)SrsxK|{ͷg9e=!Y̘"w1YdOk11U`JdbRi=?Gc_A @.q_sړScCiP1P!!RƈpVH3,'uͪ풷+טc:8}KUȡ!a߂ bƒik-F3mws}6͛ik/W#:]GZBST/yxLuk=%Prbޅs yRTB* F+gGE *4J&9f6 .fP=tGsFekpZ\K9Iy;%C]\ س>PEK8% K,}| Y( C]4X$Q CC.4qfIkgZIU,iM]i[!=SL;b\s"=b]c2Q3m3PMצѹ\hm_nchYtG'GC?ߤ}+gtB2t 7ݺ-70;7ofQ Li4(AB{\ץ +ш%^4M1&BW[)Oduxk]G𞢪OU$nR8?=?COHFqu_%7?=9=xJw|L{||[Pń1 py1:/$a(lI(e3]:2{ȻԽkwyɄ8{wﲽh:Ea|{l4 ]Pk)rݶSx!eIP*'2UmV5jy4#Rd5Ef|oZJ&B{?)`7?( ȒL@\Nqkۈ!3f.F?^%)"*R2 M3%]{"'V<2=c$]Ȧ HP2"H o-V I P(ע߸XRTUjm^R ȟÚIiHeϠD.n9+ʡ۱IA)ui:[hO OO"Ɨi|@t$;flݾ+0 [0{7([YUuJ@21FYNeZ\USl RL5*ܺy BY,9tQPǘBn{b#i1l#q~ˬGmm)n_hu!0?ǞONNpus{^m"YdTad#2oj7m~{8CM=yxMQ$3"Et$AAQY ALFH+%ɖHH(N|z3!>N޽鑪 oߪ}NZbRk8A] v${\O.-cIL&.~"g.^bN:!Jgp5,) d1QfAjL7H:2兀$,du CjllS'.]d (4橩Xl4ֲ>D_ q΅cm2.(s92H4#@Af)XeaT)FFQ˷A.'X6ڠ'l|#g D[±8+gXC%*"sbc?56؊))5 ͂Q4 f,5Mw)BO]djS390~njĥK; ,n3IiA]kReNݪYո+M?]CUWyo.3o^b6`#㱳s<ղt[fX[`'thRπ#2jq5նv- E޵aƨq[hИ {LaQB.*K֭3D8C1͸ }q!ldKƮͰw!KyPZZZ7>sSu+ŴiB:MӸv.02ЧNٵ]X {{yz}O,[`G][Ґ }" JpS3tB$dyN6pmoc'?x:bt0IXKtX||y멧7yPih Rw'},.^:u$u]_IsEv8h|ٶdABmmiJ(Ε ҖMI) @m̩Ms&J9Maq_?P@4 YsbtH$QD pA; 1y-He+x(\=u֢ }OBR :f0kղZ'Ago1HmCЯ%Ƿ,}lڂ>߫Adr Wy25an/{lPTlFUEn(i1 IDAT?6BبW#>8 IT9.@Z6TӐ% lS ,E1R!qzJGœsLGYh|[KFХ}jR{mk-6qΆxǚYCH{+,l[~e'$" eԅ)O*jeSEw 6 `Uj_cl%[לd͜y=q”4&”'zG_2rIB& BT"0v?%UdZ9jj )Nc=o֓C_ +,JA~ӻq8??+[[ü})~WFN87~o"nϟqpTNH)*@uvK'ƴhmط[Xv9߅w]ԀA<ؒ-M9!Avhmzcm-Z5^oQw\e-} CdrX/Q!!֒$Mh{ʢ9ud0)8*4õ18>,/ nU]xY(#`'4lp,X)[_fSc.?Gv4c5^j{e֮^eof{ٹsF"{:`ۈ7udU"#mZ稍vD)$(Z/v@U/ ܢ2d\+)KGEPg8n, PD+{(mv|n?K"xh6iAH9l-ED \w LYFA^eY$idd5UQ8<8<. L]wY{- w޻9crSL;5GdDZ8:)*ɘi{E"pc nY3 g-1B1TIJ-tI$kIJ"}ub#{g1kE݅,a/|MsXPbQϙ 9`V!4hHj7Q]VYR6.gb\ߞ#F;(zN~ gƬCB5dz!krvrty$ #oe@ݪYkW&̪O?~Go<&6֑=q˾y#9xefE5Oe>K<{" |Ƿw[P0FQv֟uM~Q[~p_3.;y?g8;Oxw?V~ꧾ>sG\"j@ܞ?c`$͂tRFʲWm>Rn7=1~Im1iL4Q.)#P[*1,}q~!@A3R!Eȶ> Z(zZhJWeSt*ԞU,dJ{|,`p6Y`efӪ6|0as$hfG׮#Xp]==`<ʑE4ӋBUYcp0sa24;7f{i]bG(֙3\\}zd@ocobأO ;Oa-3ch@G ^A.a 8n? 4_ HFqQ XZcD^f%^`}kl8D*~k bA1RQ}M)DQK,N (z`oGK#Ʉ;p/xRBJ,BIu]- sΟp=W1L2ɼH$ZhP y!l>ʓuJ_XaW`IkS1k2wcΟ E2 3v)\ڻhcI"RGFj PGPiq5 g!oсe>Syq#s͂Ҕ͜(X45u᨜S)Ə~=Gy[bFScƊ`x!~>9g=ʵ:Ϙl\ W2ad7h9_vUj `g|{?c\sI7z˘Wu߬xm_yY<1|/|~0|~~?zۿ,~{oY |_+{~ _9S[v;y~ԍO#Iz x9>kۯ8jg5vȲ< N);=Nr*?.` hJ2a!lR蜈.v;З{z9u98?c7y0IAx\ri}5{j6%;N9wQώx>j&gϲ~fFs_s2zj}~$C=7vv0P,NN:&>DY 4'p9*)cfX>.D#C:pm Fy_Z Ak\h­}u>uWdkL]eeϞe8i([S) Cʣ stwSpځ;zNdߵF;/U#tS/we7$a cf΅kq=3ng 63:x< -XK% .:6B`d:'U"sKycmuR"I} pľtsh& BNqqshlQYi1>)c!I60l6G@) M>0J<墢t_4)ᔙTx=" ̜YUbŐq*os/]Mx1gw>D3zKL{7\sv#`^£ _si(ҷFR+ u4=qzg5Vaȳ;?%p0s|Owe>1G|{FZ<ǯOO>n\}esOq?/~mm^U5$1ZLs{ha(J( (PMdJd =+E@%UXŸ/"@EGxOsaBEveE<_?|EFb ))Z}HE9p@݋>x>M&.b|5q )25ƌ%WSӾa>J>EDih'$P(\R'HDIJ8X#i$2J GRHeAAB*ña:AI0D9hK4dip3M3 C=$:=(d9g1|"h C:_娞Q!1 =v%5dp<}s<_3ݞr~,Y]yK792ˣܟ20$Ou8ɸ_ ؝cȼ\a:Y&vklIK7پBKy:{ln!x<|wQh A2z^ynv,<ٞ[Ys#$ Su-PDwEs݉R |(@{ƭӹJBږߜ 8kʂsٱotr[Z鑋2OK[J9|*%Iq`A'3DWNDכ\8#NtDF,i܈D:lm: j)Yhl?3`g]lZY˚+y\/rqOs<'@]kᣏ_ڵu֘( zoнtM@ȍ +] |e˞{&xN2C3kw.Z.05i£np:{:;`Z;fkWF) M/RO4 >3k,u&5kΡۄD|a=)s`Ѓ$z5+, 7{E`i>nIˢXrȺ}O 0kf,X7H6 0" L9Bhf,b 8g" |EY\JphsdYN",GgBlaxQ^0 >i> ˲.fc|3g ,hS瞧yjWЌ i5uYne*&)U+UIeK}t, l\ B8 }}Nq;=%篱~"kYIuUN9ݣxR˗ ׂdXF;Bn`̦X+R3n9jE*r2##Q]fݪY&v>}O~]̐< ||) =abim2VdS[kEuiLp &fѹ,Q.^V{%.3CuV3d1ZR)R\)SI3I}pPD+Ԇ6855eUQG5 95V踯9W,#Sag&N0'.x\-[@2?Gc.\ Y3Wغ.QN14{{{TmW5~8IZ4 Wxc{X ~Vw(zzgc7kxO=M+CS3lơ|0.,8pN\$4;;!BLj92*E{ElKm2:sF#Tƨ# f̧S@ettC(fٝ^lƀ!!}N[J!Ӱo*ջ@J^$] Udّ×c'Y[ǬMxzu%)}ИJKh"]v-.чE'X{!Ctl\\P]]`A!I?lWQgPtYxӚ>[/8,5g67Z7 !V:ʛ`B'X)c&@`,BrQ0΢)KB乧iN&g&WfVuw k~#\ 97nt-KOοm $kT{l]cWcqtز r<{oqJi8ཋ.v}pwA`WPRe9i^pxkbt>dD9bP腦 =X<Μfi"@jNu؉{\$R2}yqCݘslcKaS B'n꾕=H-Cr|"sװֲ}k*nh=C k=C <ƈMg[8{۱IK[K9gBq4,F1 >2P L!NV>K"'{J!:ۄa\*V6\+h(4 ̨1"JG}XFE Xx.#͎xcvb]. ih >yd ^7 ΄_bLHƤk-MMr8D2C^\0uM=S^|SOapK}!F/.[!= $BgHuOH&dkIJx<;*:tqZpOir6c`zʫ@ǨX}ٺ51{2;G`֤Ҩhs70rړJ!Pyҥn>= ֮.oX^,r8@x:6Z/kQu^,Iu =!PGA{НsC6:$J̇|MHk!i>-";Uxh!AF0Cīdqpнsw)Qm)puC亦IuZرt~~ߝR~;;]Fa60K]//̙ .X.*@+hhTP(tWI M Y5vFQByQ*AI=Uɰp&/,hHW$Yi0O) h/q* Xp1a5haVΡm$w~XoF`GGy˿$~yF 93ϰ}h3q>)Fظp _> nlnuOXKƌ yTU-mOaw{~sQ*-XA6h"kY5c3uKULD`(Ѩ$;`e2.2xmăû&qIʄ5$E'ĶcL iuQpܬg-jLRGϘ#i lFmG!Q猼 *R)4ܥ?hhW "ݥz951y'`70|:2σRݞ|lc A<>$/gP**wyx U ra0D&ap Q޾̵!ĕNTB`pQ/]~E`&(!76E.9LYb˂!, ѓcug;ױ׾hΒѥK\0[v.a.\گquD4TEik34X[Q'ZxT+zNGxb|/,^*Q R& `c]藫407 vk/lyNzYh&LΜeCHԽ[m*GL)Y2+%f<꓏qT3LFjP[9*1^"ExJʐo'@ M/εk/񩻝n?Y $*aV_c>y9_/r˭bώ(qUƗ,MU=gJcb;QZBv/.H6cM071K t. z$Fx4" b]4Z$0w8p~@jB`56fY"z ֒ EuԅE$aXDPH!T`P3 uccN:EYaL*a_crQoՂޥi^ 0'K&֞|!AʎlA؛ӑZ|YBUF&!Q#A"j>cgF$9p P:AEV}^lOH91M|]ӃЗO @]Wlƶ'ϗ\{}q^,ȋWV?;dksO}P@wbtqj"t4belU`=~3ؑ\ح(l<+Y% +TT J/օ^:zŃpԉeB\ S,nq PAvΗ,JN%:M.`yKQuB uJ%Lp'HȒY l^sZh5TH:Ri3gVSvjd8.ȆRS\hD˲5"2urJAy_;y{+T/~_ϟUlUj< B $f>2-_~"l<^{ܬ_㱾ٳX[$ɳ1Y6!MH I2BI2FRJ*Huu\>ED<9Z;v(HiTxR)z*D08kI] u ,RZJ΂$#M,Z6Y4F0 *K0Fc,%M#*A9o91ʚd6J 3nSk]'IMMhdB$Dx޴5\}lm(,U8uZBעo.1y|x_{u[N_`vwbc *7n0o:u)}db BA VIl>H вs'ckşs{{vwd| twLƾ7la,$읏Z9T mx[Ô譎 ]Et sAu DP c X/C\.hE\ }۶@hA᳌Q$IB-Jϓ:IUH|- el`dgcLY2y,~cȺ]4E3uAQ*[(m˝l-[Ec̦SnmsC3${:8cemuG3Q5R>k̝m?cyv$]_cǨ n~ L;Khάi]Mc‰>#qBe0 ߾G^`8 2s";'#D J Q$2=F"M(N5D&(-5:7Ku̸(ZuKKS'aJ? RZDUsx஗zg5VLS<>]@[7Vj>2M=N-kWtHDKr)Zwm#6D4{.2X'czٷ,h}`4MfqZR Esɮ(xЮIK%ZKhV$AJrsD{eS1[sE" k\sSdG:y8.8UHkB=v!~{=(r4z~rl/pgޣTs*;zDBg  ;<%-jho-ݾcp9٦)KJ3{=;-H8. SCp̳J2O;2Q5T ۿwgx^;w \H`Ը%' 澝aR,&eV:q&0Y1hFհ3 {-t!\m|`Tѓ(T[r, ~˅`$p, HA2i4R (aJRI -簱Mظ2>FE'*I6_Qjjހtx<+ɤX}C' %3㪭sAC CT*wu}pߓў;,c6PIL#qFaL3|mhA9uLke.h{d2RFWP'd$6PV SMwj逳s .YsQ=hcf9{FQgYTk9Vʷv'P>Xֻ#?l譭3mws9h{/}?4>8%)ReU'[e_|\ <v,dOf)z17ؿuR ,FU:uBE#>ܚ8 Z#H&ݹNV~;CmNx\U"ZY-{Ҕٳ Qy&;K$7)8<$ !.8+.u)F9&l*!A~s$XR(v]u60k*VZjBl U (hФB" Kɩ,x&2;vprhD0Li"k RZJ/^::CȨ[ӯj|;SqWgL"=%:}( =n|F> xA]0_}gt 9(U`3gv) J[  qM&]$s]s E>-dz(ZuRuyd3xo;9sN+հen0^|]{[̣Lm0"z/e~Ϛ]d`5(cJu=G>™9)ʯ~m^C'..dTt 2uǠdmm`}|8is=;/ލ똪 (޴(u}pfu3|GG4EIeY9o4E [`v?wW74@ JނT-%:ԋ9LamppJ_Ax  u*/;moCtx ]e c- TF2&I[pB}a&ԲNKa/GFN1.`k4SeAϦ̞ {K/XXXo1xs\x\|Ekl ݵHE$k%J ˵|!5mev94Mqmwȝ&G ޛIvgzYUYUh @67$A%7Q Q̄B1[pأH1l"4LH2"5H")R M4{WWUVn?ν7MHtdTuV̛y3o!7/ᮿa{w331vϳӌ$g5bH5BfIYFt.,]P'd?/A*|>NЦ(Ʉ+ϿK1ّ!><u/i3./Lj[ sIk2zȲDPYF#kp~ W`':HjL(9zOǬٯ刑H|0u}ȕ㊙Hռ|؞4*wZn OBcꩋKxL.pGw!JIQB,@UԚ[DB,ip2glܵb%d_R&[2BiU:dcrucf;s$(yPu KSdز&&m!QUѹޓ09ӗ8w(gJeT'{ٌp|A*˒ ] 1L3f1[]/݋&t)8 ᖪރa8~Y(iKaU1eU_x 1= vv`Q#TKK ŌHm&hr괆ekJMEHh~@'Ϲv"^xhtKh겦 ux1#IM'WF7EAv$B>X_G%((h q7v?2~iB]ϮͰ p,E/eX2ܵN"IeA]l:@Փ S!R9|Sj0usByN%;lp`1)2Ҵ-ҩW7%i[E-4BҦdxkO2VU8JNԾH1=gEmߒ6s;4`#Ń*] tKuVPX.pwo/KK EQґIG !r k&?q.cr8PZ}$g̤·.6,8Aw(.bZTA sGl'c7~a:nJWl,fB7BP"xZDZNؽNvoNNz,GgߘSekmt}{{ofEΘ& d3&PǏ'e x+]t._޹ٹm#`f sއA*D֚:i͵@'s\:w662ԽPD1rL!Ꮈvק|Yzz#bĦ;Fqʇxk}s7o uZhDI7S2IeyT;kszִaL!Dl#ry""&T:51EZOH3мozb FYN E&رN)3gШs;΍#u.8u`;sM=+.[pQ,*hn5V0 ޯ^Wc5^Ĩ+5 IDATW.m.]",ާKQX|hUu J&NP7=j 㒅=B` SBHCRJO<ԃNރ"E)7 gZƮw/\[%$P\5(9UŌaAͶq1[-}ia4.JAEApa<ux:cd4c>gZ-Bڃ'),kW"~@i9%fZ<…:A {D%zF|+'3b#=|^ LIĵ\w:t&\>9G!GS2 R JV[ٵ}vιO+Eĥ㊮JC&പY;'Ֆ掖O0 @~w刭MozI?}Qc xhv6&)ʲ$Ӻ鷒 ^d jUDlre8* ZCCT*{U$A'HS FԹFK'\:wM=ur>N3DٽƗt˟6(\u:KmDBPXFP9ďeY}ìmd]a(]ԅy4(R׮ٌ.zRrڟqBTZZ9B6Ik}%Uu: 0vO=5v%8|ש=r˰pl!.D|.BkDR*)/m4lFL-3kvt ѐ%kb +n5VUvrN!zq:A?]eC-}1F~?=Rs]V`7-Gvv5yk }k(˚^ojAYfѪB>Qb4PBVH!N 3HY̷DG :Rt <ДJFh嬹}jâ HKBL8 PSGkNR:<^nHdN_q8G`}H &u 6q>h25(!ț *052QǎO"+KtmuL`^S|U=&)]ƻ/ΧIƉll > +&X\0~L&7b[@4{}QdSʐ::['d.t O*zkjcTs=ޕ8{3rS׆/{KJ*ECQ@B sܬ-b@ sTg~l]d(0YqtDgrg^aCTG6/25pLj,>j^9(zbF_cxsΓM[k>B:tz?Hu صPpd;IW t&'e|GX,SMtmKcRBԷ BۄM?]@ݬV;#DZ i##&,T:uj/O=;Êz7'u>6Y:b{[jqf3;`PlEQqޒي,cmEBV}!Ƃˮ ɤv"}(A8ho-ds#7/Zb}p"&׹85 ?sld9'>6|seŻɓw>t~ 1b먲HM2ŶY+{=lTLJ(p 2Ȕk!-8cPB !@׻ǻ7[[ 3[@Z0XAlQ:5NYXW2޹qwd}Z]bo^.DBq;י&7OZo䥉@6RdOLODcR8OS1HP qŞgG3fm$(q:bHv$!Vd-m:p}N@*ElQN2ҲP8$LvnBD $Z&o Ion[[CgQLrCuN" MmOSh c7K"F U[2qglٔY҆-TK'.?WϟstAV !ŏwEnq4H-=.X-c>DM,FEA񷓿]Ǐ/Pۆ yws]t$Ե?,QEAl.6F@ySF]|0pMHaL%g\ɸD6 R.3IW[U.Kvx#Y| \tou)iUUTQ'ݾsM6R|#؅@0 M9uxLeSނ #U cnRU6jƫ|M_-~_k7~giG! ?-K_H/~v_z=:8Fc{g9ϱO-y3я7?0DϷyKog_=CϷrv=8(u?yi|iT՜˗/2X/v^?}O}eN5LJ([q]g=||IjB, !'r"9!j >oaTc\Ҭj>2/p!0q1d/,$魯121vGgMFBJ}NK1l[zlBnٚI%*R2Irmb^g1޹tUb' K.^ jf9k(̄!"(ַX 1S{CMosH/ݯŀZ\ Lm8O@(KF0O۷6.N4P< ;MyTznԺ[]r3<{w3LM=31G(1f7FI1o#c'ѥV)E^՜asϠP5',8}S?O`$ "'OY%J"DA.,.*3BY%Z 8'YeFrRRkL`}cg Ƙ6\\ӂ6"0QK)Rn6pXE^:͒=v>Ǚ<M_; 4}Y v{ z4)ƀmJA]^!EN>$_$s|t_R7ԈPLeyV%y9 {yIY({{{\p.Yț/ &H(є6auTh*e5%P8斡Zŋ\?w[UH kNax0wxUg *f֋*g0#"[fe>o>؝[<$xþ)9ybBeͤ1El$YQkw=齺}Eo|udEqZw;7PpCڢx'( 7,]nsYlp䐞t}>৳cA.+J RCAާP9d+\KiPa!:#+mt!ATH {եrrO0%{" ])g +ovuλ4jOq:7.M'.YYtzq8cSq.ruL V6s|; K-&g ?*/qKI >7s~F/I?Q~>q-mSWXQ|o>3*-Ȳzd&e9JhIR*Zm@0sl/)(E5>:bS&3MoGQD$$+)0Zd=A301%z MHttG肬1 Ch'=% ܌9SxQlp@sK:)t`) ,gi.^W >{v5mQg)77<¾ؿJPh4^s^KVMn[S8vQgd)^exX `),01vK0RN @%EDEAgnn(sy~6H%:@Ѿ#ZE.^2%K)ֺ\6?Ւ§_K$T$xIULp kZIor|-P1SC2!RVo\u}gnXK#[ڜ+MOpjd1ٷ clU38뛛 EIf/G,g76R]uw^fb0a#Reٟvð=#9ɤ&[FԆ:Jʜc_c|]fJ=c2tũ~#E*2)t)@5Q)` &BeSa4>$˲)iDSrQY!Zq MVI _likx٬QWX+Zڤ@w +3دkBYE1 a?F=[r<MɊsNJ7zl a@lԧ;~7 R@51"k8Kg"'&OddBv~Nkk]Y @F]XL k0haU]".^t^"%JeB$%ը2B0vO>Ϗh{uZ,9Z ;R1LAm"V7Y.$ IDATs2"7.M8m@ 䲁kTڸp {ߏβxI)9rB Y$kLw7}9ƣ9c=~Mg1%̙;f? )= N,.ddfLfd"d*e-L)%|}H1cfSHc?ѹX`ceH&+;2gQmHj`S?3c, [ߔ^~:$ \0uORjjn5Vc5#/p`WW[}ʼ$ h]TS**I#=dιiDcr5\KwI*G0ZC$T=t!ĂqBUI u~BU{dCE&'Ք8s%误Sb0XcQfrJ;AԳI$vhg3;@E(n?tB)t~!ːkk?7ܔ&׹Y@-Q qdbmxbaB\CP5~PoRIq@`eKw Wz W׷{ tpdZ BF&̩MֲW[K1]TBs}diSA ^)0ԾQRj;}"U8v6z%q\=􊜼(z1߿֊B,[R, !3zjA(dрPh4t9qmyL!vJOS$KK'=R֞ xq¡zHlLp5f^UTuΆHeB'\j.~’R*V+[l?C!2yuZg@N^p?b2FY}ɳ "2$c(&. BlIBqL]WD1(9#(Aa1&rD;b>78_8f6e:7ci.%E?9!N *b aRFGngjן/O#o7S')٠ t\MI.R$lldB?LPgϲy}IZ`Óvw[1RCWZ yj!;P٪q4eIB\}!{*]Ⱥ eb"uM&ZGf{tGAcHy;g79$[ o?SSδn0vm0Q`{ i%W؞kfvfHTQ%u'8f.桔" gcY+$3|BB0agA޸ 5tzV_zu.zbXoqߝw[/#''#'Fl&EJH:u96x#KP'R٥|RaDrϴ+Xؽ*f*"z}zf?:<&OOoXq#?rv|5@ Ow}xq͊(`@Y)(<)x*CL!fԓ]P9YVeZ8vr}Gj]grwJgw( I`W@23q#dA07ؒ'qh:sZD lE9>TPыb 66! [ϏőP/z6Sm !"L 瘭-`16*Fx&#&mfy.$9|nm_u--$+U,4޻xk>QWfQ&`l@eI?Uu)rjCLbd#wLo @66Kw2N^e[]B HSꁭ0&m}f#QYeHo؏j*YEĐ"ErzQ (\!]!P['9QqaŇwާM=3i$KX(/!!]oq8>z @gHޥ>vg\t Zk.7aAZI0h۬j ^#\/ԨǾo{'~;?]o?yTzEpQȁc^ n4x1~ cO׆7ȲeE91>|/JPeޅ?; 0gNdsoF\ܽxVrlo)Q#^vPHF47w@kQ78k u]aꚪx{烜Cֵ]Mouk,|xc)6deo}v5#G!eI(Kf5򺦿ni'C'FdCSɢoj#nQ2(B1B㐂rIkF/u^mah|o &{-E |})SO@C8u}6TW`% ^gtOOrz ;p}a;hm@w;ko/ 5)L"{fE0e8A*<*zcp3pCӔFJ2ѣ !GA pNR'ۮ4xuRN\]^7|Sz_'l6ɠ7+p.IA c"YK ޢ;s)6=jj5/}ⓜwOg'^a36Noy_3Gfትur1R5+&'>:}}|]'?ij'EIAο0O}{Nlx׿5 Eً+_| o|[靻}C}6OםkS1c1ḨDAd2@-|h 5C(Zߴ2z\S/g\VΑgMtIyK Di$>FsPە䉦 @?8 y;g&|f͵H{ni[cq'"vECm~ vsgqᐍc7QzcT,%"써_x_<䩧:K폩{C?($eƱ8mZ=\yA]8Զ%8\2Es T)c|)\]աS8C:oc~O򶹛Q)јSa{,&YOl{Ax:1}yz^N03~ɟc$I R;L|$A`w1XaW32)üg"&>0~쳌s&]'!\ ̒3$s`P~ K2ogTrT çOnY8"*{ŋ =`m;_uXx7.>ùx7=SX+ش -xD6&C&)?|} y=޼H^q08vg <}ŋj+W=ǶocUNPO;bz(Lpqʝ2Ca{)#9'cL풪g'\d֣V, 3(f ZW0xʾc.$.%;rS$"D$xD2@1b~;۔`:3TpCYAQRhyWEMCD <%)oI`;aaIsUj7?!v:61e4w['7гv.MfQ?'[9#o^ǟh:ӳAdYNYHB{l hMk)Rn,lT@);G9sM~Hy'`}FO`#vvFTA8ohg(ާL)o?[{=ޜ/f[*Svo*dIZr\.#{@q*ʜL<4?IvUjk&#DE(qɋHb.+v?tsJ0og x>';\V98L.,/ MEΛHǕD HڨS Xg9s[1S7ňDv? bL[BH1DSdH|z\ΥqQbWc5Vj}bjv5~{ZSGtLw2+=,,fay^FIEQ(V.2,"E7aK1ni?Z&̧O/MR-F.`xK{{^M"Ҡ{k&޹NP/Z@m,O?x6Gunl[ {SU\}V"0F0&!eN6Yc4qUɚHc9ر&;.R"76P 49$i఍:@dtp;7cqExU!֒bbez]As\#5SiMt NvL@ ,RfB%2L!>6az윹3wusGs9>&Lhx﻾H ]饰hέVWc5i+ vG>#w\VvU j_0ƘTlrӧ[eyÙVKD@rs)E)}twN~Po 2Iyޕ19%]@]#Z& 1µk8yq  bwZ.?,/}M^bFx!_T/ec=/\ۃ& t%__w~CP|Ta ! oy3>ϟ?+s滻A}} q 0I:-ZkyeB,;G8v K&eZM "?, a&T Z82{kL ݉=UVMOXVhAǙ}1t>&SJ0@I}_NliJ) 4?x.7 K:qw3d''5'Iv}-ynyfKrܚy4/\į[aXg;>ܕV^9BsnB],HJafp2 0X`jtLTyDEf)G;h۴T+j\i J(]*nT)UT}⟕TRt]=-p@7~=Q :kF:A@ ]wԤ:Mb&ns[{̀.}Wnsy\3gX|+ u1-fm~ P2'؎m[qݧHDD vŌj뢚:7ǠFɛRߑi (vaw^Ի.UTg.UToI!dHAuZ h#(d3Aե偓j!&m',44^|r Z*JfC95Nz o΢MX.փN{`F)?b², v2;Yx%T_Ԏ>3,?q mۥNjQ=z]8 II}MVkjYoѤ4%E2Ni(zgQ1:2MQimBF@'YR萑%Q f:TQ..+V/ZD>SJj=`*UN~0:djy%@]g7T|Ѥb&J)FFa]}=:p!F z&Ǯ{>NOkAp0=QZ%r=ZցH) x)W;؍7`;VÖܩZC670~,'=鴫Xy*s>޻ƹm=G;Sy˯a6YP0 iQ*PA1n[헆I)S F>&iA e ]EAds+-K*U vRzCJ AO2\նu tW*2Sa!(P׉ QjggRP@ك8}F:Eƀ:>AQ5i#ԦO5j҆PFJ3;z]&&}6j#0 GeASmtEzs /@in\n{o(Eq,u}qۤ؆Q ޼HqN$JNRJu$]*U%qw:Yzۑ0$(0gn3PQu4W-i݋dzEBPz$Zo8}rLֈ5KcqN Pw)q(3*v'?Slv4g!17׎nyZ?} ׾mqE.yiR2 ʆqͪhH܋>=:42UTW@Fa 0&mR~KIu 7HaKC@ 3J!5&ZJܱ1R3` k-rIR[ 7`2s[5j/%j1q.|\ǰ@trS;NNjD!?V|5ugraNo< YP6MʆA^T*UW]XWyηG)'2;8xǭܰgP_ȩ#zT]C1=w_?ژ %wp{e|sۺ/Sdgbd(nz+d[Cl.KֶB>^E { =@cIAS}\)6<ܱ104}ͨ{?u B!DiMJeM 6u٩)Dz5P٣7? IDATc&᮳lϭXJ#o80>~Νj'Or_?B̙+ tCJ `4ȹ٥J*՛ 2" E rc7k__,m5{{# ?L?᱗>ژQn~/;)إJ5al| cEQkuG:%ș}^zYPS`?Y&bPkxA{r31/^\B$r\o..82ׅ0-ש U`^Θɥ6޽&'{&kFu?0Yrys9v:OyN}kd?Dv߾Mwnk{qN77u :XgfeR0^1SJvmܿ_-a*,6@c/~G>@]g鬰]Wj=[&[T6-/)BēKu61Y]%¾uB]"'zyFö40MSO~aaMa]A0 MR w"u"b#_u;ۭn T r=|/nrUKY9dy/~"&Ra)t}nu8wٟ'nȶeeˢda@ *UOmiq gvHO?w^#Q[8Wj=3~K*':AmLaX5-*.jԭm5lRhL!6YcӘldkB!G2,7Q&5JkrUΘ0hl^Ɏ$f&A`7X)Rnƪ]~gѻI6 z8n;,FF ycy'O"6_fq}RPaaL(UTd`gp=Cךk.#VƇ`S\#!s{߹'eXz||# m㎷Z,ϟyoj6-=ofkc?7?J%_.񡇹a0㈙CZ:Ǚ* Ҷ  et :oxs `m~C.ak ꯎl#Ї[L^ʿϾq*)0C|ݳ c6.a>a' _xc/i.ՠc?Yj!`>_杇G2fQvm8U .$h\dn#09&fvPКD la}x"nعw@hnR/Ki`eyZ u:w:;+})2mj9f. Ҧ8R.gf^{ Ch;;6Pk I*ȍ3Y0ee541ZcJ$#sZǎ4b !!dq,qRvZE+du>W0(i[TRimP.wp{sOGOaW'˟DP[xR8"=S $;g۟Oưwc-7;Wj&܋^ "O$?I.gAPg u:hD \Ftz0Mn7Pk,\ŹuN)c/γXw <&aؿV.O{zĹ/S'fNFzuS1ytPg|{&4N.iY7yؙ ٝ;^vo0cJi Ll&CɐDoM A ?Z{ zMZkZ++x++ܣ ucW.JP0eaA u% i7fTDS7/ؕ賳ʘ+wLåE !uJy|X{$^UVhAy+5fKE*J3#;ThtE+V!a0N'C@+"[h=K,ցzvvf]wa#9N?d&ZB#e{U{Z ,K]mʥtծMĆ$~P:Ӡcy4ΝūV{G7iש{]w{`&:A}FJIN , RJ_RGF7ʘ7;T[=v?Wj̐! Azq:\f{u'Vjdɝ{}<C<ϥr uˈd@?;ֶyT !Ҡ].cNMlɠE"5D1n^B2rTڗD*ͦfg˿]ofMu\@NM̰]'1"QOe MlSrm!e_ BsGdGQauvLwy8*S)~? !C(C# ۲;W xIJIٔ B M*U vRJ(4RJ5]`TBF ga;\IwvMP_Xȑ9SN*K,Tnf"1Uy%%Rz.g gj^ǯ׻O ~K}DF"Jx9'cc_b{1gMDlPkX_k;w2wӾxk<<-1M-,5F(,¶/ZdQĭ(j9HRJ|ϣ~GYw֨0DQva-뙾9HʒaPiKTR~[>=*yƼq :OC^:`ԉBMVV%lkl爣`eJdljaY8N|H~l  -5@/l͒n $e:R*ɒ;6b@|,v>vwv.7qrԦ1w:+M,L|j|(24"Icn7Z)D Ӊh]0 NM,!ٓC" ww)7. s"ve&C[vsO=W+ "h)MzeD@ӏS;w'xLǹlp!3hG -0KZSJǜG  :3zP7Ӣ!)ӻRz늩8{+wLN6`G>{oG9qrb@uwvle@Zs7(V~Sais䭵=4ӔS 6cӌ55.7"YEvLQ*)Ƙ,ȃJM`}id&12:їzi>k- bVunX].~L0uRʞ.3>؝w`=yYD{>=O[9C{xAVoȶfgԓTNoh9=X!O4d3MbZAym_5P<3q?i߹RzMkmoCw+9v^;w#c]K.)uF@A6O2 n~4ugy]S,=s:|l>t?o_bG/ygielf-3sC2k RD%@ácgE1h] fM6 ¤aD;Af妔B ;^@I\Tϝiw^阑'jKpPx7CӎfjuEzuJ:Mj'4s$q{}>s__KOMD ҷz©.:8i$"#h8jMfa 6 GJa0~vRJLv^2L~WCjg/#<~;o>HʘK:"\|v lp/3~gD׼xǻqkvg£Pկߘ? }wy5Te%jH g%X ϭ3L51o]*~EPcGXXȒ+: 5UI4KulEMaF7Hnjk>"f(4PG1LNV*ДNw+јaF4)M OXSCfb0w2oѣQ"oT0FSH RRcߡpu]ǡ3ӗ8oКK{-~'"mf$0QAmYX!`{#[ΨK3cg̒i`^5*U\F0>Y,GʘJfQl. Njn|cd"lU_q̥\wp9?\u܀41 @Zl$B=F뫗Fg= Hi>\v}p=v6P wq=ˉo}f7 ꒓J—ڭq}2z)HE !eiRa4LtM{/|gfzx#]ꆵ |G?5y跸&39=%^S.Hˀ0 PzSjږiJ"T,Ѭߐ@<})o*EZMj*M*ի|H\٪TvzDYKo1[Po\R7 %*WZwfH .]9EfBIwEkCa>LM׿=$ԉvڥ"1KKkLT:}duZ<u]5Q kZF|Ds.hF784Lpѯ~`tk5y̽2Go8cPWXp1"AyRX$8`ҭ=F !JɘiQyTz]a38[w6RW.yR9ui |O)dوЁ=`{fV&r 23vaۇPJuV6λފpG"Ivz^^Wڶ(G$nL>}ӛ2C0m{h]gvuv[?_~-FZ>k_ g7}NȞc&v|?;+,|o/wMfbw̼!ARJVh)MTmZUJuKUw4+4RxJӊyM@jC $A2 ZN-0 ݻЎyQcj4ML$ Clfꖛ _i.. z|$فbnM)r ؉$%^zu'{iD?Zgl= u~9|0O>Y\gG6TA@N>ѯ}W_68uɺR dm;3Kːz96RR2mT*U7~6jRJT2}r۲6K5?$MSa :BFѺQ@k0g{ٳ 0,L&C>G敿~Vzc t:)!(bƮSwɸ[gYb007xB?%75>)6O\}]%S)E^O\*Hd!ys=6Y9s_O1s]퀣݈LC4MvMOS.ZS_nFn{<^LI3mT*UT?;5vRzSך q&@lX+Fh-D&aRAIhZ4 "ƮS9 uf.GvgϮ/WѩڹBcB+1s}X|i0GAAAN/=s/c!n8V6 B7橞>+P=yo K2ѿ& Cn"UtnvsƻqhHʖIVHiTR]J.UTW 4kXbu66K-2aIN0PhPvy￟'93: 3 A=`7SK'bv4e LS"[.I\. uɉ|e6A Ct!G|(LӶxzNBP[iyaqӶD/'Men*% oH43UT)إJ*jEER&Au ffal:;c;mut"Ӷ I1>NӘ]TVtJjVikE1 FT?ד֗ߨPANkn]Z)l/Lgll~.TGѬD_>ImkG#E:l>O~bLi5 Bڠ^3 ֞G̙{ KmXzQ*10pm0nd2}6Pp-i'UTWIŴW]XWyηG)'2;8xǭܰgP_ȩ#*sw^<7>J78|=2{A:|GF2o>-cгUȕajGyn176oXa8۳Ol$39A{i^"u'o[Wam]o4)-ࠁ@H35k&Qg箬PyUg$44m|e?)c#oHLL*U7'dJE!1F_܁G?e/\ן>K[XIn@]g鬰K2hJ>$o[7@)AH3T1d N92 ֡r2Rk6iT*Ln1ӯT+URiSFeٷ4s6Xa u4B&HQ;mCs s̖ع<4W+]\no wAsw] Q:eA@N&8? 7 !10mU*U7ڧ8};$;/Mm.6D0A:|4r 0-l9: BhV#U06E<>Gf.Gv|lw&%3~/=n;!f}&*iXldaT:{uHؖnжuTNdG?^Fn}*6wa@qp2^gK}abG (JI4uލN*U v07,/}1s~mܺќȏǣO_1[{C>O9k9,]8sO<'jǙcpZvOp/?{Ϝnמ1o5>rOͲ~vEH+Ԫ1*vb9c(n֩ۄzH$76F\"?1iۄՆiloM܈P7 $z~ur],\ u_+ g|"DWa@ڨ\inN5 _OmcfL~OܵNHaϞm;fRW0ex٪ ցDۯYC&ʼn"_gv߷s 92!4a6n9hNPg!,R%AպKI( RZ/f;c|z|.ڸ:Z{SQqWZ0As夠dd4oTR]`'(=ȡCɧL2Y ?|q `c8tDTtl|xH!A=|w͌y|O}9*MT=Lwͽ'7OŠGf|۸mw_:NJ94~I:~t]rsuZv~|A??}BO\/~"68wa>n~yvwChRe8X̙94Փ0lw%`+ hiMK)|Cb[#qfxF:9yg :5`җl`Zv>/`:ibZB w]jKKNu(ΛCZIRbMM93D~ B4+ yOk Rb|8z~]PPy 33`n_> /3}Lh &bTkED:tSaP LL6O1| O'0pyˊez@#,v&KYbknfGI |Vd Lm8yg ѾuEքkd| 1 iTR]` }.q+?oweÿ?{S_˱='>r~$D>+{p<t['?!~:_FAWw|wQw{o IJ6?|OTO//;豕n2׉55J ǝ3]bHieʵ&K^8;&M ñ1l i]Yc6fݾ$KB;7716DCIPjc(NMUh) @OGRbHٍmLAfbIx4$QRT㵯| 9 Sk|ȍ3Y0ee Dz>ZK<_S0ML莙*XZ1VgL!1>.ꄏE~rS2c&߮)23,su@al1J65hIPYAԛM'ˏQXrPYEcQ#@]Gv\gKJ*UְʓO"ЭSZ ,+j`X- 4@x1uX1ea[|0-߁|s0Ą b0,iN]{r0jN;&`nЫfB8Lv' 3w0M(֞i@z7]2$U6 MC RR0 l)RTR`zKRxH!G?~edx烅zQ]- ÈߨxFOma Q6R4Rt10VvՀf.0 tҪkV>(оpyIcONF`7&S.dpjJ!7hF<:Νݖcy,>4Kcxo_yu,l ZW-DôL_1yLhL['cyx}4 1lOlQ{x6d38CX1LˡVQvmƽ5eGR3!P0$YRTR`*U/##Luڕj}=˴e6( ^uIE"!HQ}I>niR W*ӬV6t\$ m&Mtv*DerѣPD:8 DS!n*E}*' k69˫&Lx#bP> Ospf?Z3 <<-1-PPxn h[H%ݾ)c7S2IJ0x@RJ˒8ds9`-, |F6K~n;JEzx*2y%>*{*1qZ, ݶ͔Რ.UT)إJuIcBM ;}I}ִ4h).a։IVӷw&:adRf:8p#"%׎b܉155#GX` b weDF!W>B# |RQ~u)/ |W:řke򦛘ZhMjѮX9w^cqr ngcg}<OSx%w>Ll,c:)Ȕ*;:;Į 7SC q[mڮG&v *hGiz5'84hNl ZK~/s!OGZl|RFQ;)7RJHyy]dgy Ab;2wxyյvR vpwK s/{g9lrߌ9J MܸƭfiGա" CB1:.}B8dԑNV̙;gFrRbXbk{R1NiYwv)I b],x.^MHˊ C0 {oo_ -1N~~?5O=/Y-;瞷4_~zE~t;>̱elfK_>4x*xcWٯ5Dsak$P;T!a'b=;|:Jmr]X3] i#A]wL6S.Ә_k&u:(Zd"): {iMvuZ G`IWm/(2H5N:&z;Fb2'P&nҴsdi0(c*l>Zkz@¾ A٢[q<hds9,Dy%W}9'׻R*@RhGRرYc;lO;cRbbdQ{^Mݸ>'ft*괟vҼB AM<)AeQF v9;yN^ȷfQ ݆wIk qğOxlFRms)":ioٗs˫~iE[/}ӣc}͛xռ0Ut0&dȷ=?>!?_x3_~q+pXl z?[_b~:L1'{cyv.wqq).{ЭWf+bq; * \l[:QxLe* a;p[:#}*-?40Oܹ|1(V)LR`Io[&ֺ/N2% ̕(nt:B| .S!bR)2wWWRĝi =wzݡJwi|ta?.0m62s-M n4 W+ZLt A05z5TM=gnbHKs( `YXNb"&F7d2Xmr׷8 5 0 j6G^e"ro2((2.qqJM+ *͜U.~d5Ai0 t麐f bf.FBB]Mf :DZne۸Ց.d)tEQY&Nkv%h6\p\w`* Qi. Av{Hq)It)BC݆cHkMݡInT \%cČ3vƘNJ$e͎ &r -9r,]ϣ^S($vTƠJe2(2(KVZmA@K)ïֈ]zd_)!v*>¶ٺ겥8ĄqR"OBGрEVjf'Ynj)t: YVdˠʲu.[)cvZ_b;R&f])6.2؆c;q2Ly%lgN%'jh..>*Be]e, ?ӄe?OoS$;O͎i-#?kD[M+ɯlx.M\]VFYL9[6+. $UYW:v;7-ԥ?G.Qu]')^PfpG*֑.ẍ́EҟI|d*J16:9@ggG1-.fSlV,,֭7G1>j3I\("i~ A$$Yd78 k!HVB$ٺ,>(L=0gAJK pxWp˫oT2?xc.[eQ&o 1m#\?kQt_biur/y fRE9oeqtzbݡf+Ylz>nƖ.+ԚBkTFNݩT<u E4 GB]~zүq -ǜmBYi-I3*zfBUMY66=XYJ _&JR`ygt'YG[M:Z qp,  Cp'Hwy~}H+\C,cI2V[C+EcvYOpđͯz%wM2(㢀]?ۼ * 4}_!c>6VX~K6y<(YgWVX][JlTziDswa\kBcX.4&P+gg5ܝ;ÙÄ!Ӭ'4=u?K!!&I]}j`m-?gT.y`nh{MiPXɔed +[Y16ի2 6ۣlQY.4ULLk7'&oH&/nP~6R+&s+T綳eN#DCoW|-2.GLDsTO$!i}+|өIsHqnxʱQƳ?Xh118Rb.>m@jL@hMj!U'RD*B$~l{^ʁxB]^~3qj0dzdl&PMz,vF#7+2!c{6zHBB]˾L3f"r[T&%"1 $Vf` (U&5˲&΃,38MSrB1VZJFu^;! 吢Y4sz#J4& X.wm:KKtV֨TժDxuD oeQ v_V./ً`o+r[T?,savޛ[9=~O}1VKrלa_sGE>q<.y+9l^\m?u'7Ŷvc?!CmK:ךMV]AHVeJ>tP7 |҈] ±CG!q#ůT3Tٳ9; ;3;;7?ۘ \]e4! 屦uFnca[̬ܐ,n֤7S?v$4['ց,Od_gd?q`''jZ|m'l|O=$VCo]'zK!]7rq|U,oۏ>Ǐ\jԫ] wk!/~~Gw붲Vv[Ϲz|W=s oU?}oOeuiu39d- (vl,r]uzyP'冴k6B38BuĽay.,bnyZc5mxWrmc䗾LlX~9 ~mjǯZ]%R7Uy LH`%GBN7,DfيO%?y7E .:5H:&)υ,a8 0_&*E8Rb$(a^kRU*Xyx)Y†%11vᙐK=/CRb9~F H,!p͒K uveq`]{ʾ領C}=B|1{}ޯe5Q9xy?=G-Oq?7c7w'w }|>m>xξ/;_kvr3Ǫwy/~ً9Gv\yZR#m%?eY(' 9R5`IVr]dG9T2Mu]\۽N׾DU&eÊ*i@$b'6pzvM/aBwuv%X=fL7a,8eT6yi9as"% 4eAPl͈JB_ KIK2sc([8 a@wmN8RX1h)CSB5FMdTgsm+fb-Jj8k?PaYX[k+*QRJ+^+_=q^kG_eQyݳ!ڟW>dmtO~{bwpq|+\䫟.^pMϜng+<["ky{ Ajsg94t^4,wh*"ɲqR{} U^!q$peۉ㠥L9S`i9XwJ{,r 2~,).cP`IyN , <Ӆ9i; LcQ'%y p@?a(W>Fd zj 1hag2@jƘ\64 &3y 1{"d:@, ֆZp"gOa%0X$o磿;|r5"r}wʸ]<#-fqǭ7 _4oeQF=+2.М Uq4i]Ե֚& +.˞e%V2ǰs+8NTؾ{ѣO>92q#(H ԉ>@jsUц(Ǹ:MATx!$9!)H{R+6%$̚JM|YY/)c4Ii!bo_`/;IKٸL$o|DN3`乌3 @7 3\ aI|?1<,Z:PU lƶh~cӄAۘG`4d׮h`R²qu4 ] LJ+2J{vD۔QƳo^+֎}O6ܝLj[D:> )+ NQz:PVALs+"R#LKtJ2rR&0be? &\'L_3ES |L3 {p5q7s|1/f!wͯܺ6e?~wГ3 |^wzKkk4fH0R" [p2i)bL!(1PFvl;p%",1Rb+)J/79NN(B]v IfQ"iiYYKT/ 䲲Gc~2$'K9M`ÙYSՍ֍L1C &93(T@e! F]79S)ZfVw-<T]Hk"!- Ji"A;33e7"L6x+'O>G!l6`^m{ *([0 ƫ %&SCSta/;䈥"QNs2( y ly^!K0Q.(? xF絵ϻZn垳GGxl@:eOxɱ;{^ڬv{!8t=uC6lsbt{v"ӽK)ѕ Ξێ=yr"ǚ%e~v `YL֩wJh4er+9 VA}lXi)ajr-%K8ee`'GpŌb\6gF!6uBknkr sYZ9=wM IDAT"-f e'~pcB I$ ҆c _w8lߎYLc z~gԕqf.6[ }|ԝj͑o?̢.[VO??qw\M/;ȭ٢:ZC_>9zF\65mY-]I0qX." 1;5TJF.cߏX\ y1*S 4%tB*5nuC8qQц0}o2@B[fi?,7+gN$ge >pʂx4?a+d0LmSק!>? |ôhY;-2v3w$"*Bǻn*7ވw/ Bʉz9:uy9F1b~{pf[k:`D6ƵAڈFcn"3aϋ1؎Lrl^LJ2 Wqǭ; 9>iq2.?(g,3`vÀ { 7DSLࡸ`>3]T=5??}9&`Gh~˄c!,ۧ+%uz;*I8؞BGf3?z^:y- lg& T3 0GD_0?צ`UWz\W}1e6kS^'6XkR$"3~L7k Ql*AQy_7 @IACJ{PQݻ|?/+uE˾tΜCtU=j4 VP)W\ThuvxqOhz BTouCBcnk.kKg2(OX)3v?xcK.6[ cyW^2.ymKj.]6Z)αX㘰ӹ`PW\R!Q;ńea-lý •&"]27ˍ X[:Vm`{BJRx:(D-d !J'\(/l"WYF1,0QP4Nq!?0)89t)s1i_% ,i[ZgaORXϬͨ˄)B߷60gufn7Qۿ[ZL%ހs'>^ZVy(L++>vW n颥D:12 cL]+2JCF^c{Ժ'/ i-/L&m(Hu:Y]m`&Nlu,Xn(޻D^vDtӋ>r6Z-;1ư\KJNS&Ƅr΁E|qjX:=>1ލh>{*V^,dA?23}@&" p2hmrMR(擦b1R@'o2 3i1]ӣ4#7FqC{JY,Xz+ĭVM_[Ō݈:`gAT*{1B;J|P"ؾR&R+tqlb)"إV$}Kf)?(?2(UtB`93;vP?pǙzZkԩEZ_'(-,OF ҄YؖL|)wRݶ y! 21ړFyfk(Ȼƃٖ5ct)K$p|/rV)F w4ۦ=6ı  5~]# :`4(rb\7;lߞٺ"M;5Z)YgERH’uq+&%B&77jUj], m =Gqn#{{-]]eqFcWFef/+ڮ!wP]o#7KClE rarL]GchlRTWoT$u=258vD1-[e:E$\v€H=(rʂW\*^mDU@ 3v9[ C ⤅V}:!7ҌϠh ) >C :1[fGդФƮpta^N!*N 6F1lF;eYS8 !ζm8W]E879u F^x0DfxB`(Y"Э&V FCDb^6QFφ(2vaH;hM`Y`L0crO.1ZY}_ڸ+m2o3X+UPzڭu@ $}gˢ}V7f}]!xfn: juB毻̌d&A8+fl]KhJ!LέePA"#qR^0J&2H@Oi 2(2( e kA@K)Zf;b 6ԍ[g K ܕUssľβ5jVI/ӏ32=2de+>+vZ\$q#fIN$B 3i Ahy B\;0:,xc\VqIg=A]tvJ3УIzĀpANW i0F"ڭ;JME uj_O:ͦ}ӿY1{]q'2 ^1[ASlNu1^Si^첾ҼOɅD b) bM5|,2 `', eYP>eqF3iE1M%H@c.8"\[#<{~933HC&c;3ra=NAR-~a18Էog1]3y.+4'/""H,ϻGY*E[33IcN@f̲uɗ2/M2&5"IIP:k j0Z&U&< ۩by3n˲PQz4;:45P۳ƕ ,ʡn2"i?n4 a,niog_&9~+{~h""zJ2(9v׽o^>?Sx nkScZK8ȏyƶ%p\M^y;W_?q%A^v!ǿ^ۺqEm #|f.ۢ=ungͲec 1YZ9l41}EF?^"Иb9p})< ɀhq^MuwLH"I)|-,^O8r%ç|߲/]@ >l DsR3uVsvFJ1?I0T6: "!B"4 3|(UFlSEH'70ӗq$+0Ʀ@Tmn8ioݴu0vſYF \8 CdAJwvVnAW!h ʗ/r>E򳱌2ߘQc.z>r5p3~c~ Nٱm -x&vj.61f tNxg=2$*Y+f *ME!F)9cYBff+9=v=Z`aW)ذvҷ5g>֞y}/ud}K|'XU{qor;5XϿW" _͓{=5/;_ž?4# SeKLD`WgYpPgн&˝x} PhMgm Ȳ{~Dw{$%]ҮPW "igY2-`,iKK2bɳ^70 0Yg WCcEevBBNE4.qۀ۠ZWzr싡]_B"f3\@QeJ,_r{+rF!8u~Jeao{%۶cX,TZ-U'h>qeLWsl$ʠZk, x)m"C6)Ҷq|?} "e7 sX怇R_]c(lkT-aתZ=J҆1h,gy͵~<3 ɞ~_= \ß,nq|+?( eW\Z7F>|mB`_ jwͩd< f9x˭~'ݓ}/{*gB7r 媿w!VϥS_5+]J|Y_ݿ|3Lӧαlxy=}o{oy \ed91Bi@vR^u0bA ܨX/rv?={qX1ڠz=ViǎzT\~^oO!G@y;ɛ_zC)j/_f:gwWn}-o#dϽ?Gx'fᦷ_M]?po~(1^w^ϫ!>r5] ~<~) pwr:pǧOZ{_/C?|y;qm;ڧNw-jɑO ~J 3Nj^o wn\˙B`譜jDfٵjtK&AWqL{i / ;Xiʾzuhh^= P)I1wDiGnv31ڐfvbWD )i5:D4 C4~|U{UYun4I'Ueeeeު_s~?0F!Aq8Fc76pbq#lmяM¥\y` tbl2pX㞺4[`81%PgD9j"&ȊETsre?J̯GQ10hG6.P7،R,;u,PX8O偱f,z+8Gƛ\q"w>3V^tf\V IDATt| bnZ꥞o8XabNKNAᐨ׃Ttx7SB&Zti*Z.+ Kma1l3t}^%sMΕ?+Xjrsx͹"(btmB1_|6 {;>~?-W#.msxx=GGevxO-uW_QOhʆ|=_sߝq884{k_#w*i>J^%l.swrix7')B u*|:w|od&p_1-_hǩ\#) c?AӭP*y׈=ɝB]~t:{࡯~[GO÷[/qLM L fX)E0kK4ڢ}$kGOҏ4*=[qn,"ԅG;N&FPN{Emwsc;}sF0ר!I,C|$3d3aGOT)_4֘ߊscE\/=ϩmKG cJRZ+.ǜ?~N]Bξa{Q(?}"FG c_ [jR[?]W,9)vLS8 +[+)JFj+I)# 8%3HkEw$~:Tku<۷U!ˆ~wŴ3X[r+HpgwӐgfpg'r&)\MBONU ϖ1asek'*&i.ؘ֚4Sg0VcF5h-4/UM݋8.U~>Ǐ#98Daӥ\%G$ԉac~`AHCT*caHqOm1뺔摎"z>Xp"4?iNV!ɸak%LdfҡT8GW״2u8y?cO@l1a0B@n-Ҧ0b3;=*dZ)]}- /R1wNJI⃴'FN=7e$+q}>:Q^\+YColk:-PV5">b@nC$/c-h`Dv]Ұo^Kv+_h*ˇŷp >_xyʃiv*[7ơ/=Z\7‹NwWn jȅ@ը #P87ڍ.en>}jyG}=?/}oߢg]E/%y?zz,^~Xg`>ABMaweeڡC l FeI/A:vH0,RJUޭg*WȔȄR Hfc.I[Q gʲ5XŪTPf}[S{kuZT*5ݱ% bVީS : $SgȤtΒf ȁ`#MdM}&>1d!ƎawH0FO Q 0b2~&=vq`~ŕ_~8F&[w6Ӣ(\ߧe'7Gw]LRmF9\Cðbȥ%zU.ߊad}M#PgсS4g%TVAHK˔t5?f~svMTaOz0BAj(W9IY8 O)pwMsB9 /BQ/AL !U^b 2' [[xOZg| "z>b L؏TJ?D$\|6ҐzTsиwΎQG=w{mNvYkyf[Lݧ"t=yp3m|Sw}=Be%#XV , B6)E6֡1_!x&g sD\;y˟_:y{OrGM.'ظ%\w sx9Dqn,vϾ4_WYzvh28mD/zpr2Ju~L5x˪nf9ܻ_,bQ\A^_~K5=h!.TYب˘ns@p<ה| Bᨴ' T4noz!zG)“<[>DG1Q+N+b|(vӉ~IKNp(!ٿ H,[C }jWD3]$WHL#Xu\)$7m-}>1Lq˹k)6 I3.I4q#LZhM剰#@Y\rj_>mN sscwОOBiHBr|LD\5pgr\3ocUWgGMC4c8;l4QP%F {}$%aٿʡiۏZ&'QsuM\7xUoC<}8bM58F̲uJa:NnJ::f;6<|fhs<\ ȹ,scE<_,2vϝmu?a!Is@}Tc<&/eyC1NJm oZcF+BlXaEQݞЫRU,ir* :lHw{v(jl 4e@D:0+$$Il 6 )%Zk1UZN,<#B븞yE3;."~)܏DTt .,TqF[J\W>uQ2EWc0Z!amm?NA= t<)8d*rR!qvZd&u B0t$[a=hFE"("(₉Hk6z}Zsu$g뢔J m TJǩFq s\Rz̄8#U bJuHLƳ>;YiJ29RS!:}##j6nVC^D"x' dV@޸Ld&+EqvB%i,ҡ_CIHǘ0$j VO2|q'tg|N^A 0i_+֌2J,6- Mn#F; p#/e+jDr"nY!ifKtXך=EQDvEQDTltl--5]mk Dqu,P|bk =9Yvk+p v3zzuRxRX$*U2̄0HLJ*$bvT6v#0QbV.('&IW pc7nX^^u$`grg7nW/{);⥬/P_{/%eS/6Ͽ?F灯>M? wbƉ:^\ .-[埛#*;MLt8v7(s*jtGĮ<>>&JJ8W.W**faM'vqY'c^0րX,#HҞR+PtR)"2Ь阵 d+"("( -:[aE8R06Zt4s^L IDAT$gaKF=i3d0@4K`.5N3v#K0I3x]+^MkȬoqf;W.S^Z|"Z\Lwa:ҌkG&Q&AL!n/iۭ3RLnl3$?9EW; j0[B)JJ8LXӚ 4'j1:!f YV L_ɖ*IAlt4a-8ٺ"(vB)\(?y)"qVZU)Y16"]P88Y!*PƜ@T}8++DN"~PG{j.H"\KJ)Ek5&)L_guǩ' e퐩Ҡ;?Ν )jD@%w:=\/Q2Ý,3;[s:rKX6S*Ƕ(TCUJg?ioؼK(a!a'i "8,华 *:8@{ :BeJRV}xϔc;)J)\B'uenR`{[mڒsNlnu"W)S{Ze4NCZFJ1:2vL*!CdaN e*#_RLà;M3/kb񱶄D QYfގd g}Z y#lo38ujTB+xLbkl qeFHBHd([hDu0^#1'!01f ƧrQDSB 3B÷_y#/h я:'›x43؝9m"^3J5[MvRu DsQa!24wxdqؙ TY~ዉ6}TseRHJcz'WC*YCn73&\_G7zem1 @ H/?= ץhPY\D%3$^Xq0oŨT Su}*:>XQZ.!{ C"(9vC|ο%Ͽk|?ƽm5Mv,{߾/Z~w:)"Vu92ɔ$EE %8Mdxv9{h_v]܅yda}I&A0 h/EfW*IT\2uIljbbڻAq 2sS o0bM,IJ~ R0CV2`F}RK/L;kޥWrm,\~w&k#IfJ: 6ϧ~t6y[c|8ȝ)"Y8X.:,@ؑH8BuA3fTJoqpX}\)&-B`<2U2]TxD\f0yMf Ը\ɬ3{]dT ^qĸ}0ihW7V̬ Θa: eu[_U=uL줔]< 0qj젤sC/:|l ub'h>y×#]6&SO1* A[<8x,~q>-".d]r ]ȡT_=cJ{J¥\[N,mgy oO>ǎ]ǯqm :-{]^I:PW_{ +&ٶ򁛸zJRbR:p3o| ܰa8*B^:ΝO^˯(oos?t)W_s'ٴnl l|E "2jc;XS9]*y8֢P#\Z${"B)ä&8QLUhc5B9L_XxJQ2Y)dڌLI4R:~dH)p|\ǐc|wl@'dɇ.#,΂"eOTNs1 cGCTwEPB!U i&ٻ*%e/?T:c=ڢ# x"'S1Gw:Tuԝlõ5`@6j5JPJ8NyNdP s >O,\"B\xȩhhȱ^R"(Br'{+r-_Z_wݏ1M{l xG OҊA/M~|HqYcu/ x> :Vc?εEW ~WNz;?v y>p3y6 9sZes'%59hlT:CO)Dv8B$~aJP148IJĆd)Ľ6&[t]M  Z RI2̓&F!%5w= @9#41k:Xx@s"( YY&P`{Op%N*Dr?7O?;_/w񞻓OOK, 1s\?_H \O?@S]µWVg-2iB<^g=(C #SL ?ӥ.˃pT/9D)T;M !0BJK.ʄ@'S&&VPX,VevXku 6iUz](w[S=JSZ'2Bca@$PAo{E>BC\B}za"q"IBH}Nw Rۇh $CvDdN bJ4GЮOl"V>G>Ch)NEQDvE9C|}wSM׿ɤ V+vcfZaXR԰|$u3c`VY\B[K(q %Q)m$u"; )BY`l4]IKmj8n X$(WQzzm #TK@&3l&U8w*'U^=qaݳ+" '9#,(i̎XUƩT@`" "5#G{s^Jm~*8R2x0 l6:ETЙX#i2h]_glzF \E,|x.ゐ` ZDmVHuy!}C\H~awXGV 9jFEQDvbBəW*rcW?O:yb57n>H>9#YJ*vo\=tJ̲㰨$9㬭#;>kKrU!dn:iN(<*-ǭTP( l aR 5:I1ڠAXfL,"-ʃGcHa]T){ H uq|rW.~>z6$O 6webJ5J45x{M' "(g/me.b~ޏp_z=G~92(T㩃E(Bn*b(e:yZe[R7G ŏEQݳ=#?{+eyx j80 <͙/޿n?uf끨ՙSQ`Wyuݏ_/}Y^^ʭ/\fΟۅfl݄dC "X08NdP8B!yyN'WApik2ub6YgRN vYnݠ.{73a:ġΜ,-|ÈX ϯP(toNECkuQ>hVp.IQ !Ux$a/ZQn:OܧjU߯}r3KZؾJ^ufώ=ȁjҩ8"}8*xՅCw!:yj> Y$uzV.@ǭ]rI[fBs=Q)Cd%spGtY@Bb#)mK`QnB%%XDqE;BbQX1-n;!rAF314[!1hgJps an9T4c7Z1^i_VL{qV IDATA/O&Qgn vHtu$:aNV#aZ4Nx4/qzIm6Zyfxi^ n$N=~01֠1>W}LÝ1ć \ cL,FK^ 2KѦNgAbذR3Onms׮%)?CRJ?QwLZ,]|nW#_sG.%%<*P)bv~~+7)U_iZ`In;+@]GSx/l8cw, x yNwfIaX2얋@X\[ii'6FcR*ws٢F16Ͱi \4xb7jjsQEtcb6NHbn1ϣY%\>kD>]:uĮ$fg\}U8WRLfSlWx˥YnھRJ?~J9j=&dBZdu{={EjK!B),P융X!IqI2lL`oye͛bNϑdzsa;_efҬ012l"@kNח~7flI-T+1L 6G5Y1` B 0UE"TIBٳ G> YPE-g}\h 7isCj&`dl/3x"yxfCAubTRvMZѻllL(ETdRz3Lw-iuxVaZϣ!A u#څ$#k& /00YK3Laz0cJ!Rx @y.ӈ$A?zz:7Un[d-;!} c%BHYei7\z=dQLXo7-Ḃ@$&[vy\IJĥ)$R B`sxHOj4oh<Sm-v|*B+|G tp޲Ps8Û@O5s̥z<  xlfNiPRJ |VO|JeOXue k5&|qqI.wp4I {ud}y:_( .ݝ{v@ $:1@(hIY&|[Qը!vܗ[/=%e~\ '2Ls|CѮ+?4O)*p#p#S5FVoǠ;3`gJ*UzjRJYS>bˍvr79L_CbB͑;cqYF$IH'OԚMHH tT^96BgVͣU+N"aW޲ߟ pGesn# ILmPJZK?۷O "ƒi^TnGo)|`ahRhX@;`(weA51DM|f']vE:"*TR *UTi4k Jr0N>ֹ|{ϫBcβ¦#@],e+l#}#,ɒ?'X։k#sjRHruT ^'v?t4Z!"-u,fD^s >[I$/g11znb;OdBw<'T+BJbTV݂Xk,l>~fq$dq8Et벿cSRJUTj<13x2$m!yq-scr+V%PWX>ҩPt98γҔV0$7xȲ!XHX0wWZ`p\엦VcW2 Å<2U8IV)U8Gn!*uƚuڍD=BcZX]<{׾Np4Bic0ZsnqUXPTH-\R V,IaFz"x VxYߏknTR *UTjOQ}=EM*jEvEneK [D( )8Q8c:&B gXQ %Z-KM\hVpؔ{ϋs077ޜڭ؂)΂Cb9$y8hknh7j"P10{ǝxEvL4e/yUoF,scEU;ߗ<^;Inꢳ[6^m$VG+YĞ4Gf9<T1*Ut`'*WUJ*QOM1Ѩ3+ڲf)*G[`+B]Z^aJY\[ˠ9NRq.CG!SaЮPz^ȍ^Ƥ)֚K,<--9PL0eZ7ABm$9pNZjE>cc [u!qHY')=08`Vヌ'?im߾ǚ"cB/l\!Ӛ@)\X~4;A@F-sA*=Ø3yl@6Z]]H EYǷ1( Y)Ҩ9kqEzYa]l"NJGp XA;3?VRudN{ZڞfWBIPW)%7+E1Vaibɧ+IRW{Fz_Tb<h\/rP+!Q!* 0Q4 1P"^0j4qBi' :sH_jL5i7k5sCO 6$8*n3rXCx'9v&sk?uYb8aI%I)W1xb1QYw?}wx^;0=Îh@hyJT2 qwx3@uZ5O!Ƥ it̕)y#y;kâ+e(,RmZccu @yZqÙcYKϣn :ѯE7^%-λWyl[_';پc8[uTꀝ`lE\|{>Cvg8^B.0q>WslJxͳs7-ģow~한S3 fm^Ϋ^w{I>|C̘}lz xqϮu0ͽWv'_w#g<nx9/?wsūtatf;uIλ^p m]ֱ7}R Ʉ7 2 3G LN6P:W:5яp^#a@- lj)sL iZ4[-jZg<cef}OưOO ە)i,,-]tfoqƆ5OkQ|$,M'~D; S̰=q~Lt#;7|OfZ$Ue;T8c0+:[K9i9,{'gEW3yd1sҽ'evVnxrg'vM~ xܓܺ9گ]I}f7ΆgqEmʝWTiuNC#U-+܏OEyOŅx$S+a3o~>n}i)oo;^^SR}T%k s^o|ON4_Z^{ĎV3~+ElH3S=I EyV"]8\ &ln^j)=ZzaCf,>)|+%S8"$ Jh0 "J.szKV;!j5kP}LG0ZcXԊL4[9s8J*LӋ(G ֘`#2s*jsNWH l~Ko$gܪ]m/}1׿ƋEΗ_/'z~*U:)-S=·;ڳbo=8c)yD~n<3]C_o'z=91ɹ&OݦӉ~ˇS@@z۾Cfs~s\dgnPɷK0}5O}#鉾~|sszst#z^jΎyg gnݹVgp/ڹ\zQcߓ،Od1O)iU}FR<1n,5XX#0PN!  3k &Ȳ4ˈ`@cDZD>9A#-#UJ*Rz FNS7߳‰<&FR8/|۹~AWWoJ^১ZxlG͗42mݢ\9/zr3ܽ:םp2$F?wsB`7Ęe^޽L199`i_|m# fe˨eEKfS8gq:Cf X_pYeE0i*2Uo˅xZKǤqFE¯Ȅ@)Zk#|h_2,ni~ZlE*{3rAd-ZcxGcmk_-|-lQ+fY7U]R;,Z1G<4~Zs'Q3֒YK! k&P&ga 6B.C IDAT'..|KyFavWZ.J)|cyPeV=8ji`fZ7b5bgt{ȃI~BQRyqѽs|W]φQ>~Wy2|/{GQk}JU*M.|yk.`RÑF3pZߙLc)^MgoSV^:9|=>Kʳ.Eyd~#K??^x&fg+:zuNy|`EsV!$mwPB0T@aj`0KB sԚMZ VIIE= Bi'IXo k!QZL`}N'SXۢ pq`Bj]{CL>cwG Z{u9s-[0x cBE oYyΡ #c1kkI!< [ؼolBIL2ow$RJ8Ua#n$j ;6R.*UE῾<>}{<1 rõ|^4}4gosn>KvNsO|8{꫹_1xw^e~*^?Ƿ?x pt)952e+ZHss :]4%67aY/ 4^@9 aw'@ͣngPlQ+/Jlۏ\:|K9kgYDEЎZI­k 7e# Nk5yêy8_xGnuATRv?β cl- EԊ6^p.0|u;;s[qn);a>wo3?yʾ#5sq\{&jlڷJ'$hGGnfdvNy0bȅ1̪~N%DC sq eŸqp.B Gptу!mXU b9lÔ0~Y慔aHhy9NR8&I2 @ +e.of&94%}^?o,hȒgb|DCzԕ91[7yI'HQE\|RqL+H@ g/g Kխ!DH ?>eծV_tK~0֢p c-hҖ@n$ܺږ6!oueé h5Xkō !}BjTk~F=d7.s͛x=xݧZ_RNsh"c@S;a*<Wqm`r|у_RPkԎ@=mJ+ sy;^Ͷhgw\uOoy1U|Hk޼By%cG4R)_7`#e-=FK8(Yϩڟyܝ\eA׾k/X~/㦛^.8s/+/J BM& YM(LM-Ȇq aH,3&"h4 6qړ&'h)qe!kyY D>I@롋ٻb`#uID>ic )%* A+殘nF`U2! x?{S.@rWZl`j]9s--y[lrS՛!VԕǃW@vxZscY2=ҿGx#+E3m@XitDh`m1ƤbLg8%yoF|?1v?~z^ k}Jlqc_λ~mMCk=gDw/y>^}k?dws os BN%p̓a0jmqW.󮘣)0 u @ A` AyBJNF9 #3!/I6C4s4Nuy(!ɴI9Q[;n CEQ+iFS B=œ]D˯/1 wn>RGmd YE1'ۘ)FҖpg%Y;k֢Mђ:CXZK6x64ִmp1X#+!,d)@/Ƙc|OީJ~񆳈O|¡6}[\W7ǎOkduTO8k7W]K &,$?@oy }NqgX;iR u3 /㚛.@ p,;%3t+g\^wxQ=s;m[sws+ ?z/71oW3C0}v|eREGWg_/s~>Dgd` ^t͵\sų?me^{?U7>Ϸ9sH}oz5kJ 3;sܵ*{_՛[80~zѴ+AA9  x˫xrYuC^yx |RX@a%AΑeB \*qȃjF RdeTX eb:]"i^'[s҅ߣjmڡ7$kLLsUf+p"wqΏbr-k[>&.QAОdXOVs^^c5u|ҟe6>㞔p&] M ~H]h64F %S+*U Duec}ƨ kpB`&bz9fgu:^ue#(O sŜe><6bΏE(dOVZ A?{755?A(@`[\+Z(uD>Y&8=i9W[f u6ds+jbeZiM$$il-՘[_#R'#)DA`jx^Z0BF~D!S 23;Kg}Md*U:FWLC::GOFҝ@wLjƫm%3s JǹП>iڝfXh^?jNuK.358~a*!`DkBY+ӉE0P:e^ 7SGبsqZeI4?&xkVbUlm{n86\s[?Aj1 `,KF"O@>f@>' "jf&gI&igm}_*y4I(YX:ˑONaqy9d+ER6%Mu!!a>lnb9MuXhimRM+vZhkSˆ&JgJյRJNnri';_W֫TӖ6Z_M9_iK`YJD"HVjC+ߤĉ|lWᄘw)QǘqRsgD ?gP*]R".^)u\ 膰ey(a V 4I$w"otO;2)ؘ&`X^y Bݨ=A;rg$a:5MMꛚk}!^aMf0h%M}YIz@mZkIɲ,cm\R-Uގi]J*=I<ƄݽJV-Rz3LwgFd,d# ILy`B qK^Fmp4b(F?_8iOQA/G@\-k-NH^G]/7W8k(km%+ez`7|kq_2 ZA Y]O{PV9m-Q}e6gmeݦ&D#EI1)#\9rXsL Gu0&.`dY^ N>ƤyG|RETTRvSG>gg'Rt?U:A }ś,N-J ;J*J1AB}& H$8 -ahj&$&KS(s¤6A)eM'8kied>'Y ;5vFN,sng w0:G&!aMlĤcmЦsW WB*BE,hm:(թR%  X Ʒy`M a2=s<»Bg ˫ry8kp3)v8qΒ@B"`4NJTRgUTOJ&bњ|H |BX|L`==R:a*paF&򼹲b6:sW‚B R5b2 h @ULj}cFHpe˪R:Jxa1G1]=`fҫQ+-!PëP@sS*oc8c@ktb]dfI֒Cb-Ym[i=k33xͷA҅9[+bD-TXR Fsΰ`e o"DJR窊]J*U`WRJOSfPk x^ނ Ӓ,(P}MBroj)zP*;sXq]t#;KWJBj&Iaxyj'%X {!Fӌs/|&NG䑏mj%x"@) )%X[«u~~1RWbQ6jWo7 %T\a*2X5f~tn81emI!S 11Fgm¹ank9W̴pcB^## @WJz7A-hS B E|rKGD4TȄ`Lix>LNZmFCXJc" !Ɏ\rƐCl Zq-lcz&Z 0@J1q^~C s ]T ( F>^9BϢ:TR *UT")r,2UuJb4d@ʢuM0&:Yt;3)5GZ ,6dkwhlƼJ^DTMs_q-7p;H8 {>+6 `׈w<vyd~uxzR`7m _\Sх"< IDAT7D, $μbXw#lLl8S(k^+'|Ƽ]gnrwA~]MO]<}Nld| P׈F4bq{<~,Zb(1.Rk:R1B>n!j&A=@mMtjhSTx"~a #*Süagu@8F[8ٺ$f@*t "|ű%M^zr EZr w?RU؊AT(I5dR7Sv2-3;;YiAОGD+eXMUR@T9WQ *Ej0.B4}8VO(>gnΎe[7s "D&}U8Kk3x>׳N.a<a c ZЫA;["/>ծLD2.Z'&jifHWֵ?qWϞaᗯeXu0c/@O3nX,7Cݔi;cu4`bq·5;J ǜ'N;ҥ%0l>27sC3FB"ZXx,{wrԥWpMy7bF&,ҫa/ueS<} g}x1LOkhس bYe+X>CLBmӇ Af<CKd,͆*B VE~R5OOӤV mE¦<*Ph &NTTqcJGqj :tG )a.1aǘ ( 8 "`1HYETn4spV) f1ھuWk}Ӯc nlAL;>;'{/͏=DN" =-l@ \ĜIE]7v[e}0s(Wh!yo=??R@c̟v" @ͺnv!IsqKYVYV|zO+GW06L.mӿI1O8S|%Lڱ(`1˙Z|u' K峋Nà1ex9g|_*řN0] t]t=cu}{^;\xsͱTd>Jqz*'Z.λ/},^:.fE8&u6r%v,=7r×.d饳Ja \|BNpǦ*hw~t٦xWKK(m݄'=IKL!LfΨqrVAsЄ܉ Vu>mYPeF2 R!"@J5: 3&Mf1Hp*~'u>[cҤc!C;Xuma-$:RD( Iy‰EGFab "*!:B(ZE s8кU0""V.Ɣ1 c,AT!Λ΁;q`!BK(Ucd,FHʹ#-,ֳycRk;ye^6 &7K(gw<Ͽ޹=hީqgIW|OaucU~b[kcը#о$VL|߯{aOM;<ǟ'zX|\5{0-c8쳏cj_{ %%us\t-yl0sQ8:.-tޯ^Js Yx˸v|suę9Y?^tjf˹Xe^al7Q%7ȕ']M p`yKlk|k 73?E>y曬'ãGv9d: ޒ̬5Nj;5w 3bj$`M`'Fhd XbELI"fR2+(!n*zVղE}Wg2@R #X-.gQpp8&#,#PUր܄<"&r ΀[m*:#k5~61R搪96Ѿo&1yܾ6|6I+Kӷӵsk♯W@Ȝ>EX[Y[WN;N0\S7W@4//U/pf}m0G_ q„)׾Wh]ȵ-cp=?to~o|f6Ћ]#f {9sنGitT<qUp=v [_w5|˧kМPy>yX̸8Iz8kg>gdZ_W2,1 L(I-&)[Ŭ։%beOi+Ҭi-4Ey :Gh-bpl\K -r9th5MA]PG%I@&,_$Egb/&"G)3@$I yr  mT1OP c;OY̘Sgz^%Ν[)BDO"8vʗoKiP*G>=ScmR)ڢA)J*5pZN oa9n{|%6u߼sFJpWr޵7VŽƪo/ܣҰ<ЋOL/^lX:v}|Vz?AG-]ΊYtL$؈ 闿7Ckw웻=‚F{]F1I}mh 2P%[.;!.$<1=|h frIkRMu[B)`Fy";;yY|+l7wk.m‹/ldq;UCnž-wO[ԈAs*E& Gc:z̤\@&L2wɿ=5.?_r%Iлp;{3 }Td?*6Q4fuQ@^& `Q**R0$(Z[s9Rno JJ::]ꤵtðO2Gm9%1/?'Uupٰv]Βn%[ur8 *;O@3SA1/pOahH0K>w-:vǷr-Tt(mAOUzߵ<+7\]z4o$[z"'_;A&Mߖ'{˓ tL:YsᤳbǶ?x&DZ=ZP60~rnH΁}OkBX`$s5IzT aj.%3Q̊ԀD´ϲUYbRELfS("B~0lf\ *5ʦ"r򹐁ݝ\jf/{T$ԜhvX_'Um&o1EmQآ@"sF@+bl U'>J&5`@C/&ar(琲X[Ek nF8OJRJt0M"Mm,3uaxF 5>} mekڏ4@hj(0/OobNuAzfȒ ̘юy(i5k)˧ ^&~z-JЧ;w{wƍ+f f~=FA{_'L]2s' S$rnO؞Z-5Vneӽ.ӕN=T>$y&bɴy CUT Jyrt)M4M srLݟB.G8~<-<}<;)1g]fRa8?Co'L\=Ƀ.\AbgbN {Raʕaq,2t[Dj)x0RL0v6+ꀘBkMsADE6sL]AT!RP3bJ9`ڍWZfpม̗Ͼk}>6L :v=NO Q6.YT*?{k64[oqIB~K57v=oohsb3MXKw1Zޱo8Q^?4潇*&q qðk8,)(4;koTPNQ'>lbm s5KPwmK);ټ&A97?s\zLfFo=L>K8{RN9rn2r,7T'.`"{jϡC죣l:fV 3f]yԼw,Cw^ Uf^V%Ad V*'9DwÎ.ocsa5o⋼{Mb=촯iޤ'Ad(fPiC.x4in҂4XeE֥J]m$h:TgAy. ko_`[h=w  ͑T+Ɋ]`ZqZ:iC;j5""8QGU JjBD(~k8&bHm +mY=qB%)=[1YQrk3~"M]clیMAd6]p'}+*Ϳ2/ ]ŒַaP0]2ȩ9=DNaOEfɢd2Otę@[3j#(sz'ߨf.a`{0I17>zb9y9g4Ƽvm\O|)O&`'2>ndNs4%Y[Kf=/Š iz4'u|hc&⨙ǰ`ݥ- IDAT_rVf^xq[;Sg11Lnֵ2 %oo,8 LSl1@4{ 5 ص3Sk~&-ɚKjkE0%_4{uvD8f_ΙK':~}}tT`=ޯ6(5`e0jD>>Aq'ORg$1@g̛Lɘ\MnRn15Y L7X"%(t6@[&DcF 4XPmPՂX{H+ŷK#,V>kw|3͍gl-59>a,_.IMɡVXWʝG$J9UKi%X o,x;lۉ~ZAXGAHraSq9%M X][MEں?"8!(Tn$'䱚f^2S/JM߳=Ǧ<͘Vo䂏 ZdK]2'ѯ7Nkm`v=.#:<޾n3]U[M}5ھ7\g{\ȕgL_Wvژ~ɜh2oǎM]kqk/f=IJHǔ#P/¿?u r ׍yNoo ξ[aӓ .2N<Ɏch˽|rgqP%RUz6=/nިuV}5[8s8{q9ȍG}lY;~poyl{}no|ĵWiW~,S{#a`z9X{7}vpfՋPK&Oܲ:C飣߿1"MqT/hDੌI=]u#S$"ۉJ2g@c)S&RN$cm Uq5rb#aMRdj3X}thÎNn?>zl]d kB)5ܝKHZ:׶NqggM9pPT%rhzS1x$uǚ? 'BTүj Df];-Pj2ct>e>gU㿜w v_;)U?;ke\vlJ[F67 -!~ף꣣߻Z)hoS)ZCbAԤ($tKLR"椦LE0' %I.`2Jj`P3g,`F'NAQp|6`yՃ:>jqa \{4QRd€4U*Jy\0tia?‰jJřCT*T+Θ a !ڎm -hnOssMv°Xj\`pNEOfz^E>R:(TQJ*K* 4{*lBX0@hAS; x>Jo*1ez)cy9<6=w;7wwu>YPJ.vwC=9:}L}6fk'EGׯGGGuwLq̝ kkRN$ \@>/E:aN\-USx41 U˚ulHȀHdZⳗ}aּYudj ge DFp/  $MA)^L@"y74 6[.T1BV 3gV#E iKi";D2GuŧFS1Z}\w`O@43pQԥi~Gk-X8o{0ulݜlR.7s ttv9|O,nڿ fͿ7шFJ{9QЫ\3vl2)6Y>H2kM&S;'WJB>ʤ EݶAW=f{g2ol}s9 (yܩLJl쒔XZW:"TAA˜*q JCǵk3m qH7pqV4m2SQ1A#sTM chSS1zkY5(,J<ðH1r:p_l*ڤT`k 6hD#{`Aq2yِmlD#9l.*wYg!(8)y)C0QtJ:Ϩ`A'{$H.MF,%@J9z@kY$jut,&kDy6Rz /3;@@ݾ|kK,ma3JAާ`JTFƠ+R6ڨFT*SZַeP :޴?w2+IJTHz*U9/R k6C55*+DYkX4ZW^ dXX˴&̉4ƅF4x;+wF7e_xWB Knz){WKɴjr΃ g:Jenƒ;j&:g[V#Iֽ%}5)Gen?/wK)$Z: cc0uWtuq%b)5Fk|JF0l"J<*CDQ?Txx 4M!V-扨LȒ:7uٳsCh]یR\J,q; 2$mDQ*YEJE҉1&cZ.4ľQ0&Y`EF4ha шF4Z^'2^u Sa 䄯)Zy LAJu5eKlMfγ Q c(>r{o920l!Sð (q5/93Mkp,YcRP*_usը(NubUzm.356 :3lI7OT2U!U2+Teqw%5֤^z0O. Fq!=il" |&YxYz7hD5hD#,UZIm:y $ O̪IaŤ@.Iof]2 ;}2p& ڽ@فꔌE )aN>B65MaS0 g ;Ǥi:5uJe9@!֠hIZZk,&0^Ľ  d8O;;P'e 1PHB,RcEKX[ʘ'Ԥ 5v끞n0vhD#vhD#qE#E͠]HL <@EkjD IJ)w6sI]jLQ=2xk@7&FV\ojmP[2$oKA]>B sE"[݁+2r2U)MLp`;&D*@Tx)EZb]&j5vR!Zu: (@C Izά5R cBC^LT2 j)i:"谀T$T5 'PlM1}Jfb2+jF4hF48 CYu$ѱuΪ=î&ԡÃeֹvK5g)^u ؾ@]7֠n8:+ ].^Ry_gG )E5wv2U$um{2*R}jbŋ( \Q&$Z(UYHF Q4D5 X1yz:(=s!A)~&>}G>SPȏ'z/KO1v5F4h; RidN4hEqMdde9#rI%@Iy뀕dR䒴M|.̼.Pʚ^};\Wso1ktN2 (<zA)stz`W{8QIlH q%°ك!Yۖ FXL31ARUր{MDFTa`h]ERAyzM5.mS߆Z,1$ NzO;2dx/F4h; B?A8 }r0y0q41];KFEǟ}:GG/ÛxL8Y > k^+ ̌g7߿Ms9^}߽箏w_16h-2Y<69H)>实smN>tq9am2: &q:JoR92{غgץWztZ :uaX 團KΤ$Υ<̨J͔J9x+p(0V靖 Zzte`-XE=ʝu.#hKA~K\Jw@W'C@"e3З9fNiAJh`w9F4VB +hs.7|bkWY$ _=]״z˅ ~p^t*ekq{>+6淍#k]t.{.kc ժx3j/6A퓏`BeovQW,ɔKjƸ?4)@sWڧ%`?ua";7OI¾$ucFcD!ud!a#:/+_"fR"MìYIzW :GT+7qpaX R4\Dәq+j1SLɘT* M!ZUqd`2%CWj%ʥւťZ FGrQ4Db@]oz#aM5S ƽ27|6R/ݥ,\𩋙g_oɚ qSQ+݈F4ql}:h֖8N&BJP1֧ o`gS7V~r/Q^Х^)HPfRDŽ%I\xȲ\ Du KZ:I( : %yr"Z)xJTy&d .JЧ\cE.^ 1&$P"Uua 4q\FEZڂ9036XzhD#шQ;Ŝ{$^_u1ӎD%0l>2(C3G_ ken~r&Ĕs),ކOn~t\¹ȼimN^}f5>Ƣ~@3VdT&4K}#'VVrY/\cV$) xr|tAd5Rj S*x&1uD^3&#j&5a7T̰THiSBdR0ZE].4Q/B)xۇEt*Zkb(O{@5#Fu<-MFGS^U2B' D@'Лxn Vq6}]ەsR] +N[w^_{&h)z`VPb0hg;¿VﱇQ[^߈F4Ŭ鿳z 똾WG_ecggǟƛƷJrS8_\k]fۼ\k7~ɳɲS8 9b\<6Xnj8:.-tޯ^'9WϾk?7W%&T&Yw^ֽs?U*6MͩJ0n>}3k8ﳪ1/U/ʮ;=ԃ}[\ o_d31,.Fԑ3οo<%όn^1N#wmF4Vx'"@u'56l(l|Uw\h7kWe :uB}ȉ,\0{ d'qqE# IDAT/ZɺRl.5,e~Y?l⋜EST4*yM{6}w,ßpYp z;CtKufh^ˈcν?nO*l}q6jIǸ'VCܷƽO7Wm3=ZAVNtYn#+eZ԰W&1odH T{(>[UeY-,KHlc'f3BӁНt7e%~NoD4& ah06ml ɖ%K*|s{V* HuW w8~?>'9#ڕϾ/͢]yݳ,c{F.]ϣ B?w?w>?s+^F5߇r?)'3/S:Zu.GrE)&@.b۾ z˝u3##Z  |0uzI1};(#]GsY矓b{7BB;EչF0JiEM)^~9.'`,Wʼ7nwW7;ӺM;qHDB jcuJ6 bI8f1J޶ɂLI^#M$Iҡ!&) EpT8Q%󊫣AK4=[񖽚8T͆Гo D>wQrL檦(B떜/~2Vmx?Tz v/a,lnF`҅Hf6_z$2~9u5j1Riwrƚ,j'Ndz)Ui}U̘jW]oWh< ?cZᗣx<5 {:}]{ac_n~?gyޝ<.v͎xɧ9eq9g^/=;f7{nƏq:}`u;\}e[0FPHvR&r_DPS> b B(~6<[:,\#R?,iTBJ'oV2+2"8/$ 姞 ., %Q psʞC2t(:$$Sd&kqH7m]/H oAs|R^[h[d>A#6pλg@&BvqFdCyo])Akr\)\0s 3e;f!?:AJAI~z_ 3\BA,ҩE̪Wphua]}$㧺,{yͺG>^g4GSX& 11ѭ!fs.iaEPrvg*8erhEZboOiV(Vxkoz#f͡{ʾ EJSF D@4 XJ;\;Wm'rg+'8b;/98FFڔfs. Q$[*.mte]t^o$,7]վ s۱9UIz5- $=w{fhSF@8Q3{C_ƛ;1y۫w͂ >KXlgz8,~![}Q㕌Se*hry8ly&WZk,]<+F Zˋs/$5s٩,K"-;oqs:5mMbu6PӞT׼֬?;>U>s- (W]?6\.ڇ/dq]<"p=~do>ud`Ip Mm9 lZ.S}/nfפxud#'03VsSz,d:c'z̠My.Γ2 T"P27hMAm&FFÿ&'7q<@~ݳׯ>U`[ o$+[^"S8bۏ{W2ۍ<΃]7}x1`ʓPO~/32}bB"-`}k8Գذ᷹bx+O>,ۇHD%ְ~YNvP沜ϻ>)zUgl;O\M?[XPkgxq"C i.፯[mϙ`cnݡe b'=@0 X'BU1Fct„u0JI^.M?wΜjsDWU-WەΈ2$]ɼOs.-ԛ|Vn1P 9TgE.z($I|b^fØ4Fc:dYcBd+K)Eߍ I7I7Qnm6 (S =p`L=PJ{y@{ d177/-1޺}xxMvzk.k_wI{A/17S旞C~l&s]>+pv6a{?ȃ:=|/0rٯgv-'ft[^|^niCOvfyEaNg]|?o+ʋ^7弼)`c?7};!-g[BW">g"arϿ{O5~#?"޵bm{xhv>ߘ2>6eKIqsƒe "": d/CA-?(+2NJPJ(Wd_Y$dpn[(9+ W|uE#g,9Z28;)IQwv MJNBjwl-vI}>EO䨐xfZ hgpd.o~ cӘ,y)jqe5vJ,d`h%rMg|~;bK:oO,]ɹoy'`{#lk|{rp(V_kxeC| ܯ޷~ÈY t3͘6ΏZBwβ.$dNg7if9H?+UҬ&!{f>? Lp2tb_-K-*.nR<9Ȩ <'ݹV}z6@y"CC\ɂ\ʂˈ!qӓ 3F$]&Geddݻw0EviKhwNבe4hp1P]s[}Qh84Oq.#K29®ٟ5Idm{ &&;czN0ܝ8]ctW濒1'/Mg=ﴦ={6~\cr˒%ii)'qH+:=vȸT)qP> u &R~yre$ &i;@#$XVf %ϯt'Ռ54gn?X{;<+gU2ݾgcT׍+)i6ISw6 &0)iE:qR>n)0LWcN=r|LnFW,2S}'{bQκF/OLNL.&IZ hĻ.*U-JH#XaRI) BHX"PDkYd EZ0O&"ۻuf)'6Į,᫪u*$8Wl<;|*ᾏ.ڌ,u'}GӕIRٜI*̲3ٔ3]:mq8ku 6SZTNc&IIq8k$ ~> Ћ 2݅RjGkԨQqM j `Ё8mfRFGHꒂ5抣5~q:ga%,\0Df$ )d3tE9dA&6IDQH(!H eLy@'!jZ߃eSMȲ{ ΥXs3~l2{8>UGq<}z}9,(r콼̓.i&M'd6+yNrUn&'9 پIzlK45MVrR\JEDQ !dDQ3iٷ,d]oP[5k*~5jԨQp豫QF)V(췆'!~gKe3"7^DQı-` hAg)Y#IrWYF5jbWF?Gh;2…C4[qQv] U;2 iE~\:k(¹,(4žR ҕ䊙7 =D$&h%˺X%mK*aQk,2ПFD- R-*BI2 Fe檣s~DQ4m=O]AI)fk6rV-̉Zn&}eҹoEj1c*: aL^2Ld;J!dҤGyRpdo1OgըQFըQ9vÊ28D)!asҡt.y"am`d≛'+jq.R$M{\3N7ExR牊(]?i(EʛmUEu Y1 J FȘgv9->.b$q:ӤehtRf U\ܿByEQT+7BzFn5\ut.!FIO NyrB5dYJHn.)3͝2-ɃQF5QFƧX`y7C !<%*alŜ# J"9Ƞ@)s^U%!22am( {y_@k&hbu}Y`LL F%eidIL|/YD ejE |j R e#4'p )LHZ^Kt *)ZDQLWA^JxmEL']3!fog$neʦ'vAyP_LSHiZg$AZ/ N&r6֗Zk2])MɎ7q. 7'w"5jԨQ0;l5jԨqdc׮,]FSWΖRi+<QHyRoD)Q "NHYuCdŅrե!MҬ\1 7dӅ:x@=QeKb^ǿ}I?+wru6p櫖2iF^bOS/L-+ 8eQnţ#}]Z|ڏ w?;/V10Ni38DHý$z<̫lW΄Dߣ DWϥa9Ow|tպ,w=wN0Ɨ]*•r:iɕ3_&+yɤW⸉TzNMh!Ϯ,!e%J`Qba ql.Ƙ4T͖ajw`RYd\B u!W|&wsbɲ8+yi^ƄK/ΚVdNkKc}=C~э.5j)Z1]ek擟x+]*urr9~3om/A.}='z~QŘ2p)U8A E|+` * }w\N)Q}X5eg]OK))E`cIR<j+2\ثrқHTL7QUy.Wt|}hԲ {ƤA,$Fb9U[Ui]?;TR7]Q#WTD` :)^1 ua0R=^ٵPkL}RzpH%ﱒQQJރ%CTBɐy,zg9mYl2I "SHT$d;{R]5N2d!%z'QoBp V> 07iS?R?R6#?)M\*%VFe!'Tc:b r"oR<@\X%MOda\_/ GLo⿛iڗ819_5ybX{ьI~i:*>|P :J>+ ȣοZe3{r=W6On\`UpUn"0#w}Mu{zoB^^~nٗrͧeDyλxp{:=S՗\f}j[ڽS[c0~S9sU,&gO}uq:͚˺yAc|'3wY_D.~[X/osx\cF.r6c O{2DQ3WHr5>)D(P˷DGkoE\89g f9kAJz'DcrV@`.URɦl+MJ*T(BEȜB%QUL d_OO>?5no"T^T[?Y %Ƅ(z $e[5D+` Ocw!Ӊq.1yo>&Nk:c#K8׼a~|7M?aq:Nj\mcś^Giw|<0_6Y"x:NYڕq˃;"Ay.RcQJꂥ=齨?]PB&^󒑭@ 8kpBٺc)d a8_s5i#_ ANDžh3O{;_)HioӿL?|wƻ[vi﹚c𞭌kkxۯ>uY[Z~_\e9?f,ӿdy?| iS7$Z]·xk['wH烿1xu\_q稷3>vDj}.G]ɚ G_ƌbq;ąow&!ӹnss&ޟ 6y|)Vo~8?|g8aל|7=l'ږ(&@Й@IoRW(2',.<ϗ{ҕG؊e(7 v ce-V`q}Zp[jPZGrO#zOlq{c#ΊAg-TI`YM'Y*M4Ux}Zl[OؾݴW# RUU?!$|wuUVTUOƭ_[*uZ;t2ql71T&F|U˗?ypgGS$q+3f=npUώO_޴Rl̯ܥF:f _{d ]=˜:|~6naRN_|<[ [W2f?}a&q݂ˉ;VVo[fwK"{^?w?w>̼7Q+h  dĽ= ٞ݌X ׸-wqӝ X}%vr㨷c{F.]ϣj`No)'3/S:Zu.GrEӛ^y7{mcԃ<:ym-ꤍ|G<̘˲^Ы7("M5*R(MUr'љCIO uTD02)BD@0bW9v$T.Wm,R/GkEA!P! -ꑬO%W<ա2C6¶߇bdćhNqJ$8evKFHѐ6$}{+{1\U͐97Xk 7Af/fc,֥AiLк1ݐ!(pkm ^ʓgc^B\K?lOL $h-Zem\`ɴhGZĒ$G*!/~2Vmx?Tz>ϵ?϶iz#q̞1YbK$n.%ܥF9ODd{_{TX7{lXrwӛg7b=J{wfo5^v<Ͽd;y]޽۷ԓOsr$ϼ7^zwn{hgNcZkmnsq7sֳ2jt<aQ5vD[!I-N:oT}@f^ ػZR8$p' +Wl"P*q+@qr(ȟk/Z{/e؂|.  QN: m0Ȃҥ*sbrKgZuZ*_:C^{u$$MG! އ7B\@ y'1.ϙR?HkTp31y\&*|Ҙ]nuXA^RU"TAGl$Ub+ر2KRgd u4վ3$I֖^/Ȳy,\>~ɰ6O0~cy2usG|]FW4C hwKvb`8]΁oRj\$Z<;xlX#0V^ʏ5V#}8Ϻʘ3۹]\lZr/OU&!g9&nBd(AW`׎@kT.<)/K1g|4u/yX K'@(߳L/%cA5 (`xc\ (üRTrڢJ]P* mڗcӢλcn 精r%EfWT8>~+ Z#D'w^O|Fh2{07&/֤DQ4eGϗaH&lp6O&6A3hp1,dڑ"< nFh$UV\~%H3cZ,Cn=hhX7SOa7t-.5jl\N&MN;o'9D/$םÅ;ur^b'? ٞ?>7ʫ5qo=l&[3ײ#+Y譺m:9~:D2~y'No݇wѝ=.bpYCsm^9d9'<<ژ3NN'EFJicG8ȗ)&J#^9yJ d,: @'JgE 'VK'0V|Y5@ܫHQP {5)R"RI(KSK93'v^f$8ֶCe sIyp[Xf "!k)v~rrW={j:.?}I*ܬZ%ӛd_]Ψ^VO{62\tBkKktR&{Q<ūy\ǦόKVz|JXElW@P_AQ+pgaosV|YOKVaXnqHۡ9G쵼%)O|q}\=T#H8ĩ/LS qT"OŚ`!q5!KbmXs tE«'_o _'\ٶ[?KuWr[ɅAz5vý> ֛u{w+C# Fי~=o?;ܩv-'f7K틏KM0m}詣T?f]|?o+ʋ^7"`c?7}q:u;1Fy聧#X~{8p/as1<,΢drmYF1ʝ4imF[Ru1^h v=g6J3xÛΘ,wŖѿy>y#JsL3Ln77aݡ\KK+pzߒc~㣿I.>#.gg$s#lq ;L=Bx- v)1z 5Z(cblΐ-XLax2_1ou l btdv掙qzPv朇wI)YqN8a5i""c%&ZGFh8)| CR<ϼT8dYB\i3^ e8+^rNW8$d蟋*"y@^Xty+ T:Kt"`65Dq$b3'uJ2(BE+pe+*nq y@dAPf`V:?`Ҭ]ҴM7EL黭`ڠԥd%M=ڡMM4Z1Y&HR,ݮfbǞ=S ]_VZ,]8`X c_w.5j梱# $QmE5 GryDA1@ȢוֹDqz, kBDsU ޞu<d 7 RΗ<%%22Ȳ,Khw`t+}56yU5~n  q@~ۣF&ZQc8ZG &&Z 2 *uDϠӑPdP! '/*'%YQN9Q R=ckh_J.xΆPskF916!)ePwuMQJh9o6祓jYCr7PQ*eN,sKbV2RT*veyI]~ ƀs16B %epHm`L1qܢDWc"Iy p"!|6,,1F$$tqCWDn>5j,q|ըQF>Z1kO;ŋT4ՂfS00-Igg"RPWu 1AP*\0'< )ZͿHDrKy]4wc3tu;Y.YF)TL?(O +Sn[?d0F=o<2B8(cC]C)y'JFHDB4JPj( Z1T;|^ʰfR*x&Ȳ^>i֦ככ MvFȲ` dl/M념vM2m+Αe2azҗ4tI&',I]}IeHW c`BJCn2ct)jϢ |Z ^_* b(Y!>4"s.'vR@L("F!?+Oc}e4uYB%M%Y SɩZQFըQFC_@ )[H)HC,!,4wb EgUD7ħ$.BX!Q41c%P(†w,leX@H1y_(q+ eޕғ"E3 H]j °,ıIC\IBb#<S*=ep(of Vzbg R_QZg]!ޡYBFØsA^U$n21jԨQFMjԨQa@gx8^1B(::?slK{{o1L 6IL5yg$L%3$%clcl\0ʲ\d[~i{8Yf&]W垻{>sbHK]C)PHeTIm\EJ S_?`9o">) 3 /LhC TJSzqӏ2)%ӌe5uԵ}4 X7?i9O%MY$;Sau] QF.)Q*9a8hۗvMM)}z/|yOx$ZJ8u=+e+l[]0ۤ/-KôKkTC4@\N:uSֻ @3jkϐNIɄax&++ 1_k^J1:$%WC=N%6Y. _iKJ+|!8Z8{2+r1"le[9]{;˷SEx)IMC+8/'n/FVlOky>b #9-xIir%-#G"u$Jy%'4E$Re8-m3O)>K6p}%O özژ zIu]pYY ^Zvpݐγ6iLZ>p}ٸ&pOn{(RhTupl- ]Y`YI]<N ,!Eg]h1^_Izƶp>[ىP5|678 Ojr]mI0m Tf]@ЛQ'DjKl"&,Yʄɖ]'vh&t0< X48^ӌD㾼1kICFl]O)a/wF(E0l_1ӋKI^t0%Y=w̴#Pu -\7p!AJZ&sks (e$!_E W(eEtp drt :H apMS!QBKSмq|+RToa8•^^:긮8: 8GLSb 'uHD< NzPLTxG/ViׯF1iSgYfv{ [e~JQwpVMsY"YtpU /44 ]뚯y ?3\lOB)˜'r^( <_20ma؞XL5`c^= 3hŲh+H7rɴ2q]RL61iK;$|oEsFJDZ"6K4rON8JH!Ji8N8@堳le),Ka.icZiI) SF= @Xb(3"5a%[fZ c\;sL7#nİ#ׅ6 eZ.t]jk9x6.Hi OO1@ɤZfܝʨT& !H/$@YwÈ \uQLƓ&i:n`D4t]CÄđ2~Aߑ{fZQ2Iؒ2-m-n]7J"8Q?p'`Y #N! @ DJ̈A8[\률mJ'Ly \ tI$/7]sy a ,+Hk @W},ib_Mڹ+E/CD]??'cۓz7t?6GĒ3nV(-gu 2=vd;Ljxz]C2f3qpFC6 bQ!WqgWFpֽ̋ky$\07oG2x@Y86Юwyskͽ5B`1 (݉Q7pKnV7!nٜ*/ۨi47],vfb LKC5T\W&;;,I`/~ϟ]m?M ,ھrno%usi&ug vr$yS<2&m#e #*1l =p(Nd O/xqvUu+}K;N2fNaY:gژ;&KH$,qE".Ƽ#v)/A$;L7jG.a0 grnCrh4t?E2zlVq< M5ZDfp㤩L-}~Eu n:fO}y)z錚8٣{ISGyyw׳!0JR^6}8Dd|~nd#QfyL>>~=̳%GqWr̦T״2fJf,X̜yMkM7c-DcQBe^9e ]0T*yGD !ې"3N/f nF5i$qT)9dF8WL xIюIWL,r}]ӬAӲ r,OlB<$t*ê鉢x s]xV;˒8v\J`Y6G1x,F4J"!lA"!E5  Kv>|,cc_C߻[_~ϾK\u_LO=N K澯eq'V]m[|?\E"{7= Ϲl'ca3/ǭ{XD7^~ΰws(Տј\pB%>?OQ/;HڨN/?%gTM}ng6ӟQkohF5 !ZTOpzۼU}iHH>fMeQj:Ld.;djgǘ>K^YLYKg~ Gvw \1z (*А5t4{x˳f;U}ӵKAQ|4Xި(d'? c?Se97d.Jsqs|;lH !N!L3j~r/..:3OAF- d,Ryy<)g&sIy@9cdBnOC`g/㴢뤮gO\*t@J/} R$R \׳|JҲLˏK$'$&e0fBU6~? @]J(4]~֪_N-huPR&Ϧ\B/PRH~znڐ|Rv??uc+m7YR]b's^r%~,o2/0RȤ+LunOt_dFɟ]1~J\0]q-qLqx"G  @]9hϓtX:ַ[KY6cq7e4F_abΜ CهkZumRti=P&ٹ䠈镺YT8/:w)NoMؖCSS RҐ(F:)ݤU΋>!1h i('c鑾tԳpݐOԻRLEL)%]FL,ua3}o2/^y o\&8JdL?-]/}B_znN W S--[b3HhRʲ5{ Ŝ<ٖheCb\>uʮnXxŰ IDATё,]]CĢq-1X%pUiK/A i*ړX45,qh,Jkk+h+xXxxDl2l+iF,nqZqV,ljaۭv͎حf3݄i֓)uR*_y e-[6Vp=WٸJ(oW/ĺDuyh"u$ I\6G7pWGP#MQ =m=kzL^x[X=Ql,sNc{W;fփ8֮蔕1tNԴ :i. $wpzn*ֽ˯ܿVU=YCضA^~8k{JH) 4_C#b CTh¶-,Sb̊-ð z]70 /?MfSb8;'e8NێH4aMn9I]{5Rvpp) GJ$c.J[M'iY>R*Eӷҹwy.vؐH( O%3  @]vP(Xӗ9jvkw'*^/\qВ؎r"2񫥨]s",zoB&N׵`u(ĖAݴJ 1bԵ؁Jv+ [;"Aӿ0,͡{SHuz:,/xYYYdeB.,t:Puh`YBF! Yt?[2ɚyʘuM_TK8qtT 9^tRQ&#wir$zmR~Rt41k{mLReu _$>RJ2}eu=9S<$_ {~(5D{%.V½|&4$ಟ?nxsN-y=P8Fjc}][C,q +K1 q2"-I!)7J81а,ٶb &v`s˽LP 7\rvn.:llmsuɎ|a M(tCKS>y\0&h:M)M]yߩihBK(M)u.ʨI+N:A֒x QBR''\"*|A6<)6,I+lYDӕiMrTff\0 @Ǹ2b %e&&<˜ВD-);&R0@] 4RNxHBc뺴.9!s„BJ L7]S MxEE5B!M}4/y FRtt[R3~ٖyJĸAҲ}ej'pIK&NN?t4\k1@:b @.qhjvhZD"&H]z)4wLZ<&04 TDLL'_W*L)2cN!TM_AT>$KZy) GKyy4l[an2i H\]$p]hԋs4bC8 5н;/ϝ A]1,{8[)ݵ%XdX r'RĽ2dK.Mmi rtIWöq(@ƩSwRz^_D)I\ҖL=cy.c(XIL3onȚlr,œp:MT|?TOn᪹r 66Otr,X˦N>5EӦ[]gYfE$طyx ޡ1qbl?rXuSzΕ?qsi?)0OlKT%@;'Oē8-{^7g|ǛhNn[B'W97Uf25;C`طO{\S [`qW(W䍶vc|Ѧ@ &h}M^8$uaˎVM?zM.eY M;,<{M/u0&˲xn03mV_s&v ֝+r\y|KnV]rdk **99g:aU.;ɹm5{+(3˶;-aSF2HR}v[gLSy(dyc&S6rIO5'~b0U-#S>Y;Gぴ^d S,bAύ`M:}+Td6^ĸVbA9#J0jvij6nhG+gՔl ǒ/tB6ٲwxt<&d]H4=6¡ )N{!Kad'v+llxe-[O.?fLg҈^60~6UnאܸQם"[ G '8]z:;t^oW2x,z':ɹh"K-/}m-o9WiΜt"Fl>,MAr̴̺Tk )aҵbu+Yk(> ,BұMذ?t@2zlVq< M5ZDfp㤩L-}~2c r7|".zØ0u.Rk{u<(ӽ,ZˉN2c|8f&zᚌ6x#b=y4 &֭v.!-_[Gec6x1V2cb͓~?;Sgh g,~79~/=]:QOʢpn.\N衺ߤY, <d΂yӟ˂)/8 HvVs+Oo:ե97c[zcWepu̻j{-kvZzyw:)0aN>5.[p0t]iE]C߻]ḿ Q|f?Ρ?8&?]o6AK?WRE~ϳU,(R>4韮ONߌ6WYg[l;@Űn9%l^]">gXGxjw7\tl;Tא0/ [q77Ҩ\(qLK> [Sf-s>waĜ9ݱGP2?dCRj+LOAad.iLrdʷܺs5#:/|S"NoLR45쌇W>΅.;\=O8;~Pì~yMZƎ-3EzϬd^Q+۞[$W@  ֿ~x_wDeeEhvYbĬs/I dz]]7Č3j[Spؖs˒qnlfY(-=<)HbF >|SÆ1vdT[ ,+EkI&.DcZEF=RO" 1Nt`=B%ENd5,( ]*Fž*dkjˬn=s]6m֟A "4$R =ލU65ҤcGԢ˒au,ֶNUsy}SFLjJ;)od'pcES~Ejn  4Tɯ}z)(ꗏ F= ~ IDATGx( t]94j/ߵ'X Cec?Ae!bED!?Yn(eOc;\zZ)@8}&} =[iSY}-qg4+@[9vh\QtO% /| B~ 49 e ]^>{I jh#O.&.ݜ ]^wzVug:7n3>L^P8O;3+VKuKl Up^1f/3iٱ=zJ*ww|y؉?gj5MW1stzEaF>~RM aK&:|8NsS߽6TrTAPF/e3 }Jhd5ot_6D9w\DdבS킉uɟ_~Hϡ.8"$(89r.\<'O&,E$9ݭER^ !iBK=ʛ?d'ɿ69jkk9q{Tp|=ǻ'zc5oc΋tn݂7Hq9/0#J(JO %ut<jjXb Z2 Nȝ KÇTC[JA܁KLS}-߯`Pi=#5FON_=7o]DSe-p=~r5i/dnę]tb oqݩ?zyպ)3EGwϠq?}}3931f]{(݅1owt?6{^῞yNkMYvf\Ď'6c ̺>[[&Z<{w",*8ޤ@VՍ52hP.zE:#>ϊѣ)0b )ө}8MS;Kh9μswC1S>6n:G]M{XGǝ9ڪwu9Wgz  뻺} +]=dʘa,Ҝΰ'C2k!k)ݸ抇eOʘB(N_Sw<d fP8û16<=NYNjz̬ͣpŘ4+l<֭'ϱV4heٺNԭʁ0㦌#bW=uOmG %]X35s&v́MQx) 7Eũ!qn7?0t&Wپ'ɩ^\fƹ~B(AKaB$(>G7 *%:A0rQF32|A]VK=a $6ϸcviH³Oѡ%EO(UuF'l "K(uzP?EH{x{gQT23gR9>qŮzŤE^@ :/k.vuT /<{S|q)Do~:*/IP,+)&+6:X1p>v׬}^xl V˧+~[/SYWXg wo͸à_,|=^}E( : GkȟV2b%}3.Ly-4a>#z +D6VAl)_ƨiMػl:-ȷEZ߳!r`?ζ2l& jf7W0k6=TU5`FC1jޣ4N`j'UH1QfxϞ5N,ZNΒ.%u(D(଼~e :HhjDP+f`mBXʲĎ]֢Z8^Jai1CR=:JsHF$TǞj'Eˏدs=bGy%J|>Ovnٳrmר鎎@א?qם^L9@3zV/'sUn]8St\csqRYuoOU9ϗ~P4sFٹ4l{x|{OK8/N=`L.4ͺF $`fA]ϟ@<#9 p]p(5L8h4(̬QEdC3^qc99XG5yez;!K6RgRf.O͙N_VI1`LƅE!V8l&#pL| 2dtn*篝hJ'TrчOfr `пvB_֮zNK 9C[C1}L=w %#G(-~g}>T‚[0BD?iZ6(q'}jEYkロv,^{+;k (BǏrXGRFBuuacrNگw w|~%#hB5T7чNaF᯿3٧;]ݻX,(ԡDBa8|teh.oqݕLsX2<~o{>)>1m]Ѓ/֣3Ewݛ }SWV < +_-=f-L8y\ hX8L qkYΆUt"jaO )Qcw,M9b7/˯Ok}ʉR[uO?﷟~s>Ο}oT7ay_-7ͤu#';Nޭjq|aoMuz.(?YW Jh4K若,K3׶lE Uϖۘz<.r5s))ĜCڑV4mowͯbF-A{כSWo/ fGiѲ1|>*_hziKF0fxL&Kسu"BQʧgum=$ LY6866j5є^Zxy|>0jkuNә54z-;cWڭ#Becs |%!6~ABg!nɁVpC'euNj !'nZ{#V#-S<)sg3QmDzݷ!TEk8z /̰-ur(;YO/r՗D^>Z*zAy:pOL ffkxڻO8'uVΜD֩5Tg.ɺwqߪJ۽86eZJFd҉ L[ճdtstgtu\w~j?cwqƗ_>=Һ][>K{I]ksdJnÌز=ꉪe{u.~;+i+ $ Q$ы)6`Kl6.oblj$8q$;.1.jcS :^wjwDQ#H<'O`3gS33g3>y Q6_kWuG:4h]9C7wD#;vUWf1cF1X7SX\͢.D!g5*ov)V1΂ !lXVﺊ9n{hYYWfU<vOCLned|^7R/|3XKǹ~Mu(KƫoV ]l_}|% cغ~fr*p{7_|xLǭÈa[,qMG4ww;&ԝ_iz;uRv.w/eI,Y}qioTY7>T|}k5xYW~81mm٧-;8s@bETZ3/_A HdR$8ЦMSZuK3(S16K-qvXqDӁjm6 Kְ|L.b⥸Upa'|@y_i\6@Ƒ>sMx2E))vXl:uSnR_GYZ&1:p&(ഐql9Kg켴*'3ی@?Ԯ['NJ~\;oGY[x+˗d;y2c?} ]VW튝A[=>ѣWލ8f cC΍mnĸnkfo套-,]2 SՎ!0nn eȜ=vTuL pG*$/ukx,i}Fyw(>cy'vyw05ο;\7/@;{^3KJW_8)N4k~O?iy뼃䍛j\^Fg $$ Ê:x탛ӌ'H{N7;xi|@a{~zNUV\kF7(IYyz1`}pZ^7 ?/t3F,l}p.zG <(gV}p=<#0#[p590zZWu'_Q 5*4xb0bnT(_fa쇛utՉ>+(3r4. 7Վ`vtji[۾iW5[ۤX'[ ?`wsyEaxҁ>ʎo7F\w,4z_80*1)lmStW;A+[nc`J;7xޓ$3GC:o>y:?_DZh4.@B;,Z۷rR]IqueωYW UGZ372kUFu`(Ҧ6MGFFƁffms9EwӞԲuobrz'9qă]:΋=QŜi˕BKA!?%Jeœq6g_˟c餑L<'QRu}6w$-><6'I9[|ѯwMEA CQ>,;:ls"]971h_;߆xWJ;Qɾd|&ږF.gsLԜ;ƉYTZ{ٔ^cȄ ńϠP|.WMˏpUWPnX!:ەG#3g/'u6LW㓆zB|*ʩ:zao];1ꃫyD;UŔ[vB.߱FfSiyUDG'T{v-i7yQŷ?ފeW1'< a>̚-Nb1!eLq4{"#.'.̹k}?-1a{kU}[}tGQPpY0!qx}|d>ޔ\0\YPPǰl,|{ [~VüG0${3ol>_p '3$F싟 =14Z %#ZJ{19{)vM/.|CɵsGnv3n9{r~,CӘNd}KuoSߗIN]Zi|ˍB!w"Wqrhiepg W"=aV¨Qu7t6 { (DI`1mY#y$<++?aq{-hpO7`LIQ9U:B&2<.oȫm8$cBy6⇏"B5RVa#&$ _!nbIJB ܅e)lI8ؐ1G4(2}X<7%6u!+o|Uo>y>̲a+*[H4 ͽW 7PYY4gu,.wOdD\"&lovSิ`;v,8e]<kgh }4X eTXT!{p<WH_T| ޣ淿!^N,%[5L섙,(wZ!qӘqvۢJp̚ɟӞOLHa^1ѳ8goDߌ f 6{r-r;+0p, nW Oowl_F%$2~*S=Md4G똗OɉX̭Om79n/YA](Vfܶ3oRm,!c9*Οإ YxQz'̌G|o KRWtA9 :.|oɾ<5ܮzFLcF?x_g[2r _^╽%!a Ҳﴣ8Y0ٍ/+egc2eL9_=mlyo2Oq-E\0kW(>LXv;IY:2Ե{7nYVK丱[`2~~2[.5v6Kﺍig1lqTp搑dNh8!si'Gc;꿮)5e㬏L`V[Ό=SI_7mC#P% vHRg]YC/?Ƚ r9}^/jfD|1R_^'vGjAg0O1[o.+Ɨ+~'U>ǡJyXquQkr&R /mS^CޞC;5_>!Eyyv%~~mt7_:+{~]721 KD_- }i Ppj'om74!č CSzۮ$'o)ҡz !vt_}:p96[2*PCS$ą4JonHCA1M.g;e8bVA51*3}8ZrĆB6|հlNc}~ @ @\=j7]^ٵk_)g Ws\2vo"&ANۏ5fūuI x쯿Kj"+B#-* L|%1*T(xG摑SW3ؾz5G*j?Bv<;Ɛ4$&G{*?IXAKr߬Pk4Rz/6Qz\zJb2qn.rgsMyBݵP Μ)j0:)>9gWY憓IfF4I־q  >zDžlKn(/ҥ鉆wT3F Lή 8*ʣfŦr U@_VM;r>m74~ipQL~d+(5@?lS]>N1a3|gYk=Xr2iɥMh#uƏ j^1%=*(t J˯$qRJ)[mi .@_44|'Om66ܔvO/}I]* *frSif4z̆cq^F604PBt7(ߟ< N߰P4oɬ\yͷ6ӻ_6yɉnv~$FAΚݝXLJb-.JV,.p4hF>I֊jr"_8BN8isgroh|[9=ì3|̳lF)胣?+]/ x-Iw_>ÒBr))) /g)4q4lijA;x!ݜf6UR8˅قb*k_G3}Ɔ|!4.H5 G;[\Zktv]8-MUqNܶ;M7]ެu*`fޙ܉X}j(Lm>g:[fx.fQXOn9Nܪ&;H϶savo6!ٙ4|AF _:(WZh?ص ,{#&{O]Qnyd*Ko@ч1~\kk8]??ǬZAM>I9Si2upfl~_}7?ÆJՎqx3XyP6XSPU(.3mXFπQ ۙMsռ"]q>ۥ2dZ@eUUi +8ųf4!Õ%76{nиm[t)|vlwsils퍇Iu|jL]3OeOa$̐q1շ"y~{8,?h[x g`NL ~ji);y厗O17i?(@K[IN c\p:/\Mxjx)}|km`8x t\iUyyOq;GM tZhv _q`}<`0?uG6^è;_f!>v4'UG<h=NtPϘAO}M^]%ز9xmGI-łuc% \:P{j!uj=n]\~*#G򥫾ݺTRuu*N )|ZO£4<ɷ,b~As]frKx(z]l4s|ʭ1 p/%ˣ{2QoX9=X'{f~D1}v#q#۶;}ק5<[Ƈ4k%Bndzxqt4T(mpGMOsy ~>(o틃┏x6ܡi:WVsgl=6lcH"8q,YEFl/ elbk]W?QRIi$5lάEؖ\K +Vp8w0n.;t ӦCI@My~~0d#!0ƱtV)\|*޿fBIQv|>뎖8BI^Kcԅ$xԿA_9{:3Rv.w/eI,wq{;igWu3 Ej7qd+/O᜽?a`9BQJ;g^^}6gbf0~V}k޲M'1,A|Iaof[ڕ{>j‹XYt&ewUCe~h#;a6{+/laL2٦1d3aX5rng$C8c; *3yenǔNLyGYZ&1 pn^a$]Dݸl\ ?@GH\ȶꞲui/%,7Ž혈xnloعOZpZ8r}L[v >ѣW]8f cCvϞ;sp 7Oy#t+׮#%asdL )gŲ~Ut#Cʢ$s-zo?Ԕsn?GHN_z>/_v{1<~&+?FK+1]~ A.Lj\FZLGoeҩgy|qjW?u:?_DZ@/ !npTK-pW\+>d{ﲳPDp1,[>O~'së|$q3ifNͣtFe5`%Oc+M'J.MGހd]9/8aSuŨ̙Grj43 ÃBIZ7>>y) "}j\YAEU|Ϫ-پQ6:t > $GITHUB_OUTPUT id: version - name: Build docker image run: docker build --tag=swayimg --file=.github/workflows/Dockerfile.Alpine . - name: Run docker container run: docker run --tty --detach --name=swayimg --volume=$PWD:$PWD:ro --workdir=$PWD swayimg - name: Configure run: > docker exec swayimg meson setup -D version=${{steps.version.outputs.VERSION}} -D heif=enabled -D bash=enabled -D exif=enabled -D gif=enabled -D jpeg=enabled -D jxl=auto -D svg=enabled -D sixel=enabled -D webp=enabled -D man=true -D desktop=true --prefix=/usr --werror ${{ env.BUILD_PATH }} - name: Compile and link run: docker exec swayimg meson compile -C ${{ env.BUILD_PATH }} - name: Install run: > docker exec swayimg env DESTDIR=${{ env.INSTALL_PATH }} meson install -C ${{ env.BUILD_PATH }} - name: Run installed run: docker exec swayimg ${{ env.INSTALL_PATH }}/usr/bin/swayimg --version swayimg-3.8/.github/workflows/Arch.yml000066400000000000000000000027371474536441700200710ustar00rootroot00000000000000name: Linux/x86_64 on: [push, pull_request] jobs: check: runs-on: ubuntu-latest env: BUILD_PATH: /tmp/build INSTALL_PATH: /tmp/install steps: - name: Check out source code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get swayimg version run: echo "VERSION=$(git describe --tags --long --always | sed 's/^v//;s/-/./')" >> $GITHUB_OUTPUT id: version - name: Build docker image run: docker build --tag=swayimg --file=.github/workflows/Dockerfile.Arch . - name: Run docker container run: docker run --tty --detach --name=swayimg --volume=$PWD:$PWD:ro --workdir=$PWD swayimg - name: Configure run: > docker exec swayimg meson setup -D version=${{steps.version.outputs.VERSION}} -D tests=enabled --auto-features=enabled --prefix=/usr --werror ${{ env.BUILD_PATH }} - name: Compile and link run: docker exec swayimg meson compile -C ${{ env.BUILD_PATH }} - name: Install run: > docker exec swayimg env DESTDIR=${{ env.INSTALL_PATH }} meson install -C ${{ env.BUILD_PATH }} - name: Run installed run: docker exec swayimg ${{ env.INSTALL_PATH }}/usr/bin/swayimg --version - name: Run unit tests run: docker exec swayimg meson test --verbose -C ${{ env.BUILD_PATH }} - name: Run clang tidy run: docker exec swayimg ninja -C ${{ env.BUILD_PATH }} clang-tidy swayimg-3.8/.github/workflows/DCO.yml000066400000000000000000000006301474536441700176070ustar00rootroot00000000000000name: DCO check on: pull_request: branches: [ "master" ] jobs: check: runs-on: ubuntu-latest steps: - name: Get PR Commits id: 'get-pr-commits' uses: tim-actions/get-pr-commits@master with: token: ${{ secrets.GITHUB_TOKEN }} - name: Check DCO uses: tim-actions/dco@master with: commits: ${{ steps.get-pr-commits.outputs.commits }} swayimg-3.8/.github/workflows/Dockerfile.Alpine000066400000000000000000000005031474536441700216570ustar00rootroot00000000000000FROM i386/alpine:latest RUN apk add \ build-base \ bash-completion-dev \ giflib-dev \ json-c-dev \ libexif-dev \ libheif-dev \ libjpeg-turbo-dev \ libjxl-dev \ librsvg-dev \ libsixel-dev \ libwebp-dev \ libxkbcommon-dev \ meson \ wayland-dev \ wayland-protocols swayimg-3.8/.github/workflows/Dockerfile.Arch000066400000000000000000000011371474536441700213300ustar00rootroot00000000000000FROM archlinux:latest RUN pacman --sync --sysupgrade --refresh --refresh --noconfirm \ bash-completion \ clang \ fontconfig \ gtest \ libavif \ libexif \ libheif \ libjpeg-turbo \ libjxl \ librsvg \ libsixel \ libtiff \ libwebp \ libxkbcommon \ meson \ openexr \ pkgconf \ wayland \ wayland-protocols RUN useradd --create-home builder ENV USER=builder ENV HOME=/home/builder ENV CC="clang" ENV CXX="clang++" ENV CFLAGS="-g -fsanitize=address" ENV CXXFLAGS="-g -fsanitize=address" ENV LDFLAGS="-fsanitize=address" USER builder swayimg-3.8/.github/workflows/Dockerfile.Ubuntu000066400000000000000000000013641474536441700217370ustar00rootroot00000000000000FROM ubuntu:24.10 ENV DEBIAN_FRONTEND=noninteractive RUN apt update && apt upgrade --yes && apt install --no-install-recommends --yes \ bash-completion \ build-essential \ cmake \ libavif-dev \ libexif-dev \ libfontconfig-dev \ libfreetype-dev \ libgif-dev \ libgtest-dev \ libheif-dev \ libjpeg-dev \ libjson-c-dev \ libjxl-dev \ libopenexr-dev \ libopenexr-dev \ librsvg2-dev \ libsixel-dev \ libtiff-dev \ libwayland-dev \ libwebp-dev \ libxkbcommon-dev \ meson \ pkg-config \ wayland-protocols # install gtest RUN mkdir /tmp/gtest && \ cd /tmp/gtest && \ cmake /usr/src/gtest && \ make -j8 && \ cp /tmp/gtest/lib/* /usr/lib/ USER ubuntu swayimg-3.8/.github/workflows/FreeBSD.yml000066400000000000000000000026061474536441700204210ustar00rootroot00000000000000name: FreeBSD/x86_64 on: [push, pull_request] jobs: check: runs-on: ubuntu-latest env: BUILD_PATH: /tmp/build INSTALL_PATH: /tmp/install steps: - name: Check out source code uses: actions/checkout@v4 with: fetch-depth: 0 - name: FreeBSD check uses: vmactions/freebsd-vm@v1 with: usesh: true prepare: > pkg install -y meson pkgconf bash-completion json-c freetype2 fontconfig libinotify libxkbcommon libexif wayland wayland-protocols libavif libjxl libsixel librsvg2-rust giflib jpeg-turbo openexr png tiff webp googletest run: | set -e meson setup --werror -D tests=enabled -D heif=auto -D bash=enabled -D exif=enabled -D exr=enabled -D gif=enabled -D jpeg=enabled -D jxl=auto -D svg=enabled -D tiff=enabled -D sixel=enabled -D webp=enabled -D man=true -D desktop=true --prefix=/usr ${{ env.BUILD_PATH }} meson compile -C ${{ env.BUILD_PATH }} meson test --verbose -C ${{ env.BUILD_PATH }} DESTDIR=${{ env.INSTALL_PATH }} meson install -C ${{ env.BUILD_PATH }} tar czf swayimg-freebsd.tar.gz -C ${{ env.INSTALL_PATH }} . - name: Upload binary package uses: actions/upload-artifact@v4 with: name: swayimg-freebsd.tar.gz path: swayimg-freebsd.tar.gz swayimg-3.8/.github/workflows/Ubuntu.yml000066400000000000000000000026541474536441700204740ustar00rootroot00000000000000name: Ubuntu on: [push, pull_request] jobs: check: runs-on: ubuntu-latest env: BUILD_PATH: /tmp/build INSTALL_PATH: /tmp/install steps: - name: Check out source code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get swayimg version run: echo "VERSION=$(git describe --tags --long --always | sed 's/^v//;s/-/./')" >> $GITHUB_OUTPUT id: version - name: Build docker image run: docker build --tag=swayimg --file=.github/workflows/Dockerfile.Ubuntu . - name: Run docker container run: docker run --tty --detach --name=swayimg --volume=$PWD:$PWD:ro --workdir=$PWD swayimg - name: Configure run: > docker exec swayimg meson setup -D version=${{steps.version.outputs.VERSION}} -D tests=enabled --auto-features=enabled --prefix=/usr --werror ${{ env.BUILD_PATH }} - name: Compile and link run: > docker exec swayimg meson compile -C ${{ env.BUILD_PATH }} - name: Install run: > docker exec swayimg env DESTDIR=${{ env.INSTALL_PATH }} meson install -C ${{ env.BUILD_PATH }} - name: Run installed run: > docker exec swayimg ${{ env.INSTALL_PATH }}/usr/bin/swayimg --version - name: Run unit tests run: > docker exec swayimg meson test --verbose -C ${{ env.BUILD_PATH }} swayimg-3.8/LICENSE000066400000000000000000000020671474536441700140750ustar00rootroot00000000000000Copyright (c) 2020 Artem Senichev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. swayimg-3.8/README.md000066400000000000000000000065361474536441700143540ustar00rootroot00000000000000# Swayimg: image viewer for Wayland Fully customizable and lightweight image viewer for Wayland based display servers. - Support for the most popular image formats: - JPEG (via [libjpeg](http://libjpeg.sourceforge.net)), - JPEG XL (via [libjxl](https://github.com/libjxl/libjxl)); - PNG (via [libpng](http://www.libpng.org)); - GIF (via [giflib](http://giflib.sourceforge.net)); - SVG (via [librsvg](https://gitlab.gnome.org/GNOME/librsvg)); - WebP (via [libwebp](https://chromium.googlesource.com/webm/libwebp)); - HEIF/AVIF (via [libheif](https://github.com/strukturag/libheif)); - AV1F/AVIFS (via [libavif](https://github.com/AOMediaCodec/libavif)); - TIFF (via [libtiff](https://libtiff.gitlab.io/libtiff)); - Sixel (via [libsixel](https://github.com/saitoha/libsixel)); - EXR (via [OpenEXR](https://openexr.com)); - BMP (built-in); - PNM (built-in); - TGA (built-in); - QOI (built-in); - DICOM (built-in); - Farbfeld (built-in). - Fully customizable keyboard bindings, colors, and [many other](https://github.com/artemsen/swayimg/blob/master/extra/swayimgrc) parameters; - Loading images from files and pipes; - Gallery and viewer modes with slideshow and animation support; - Preload images in a separate thread; - Cache in memory, no data is written to permanent storage (HDD/SSD); - [Sway](https://swaywm.org) integration mode: the application creates an "overlay" above the currently active window, which gives the illusion that you are opening the image directly in a terminal window. ![Viewer mode](https://raw.githubusercontent.com/artemsen/swayimg/master/.github/viewer.png) ![Gallery mode](https://raw.githubusercontent.com/artemsen/swayimg/master/.github/gallery.png) ## Usage `swayimg [OPTIONS]... [FILE]...` See `man swayimg` for details. Examples: - View multiple files: ``` swayimg photo.jpg logo.png ``` - Start slideshow for all files (recursively) in the current directory in random order: ``` swayimg --slideshow --recursive --order=random ``` - View using pipes: ``` wget -qO- https://www.kernel.org/theme/images/logos/tux.png | swayimg - ``` - Loading stdout from external commands: ``` swayimg "exec://wget -qO- https://www.kernel.org/theme/images/logos/tux.png" \ "exec://curl -so- https://www.kernel.org/theme/images/logos/tux.png" ``` - View all images from the current directory in gallery mode: ``` swayimg --gallery ``` ## Configuration The viewer searches for the configuration file with name `config` in the following directories: - `$XDG_CONFIG_HOME/swayimg` - `$HOME/.config/swayimg` - `$XDG_CONFIG_DIRS/swayimg` - `/etc/xdg/swayimg` Sample file is available [here](https://github.com/artemsen/swayimg/blob/master/extra/swayimgrc) or locally `/usr/share/swayimg/swayimgrc`. See `man swayimgrc` for details. ## Install [![Packaging status](https://repology.org/badge/tiny-repos/swayimg.svg)](https://repology.org/project/swayimg/versions) List of supported distributives can be found on the [Repology page](https://repology.org/project/swayimg/versions). Arch users can install the program from the extra repository: [swayimg](https://archlinux.org/packages/extra/x86_64/swayimg) or from AUR [swayimg-git](https://aur.archlinux.org/packages/swayimg-git) package. ## Build The project uses Meson build system: ``` meson setup _build_dir meson compile -C _build_dir meson install -C _build_dir ``` swayimg-3.8/extra/000077500000000000000000000000001474536441700142065ustar00rootroot00000000000000swayimg-3.8/extra/bash.completion000066400000000000000000000011701474536441700172150ustar00rootroot00000000000000# swayimg(1) completion _swayimg() { local cur=${COMP_WORDS[COMP_CWORD]} local opts="-g --gallery \ -r --recursive \ -o --order \ -s --scale \ -l --slideshow \ -f --fullscreen \ -p --position \ -w --size \ -a --class \ -c --config \ -v --version \ -h --help" if [[ ${cur} == -* ]]; then COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) else _filedir fi } && complete -o filenames -F _swayimg swayimg # ex: filetype=sh swayimg-3.8/extra/icon_128.png000066400000000000000000001012141474536441700162350ustar00rootroot00000000000000PNG  IHDRL\gAMA asRGB, cHRMz&u0`:pQ< IDATxTIguwxSsfeV*YD)u[ ճnxc4 0l/w^ zdneKT5ĩHV85dVӝ"ţ..p w~w6 "b۶#Dv<;mFZ"fź7""`jf@vsu<2LU ,i9bʢ,{|7. /;ַIDDL[n@D`UQD `fLf&"W"yF?[9㵯tJ"d(r|9vjvV(jc$" ܤH%cD*1jAM9"JBpÐt"oM3s>dv8MMĔ)gMLѠ*>E ҕE_WތgC;n7U3z rsrTeSMJghvcL%LDDLq9g9zru}S7M i{kv\!aEœsV~ ^&:産!mE'90Xs. f%G5)*0!@&lS(68F)/" va\!M 9E4ػUB@DH)$V@9ǜ$6p̥g, 5S|GaR֊|hCW+cC1{`Y%ĒDU̠*DMz1һ31q$UeT'ǝe1$5ߊٵ!}?64ӇԐ߼/ ll&1"z!M|yb'vFbX. :h1eD$rE9fr)_ATM7u{VYPæpk{\VL Vtq?va>kO@`ي'i݁ZU ~CUA*{1 cp{/.cf"ϲU[fT7BFBtPյ03s"dR,% ҇wۛK׆?*"qe]EM LMP?B΢94b$SɭJʖ ',!<:O9k\kc24响2%ɊeS#vy|Tnd0:jO Ȉ&PLc4Q1ENpql:Z`:ij30 bT8U]aXM țPc0͎Oâv1.ϓg5 kʡҾǀp\BSWGr.k7;'9, Ί˜B;'<$Nj7EhǑUՐ|i2ͪbuNf~R8 LƄF eƑ8uUWwD)EnV,-{)%$4ɞH*3$%W9WyJ̎l&1ИR>q\“fl@UW&9+E"@%xocDFTw;ߛL''gt2Am{ɂ*Ph `hy6sIiι"]7&4$Y$YF3#BR"4 (*4sN'4eG|) (Iݝ)9uUۮkW>m!k*4!IUfb﫪nvwbuICΛn @GNOvsY7 n]6ߒ9q*,ɟCMa,%OoLKo<Ƅ+u~nAy?{Sޓ3GۻU߽kH% 䨈FL9Bv>SN""?ѓ+)s%uIEp4P('%ԡ@Y dBBUUEPn2۪'bU3w]Ե#gN8 c}-3ڮC8`~>/Kb۞qD"+Y!9&bNY}ՓӍc8&q?w%{BAȣ -q6i?s[\efo0ؙoa5=>`61"qpL eGYMIf#è: #AG K52@LAaQ0CbAnrZ~+CH,IΪvcj)sa5vxvtv}{OE1n1 ضi8 `Q%BY {$*\ڭͤ7>[7  !AR D~sP0XnCQ|cz x TuxB^m 0Q!&ׯQ5)'F@GPh*v~INj1K/|/ܛMjǍ3+X 0'UmB6Kx6{\w9,C23! i>{[7zYw5=_Dg_L%@*DwO Veh|zْ>3,F)zun~΢ bDI1f3U ]({55PW%3in8̦鬉9[$:;},4mG8L\Mj|/E;2rƮ$ah!'[SR2`ˆl HjD(9z2ng~z^<1C*,ylgo:8}x/`D?g9r<7o;7.o%PڦO]:|{_~R㿜}'.y{9یÑJRDZ[^}Cz$CH#Z2=C`s;^+V N)g]3S0DF"D'9vY[Wb=!tu؁B|u4)%ˉ~7(j>9..]M'L|wd{&PDMCZĮɃT}X fr vP!#{.""^%#mQw8X_]t[';cnQ͚f[;oo7cBf^?xu40$/[w'~E>m~}ꚈW`=PO:YH$( ꂦ9 ,Y@8ʙ,_c@dE"4F܎|%9}R*&$q, wiuQ !$&&!z2[jcddnr1.3X2v9&Wd03D B ,Io.lxC"> p6O_=8;cGNNַnO;'[9܂>}sF0zt^^W8th* ܝv(wbPa74XLT867G`(HJ";?} ;Í:>W>5cw^Vd\+*-\F96@ KsC8w6}35㔍yTE29#-s>++3<(6kӳb>s6: ZY/?C'yks޽_|?|wc.?[{gl, |i>Clq2^G_348TΗC1LT?B0e$r3"d@/ީBaO gk?KgYO:KFo׍_\޿Iɩk!n6ZojidpLhb@HA<ֳENjƔ-JF xhJr*xNsN d8)&)pZSԥ rw+C~ B~k{^ Prl>ܸXZ~3V_@:M1amyxMT2yWiOVO{ǥ8Ҧ33' GЌ(5&P (ISʛM\ ~u~6Ujc&3-P3l}\AU@ȹtfUx BFG L%ʎ51h}忼#o /up"^~zepN{󻇷N|XcӀ;ˣ~.ܾ!QFnsrg_ϏhxO$,:d.aى#NY+b3:ظݫyIRhfzf6M[ۛ\XÎR{OM{w>מϺ0)])9Xj:ܪlecu8{0 gHȆ("95SU3s)eMd`OR!)efHFj=Zf@u "zºAd&ނ'Nݺiv\{0c{{oipw?r먩E"V) jLt0W{׻&f\^̕W|X~UϾʏ; DOYdg_wvz3BxI9$?G~㭡[A z *"hFd̤`*AZw@RPPEp}?`sU_&l@#\_q5H"ݘCϙQh-zdQPtEj6Wv4G?wY΢z ]E̅BA@]W8ldWg49^lO7?MݺmǦK[>Yt;auh8) {{V3߽zt6rG*h*EEm6@6;Lu|9t6QO~ skjrbBUs Ԫ29h&BHZ:f #03<@FH'ǓhK­0G=wj@<+qUҹ9 k@T XOȴw / sU6>Ы V$J{6ރdD0TIlB!#"j6:$4t*y@. "Us2vq,ARޟuɋm*#rNYL*6QB;\8v 32P2#:͉%*8۟|O_{m<Ĩ?s㷎g5a{u\ͦ%,ե5lugv}@jT՜łŔumvUv5(K3ݴ HĖE\Nv=lH9'PăԀ T2 IDAT1*m'"Dvfλ;@T7]}^<XBdeMbHk+=s Ηl|a*tˊ3f@1CB~Ͽ|í7p>wZt{o()֝^6^y竣ɏiB)HmW+777j9ȩ<؎ @f9xV( qp4 {H_Lؔ'$#390`":gȬE&fGK).x;Xv48JC#{WFԆ,$]@+y$VE5bޝ%SEw !Po]/G/}jyzyR4wzp-on\߹ZOݼ9ə!}7ā5*\B[B`ŒV"%ItcU]&I ۙC̆ȦF!"9NEʖ3zT9`1dT!BSO@02R?u9g4PV#{3"TleJ۳ZQ3*6 "#8Ʋ RQiKEƼ$C9D M.cs|vý]0J>t7//뒷5oWkS8KIWKUIlZ p>ni7; PRy;,M2"9 h &h&a̦1!"bGL1lʌc0UrI3iwjs/캀 ( $"X"AYcĈEOqeETSI3R.1`h }&YS !(9"ncӋHŲb0Iɒ1 cQm+~ f1D@PoN=F]R&3Ob;kZ푄&GD 3 Rȡ꺎PU`^~g޵dCL+ݼ_=z\o)\s0M1rvǏ6'Iݙ9AK#̆njPȜ\AMAE."yр<"Z@Q# WgCB* K>D0gB jPooNVs yrN$ C04  }rp5Y4_H42&0z% 2=jU 3#f%@Y+ȔǾ9|~sE*DܾޟG-g,/ݤ:p,ƔQa>őrŊ3G$SC,Ţ e#^h&JD@za%zf 6 M2^4Sc &0Cd{pʹIs?7εx@yjKhs-!9 !" (SoHۍ1Iq=*(;ZMU:BQ2MESv|~}O"a5i6{:F/ޟ?f[%/ ]x*@@VkwsM'>!`4O3ʼnzh#zC$Du`hIx΀p1 7!V,vN0ɼSUdgkW"5E'F[y*š&ـqx <#c4P 'ܳgt8z/q92 ;JcM4.3 /K&ٿVLРآ+~]~G߮{?;~!Wn9~Q9ê늽/(H(̙)(tbBWsBSLɄ8gcBz  Hہ# aG&_}|\huƗ~ծ~[{Z*7 ~" Rql5(xsXW 3+!:V4M<`Gn3#yimU*Mi\?|$|ahG=8w+[Ie9$Z4mf^NOJ ?Ztv$p"DUQ lr4EP R5fCd3%SX%]/18{߼BQ23ߺf}Rӣ}~9QBr[[tUq:îAJdW@%I[;YP׫Oƿ^G{itqTQkOO,P񰹿;Qsr?u!iU Mibt)+I9:UHD*H`HfjpQ+ 8fOL``_Y<@צoeqwq~<'Iz]>>GGgԯMP@?Jh})\M # PU$ K#c*7PG ŏߦWÓ jʪfH9C "U-GDJA8,$dHah@&X!1dbZ KeZy.d#Z=bFT#BC\q?"O D[ PsOދ]M*3yK7_$[|;M ΐ!9@4$`Q @ _m4lH0d88 =v7N_&֪_Z)1&Z':B*A.ckf.   zd'W;ߍ*r $+c6So>~s lE$zf **fi*ȢƤslE$K۶9Ub.~l bmɬn|qӑd3}ٶF%(UEU0d?+1 ?xiڜG@I;' yH~T/>sLJ9(Z , LB J`5mv4tb}+I@d0G;VѼ50cDH9gR[ٽ5eQPM!#zid"d_nT], ,O;>[M{…lH A-%.U'|d|hqi>3OTHc\ͦoZҟ7'n=ܘYYUc z݀uί?e3U|'C?yQq8KZTrUbdS-ep;7/#ZtG0 '$00hNUi|<R Ϋ!n9x?^@M/8NEN$Q6w]/:?r ^zjS s|r5{$Ͳ:F]}t߸}u, k c7-t =w^;XTpIEvp avvʏG}9`2hTr-x.6cę"!2l#F4'!f@5SP$"`yzpO$e1$"(A^;f%bDطHD'LD}O6L0ZEvj.;- >sL;{;{q|sW=MBj{WDeQ޽1˝W'n\w74~vM7ΘAc> Φ̓y*g7TXPp0d8 +;w"@Ur`j9k>!o m7<] )&fuz"xnI6f+ I|X>}Lu Y- .g՜eV8_9GFk>wp4~l]{_ͰS?veĪ)S!R*-adW~/6ftyF kƹh4x;Ut3CҪ`^?=u>sT&rf]D@睙9".)YnΡe¹Ϝ*30#$ =SrCΘX}(LbӶP(okbPp̴ه\4ƚҳ0].qcb/RwG sޛP)D15x@i $k<,!8 3ˆnRdU Tjq( 4 |>-fF >mT`FĪ@)^е=EU(4fFB-9aH$#eA3+s&jLZ,PSlZjT,FpR{ޝU)  *߆d<零LWxV|>u_4,Tw IW|vGx MPh KʓBL "`` (D5Ӷm?RB2"nwNRʢ>'ď>"}ܷnQL !m}_v{r%bb*9Pu[&d]ꤙv>wKͫ_}׵(E Ecn=(U󀦢 ݶ>"_>  /qyZ{"9(EoM&97K=1kO\LԘP2G՘D"jf"tZH`'O3cmwF d2x iKI*¥E@3N[_ރdcCl￞fG\ WÓ'==4ʝX>zfZ8 Sd4xW]^}f5zЉ1fRHTBSeׯTWG׮LP 0&[A=uHI`LvF^e `)gI̘(" `He!De$ "F_ yRU.Q4df/՟Gh6EҰZvs1 q,zqc3w74U88]|݁ӝpMoLn~}|:ET3,JYDNwCl W A&"t>5Ğ l*Jd> dn:SpDDTQ[ z1 ׍6̈́Ŕ@X9iLM9eO&jQJoߙ>:=(vyy,#y(UqNW?d&W1AphXܘ| h(VW8`;@cȽbQ͍zNcV jPxitP0 7.Jѹ#Qn25kqD̜l&DadfX6۔4%Gc FҢ%r*XyZd]/oܾwxmud8y,k,zt>}/ӕ%eſxx‡{G1Z+b6-'6H:˅S>IQ@c ˌ^Tm6"GIϯ**A<(G lRKһ.C >1 6V90IĹsF4sL*u#93; 11mnہEҙ)g@.V}'6HN9q]L^VvuyWY?Uf ɿ#w-G#.or:Lߚr<+T,<{٤×7>+g eXCQzd0J]Qs(e"ȉi6D9ՙowJsj15}p󷯺gS 0dĔȩ㕺2͂6Cmhι"Jմބֻ1x(VH`Bm aVRSf'o>D3DNQzWufjC uaC$t LC%#/PS8:7q݈kom\>Munڤv޽a1uʩNy>(pQ»G"PP,vF(䄋̞ ЀL(;k8lv\$`v{fm'F{72H]$G˴lRjb!˗k(X@ټUZ#ghXV/?bكw,TbIzmnͼ%\_A᠋li{VC dhf 8xL).w{Vhzt~4BG΍2mނfoLn:gA*u5ĎmN.7o33KmPiUl[)J"f|y-X2)ZJieVŝqb\ݬ5;e(c3f7N?|>߼Y׏^w^27ޜc}1d0 TSH'kC;]/ˣ6k8x&quD6HcB02w{brΜWW?8lISxD2y9Cr{Cjܺ) *"u 9+x$Ƅ]{NjM$nKrڝZojp/~OOhwnw7И??zH_V~\J۩WcWN+P wH/Ѥ(4jw<`FGC4(d<\sNN`Fl>/;%2ɻ\`Fُ Ҩf*OďLiȒg @8 H`fIXJdlW MJ2(q;3œMY81玎ю;yqffA ;|0Y-q͖Z_:."ׯ>|QwUʶOvBsinl ne*mZ\A뼀e`FeBv\:(|@9)YH}t*eM}|(HKrD_YwoNݔc7(DtյUyT)~!V0SӘ_P|渜ԑXȓ('G`*UI|rr7֫r$F}kޓKq~,YF@̎IGd,Ug7n dWϯy̐IX]ۅQe`j0DQR)cʠbz?3حG6odab䢾Т =ߜ^QT$xSDvR ]`UգPR/Qr;?I!+ydB8) e|S}SSݍϣxQ?{w(f%6KjDDJ3|J+DGU.Wl:;t#916u3eTr&-!jFS#dsSh4Fhp)BޕafSl,ٴ.E/Pt9 d(fvuI!bۮFt1gdF4"f6 {O/wn8^ x+2ƌ  cvKJ tPO*KU]z[ţ>J ^>Y?Ӝ? (Y kzy5`pyJ~_Eq]AkXW 7w&Dfj"0jgj!^C`-з`[#0lڝobMm Kf$Pc-7&\吁u:^%)6A͈I`R3oL#mCeD^҅gF=/<֫WgtŞέ_$awT?{up5߰؇Q .et=ŋy; TUs+? Uc!!:Ά@TDEpfZtM81U4p(NRԃ!!Â!!ޑAuۦmcaHZ%d#m)n>(9goN~p:*:J(Py W^ y3WKH= 6#/fG`Ae;Gr=wM\FuW? ǣq4ԝ~vs"S"&i+X5s3$3-6#@FvY@ȬjJ}}//l]BDs#1$I`I,F [U(nU>3f30 0(3۾eYlA!wkﳯ/!5ـ)E2o:lHiΟ*7K<(WOGGGzYeټW`H}[:+4CM\nR=*ev>%@y04zgTmsy.h͠qMF"4~Fy\Y@$9(g6![&DM-{;caHjLޑ|L9L[EOh[/m!OhaI"o_%E_j!x}l dy5{YZe=>wӫeM{Y@I )-aԭxT᝷`Zs;^7rn";i_}ӿ[wSNBHUBR`3~h$`gq#B+p:W 7mzt\ 9N3a60͊&cq^ލGe`&ι,53B3r4x=Ns$3~ovV=ba\X/_8PΞ_2T0_)gOWYG#ze84IbN48?+q@.e4?=I7Y6-[;֕5uso9wZ TŔ8`?+Q"6T3rq}ȋȧWΒK7)oLa\.nvӮk%g 3`L("#"Ħ<;k%E`b# No!`B?gpRh[JL0Z n4yLQINj.o@AVe& |({ϝ_[ahGvėt%0J?lAP(h?w["Lt=5Z;u-PI f2Vm4j^TJOބ͛eYp[_d/'?e/N?Y5yܼ׳EHL޶( jd"dbb&JJmp=1xOFL^|<yK1H,IEAs@J:g|^|?Y'1j=Z-;~pE# 9ui9'e>zgb;x ]VwI=GXIBny 9$Lh%Zz^ k"gOZ 'zH L[Y lؚ""@MDvgSSK9sVDDL![Ip+X7տgaWf1̡)({Cu蒨w]Wդ&#b ?1*5hrG?`Ry~ݱ? 5x.2wFm:9P<?/i4 D(0'{tM (UB2XK"ЌuJY_M۷0wOR}#窢03>-֖af\w4f3c"K1Lcף**@ Lz;q]M/N\!Z;*ކg3\2Q%}?䈣9΋LRŭi_BRpHFxwAĴKT>7{YpE@0 ݵ$tP&ίq`vBlȠ PL bSPXigQQ7D.e? )Tż?&Qc0!^ y<^""PIp= >኉HUzp\f;yXmɰ6PY,ظt$.8Wʤ߿mT_5M[j?.L  yfB+[_]F"S$ : nr3h-W'yꀌ $hr41$AzD Г33Xli#P.po>8*˻˽(%ќ(oD ˶v~|,=SUk@9B_)e+|#?޿* ]p·N9SʳQI_-i^)}n?̗BQբUwRJpO|񳻓inlZMcjUQ8#_{wggA{ʾL~lX|NðƪOb7]ޟq낝{n|q~/}e^=y{9;Ct&-SY'O7 ڍ#$و_S~(OU/TʱDT.~`Y. #h x}eS`DmPԗvz<%#5 ߾vD2Ǟ=4_gÏV}dL>+fr>(Qo.O?'_ťpv4GOƱw EfL&ycdwɅݢ+~az;j^^'4?Žo4 [W#KQHj[nݴXt02d0V.HQ4ltk_% ?=s>S\A17mBuޞο ,+b3"#1<0luM1s~oh/Q9Y!q͟~rvbwx GeqZ^ltg}}t~4ݿ\ Ey"&v/_!kf:@z 8  =|ץ o$V( scW(cF9*}0ɽ@3jL< [' #VlYysvú3[߸-t"rRowwo!or1uRVM ^ܡG-{l?vƃ;| h j]_6QOc%4W7!&қg'M!xSӗQ0gh DH/o¯1E pYcc 4.JJiVe>@b"B{/TJ#dB \:梬Ӳo\,xOLʦ}y8u=lra}(KLnb~/zھHK_d=(b|cRƙߞG|wj|0zqS\nbq"NE vWRb7ҏW`I^sŲX>ï젺y6qTnxMS9WiTcjPN*Ac(r]Οt>|\hMjX݈:3sףʟUDUBOfHwoڧ^o&[G;iup9#ei0_W >KMWxCIE_F-~=X']=_;Ăua#gb#".Pߦ!{결p?wy~yvUCb&ͬQoߜ|%!BzQ f|r:@\PWJbmˮ9W{nWUՑEV_$KIT,Q-EN8 ~u8[V,"-L=)6EV_sO۵֜su(lkز.)*BD神sL \`fDk^@L=('9Z>HOQ|87~!rW`ut_O7hi]7hP ||EW?iZ'Y2,_;U{ޮhsB5?d_u#ꄌ[" K‡W`rڼiԪٝޟdGEt8f]lHtK@n@s\3{D2+E\ȅ{"N0܄nL[Mf!gɟ\`ק N2{\rĎx6ciVϗŅh5o^S-mFǔl*ƪL,ސ3ˬ]ɦΗ݅&.fl[kn/rft2hw'VR>tQb\k=4VUܸU=[['OҴ`ƌHVu\ĤQMY^`D{B䔭~ J;~b` фԑC.6šգ$,:HԽ߰[eN\Kǀå^|'CF LsQvĀ>&[rYJm0ƘESo,Bp9gHҹ3:)tih+ IDATt20sEDz8͋8{`h)mOXݯF;8;像x*,}>؏" 2f4;}Ef0_0+"ŔъiW9SpAD#4CUvΐ9NBPf@)B?qSt~?ŧdө{`^K6ҝW]Ǥ|!xVH!pȒH]bsmn.ŘcΒhؗJ9,cSƜQ2G0ՕYNS7nu9%C_XW}ugݺ9~ypjý\Eҙ Aqm^1vh{LQ@FvsƑ/L,C@ "*0QJ%bdBN[,޻;y7nύllKzibI#lf"'2׌J]-u)|Qb>',ec읎 o0ܯ)Y*4}~㠶hcLu-R;Ls}Ā=?8'O֫jBH$bgj~<:< !덟LfrZt zۻlt4.8lgwe`>}¤9-bNY/ yf]k_d.UL9Ni%*lMv߶( Fڒѥ=qWr&,rcň 1n-I//q1|rd QGgF*蒊,嶭KYD EruKc>8} :CA}! VMsT$CLt €t|x|/rƱЇM'~K%f5#r2 ՜`ӐAmn*4lL'nhy)JY̶lLdf j#jg'vST*jvmmI@l+qGqi斄rfA5YLQ"=3ϸdFK;}ŇRlc"4|Y廯Ghp$.**?ѻ ׾G~~4o/)Q*4RkbTb)=/!Ls:QʲtaXP,L6xSȐINόaGT^:ݭinv7'FbV}9FbLK5@*[}t#XQ`k ]fDЈ8 1N1"97Y C{SVWaQ8?=9]t]lEh2!O|gO7 XvIӿ-}g qND\s`End4isB}ue|ߘ>Vٮ=K!ygcxpАWqfeP@5 uv2+sA-f݇ҜW~e 1 wyw{ vp]ݝъO/ejeya ( 㫑΍RlxfR[uqz412|PYMw=>FKЗ ؿo;ى8_@*J7e-]g;kt)2C TܱI1̚ G{~ѱDCTUE\(9Qj{^Ė>I\2"Xf*윈  ?5qG PisjЩ#ǻ;h"9c\ѷ'orPW_ShPM&??|ڎ?Nn 'N}ZIQ086ƹˮΟ-tP!Te+ 5<'fEL`DLӂb$V6?rc|v3Q|B CQ?<ÛKR"3v`4p =\j $@@ةQY$aӎcHUVU5SӿV?/ulhP|Ľ4~@XM[^ 4-p6]p~SG;x|!Ճb(!uJ-*n+VPC2UYH`DB4'uUo?+9J[W @׏58gѣ@H[o=V6_H]$d9v)=̄'B ;8]Dc.g"ȡ/шm6dAu8AKl_gߋ_wfak~9\b?1vAssc [<}_C|]!k~j1E;hdž1q*؋)6tf_}qAV3$C^nyݮ-W9$EZ" OHD$XS*BM9 ~Z`_s7~p"v YdFcRgTrrinXy~zx]g-?9qU׾FM(wA|~i%2 N#3H):VR19f0e@Ȃd%Ifi^DMv8f}H jL$rpI U:E5 Ir*=D)%0p9asJmcs ƐUr&f&`Bղv2x#tL&-}u 0~ &\QK(VwY;]0Ρ.|?NW\¾ ."s3KM]) 9Aoxs'l`VG9 zu&-pc@IG=z; F8 ֶU%.<7y)b{öfl ?G@.,j㲲Mt:S!)"Qn7 ^[@7]h?ᚆeR1؈%f )ju~wN5e׻mG@ȟ33"g51eQU4@PTMTz3I6<83g؅}EJxJ fIfRz}"[?ZtoyT<&/̮iQ{rw[.lb1kd4:B$DCOōy|N˲ꭊ™+\bݛia`Qrj ۧi?ޑV(Kvش@$P}ȝcHaZ?qsax6r9BQ;@2{ԧj(jUuY2sg9sұ#-l>xcwHz=S_>䬒sWdXXJg?Dw'N/_Ud,jUS#BLUo^7jb]Ί5@$.5޼qpvtQCޟiSc$&f* ] v2AJ<S)f~ͿYBk3lX(k2Iͳ$3\=бqi 1 #)"x{fG1̠.^\߿39DDChSrsLJ90\k4d&1(r1u9>ȕ2I!Dco\Y%̿gͱvQx菉.jf:|ȩxUC(FzqEQZd S4Qgpyq~ֿ*UHXY E^l(bFb`.1W`qNIrRI15=9WgXy#]E$I;f!:{ݝVD/ޕo?{/8BL97 kvrtۭȕͪa,k *bN֧Hb@#dw~nm6Ucx2MǞj[܎ʍlPaL>uIR&V._C'r8B% DB i.6xQ5}bDI0pdIk?W_36?me&0N( U9I9v4P f^w޿p}_xp> 7v/l"D]js`#VWrݴPQYS7%Ƶ3ml@[Un-4p\lryW_1rwcH W ؼmJQCO܋nG?76Źbm Pwɶ)@9݆35Dӭ"R݇m~Rc`G)q IWQ~O]I>^XչV7P  , ۛˣ7 igq}qSvEQ4]#Uc4$H S6XP^TJp$T ̏7 P6 s0s$Ն|s^3fMhE6jbhb 2m;m._q3l# p.K,Ҝy=ֻSI[>|&m"V *LU{PUfofY_}CW?ϝnM,.Ιa`=exJg2ɋ2*M@@b0f&BpA2Zsmo>*h%3F`9סB99nNpڌ{T;\,]^˟Įy48ptj(J>e#OܥT1Otp:(Vn~h;3DfЀRJV uiow?wm裺X_o2Y0費V\%^{}ğCe1pȒP-W@Xj_JۻJ!kq\0t6"8G U˙Z\8,1g$,m;rC("σ[,|?o#&jJju5b}#|o—@PILXADfL,"b/8 ܾX&XeZ9ƲɨJ$")vKGbpYBPJH\ gzK٣i%$(W->39<7 gIraVӜ:T!vb* Y^_wwWg6##wA>&ЧxW#"}R3OJaRоe*@<(ƬXNټ)\`ḋLTkZ;z$}V朁>I1D +&',F p "8t(6 8 Y4S"xQJXF  2P, +7O?km^֥n,9g"O!]:ܽ<~Wb0tFD`h *<& (!bJ֫F%`2xtfSY^-KO>8X`ȒD sƘM E5F4|TDU9! )9L [42aUBt ZGeacѪFIDATs/,?LKT%^Fm=1 KW-ҥ?啇+ 09FEGO3MoBt번mۧ@)~^qʲ9|Y!"U끂gbU5ĘbDTi٤%Epl`A,b(P Lє ̎٧^첪Z&BG#UƺkH}f^n(2I~r"" VH}s!DDr"Ȗ,͞1>NwGԥVKZVUA;&&X]@hTԥo6]f&)1]U0Es\V#W)nLK܉Kdj=-lY>sL΢h購}DU4|q-E\.f;cO?irF>S=x,iFfh&墪Tś9ϻHIENDB`swayimg-3.8/extra/icon_256.png000066400000000000000000004056251474536441700162540ustar00rootroot00000000000000PNG  IHDR?1gAMA asRGB, cHRMz&u0`:pQ< IDATxLYmu69ww{νu*VQlDR$Eu8b e#1 @A yS% 1A$8-EV`(QYŪ[;>[F6%g>lZ{ c|c|P$v]F...MӮm1}3demm?pֱVۺ6" 'J+v@ۮAy=&FmX1ؔT 0A9jcšrap̑it>Eb"5MVi,6&vtYjBs{OϺJ >@{?wD #+bQ@DbIC7#Ĥ!@D/AD!fV(HDGD"b&DBR* OD1"c1#"@ D"כ 'h_kS}Q[wo儙zh͔2>B |o̕bhh" ADB @&IyW.>% A2͛8.ƺ[o7YN&_єf%3A)3]_/=xo2Jǘ$FbXwI,M 0 QQIx4QGm<I `YO&f|8HGDZ [zqasyEof3&ޮ**MC}hޤs.OR%ƑY9"IXwpz߮,ML$NO.#RUbb:Q577W{g PutQ 4I2BYF*4fhʢZk=Y]1>zړ>ytD(BDh&qD-BΛ$TqD`}/}n! HQfakM:; P71tӭhu]6;xRQW%b_l>l9Ⱥ@l铞AX 84MDp9 "h&Iѻ,Km?,zhulџ,Ɍ WiV&̈́udb)Sm$mhXxc4Xk "IDERÇafk-DلM3Ƅb1F".AkAIMmX;9,58>~y>eY@'F,4C@3)r;@GC b74GBg 4giK51ʏ>[ljV`v21F,>%ଓWEDۃ13`Rryg4)r[7""oLDPND@@XDD85a^<\י("Bit} `efDXY.!.l&ϚG?쫭ٹ?/'Hߕ~4vE 21 F& Ib|86Kӡ9MѴΒriPWz,fr;]b֬&Yvev@)108*URfrc$BEe$IaGjضm4b AY̬,(b^U?8"o&ev5Md$RգsQnk b2;<;WZb1"2&zgJl^\13MWu]Ϗ\um;Ν]+fvtȊ ]Q$TfY]$J3?y(!~$O1,}.iusaكzE^@Q1DCDDxp~Ïe!D +h#E {(G9#9fĘ劈%f*W50I`w駾9.O>XY" qQT1iQ!HHYX I(1ιqQ|#n4hdLҨrZڨ[1xPaDhXv!B%x`J<@\6W秧2YGDA>Fafe"1qYQS!Ed/nu,q8gsvCҤ̧-Bk8?]ݝ]4Eb hD蝳vk+#}$ح7e(ܤJҢȻZ`arNu<׫"ɚ^泅Mi[RlQBT" 0B9+[vuAk4MRJD* Bb2OYΉ"cZ)L!F$g~D9k!EuDĬ| w| ~Eآ:{EL::BL1>ICZ5vUZ8bB$JMfkF@9Umv,ngÈvt<RR\HMK΅0?:t^]2sVVHʢ?!y$ieyYfm3NϞ=waTb$f>u},OgbYwI%}&ZMgV^={Jy(b$M*gj^2؉11ӬX$ɩ4iQDIMgs#u~xq񊉪L beH-C1ē{'ǟ<}:-F,K58#⾦3yb<3gl:R @qL.lRyqkms}:C,M"hS_"Q19giw4Jk3#|㏟@ BDBOϞ^Sy;wON~1ݽ7M "*cD!IUj8xk*ZY!xG )3c9\l4Ie85Ύ*5YVL~7͠4+<KchFLj !cUpG@4j;dNDbXDkw1dơi>,,yatZՍ{~~qn|f~Dsqh$QyvQ|E7:r%M?]8 ɨ`.NDBzt꬯7wGfkީ4=<:n!vKfn"Dfszˏ4ۡ@w;m42y,8:(b\c *#bGo8&8ZgO.W1zAً֓<[4þ٧Y4B@D؇@v[̧lDLD^"!*H1Aߨ'>~ͣd~/XXqcYCAiO'ynEۺ1Z/$N!"xmpX~wk,RLeQ50DIbZ7;t֐ZaDbyuEH$B$O?~ųOn着Ap2ԇއ ugUe;?;^c bnW7 ri^fFEe]绺 .Jf%Q'fds*82R,̬"1D18p-!FDd>TiBL˶ Y/p97.vy>+ l"c 6*PRQ̯$ٻc7!@ ^"iU@D`"OQ4ϣNMfDzڗ[Z 0IbA@paoΦMEH CB$1ƾR.RRio?HL,H <Ҥwm۶uգ4 }&y|kIZUI4{w|t8V8N궝8H7Z'G"5]IuF&1QGdpiVC?"zWX}ӶW7ˏ?\]~YJ2&2FDBy!=v *>wF($f6bZ:#DAB#fmӯ[_uw?wm&3{h4!&_̓Ul!5D!DVZDƤZ8L0+;)Ro\%4\̂Ͳ>RZ9 uXgRm 〄0Ug}fZ+="F!I,^+f"d$KzyyZQnY}cpBj5v8W]?.wn !^//..۾#1wQ{igO~ضM]Yخ7mg>y^&<鴨nGѻqw|2Kz49: @O*xV;BD0Pن\5 +V*8)jRƔeyrr8NYk0Ha/ R 5 V~W?_QWo{Ze=tîsni"!B $4M9_tV}Vjq!(z2j*m[ghFGo{4|:P0B fBA1*QpFBdR `1"њ2Z+Vbjb"ѦZLjҌP&% \-oYjn hJ4I F|?bMwm {(1q )R;"طJ)/"LʢnnSZ.l~ceQfZ8ۏOHNfGOoM^,N菾?:>^mjOb2a@c>xERf\(v!km&٤iyq%~~,0-j~5}P +R"1Ssw2f?||u]Y曦%&o]\^wF=2Ac_|_Tml>u_lq\ԅz+q1Y4u֏{AUooJ-KmD?D uoGe TG;*_cO}vRl>9:Z\-oԻuo.׃wHs߂TW$JnG`V3mfn#ց:BDGP$tzێmM6w+DT+b iID@|F;z4&GulfaF]+"*9 $E̦yJZYo<-~n0 c88^Vzu}l]ƆURwsqۤ(CD&zMUkm>XZ>՚u[7|k|'im^}a9*vt": 5IҴmBh}d⇱Ĕ'~̤RL(cFk泪܋7|ݎCYir=]||-J4[oNk!SPg~K7_zdgu]|'O7!m$ޏAXl2Y.Їq'fc !j1N~no4(zo?qʖWOVR篿8nև՚F(?\.ŻX%\=w"l[L۾7eu0D]3LDd%_xѻO/z7XFm]y~RIz79>d&` $M3ɔ68LJ %&|2c#dY&" Qom߶ZaCtbNc-yQֹ,i V1|%NjAJz2ƘeٗoۦͲ,!Ji bb<̫r6)ƮkMuhm/φG׭67 Xfm2)Scw84]\^u͊F?0vB`,ճդQheߴ.D Dfm4_dusD$xxppsyu{>1wY>v?ww_?Ͼ690 V)lkSr C=&JMRe\n7~w\\\,fq]p%;V%po!\W/a#!cY!H!PMM!Agf+-K1N)Q!iz(7P1#Fȫ7"Ut~vLM97wHh>m?LA{6R)Cv LYׯWw*1IYa~yubaSkTY,Yх-G@fq("qomX)e`RcmlMJ%n6|?DF 6u;[UebV0\_^<{&i][+f1N3?Xb50l:-]7e&+ yۈIbMhemGi%DT:̎&Kn>xgtj!v.UBEQ1f;At>;>::X,6z;E هbz6R1Vp$HMRz DUsA|u}Y;ت P7TU٤w|[xo $hX3 sEg^H%^BJ|Q9Xu ^3WKc)G61I1%3htTlUʜ1S%@A|(Ab۬C?k2. `gC bфgՑz" aE}gfD Q@Q@zQԤyй;}}A%{ 믏(4%w)+3u(,%Z>(3ou}s^޶0 O|wR0IUw''/_˻v, z0vQ4um:'f%*AlnQbZ=r֭wj#wcާx7jZq߅a1//.JW7Kkb>&1&*?[-N{,{wOW&1YwAUÅڭC?L+ukn|Fb.[sp.zO>y " w~:;]fpΙ|>%? ~,8:kS~!D v1I *6Z< :t[CDIgG ͳq>{yMaHwuM?iQwje!zFj0dMZG+@ 0xgX9>dt(ZpnD{ǔZ2RG>φXRJø\sy^eU75+m^]Y:U IDAT/Dbf'ǬTeMu-};˳.ռ]-Ipbip\Nӱ뮯uQͫ6/)*r63^ e׬1(s>; Y< \YTz؅i>LE7txJuGdδ*R*Էk](Mbn<*H+Dqq?G1HӔj,*dQDAϤ0.6mI~[;d1,qڶքxrz|u}34իbN ǡMgϞh_tð|utvsGtsylV+utrܶ't(MJdΐ9$P&UU.ohq5UUn L&BC2{[r~i!T&_$8 19 Ť qi!UREж}g3 Nv|vQN ]|k^کYzh'˷~Qdr/o]n;O"]| }iُ I2,0xG1t:L\= !0Rp1GfF*xCZjqcDV m?0MdEŬP+]vޕU5^-y2`sI(:l@&6s ΍C3ӲuB$&轴m$jR{cf#XogGVmӕӧOa@JBvpΓ"Bdp1KFNbt lyʲL'd6뷻A;ZJiov,| nrrT¡ɇn^ۭo׫UJBXb a2+H|p?~%=[jqq}Q +2&R߭֊u&YOg]3v͏ڽzZ~o};{a9/ -crt&A2ThF321Q/чT{ʙF(I&e1nƺU]:W/~z2;p!tAҬ0L˻<˱}_8?;|pqx~/~w(Cz dYwXQk\(9%^m$" ѰRBV bao7ޭ7*mez˒+|oz]- 3cVHߺhEP4I.WbDNˌVk̽u4v,A`!@D!(Yw\6b>7Z8}7Ml|D|n=>XVw&WׅyQzlO󏟔Uu|v/ Fp0./rFݶ-DM}K|/XCd֬[]M4u'@Y;Bv.1 +utzFctv0/e˗io)Gg{VJ@FDy7d$1\v7,l 4بs*8z A)DAqP޷|@}/_OgAY"afzxߪøݲS/=*IHq N3/ol7 AiLz!^ qus4Aͫ(J" N9Ac ;1">`̓)1zD sz?OX/HG\'L`]Fgts]b!z1xQ0~2`fu~7Z,Oa ar ! 8oǮw|J'(~49$(4GgJ4'0j:%´wk7Visbk<`Xc4Ƹ]f3^zZetN4 Yv}sfyV7˫04cw=1i=%:KjU>}9뷽w A$6;D1*PU4ںn泃`wV 4a$/ɼ<ֺT%ioWWNCzڃ_wW&n_)I9I|߳Q'0J0;#17͆M4NV)*3>꟬:I إAQ)&@lގ}jflO׷Myg/wWO/ON?ofƐ)UNdR!F̀(Eܾb/,_-onLj\[ٌdT/|:C]T"Z*xA`{,"̨)%Pm*DyQUje^a`abM "ak` Ԯr\mѓ4㐘$2*E.rW^|nx_<s?1f}w{ۏvVY^ ֚Ā@{LёvhI lhۣl* 01Uur;&m,C^V%tpI%Q5:ގ8?<8tBv- d/?Z]M"=[LO<˿/ libnWN4=~!:ci k KTj^j\V0 Ș39 !F\ I5uD˨hD< Ә,Th<1TU.2dҍR g-97 `ttbmLq̦AɌs{SpFYŬ*"ϊJ'4X.xOAruŭ9d-b>H$B: =N(S Ŵ`Vg'}íh+>dt}e 0 rfk9R |\cʺF*Z"!c @L'-s QE]פ2yI M!ZYZ@BlLrqΗyFuj6?vSϕ'(VW ǣ;?;*9+Q2DGjlq;zvrNǓ#fɠv.:^V\ۥ:[ X_;֙͵d;j;xC*x u(D: |Rށޢ* V#"!GQ`+P$f.`SXk0NR!|cfsk%g^υmu]\lX뚦@y aUU9ZX<8$Vv]4iuƍ[LH$VJnlnvA_y J؎H(\l:hSiV5t1ZߋN(*j~Qy.ܜ5;sk=ǧVvSFppDjcfBTCRl) @@bAEN,"v:Ψq#J/Hy0$dRSܠu2ŜGf5n X1B* 4 ΥLTUKcˌiDuơt񸜍ˢFcl@ƴ֝v1(}Zsˏ{$Xaݻ[,&׶VUDQYNwoG!&pou&ETm- &g,o+B0뫃\,&ӇͳHtRaQ*H&k-~_Z5?o \x -'$ @^Jl|%zqʁ< gbZ"O,kL{/I$H gBE !.f! g2;ߘ1TJVe:>vޮƳT}Izql88ˏe[ZdA <](S~ߌ\ VsAxESZȩ3v7/|{?~t??uj;43Ͻ;Ob䗯?:x٭(Whf;Tj:i<մ apటĽ}wdC]I;;SQi2;28:'},'@g~SIt><|)j]s)Ñ([2bTwx{Q=5޵:qW6dl8ј!:cM+'xVUiŃm5Dwȣ 7DV.fu3ba:Ѩ!$ւ p/+ŭD,PsA g*OyD9w\J ^pNZ8DȐe YGp5v7xZ_Z?qn{{ۅý[d=pd/^Y>DZVƶ$M-bc֫+ {'>gz>VvYdmu7gwa7P̋b V7dW>{wAO/~T"V]N;.D5' <`"#9 e]RD"Y1cՍ|8Rqdz)unX;%$8~ ֚HK)ac A(fVYgq:Dkkǣ#"n$eY !U=DhVr+W d(nwLDQ$9ϒh>8ޠ]]%`fiU3:)*]U-c:P`:+[V'N[̀Kr>|0;<']kO +1#H_GX@>8@I8j!1&$` !0F/jBPqT B,EP1^QU~6=H;vģ<%6ӲSE_BvS䏎λ|T;k%ʺӕ7]~x~h:̧L4sԐ̣itK: IDATg~ƃav$Sܹ{g|pc<FJ[c#B$\hrD;it;Lӥ H3Μ1(QSUYJ.<:::u+WL|)x^d)2RYCH))r%G;$J#-Z ak?+7 ae]ÝhU$\{Ua4XlV};w6 :Tu+م^Doz;{lmÖC^/[t0cv)J@Ʌ-qn] Bd](*m\ڝDk&4M% ol `Z}ȥb GIj9::c !Xk(*z)Cb4֑Q[}^+^Oݾsn X(vwv.1ܾ}+j l5S51(cIu+)E,dec}A;{u2o ј aNG=S; 7\ KR1X`4HaiRcȼg3. 9G a9\CSk${aጛfy-fBqmp3q-Q\ߋ_|e<&Să\{gZΞy^*vwإ]o֦qG9;ll_^Z͛f:i?#;է^b32qj:wZJ ܺژgZcWYWRr v{!ay cϞ]xҥ'2.Wn+RJVŜ%Q"&@&4āSy,ϣiBP\&xU,b'NY G)YW%GTJ5kD2O?\Lg 1guQQimJz9ol7Mk= ?߹sw~GHD2Jqp%qyW;Yx\[=<:&>pD:?xP+m;<: S1d R.?8콻v t\F)eg5AH$ gI*1@E( (8$G8Ζ\2qd5+X;[XgB#G&ीs͸V7lm߹_r$HR;Lcɬ&+?{KyWZW>l1^LTkխ-EޝSl8:Qnʉ튝.?[EO: ť6k#OtxD rkE@.?=Bh/?w_K>ѹ2N RxIϑBeb(A,}cè}r]46wb!DZkav rXmD2}uby^0mZKuYA޹vw[!!Yoa:zh%(N1L*E9޿ALƭ3nr<^ki;oGt6JTE+:\nvgQ,D! &<̒W(H74 Qؘژ3!JѤiyCxB@!x$1IgHd7o=m4Q=1kblƧCƵ 0zW,k *,'lpB/=칟O(Hisxo__ҍm8kj:کx~U\m΋b~G7L!+[\ɳ"m~OԦz/e1z^}wpSV7ͭ__Xu1uamu\3Ԫ_|q̏zȓC{ϕL/Vzjx|t!gIUu:^D J|`Tnvd߉Y Y 1PJ5Ǫ%Z,sSױdb>!>yQƺ͕t>g{Nwz<5k|vL ラk-64E.ӌO}{nTe]Y'.4Ng?]C&\'(Lho$MB\ c>9yH3ܹRcÓW_VK|hHgs<(~.x!o4dC@"Қ@.)xcY骲q¬#-|@>w *ݵq8^=8<8 9-~Aɘ !9z8sx,[~i&өÃ}x7nO~GWy>ǿԭՋg߹}=v%ܩD>n `u)|s/V/<(~פ7^iTӟcuQw+^\l,cjwnIOV?)\nVZGGETDhl9. 1B2)Ucˏ_LӍ|/m&E!,]RE%tSq-SԣPɈ(,os'`$0:Xikr^LE,mEfy*hxpgy4L1G.L Mv/PIQFi&ˢV]JG2]- ~^NmvϼvK@ c2xJB0I%_wU>](yZl-F7[Vi.FKuyP)@tjjcpr'OwjQ QN~{2M0JgXsJF Hk=+J ֊qE'W?;{GwN'LU\MKVضe1x1v{N@4uH^j1.Kuڍã8,=짿կ .gnyv}{+n8\9xK'u3#$s9 M$ $eit)s,C@ !|$׎q΅ f)ʺJ"  /{?'YW)MmJ -:9U4Z b拈ԥM;rx(c8⹌~;ͳ,V7d1 :QTN'h4IDrI,P̔qZ[M1Qŗڛ? @@!c@\rΔH)`\XWp!˪z(AlL@[Qp+, G 8G΀`\5UaY(f~jVBR.ܹLPc~3rItGmQ+ >LqzO9!˗Kxw΅ /?yԪ{pXgŵՕbyrQi=b07e'^+$.+u|F']#c%->ĝ'N^L 5c,8/p5pwT%"u Y &t7gZ u%Xg!5V*RJ! ŧilM] !#9oAi-7"9eH\gS詥Df :J6I6\GdSմ$kG:Fdf l>ݏƂ#"SUQEJQzww!4GMc7qӺҗ!H׮޻άyt N*d{&"&|~⟑1-!kHL*eiHJHe@JB$14O1990ֻ@X ̘j0 W"\~[ ):Ky%1` ARI7_o˗0(x̼_Ϟ';W~6/U&/ɁNcoLv GϮ7FߺѝOH¼:ɶEɢ{;ڢ0Fq@B1 *u:dRf$5lJDN17uI,VΝޞ9t>Vd^@$x:Irȸx!sƁ`$9MqBMC&wW~Bp( <[ Iљo-{mg'^yE JP+_ɋ/~'u2 ,Xb6tspPޛ?Ci+ogVvc(B@Y̓d17|>C0uP' .˭*g䌌"W9xz 6#J8Q, 9c MY:#BɰC\qC6kec S0)b …6ee6q]c$K9cjLnw U>9sމz>)ıFlb&F90, ^|ZǙҬ("Z@˼d xBBME"WU]+t9k*8 x`tl+1xbM%j93xb:Gib&w,čJ!UdU3)f腆$"x#"yog31$QH9烳t?ZȤ\iւ_ʘ($X)eݔ @D "e 1W*,q|"aL Zu]5Ƿ~ ޾ʐMfg7?mP Ebw#ZiqGȘVu q -[j"]ϱ*02R@Zj6hxfuz-g )wTF+kI=2UÜ2ťM#pqA(J:ä^$FqDe1&i9k^NP~+1[.sAgeJ)nuU"AmQƛgGעqXuL_(ᕊx$d9Ko1Hvckl̬/C^g lտ^D;;6UUvI@@ 3S^{$?2GD߼6n|[\"TN{oQ#cL!PJR&qk !_|S'>a4Ǔ"-WJ:@`+E5e*@+ۛ1YP< ଑z)~09 X1u1we| dzޕ #Y;;@^efLx٠߮܄4Aj(8c,Pe T+.BpC(cR(<F zj[_3,MfE:Okczy__W>sgN1,X#6us&M&M0ƫB.~wq84Uø`ȈX8}q?4L ֛jbY (Nla<- & 24&QJ6\vc]>XA ^fi(Zb2,iF87,lKw;I^O^5f:Qz\`8k`}Ex ABM* 84::\ PGQnlS;-){.(%o2N@DE(b3x6/b[_'{m.~8znOqc&١t3pk)؃_?lmO~wz_ܼoZ.H'wwΔs$qu-A%p0H QW5c>Mck$iA8ZΝ&H)B" )غnc[hji sZ-_o V^ֿճ_AQ}؏o\{SN^߾ֽꣳ[o_勗؊Άa(M,&EAIԓt8̿c7yը#2&b:Y,=B0QJb泦(8dgyGh89{HRQ.ZnY̺Qʻ;|xޓ>Hט(tA罷Vxz`l$(ZLZ'*U`p  "2kZG r3Q!E6{"Q IDAT8~Sf%ic)%"*gEȦY&I0Kgb4[?yrwt-y惦ys4l uhςHh$H`0!DY;ET9/$/J&.fw5b K;$g$iG^ FN*\Ե8mfj@B2E !0cH$-%ZD?U:+NFsյ7^Yqoњ'NmMo~9Byf?fy5~nDٜIE-ƔbŖH!v*rI,YR$8ip<|SMﴇrQM. ]{y1$`=b}L^o?Hc[suү  `G® -fӬp«gݛ^wJnOߘױ~s[7'.Utˏ^ڦd-7MtgǟWͦU1Ddble!R켠Ʉ6GSBƮZϮ{Zt:†T 0~W\4_mɜ3/Ѩ;JМ& GusAgM:Ƚ}?w=>9N^x/[/tč_OŨ5UG&FW=X.lڐ2Cd)W~߽|Tό-J&O^|X!ulFylw c|t1WUa=.M|襽G'fW.tJ2udowf/ŕ@zO@}m]?7^/Wnh7H8)fw|c_ΞH~* iG49]#pf"CvO|=XRML@fBLCƱ!Lj hON3Rq; DS(Jff})eP&1Cwsn )˨(?|wbvq[ ANPq]0%Gۙ NGG9G$ݚDNwDmdRL X}Ѭx/X}PfdMȲ) ٥wr'ɘ'/OoW/N "lՖ\|82ߛoOF5IyX aPAܾEFEK٢a6 }%i`jտ.v反>BiE_ݽW8`Ϛb{;~w5QX`qcqr ݐ }%UFY#aifɲV fyR26vbK"Bwm_p;b%0ןѸL9c+hz}͛ե{ת"YJR  =WOωj]VՅ9^4Qи@UI4i@8gvs^ᥗekI)bVao36fM):1REY>c߬"?@ bTzS9#>B*ƄL1z2 0˧.>Z[{̪+=a1DXPUbBHU5'IKHDLmTcRF)gq\ؑfeDMJQVUkv"DFbD6$ATPTPc8g"3kA͆"*"Bɱ?;>;K:g/_KG=H%qhΜ6{MS"NʀJJ37לxuM7YhAJHPp2ڰMڕCP,6 ^(E&AB菿ɕUx0\abHGwz| $ Zy|~[?;AvVW.AL~S#c3!.3 1J 9ߺ5n @T ISZdbbfw X,10#1yql`DuU0!5V ??>og?ɜb;LְJ^61%c,uٵM w1Zd޴(Ɩ!MQR cDk{&T-1)'ŜY" "xֽh$!$h!E vֈDF~*U$ E& 9* d,9Y!)u6q֢2`mʆG˽~7xm󫻧_(td>o"#{1C/4;_ְ`38pf޶OX3>%PoU0B[,l8BLC43DoMʡ-&- Ed@,I`d#\@|;}ϗIlY<|P+?<oq둲^bYXIa:/Ij/ѸfzbL)yc$HڭΗŧd IYss(Ŀ 7 󈌕(Dmӓ y]ʀhTHYVb1G_Z/?9?_D ۶k$1ezkk?o/>MW ʀIsYM:B4gغ9Olp{:t[hT1{ e&+#ۤl]Eh45[2fQf$V͠}Bdm,` n^YFL!V L_.l@Ѡ?L+!T%'jV#`MbM ֌ cq08 XP`S* 6H@SDJ@!h萋k7ƴlEX"r4fTz6q\-7=zRL1:f&CT,W??]Ch6ܲO*ydwf;]mZhY?*r]iݤe=.~y( mu1E%<}vӆx,9`\2 HOϞc5U*ƍDtccMB ݨL]$C b>8dA@vņoN7In8>>9;??99N&mӮV$R:ڶ&Tt7TڽBտtG:VhL\893b%&N[o[}l=/@-<PY/21ZWYW}hmdCzff5_FENPxA$XMBߑD@&tٱͦcƫe "c 9uH.1h0ÅCsr5^<ݷ}cMO|2HYȠAB`6>y]+0٬fOD5p:52,Yb 8-x /6d&̠lʙdT.(pڍ@~EeH}_S:s~&*p+fUiZY٫?XXUm|7ȕ*D'ZaYT6Yg|ݧVxe4XbxjVDhh @CC¨0LłLidY|a}Ҝ4oxB3X23M1'd. x ̦ף8 ؀v?o0cu cܭe1eI xZx-b6 0sNc)Jo+9:CJ9+* uF#-Ɨ*!)`5Э;2TE1GYG :+dWziZES++oї'yݷ_x?O_^ASGQ$kADR.bk L+BCƢӶ:&)+QXR":a&% 9cb誦d݊^(*Hd) ۡ$+0Qm<=}x\ Ta@λgǫlҬBݢnGgtMݽI .vHtg߻9o=|_Y-ڐoKSǺ^ϴ::G=}jݏ%_/>µŗkOƋggM EHF@w4'4|bŜք7WX8 )G(s5zfƖ:) ;dDsL$}pW0fq靦'Ӕ%e4Tzm!#a~`mkZQmeJ~d'i ,"ѐ/^).^G?Y5#"Bc1Rr@PP>^>Y#4 7ɕrJ( UDbQ`syZk1Gq+WWj?<ƿʢj :q)m; }\*BC0vV;s#j'/6P]5zh\<>yG37-$Ⱦp!4fke@hkoNHꝺקKH&GQt? aB?EQV5rԲ&CppaVׇloDԀ3\YUafc6:(umY,ֹB #08G/P;8oܾwwv=}7>??D:>9ˢ*~w~:ư-C1{{ʿv߻p0+h(1;cHbF皍L\W@Nܞ6q=&} lKATwg X (0eHır&uPTU%dSBDQ $ID10QY:P,%C ]9R5%VIYK[ߏ2K}=vjBbdBQE±5 GMpwUl'! wVb#;Y(tjYʺ( sh='Li>89+I4,όDBɂ>-ۏJs3֛]t0+vŲ[{]9d obZ~bj*2xeVntѦNQoO6_|^iJ땩mBvO(|R^k+ٌ&0FXEA{a.W/ߟV6Dl()4t Vv_ܷYUBD挸%5$U nq)nS2 IDATS"4Waqt~eoRؒ-"XD*P|R٤:' s EtRDBevdi\ԍ՗\7}2)$@da{f%uAMŽXl.,f4H>ǚ&<1h9D_hApHI$)`2BLh]GXF$̍kE1F s^ͷqwThwNjz6 叿\"hƮ8gdv: 4ugKVR֗E1#$aGfRv'eI2 !& lyP )#2fe5d8n9BEcp,!5eo֚@hQ!>b *hF".ںs1ǘN-+h̒A¬ :/~_e.T7Z2ZƏr]Mr؋+ 8,ͣDw]~2ݦT,x8~sf2 L)MGk;"[-\S4؂rg?P,qڪR"sv1E1 x?^*Bjشm72֡_1&BШ,9gצPUiPA[(! 8XUl4)PΊE3b͒J""BJllJJ1+ d.-u(/3b$DU ȪHZk<䜽|&D1Ά!Y+ @֒h m4ƨ$@eFkfrӿ>ѭO> cJgx`J2N D\0/K3"*@x} < 9-co )jr0&{¡Ԉ(7]ȔDM!>hJY|О5)'oHJdkG?ޓ+'7{&qF1>]|u6; W?림Ȟul)(NJ luD,ha4h4,2;."0 R2& ۬ؠ(Мowe9 ӌ ,Xi"%fH&0R9[T6JrSc=wR_$%KEFTkf l @1jLdlg\"%Hfo7뮬d$118Iׇ )!/Cf`Hgm2vL(E#&-ks}gsWNb620 0hì` CJ뾶 j&` tOT2%Ͱfk7f=`齃(5nH&u0Zgޒh|*9ÖPL|}on/|_7o_yF3SNخO_} -ԵKQҍ52\sux Û6{C |޹^<$٫F>柤xWWݶYe-3JsGzZm Wutԥ퐗`Enq(f26 !R1IYxZ/h?u^&[lWX`sbdm 9VEޞG;mEH1YE@kƹ `8guvӈJӈጘsEaOr%@S M?D9sQ@zTcɗ1nCb5M9මET,bt]PW \+Eʈ~h g8LFJ!fe q29yY ZJ䔙HoX$4hV;#윂ĸ5x:mTT9G.~ι~k.|`r퇛n+ךfL~nsäݓ,Il8>ߣC={h;]N=sw%>u|/^=&Xã M/q!:[EU "XW-s4R\;bնEYvͲ56B]-wWgM9&h&jav:{ӽSW^y#'wN=nMcIeuw{_{T|"؏W]yh6ymo޻9}Rנɣwޟ0GB/V.̍akq29Xm\ #]8 ʹ}{",bf<`&4tjMH}Em=ҚrTWOE3v+ωub e"v4H*!F$wff 8os($G66&I1(FU :\3 XI4ʖu< ,YUT$I91nD̬UI`;jP1Pl=mM~G~h<FnCǨBjI2k}:gAi[:umGwWi `{1~Wڧ?0:>b?o~Mte4epw;;8?) ޅ FUcQ%!&b?"з\#_޻بRZ99wΨjwq+Kt DA(E1l BPz|ph&;qv<8VXwڤӜFa&ZrQXl:f4Ƒl\gJ6X cR Ldc ge1xBq/UZ&o6~`_iz/J7_?o2Jǎȝcvb0r1 G[o5=/WCXb梄 ռKsGV)t4 ώRG Pǁ,SAT'̨9"JD)tޥd"P/*BfE-i3P0{|jʞL4 T0YܶԆ.w;ݮe* yҶ"#ǁ|HȆ)jERx4B6hRcDv>C|{/ŸnS8/16Z4QhW/fEB1A]tQu#U{O.8)g~raa0qM)׵-j NQ@f@eTTU@hlqV @@ (#*LHvCDDv6_ HT3e^\?xW_=_Ǘ.^{mʃ`^if&tfe9n=ġoG݋bf{c-fU~X;g?xӈI7Hj-2zzQsv{ut|~cSg@ A=W|z^CK؏"c/:b)x@T|Y569ep( ]ٴ-yDoΈB"@LL6Xvm[cBR3șTMTz`i7^SB]\&LX!BP,م DJЌȘ,,`hXeE :Q'oZ&4&FM21S'{?yɋOϚ?śQ񥠺 6(dsb.2:d5Cѵ>h) 릉c48 @18"ݢÃ+kw8֞j\/k m0^{(tLJÀ)tǔPгsDo@>E?=_21q[-gͬOQB4nl/7;eSn%zIM%@c(G\h1A-2U0m̦HqargK Rm@_8P5JdbUEDݸoU4 II&t(2l֔.0xsdy.bE)KkO>1Wߺuz(3 Tj?dCG}͢<:9K6 1bGu:20vd)'tD9EELPu*;gfDR\5xحׁ6g@BK)sf̲ Ι,;C&rra>]f? e]yk'Sz2@YS]{{]D,:xP)tGb"pU>n/SuUULD)rzn޼ձ3XsnZMRFYKT(e=SS'uڇr2G&t.w9$Щf` S"H4Ɣo<(?}=_[ 1*IwΖYj$eSfh˂NrJ`B!,UHjDaoM?s!'G..*V”zmS(D4dk#7A n@$4K 윘MmqL_qEf^vE`3v{/Xx-81O;rqE%PIo`M_s)ωdV<5U,ͅrzϒW\6Vo?Tˮf/{cZn24C&z3yjf|y.}'(g^ɧR<4X\շӭ/~ց!3286tF %7~3P5 9€r|i سgf_=(WD*FE<;V`i^8s\|髫|1?x ٍ@`"bj0.DZQYUw5oYuԳbĬJ^YS$8z3#OT d FAV >;;I#9̂SS$O6@Dl.%ǚ`Dֻ̬S$YXn` L Yg5'KDWl5k{;8̥LB#f4*mݓ}~>7>z;;WpQ_d șk8c4 r&AFAҫVVQ/ndml 4vu8LA|}bw\Nj5NUdMLɹ vfΓؤn=-ۓ~Q{oQ;뇾aMg>yl0Ýg7jJ)R]Uxyu=ƈ&%w$=|R x5bN99]YpA2d{rC@jps_-*W }#HbO75#57 IDATZfẢSf߈FPl Y &0EAKw[IȄ1+"P`/3?|idHY?_ >:I\"&&$#Fouf+~Z(L24ZV^ŬƤ 7P$Qdc..Tcbls4EىHu@=C7J;H,fkwx0~oEhj xtjd8?z|Ows,^}"vu٭/mؚt*HZUT Qy9$-Be@Ǹ'O+/~ǖJ@D0LĈ"j٥ p47@ɨzZ5.'4.G뷼WuA]MMe=AwFXMbd8 )Yko/϶\HEۻ)׈Cy4K~F/^[{{î{[w< +4ƳSn@Gnɹz6{Wd]nwǥb>/, U.GlFy"*1IZ>v:nZʭ #L@b3 *"ccGJ̈,T]'w. g3@@BLTR0@DdZ2)z_j[.3 !|+01p* qΘPB(YȁZo}]чV c~鳓4f:ܺ{|`.ԋۃQbABv ` wH.o;whd:!f. 닧9 DFαC@8rTɑRQ2pdrzk;8>xoT4%'l|pyת٫ԗ繨W- = ]qz<$HC2iZ|'gݎ ahmdl7y-n3j3"Ѱɴptu{aΏOq[YZ\^|iT{nj{N\__߹}T_SJvWו!N}OׅzoJriYncpNE0~aXzRVFu]ȂU‰żv32L_ 'EfD rU 1Ģ8}!P@ 9 -XhFD,O}IRlb:vi]5xYͮuYɡ)Ŝjf<8Y94}!:oL?|CEʒ:x S 8\OUo~dQ*: Ȇ*ԇNeEvf c) rʄ'nfV`!o {piwL{Ú8t rbT$AhFv'DP`iݻH<8%sc@$] @Y * ֎rJu) vka"h? !xD{?In=}!T>SV13"]Fgޣw 0dC쌧yi@ReIS&wm)fDG@HZm e)2b>)23iuqifٍ4B'Ę r^$)M2 ̱WUzv{o}:W&y˫sUU|r 'y\tM`Ejsͪj<._}VԧeMjH.Z׎ɬ:=~Ïu_zڻ>8Eh4!N* W4cvT B(~> A?nHrY @i݉3rN&AD % FG Hlnx=Ns9rwL|rU 2?ӓܦz^yj :&ǷǿOʛ!*%QЈtаؤ3J|2z,_^6;[Dr;ϒ[/Y#VUWN&`1}zHw@rF̹Or5'%My @*P-jjlCpy\VrT *KCtqWg.gn>mFo풁/J~v;G&a\QWeS) !6M]_mm.}s| #|>{KvաK |c䃅,e-bvŪ9Mvm)IDI$jID8z.F6}  @B4AI՜#I:ÑUE:[%\;QPSa040Q65N}N=ļ_ƽG?縬JǼSv dT"g7"Pgd@E44(`Y$j>$<&d/NXme)sλ84Lp=)ngGgߧ4f_*(j2lJ00|kp*W]}K^Ç@#"دyV{0[e6#+IT&3M bd,auCg@T")#bJh舩#3#O|R2f&"T[]yhʓ_泋'<|Uͮ1v9:^~;~cGFd C q;&@*6!&rk$ )ԇ5V!(M&2(I26C'b iΌ&n< V-*]:ފw%BeEPP6uH0R_{)RL*gS$h/d( y Eh:oДعspdC8tyy\.s}YU:;c@DoC !lNMˢBشv[Wz !⌜J{wcV!9$ qkH9g:>^cCJmu^{CIL0g%|=|DD)DhS3!6E͐`4ؙs(A%\Ȧ]RGFzbW^a[NG}w_znqv~Jv}xuj`phqQ%@Yɚf!oڮﲈdUخ\@%&ta"Lg 7gDdSKPA&xX;C5Ղڋ  apת WdĤFH &LE5gcƝ.v^-ꝨȴNWD윿%$as0zcLEcJzn ^r)鳩XfWWWk &OEH6WUb;WPEU\)e"; %I6tE$P7 (> ICé#Z()dc1*J͈*MNUq> SZ,q06U$ؑSbf*#jo?xRXW_\O?.m)F/>ߠo0W*O~)(ދ/_o|UqJ':0P2XNAٵLD&`U8{~sFeΔѕD*))QM/hh7d"%_ w!I6C36ֹPPp c4&b ,xyjt# ʉ'+yp7wV#$4SS:쳍~?֪_(Ŏqض]qQYU]^1yNI@ LT)z'9)jfy%s.pO K=bYW 3x M(OwLvnBUF=+v~苘W}.̪\e\={"cY{ Rt &lcQv7r>7Q@??;H$z$njDrGS=hTt0 071!x7 ތD(D/>ٱVIoKԔ.v6! Wg𳪨[t>VR= uysYVoG>~W \^o>ŦP MY0wFS̻kna_xPx0Ȅj쥿w?)Daxѭ~;_~.~+|Ov+KKL$cÝ۷O^_u7ؑ: V1%3IhDjM§'D\FR".'1vIj.h MnA f},eswAҦhyd#cnWpeHY-&CT, j"tͯEvIU'KGu3xb'''0]&8ʹ >?lo 眈 BДS#۶%}ދ}8Od()U1y 3j_yM)ghQ/'OV>S1n^7E^]W`{ʯ^,x|Mnk$o6ԙn.W@_eEØ{ݺ*עg* E2|u~jDf`n~vtmwJG|CW@wݯ/ DL1!vWEyv=;wl|aSO0:1l"2q5 9u¦ Ȧe9g&PFpFy]8<ٲe T0j0օ:If3kthGvpҫ`=ch_@FLN9bFhK-iJ&*AHv獝[73lkW_p!R]]!buCOHsvDoyM`98D5#b",2* {b D$eʿ[ۣÌ3r$6;6Gwޏ0?}˯yT$ja;+ {W^fsGm[HcwGɾ[zVd}=<[׏<=dm`]_M{cqw%,|;Iu&v{?)ȹ*YbqHR!A{!F2 ƫ^60 հ۶!h (8IjȚrΘps#R {xys4²; g+d7~?k)?gUZ8A-w18 1e[ϗ&|Z:jݺI@J%jJFR^ HL)`kq<9XKeKA.2 *)vΫ|:LOY6؝lʇf0 %c5mw40;۬c! 9%UU'7HϭQS޷{U!=HW~GۍttɻM0$vead.?9m+ "΃u6?1 IDAT}2M;.O'NHIBTkfehvu;__m/~D~Mz/ Tgt5V\µ]lb{<]_-Sӷ;=߹ߵ) "r L&c{c/n-La" ^,۴@6m'h #_Z:'0_Lۺj8ۭ=#tA h4DQM,wJ]> eljU`~^4m؏;yOy*3c@҇!rĘ$ br{F LZ+Iv 9.VrY'Zəك"#eyb (iZ˵[lvqQwyN.m&PCDLq#";BL/w~N9۷2AGyi!jºpӜ5d`r{ݛZ;dMehw25ѠVͻ qwog5]-|g量d#q{Ԥ̚ 6[-R Y }±;1Ζ(~\O/柿GwnoϐMKӋMnޅpvcNF|{7r|Gtt:B2 +)_=Fn RĬ6:o"D`h=Vذˑ0n"^R/>;k W<[I srQD "J E@DcbA cR`D+BfiRĻ;Ys5u"%v0a]ww:oES-am5iZ5j< T Vj /IHTЖȵ5LG쭖B9!t>E"qdzC=͗*MVirt]/{?\mp^AB!k@P$T$ D%"˂LzYq.(dB$N`H13WBmUVMse k"!lF2WQWakLm烟-žuռkELI)EjL,oD b(>YKH9Yg2基5[GgE9ç73w_r~ZDUiH#3nVLJkYtw)eU1,_Aĺn(Hj,$t3턅Tg QӴB/ *%NH}rcp%[,DƘ=r?%AƨJLt9l TU@!IOkI<#^vˆLt9Rd,*`dv7Ƹ,x4a((+Thj ARQ$"OR+DP$pag'gCW., pEPc $;P@ArRy6/;@P f}r8q|Qi7>w:㔭 )Auq~wg_~<عYvo#:r]ع,裟~p֋u]g)R?+};( "$ fŝ'6J#bo|fB@Iue޲흾R !+M+ )' pĪ F@xժ qqH_f#szs1gU  *qe1פ@U݇HBOP}/ 2elg22FUE(h3D@E3 从ٚxYwA"2 #q"s,i4萠Vދ5"^ĶUl^Tk ՍZMfUQ<;EcVlLD&&KDmbU&Ӱ\ÍW^t|hr.}i0,QʏWp$n0Ac0|+{OE%C}|1TFdYR? -]pmaM.羋Ih6 YW:Ç)4IpZ9' B"Du9wjDP>(Ia(IRF4 B;HId|gI"FPY^ߚ_,f]V%dBISTubH͍^ƢﺫVAǤZTÆjI)dy沌a+oKҢKh_HU,t<)* 2k}: M[BZon[ YӮ˓j\HU4Dcc.! 5M@ tibI`]?[ISJP)ňÝ`5 OWEtf&1{77? ܭNv1FШA~7=;ڟM{xqMZh9ʝz-\yEsZ׵ƀ)Rb~`X7mJ4 &l؄6Ȏ 1"RHl(Bfb?PR8K9 A#1Ie6OCF`%vsSYs dCA%ҀP$6H ,hٮbks_5;ݼyZ{U\i_UAM,ygYFD@5HK(1/Wk%VbGh*J{zQ]<8t{c^]x;THT.e& [.WUB#sNOO6,3f b>ZK`x\-<#uaFڦLMQ]:f)A Sј͟=_/$.zotv#/u*ltx؞;\d BD Qx}[Tw_z} ^f<C쟬]^ڝ| U ՝ 7mB1| ̫qRy =iԓns5ilc JZŶcZ8I)xZ_մZzi^NEPU& , DJq=cɤR:.*a"sFk7½|Ҁ pT!ȐO}Z;j]Ǐ䙆:$Hڗ<^҈01]` k `P>#@qP{@Hԙ6Q3Q0Ihg 6J=؛h+U{モ1%I)c4uHBu~m֗;f !^Wk.A/,S6ܢzй,Ne+7MA] aT&!isz:cC"w[{_Xt"C!sKԌ f2\7e_zTV,E{ʴ;a0EF"$޾.w?>WG/LlִmkF#lNv|[Ѣjwde1sd$Uew-r.Npoga{xVVlxv 76š֫v۸Aн80}l?a=yŭ2S0"Î z:|DTE9 (lLo.}RQI" ZvF4$0{{F%{Yg=K'1̽O%"\ͫz+k 1 Yg˲8><4lVՠ,SHU0_Ez[Qf9e\tݼ()FUSu|B7a\IU5T0 6e9h1I3e$J<(Pxg[M%樠.:rf7zxm>>ʭ܊/`NNirUV"L[׮[O+ڃ]5|ί6Ͻjdڴ6IADkA٬W?/o5ڹ'yl ѩu΅$]m[vsK9SN"fƔdJU(0* * N[o8^HU5EI2&4E@TR~^{ H0Z`Bcbb"(* b2=G@$%bboxOW~@f"z^EAIِakmՈ_,Ii0Mu0J H/f" -u_!#%[)Ef[SFHj6!Áąv^v8w]WwS߭ZKI' Așh뻀d @A0˿vs0|{m!d6w}50|].B(EHqQy2C3٘;z9/WsvX1IRl y&Y4ip n|fGd]xO?ԫ3gCBf~T|zc>oeVk]yx2o _5R>Kp6ps*DЄ"+RhI_U#cq)-|tyjr6˞b+.lݻOۏMr< [4qF;9"|1ٸ0vt݂>zg6`^*0;y먤w)'z7Q|a+5UD"w!" MBd"kOc(/y5ʲ]?ȱJBn}wƫ6L$ N*jZ3[Y _fP~(G1w1.ig9Eenݼh4( ̓y&H:H}Tk|p(8ɺXW)^4vw+icl)]z0|IJAzVvb<~p̵ok8ܱm8=Im\51o|ESn-gCx4_,Uk7̣l+3H4 'ŭM͢p2HM4D0W|ޮTJ5hqx|2&KL"[?h22Be9# D"4u$cՇGKl@뺬ʦiA0ko]ҵZu֭EIxgduvʇvElzyY[Ík*n7nn4]09&^ g>zє35(0Eldww}H.6d)(x0pFdY1&;zwNNPn_qmםTזZb 'p15ds7{W7v$Dѵ{oo]:?=^׾‹q^tÍly> 4xUM61O]nSQ|0} *kOlβiJ"Ķ}A!Υ`:g'6#u("@{ıK IUƦ6@TsLDYS^>ܚ}i-"@ZZW%Ƹ9@$H•eQJ)}WvQ!T z]+ S=(/>z;|AӐG8_Sj8 mf6b_>޽ws?Y PQmӦnW2 Gq-wnqU-T )D$UaU];u24y;4( Ň^#Lbv>¬)7;{.Anraȏfb8aSX&m^-$Qf|ӳ󝽭gr\^L7Y׏o~iyU-Φ4ZZ3 %!$7)BфzImcݲxz?Vܮ[$d%J8-QHA)% "0P J5BEx Sm9s6F\Y'OH{|kl8Yu!!{? ǏRJ}$(/YUID/:>ڿ/3QԞ*|~@}J+dR)wͪolc U݈g} IDAT>'f]^!|/IZx_χeab>x\%*>NnZdD]r<9/ n{}d8^b0]o}CbP׿l> >'-N\O޷7oYg׵Y{QW7|z+%&)C@!JTO,~_<:8hʗH^y.Vu^s2;X-"I\KJt5X2=))"q'6ܩ]G4nPLse: j]MMk9 %ڣY yACe=b2DJN/_` VYG ŋjfHUȲm_r\=WiX}}fˣiDL ȥ9"y-K6 }~!3%є4&6K]̦ _-@3Kl |gȱ1NQkzD<wQBr8tY㲵$z ݓ<ݹkY6ŧ*q#n~nܾY׫&rQv~g?㥫YI;_sSN3] juۿ6_Ň?}曯MzvZV#Ii0ɨe}<Z~i>n)ލ?mrk7e2HaeudZ 2'!W_#f%|5%b`.l9 ׿U"qJ U:8sP`D ()U^/7[ bpfPut6I"@Ume/=/`6HWm"(hӵ-"˼EBIֹTX0߆}$UNo-~틝+!7koLKQBB5p,mQ-]lmʗeҲi4N%ggY 3`Ξ\6dűNm^yN1/E=,DTg5sBHd@ch Ih7GZ(Y]WZu"&LޓgDFU;o`ΟtwEͰEM ,ܔ*(z=Ӧҫo5ݲT*z}"o4+i9zѴ[PnmܺCdoGuMAY+Ԇe47Eq~tX:`[ikwC8i-jGGYijGqU 84" 8wy,1CuGb!! ˚15ݬΠCPag<"DT4 Pؑ$^iCHQVs"` G$1HǠL *Yfww6Zwrt 1F}Ƌ(K!\ϒ>k,ETE%ח`O2ב}@0c &I|2{8,rm1 Qc6d::f  " D5F5,IPlYDtp3kX;C+YnaY. E1mR!e.V"ܽ~byw~mFEdB7~;|~V|ݴOΨ|0{rKr4r6]GadӋ鼝),:lv2c |oOv?vKd4fE!C."BHI_T78 Q77GbJD` XׅK֣s(a hЊM(;JhBe iRmRv"J(ZnjJ42Rӽ XU׵w/xkO-\UU R$ZX%^F8FUi^hJ$y}x"XE(bzݕYS1&(QvL)uyAYIcad,AHѰY*1Ԙb>@_dv}۲st1yEQ Nt*)$l{hUzьշ}o'D<ؾfIvp٭; G̛7eޫ7Ϻo|G|sq2;?{̍!&CC=>}~+Zrױ6.OlY?p+qU̡4Ilad0 $€ voOр$$YԺ2tAH D&bpui_l1$U6h0]b41@X58sxJ`B (tgk QK%W|K">Jӏ@_ȥ͕c ] Xb !\/&.QO"Uk҇^$Im c1R/Q$>K̺Rh Rm@BRe-Ƥ*D.SMIAqV@|Ualcmև ")j{މ^FGϦrR1ܺvܚ?z_|I3yGt6Yo)XJHk(sût;=xϾ? D1:|:5>hqݖ^ [>ed(qy5t!h pNuQ:tAD}1@,"l+XvYTԛT "Y%9I,\BYDM%̮O]UyHkߟa|jmndT0~)0rR+uHdr]>@DR/m:("jx)yrdZcB1T`fc̋ЕUJ@*~uc+ 1%p&`R F²'e`2w~T*⬞ yBJ^OCTy;KḪ:ؘ%?=;Ϝ ol|'?r Rm?xX5_ޭ?ZN]ӧ`^KS5 #h:lfm6M~S82'8:ݼ6n[CÚjdn÷p o{L#]v||1gК8Xa" ꈣ[A)@ H&v]SZXCh(4GlmS2Ŭ@w)*odݽ&+4'I'2( '.D@{!J?g$f(U C/*yLoꃯH__ s%@Xgk[Mbzi?%o z,{s}MWUY]Lz0(1! !B_OAO҃$ QR101==նˤ̛ZzY=`=UcFs^>~ӹ(+RDSc@%* ("#`9@^qENXU8CƢ3sm$cEb|,fg߇Og!6EDc(^RjqlGVd0֛ꦲ~6]\wg糝"{g\<ߔn[ʆݑ$GORXQ [[T 6էOImgoa+6,p8nzzY!s#]n3D{0MM<(rvd:ܾV!ErP$D(3*- ZlTa$ MP 1,̒ga[59dY z)M1ƴΦ6Y5l$&ech(;GOךDLyGTћ+{Ã;'Odw;<9żnu^|ы{)Oad)u9oV_}=v4XWчɧO,f8P&\|;?Y ˷ t{XAy!*AP>цJդAL&1cL*H 7 k[AAM^&˛R13;+ǣ24\=cAuM.4+[C ,Rv:9}Ϲ[oN& |uӁ[?3'n6zY;Άysm٣#B^V~>1,5Y^Dj1N~dbCH"XfClhgΐtf @f#Sʞq& Jlt %5!l 4 *9z@VBh !"Q̸FefCr6rU Y""7Ah}ro=ٺIҺ^̆U\DhU<<\Mo\}鰼B yo] Q'dL\oTd{KKo7jLǏOg룓'K)xκwu[7߻x:^o޼w{o3-;St[Rqŭ2';Z0iy:;~ۉlTSJ rf}2 J"I7&@ PB۩f5f6%UH"&\n>ƿ~i ¨J]BksRv$?KY!!°~ $TPEҭ[TG5|ԿB8g3lAzQ \XĨ#5}4v|A 0U$RBD a}yapC=͖+YTTD$ $),Ƞ)(0P$ PfCa{cZ%NT-iZgN{4B(D0ƒ樢 '+Kv4=;=_^, cßQ*|i3jb$^7>n{a;{޺vdgWg.r9<>=}:,Ԩ[{|WovByt-\29n-y΃lGF|;qZ:@=yI:~"s)F@1lP=(z&N|<(H@(IX2 *1) &1jJ1yO.qWUVʼq*ZQ\=+9ڞB־!tԭ_Fd HJkӚ 8˦^^[6bް̙7j-"n.1[?\2x/MA9w jɁggc FEĈ;MR#Z DŽDo{IC5f$zZI  K1r5ɏ'e$EY׆&E"KJ)??<bͫIo;׿|ɭWAgޝQbʑvwڴWHYFefRЧOxN])b;%j֕m̨;z xb&͚ N \AH ASbf FK 0䶩h8oiSvcq3@c6@BD6yfaV6 l5"hIF8buHp¡6M^/:6<~6zwnPLv jA[6aLB*]PM /lˀ͊3cKoNJchpC/P帅>$rhBi94S6tyabOÓO]CT6 *@ڔrcLbbP$^Y"@eVA6_$"b(A .& jl.l}aH$,2U wonk7zYi-r!>C]ez1ppPYG7w?аJrrg_p/xz5w"G$ ևNS-]Lڳ $ՉXPȵds3_,CYrو$HyҞ̟~Fj]e[1d<6 .("JȈQkXR1:fJ>f%jc%è13 &K~du[\iѡPuф8WtIRs&׺XjP l݌0+><;9? :v|{ݭ|v:B CUzMrohڽWU3;;oO?xag.Gyw6<Եuu-O[`w?$5@HTThCDBD"%NvTc 4"3n0ƀ^>]4V%RDѽ[7*};G f&3dAT}^͒s[]Hp۴D;>+8+~M>cȘ [/߂u<RVv:E-gtvC4PUˢGZ_9<$iS㽭ºɃ3.F5ttlV^7!y<IզTMf [W/4͢VZ&Ε*YGj0`C)@ UHI "!Jd(n]r)IB  @R]IV4Xv" ݤP4/u3 8BҼ"IK$MV+^J!9L[ v:F\nl7ouUJ&Tff t4i'&f _iC[)FyB2 "q*Myj-78#~1Z7uqIITnDǠD:Ct IDATjsܲ5%0U1&FI\mӐ{˵Bu#5@ID4n@, ݞ&H 0͢e[~zkX=rPgb?ۙ^}ʯ<|t uN9?p52cꢚ\ UX^g;RjNdtUjfe u}ޕ-@n**_yUkm:8 1)j>o`C* P%Q1ڨf$J!h hrC\uY]6U Il`806)f$OdT80 (&TVh1EmZOƊ w F[x9Ux. :,K D1v-;1aM+b`;=A EDUqU׾tq1ۀ}6,$\/7\ϱ"zM)!c"bDsq ?{2b{[佯|,5FF-D@M{@6V$>5aMj Ε?7YVj]԰@hAk 5_[.k/޴f]K4 ofo?mwvɀSx8UR2|P<wʄ]&;@n Nv%%=Xw ir~.nMw9.*1ވ) tF #@Jd  b@mre# jFlARSq"1r>4Uѓ8O,P8k1$j glDƄ)4k@ABEk[fxi-GoUl67EM A !k@)Dea'O6I>QjL]f|iƺٚ_DV4ψ99RDP5#q7[$v 1l fbU ~CE*~(4qM )jւs^kÆ $,9/Y%΍WJ˕|v~ mQdBB2Aӗ}ߌOD` l5l ,ϴIG\}jNɣjn޾ף,ϖgr>'*%|OF^9>m9[sf:(㢣1]ٍ `}9s$䒢pI{-ɷm^5w XT"2!uz0(xbis:?~px׿::~x`AA/+(*ĘPQP2 Ba*.m]"8DFH Я[i1ـ)Vԍ ***L/)Q7STؐ;e&Xٳյ!c1;[ĸ&N3eNk}mRPIĔI@@RnWmZwrN*$D&DDE- j0 N+,2+T~RX?z1c'۵\#yl|5=s&i 5" >;/=LI+ [ӋT&| WKzňT"snG[w}t`P7=:IL^R6wOۇ}W?Xͧ6E^3g2c?{gǻWڬ t8اϺ+3`c([n7Qwzq TFd6J"ߴhuJF=@2Bc7Aqz^7UQj'gܚƋ:z՜pI"3QwT^S4kj24k&O?zK("tMIؐV`lDJ!l&'͕P@EK9~G!TնmJ"nA`To2(!mDdk]0jJ{|sIlJC>ob V8ˁ"3Fn^cضra)7\ c?ZÈIcL{6OXG,t|v~puo]V>i|땃dO_ߝ(`K! kݛAa?verRn PvЁ5}*Z4&ָnVr5juay'{gdɎVpd/~gX7>|<{16+WG_=yU(uP7 , S͚*(cQr8FT|'q&ejUs1>9l&ui;2Rtz]~ΫX:ض)Alv Ԇ1w!LͷnvV` HE#JIԊeۏv(^@`ge6-Ogf\jŨ :btt2u2ymz?4QDM&)%A|yLzHRaɘm?o(Z"YYk2 I˦5jn8gE' !!PU' .Rx'9@  1nNV7~?~:aZEP2 bnYϺl69c>[ 7EHi"^3rջ}kG :w3ٞWv^\0ɓ*kY;ˌ]_,5Yq"~|vKǟ~:jIO֔b]5U5}zٝ\LN=}xV [$v4wb7B``5m%AJm!6ZRb&`Sk=,?<g ՁWmV>u. nUnNN]_7fÏ?~W^+sZZ{Zs:(PEѯhB Bl+j Hiؗmll(?8ݴ!xlͫiNN1cܙ/QFw^Ol?>ɥkUO;'u1?;mx'yfmv7ݒw߿7b]#4Fv89/&솽A v.>~|[K.kYm:3e% 3v< Xu֜ϵDf}R@b 7{B(mZ⚐Tג96R%1A\L9w' 0A7"%!wߌo?Y3y27 ϶X">*'V1%ݱH;phz+a$IerC56U 0t4H63!RJY@ }v60 1ƴ^7jeHS &&454}bEM hNR-:P?]OQ`r7tݪYP#6k"n?ʡ}vzY w>LƿDn_i_̰F4d2o*;/o΃[F=]2>HН,#1J-'aSvjVa=zt/}R8uwEpd^\y{/]oY<f)"&*:EbN!?{}$hCRCU s^zB H/H*9hH"zb9?cBh6U!:~cPBY !"P  117sgVR1L(1i 5mĄ^-1{{]Wn_M%_*1+/ww /j3OLۉ E+!*W9ڊ7?۝:צ!(e^} \ϞyANf;ZkAuGϦXɲZW`,gf̜>M{'ZHx}gnƐ *fBqѨ"" I":Xc1& T 1%@U5h7Mr[T3NH(ˋ~NV_{30[L(BT dH ~J1neY_`EfM֔.gRUmcy8u:6`U*(1$Fr\}{هta 8k͕,.ڶMQZ iE:6 ᘒ&/AH׶^edI(IUcaM̠ܥ蓪HTRPJXEEBI"{|1@ӣNGUмl*^3OFEl o_Eg~gflJlrՋO//`ݯw'Zn׻Hї9oyb@1*5& 9u];E>*oʢ6w)2M`YiahXm@=zRVpvww#Ԝ2.8K!Io|Qt臠"9gYK9 Ʋ'!غw1}JZUsa'$+?ւzqsG]fzk7͍Wռ!S쎷v/IuHH[KI8z ,O?/?ڕ̺?ޚՕ 昚XwjbWa]hR;⌵90H $h9Phy76Hl))1@v87Ę@r͊l#ZƐ2G^ON|Nm50c4?Τ~AzXaoJTtчSuhλup\CJIH1$Uف_"*lBUZ* lI@ K4<'?o~ga_3h6**lL H@fRIl 1ARCψj7 fCle+0I$̬k"rДeҴ֗kL eL⃤W/).|xu:u`T5`׏J>dnHAY7+@O?{h;gُ>X_vI am04 4aQImC[H> ~1̮]XUUb\*!syJV뽽+h(vR*fy6SZHEղ'S3i[Wj9))!Xgb!H뤹 &ŬsɃ1("G[[ɉ!,s̚ks9%DV@ۜ A"T" 0N|^cYf*6A5?Gӥ!;ڷ+`lmCP2Ecekvۏ^"5i,ov")/>.$P$v=WtRTY2k4!vpQT\A]\̺]WԦqqz1{y_V727]-3\; NsMs3ˎHF$FQ>_JӰӃrjb]@o PG̣ۙpN=քEw9y HU"jTU9̊ь&}ωXQΌ7&,c@,SFH:$3um ̳lpP=?il9DC-;XE6*Iq#暊HLĀ~t%EI$AU@UTÌj_}LA d^3sy2L 'D hFɱ"Mv&O&EoX/tRȋe3Οu}8@6k5F1jhVҭ];_hSg>J.O|֤:Qѭ}B`ee(M)G(sŧjݳ*n0ʳlhŠoEE7[]Lu;iSdъ -$v<mVMjK"&hg,-&>3$ ukO\n撚TOt!9ެή/$ƣpb Q6hf&$!w)<y)5FO'޿~>"_{L16S8$Y\^4k`z?:?<Q4E *LlqlgmyӇa]136$ƅͣi͂@6)MdYTR P%5m\u;y cd4Z,):4K?E-@s (&&6ь؜t2&Om {}4jٵë+>mFPt(B~P7oxwF ˙?{'e`W> _FưYӤNr*Dy gr뛯,?gualX~qgE&4jє}:F izv ?{O1"(j90Bج:ȑZ>} 9(2$UAƠY'a̻(B Dv]XU&}CI-&U`O=yK5N4IHEڥJUK$VN7^$ز:[tmfVeVV  DSEi4IL[4Op:ckn dH)SR_ IDAT&hIlD M 곲yive/_-ڔ:`A.e &xpSVʙq{qo}N^wujJm3@.fDv׵0 UB'& )9g1θk0CS$^$"O_{di-q9*z-$,0ubryFh yڢƜQ;.C12DTSEYk(X$ {_עmn7k\߮ſyW~ꍻҔ0wQ08$ ` ϋ:Y mH5ĺ[Dʽl1;8hZ8lg7nz@۴]mRJ˓ݺ}x- y/1zSUԏ O)IAR53#b J!@ՠk%5(HJcv%U]fP,5 "㆑\ i*ErAjމD lj mS,_=hG8Dϱ:ÓlM%r_?[`a"yfr7)|;_F`Oư!x=ɨ!dg[7ÓDžLfo4Muu_^-EPe$E]CRtIJ}W (||k1)g&g,clAQRnP2 !F,:Bۺ ^-W#lʴ_}kbn/vnq:(Y%lr7n]|=_%^zp+_xѽq\~݃d _I` C(\ao<]vEhrY4yV|c7^0v{KfP PU6ܨ?;9nouaoooº^}矜|M7[mi>s X&6zl]U18Ճ(^T#ria[?>/IbvĶlp}v@!Ai^:|Y=ƦNM-@$T@Ёb228K}m`JTPȤs 4^׋q*e<{c4[@' 5%uNƇ.Ԡ @$cQ&џﮰTE VI|gjo,ݸm+??;_F{f.VrڵiF/]bXe1fY.kjLD!޲L4[) U5L>##vb_We /f}u\$h6NFfUcx2 ?AZ1 ]*I@)iwQ,GMR4RB@181<.[v\y[HóÏGφyU{-4!/Wwӄ_.(QhEzaQ:3jNA1,;l7aRޚX\ bAwpdKݣyMAdy^oݰ%w]bH6(0n"?,~;sUV>#2u 1 %p*;SS2b YtM血:]Û0!GE@$FTA,#͢m}n8:G 1:44_,`eֻ6vu!VˋFʁ!gls IcdZg-$I"1.U#fG}ùj lb׾m]H !'& >`$єzl*b+RefB]nV_|<%K`E7!*#rjLH@(woIRRVoF\z}p8ro(+Pp٬}y q\ϪpCcjz֗W?{nQ]eӓEAU :whQ(ARBleZk0` 37/ &Ҡa^-A5ilB&> G6mt뷇}!< $=u} !dvjx aqN#B>JA k}b05[`q@Q0Wj)vr49΅TU aSqŘ@2+@U"4áhYDd5ekКk /|0b])<{bE.O=vag^#̲8ʢ3"!'ɝS~1S>^52uV*ͬ'dEX@hIQDѾۛmrLSYj%Ef Ք&؃O澮wr5WeQH$8WM(LVE)Wx|v}?u|MȎkG7o&y1;[{1@۠oqWǮKj -X8d fm^%zƑ* ^o&ZHLc`޶U5 vT~LJHkٟwz`-CT`&5Bnx0Y/S[ʨ*TU%KC}W5{IPܭUoVD " Qa uT`LCn &i^'Bdr)t!ĘwbϧȖlæ^^-S., Yk;﹜=}^fOE?055,Z!b$US^1>)f-_{$]cFPTiBvzVǎ]DhsMh*|j]J0 b<YΝOj|˿?l"?Q5,d8t۬t4}xݗk?_oۃlrʋMS'AE9ș|(KJllL8eGLd"$k٧,\QrFyڵ >r83읋1$%3mim z_E4UEEHzELf9䈐yUTDh-][ZXDYJ(ԛdUy%& en5m6sy5ȻHdP pQBl0! 5&6.!z}TfDUQE#-?gѠ!wPڗ]'"meaNH czSo;:4@{[;;!:v@(k.k"V 2j*'.Zmv| H#07m|UWUyQѯ yht횦y(D(e)IB^V0~r&j8 1UrLy[gh4md:mt'/y^j&Ƌԓڻ?}I"AE, J(|(hm0qn^ h AHr}jȵVi}U;3*&Joշ\ncU;91Ow-zIC bbF&`${c$>'$, @1&j*r8&I*b"ZP%{S<=$KQ亲T">M$1k|LѰr9xZ=wP1f1hbb2H$,^&*t4dz0.d6,su}C2nbՏFpGq7yIo3O c@9W >Gm2vrxohYNdl6ѧ9Ib~Vq5mkL1N: 1bZ8|A^}Rsp8-b܆.+JtiknTާlM'~ xJe51)|w/'d$y.*2 ΣhBdH}s+ !_'C0!&狏XPd1H&eTج!X.jkrz+nHv(, E8xd;ENSGK60>Pe)2 >gpU U4PsHdE% Lye- V9ǃÛpg^m5& DtݘOT ZhB'wLcMD k zX7,xToI0(* bf,'8I.η wj\\P'lC~O./?z1Gh2­۷q-'x/0ˌc2Y%BCIUȖ ` C9ި$B3I4oLup`{η.M.YfH.`T+ Êhv޲"p(Cl y# oDju )$Q6DB"̑3 rgNdܛ5 åyT9HQ$$6:1$k@EJ3xZ۟ lF?t7^q^ z7Jl*+,0w17\RDLeUm/T$>]V]t]LYƪiΟ><:|Wn=:k_|cj}Ǩ.JFvWVw~QVs5{8x洚n׭ZkȵƃҴYť'4/?o~ehZTϕ fYq/~C1:45li3MYbM}"Dz۴0$/6 Gxx7w~JiSR>D$JܵQMKug}kUcf1ke$9B5fE}(2Gx&x(}ŃA>P̌Vp@f6N|n>vknWVs~k({GŃ GoZF"$8?F><²]-W%G/^<ۜ ׬nuFU: {\[1ǏOEj(R1X!Vof)Ƙ9p}HJq^7WFE%"mRQO[nX(3Ws2QP%!vm>Aiu]_ҍ{yEut]I3c;ۭ}N,jnȱ >;s2D/_xw޺ɒotʘ’I{k_tm du;p6X]jXگdCsDrxɬb8~uu4 Pᢾp:{5"F%DPG(!6kyPP-0X6֭j(̜RJ4eiv-})?Wt>6D_Q6J"JlEiX/p+fÿg~ Njzr7'W}VAz*b)Ħ!2Q c`UC4I 2 GgailFO1_,Pa^R ()r^aZ3?0֙lvlp-?|88y+b"~7<ޖ:4yl?k^=(@Rܽe { IDAT]-ukp2TL) ^4rV@"&f0VkU5u]_O)\kK)iK܏4jn1(0S5]TTVJ"5>|_S R.l heLQPWh.7ځh$7eNYuk1%vG/ޫO慫ʲ+1,9mgՋw6֑ƽz\0b<|j%:k؟?4tT%zPy2 ц~ ƿ_ <ƈ*SL4 p]U{LQ6^sCLLՃP2U&%{LH2,wU}~<Np?Zdp9w˯ߎ @ m[lx9"(pw4\blPT˺HD(@@v@%4E3\CEMPUA& u\T{H^?o1#* BJ:&)*ITD}Mt y D-5u)0jH$@\pecSޥ *ӝd1H )& $"(;W `Foݻ?_zPkpV\tgklVmEv=^_\Un7ޮޭї_97~4Pa];1V<0$`c=^W^;oC4gwV9p<E 8 'JH7!sTw ˳b= m(ɷ1^n4nb@q͒m֒e|ϗ10\69CO&r!qerxW x$%aJ S5(%bERXeפQQE1iGff,( las 5@dP7 UI("QBJaSW~fqբK6eJ[u@հ[̛:I@0$!z슮EM1DUꁞ|8?@ x9_4.>d.@e?QtY.ς?֗WͼKt6}\*!@ت)se9eoכJbdYێZVU@wOB(!+E>( LQ6iC 64t?qPlѣ_bT앗FǓg>\EZ/`eJpc!EV|'dCSUID@EE{5DքwjŨ>o_ٟ6ӓjPYDU20ΛLME')0f) r  :cC2gQy?xrÀ`:tg+DӮ=In-צ6u__|ϛ;\,g΂-aޡv1|IjPe"_{|45Ƣ]75 &J!&u왱Hny~}¿8f/Z)W#I1!V_a݁VLI""D"B߹WnW[M HxTHIU :!&d>ė}qG,)ŜD˲Ӈ]] It8~xf]l ƚ؅[yn3=:``RȜy`_7oȵ7e^?Oě6E2Dm?Zc*4M;?:9;+|\fY>1()B?{E$s뺟8}$UmzH‚_+gףޭUzKo0k_/|k+-ZdŎ 93uo߻w_Wo;)˲M3MelXm^FY> H ]we1gfXԙ(څ1< φbs9;S^n/va-r*V6wn()9mM][kdTmCyÍ^]jfvڤiz8n$,1h/+g;N`߶`372 1*BɣF% 3w_sǷr~ՙ Q9'^h1f!3DeeII{%e@kibKbj2s0je29Chke-ӓ3D 끇2kˏ}ie_Hݶ'gSdF1ڶ[Wv1I 2\KmCՓ ۶C~b,0i;=sk𥛿5эh|1_lHrts"YeoT|~[}OeY- `/L /<_$(|~Lh8ڟY笵Bι>H0sN{ujJѠFwɌ$}^jʢ`@a[)t̒RUgV$V0g C1bH0%B7n#dЀfnPӓ*$QL!ow\"[>tRHǮ~Gۏ~4-viT zu]~U,$%  s, M!Y(ШG{oߝ $,*k ڮ>2_+l1ONS񓴊k~8;ki7h}0 [c;E R_OI#uM>m,1jޜ^ov*!)z\fW f>;wfh3&PB ՛^]a;7皫z`Ie-ѲH6 Kv9qg dd 8&؆ 1$FE칛5|sxUeJ{|)VFXeL=W(kF?gB 'G{lu5s!('vo_]]ٝ?j|/}t1Fdz~rlb>< .pHyjZi2D%6ι`dH (ww~ ¨u&Eu HwPx$fJ b>BJC1]7~f_GIDD"#uKJ M=KmspI*moӫ+mD0(#Xs\B DPmfFzo i/dg@=9'H e,7/]*T-F܈f׺(zjIԷ%Sz5 nJ4|mdy"eX+EZkp^oߎAq?9)"Y;huݽ[ad$%:(ё1m>[5_ ,46"!Qd!|-xQJ,"dF_H70c%@ !#pY )AR|g1_#HJB' Hd,㧽 q{AFi@$ȋ$wI$!6j mP=,fjsQ.+GδNv&B٢x*㶸"׏?w@} Z ;M5 sۋ[B 3 @IR 20_~|vFb{y|9: USm: Yi/anBעm,H֨#MMuWt7o*JvǢjafl]oI_ώ~pGߘmKGɯp˪eضݫv=:_m/OB vbݖ$~h?=myǸx XZ@rW.5o^|I u9:F(dU4 [R}?d{yH#-^"H[qkZVGOu;)-J"B`A Ep]Ա8`I#3=.M:M6貑ZoXk+;WK1Rɓ,Vj} c^9L߁-RH?uQ^bf@ t[b⨮J}DQ?uFj¼kVG(drxj>8b^濽9=bs5{U]UZXڋ؍µ FtgЏ}f!J Ih xBV.?X%5 k6]vv sQx*_/|OO7]C2kI0pPDkeYq›/=xO~ѼboӮps R[;t^o>M0@Y;; vX6tAi@U'D yKF<4AG@4Ex2Q{i߄ ڊC!BA(lė^VL-*0ٚyz&mWȴ 4-}I'ә0 $U߃-*WJao'1꤮ʼn]R\;4+]Y`"zUzk-Zg~" $Uuwt'jڙLm~^kB:IUWIGTއ=li&l8Q(zUn/p[6o: 7U6צ>nNF:1+! 6zyHhWj 1e$b]VmN5IjV6HLjAGk_~߹sL/ha^XJ dɟ<}WKwag>x+!1V+$ޣO/EwLZ)p:%3ԏ ݛ +n.GNGYk 2BR;D?ΊגK iB'BYHJyii%JNFR3(H =j&ʨ 8nb; h*{[ݬ^:LiW]7PvDkx/)ۏe !:gwi]?+'iɥȎ$؏&eGE+X<_]dLotb~hعnwgPJxI(B٩ο|~[g$Obj+dcp$ D8q״QҴЁZ .s rHM !F T{!9]ډ@b\&) KIcdlzɵ?Ƭu ihjtVm_U+l <""&4|F =ϧz#_o];}c鬮?};V5{et\)"/We̓(,[`Ĥ._+t˯Lf.S2~.`eQ{0czK+ڤJF}s55WFi`l[a_}pVĚy ޼v0gS体!@Cr(f˽]>Τ_9:;]6n9~?N'O^+q|~\0@%mӃ+Ǐ^o)G׭" {,@,\pi"_WXr"Ag&1 +&&bf@ *?/_|oDbҊ,v%&HBV'EՐTL* V5$Qn"bT0h/w IDAT|gݽr@uQrb{7Һ/]CB`rmuq(oP $IRsXN#]SA R2b9@m{l΅P/ZrshoXEIj$eY卽`8M.|9=|(H|g?Uhzݳ &*Ai$ by)3]/VRnؠF,ZƑNw/{nո,]%?P`, ıQm~0D_ ~]A$t>JF伕Y+f`Ϡ6i_uZ# L["cnCJ* jDuUs*09$fZ͟ ~gbC:̵qϦOE` C3(A[" [V9 *λwm]Ծ$:DQj>oR\X-mY7z6]]?C _Ds-!nw~uaʇ0Iz"&4壔sP>%Se75nxKO{[(3bE";۪$-,Vƭ;XX(ެ w_yWA ~pD/b1f{3Wjj&@PŒ,/.>}jLz4Dե(M&9Y,m?!׆fMQ́e띟o46:h u,ʠ6)(ж^|opi  E$u Gf@u@5(b8QQ聪UI۩DjD:_ GIz!~ǣuN86q/ϫbۈ8@?"F(%$>lMdM5DJ)Zo^B4F6?P~*d 7pYB BP"TP+ qd>(RFZAHL@yX/SG ^m R Ĺ&fj}'nεl$ +R @ژG?2FGwjeƹkns(7bY$:y"B$xJi\y̨Ho|"J[<ћ}|3zjmIi|GG'{~Kn;gxJ "zF!I$7^85ܬ*iNJU4?Vcz"Mʊ T.}[UD!Jl xE(HLhş?ΠND|&!i:-eyKZ9(`~%OGrt[*&6n*/)1ʂN/|Wݕa ^Ub>L}[k46O|v?M=1u(%ѽ8m B8t뺮m+ؒ # (&Ȕ($q8.v!Q۶[_t'w<5>1b/*ΝO7(Lf65TcF;'gO~Ūh8AQ7 Wpxҕ1eDAwrvU7}3>.d,|vt"URUKy~a[Qv Qad_K,~rٯ]i^؁vAEh$\WI:ӈö4FXi m&$d.# 2 5E"FXdäy$թ뚦]{C-Va֕!8fZ.vr}# B"R[s(r>0R` ^0rK'fYXz{Se[I)iF19|ƐҪj,ڶ%i-$+/ɪ~t\֪=mhSh''bIw[)oP^phb,?__ xh~Hj~2K#yƬɤMQHx^,:_np0Uƚa?ºއ?'7+i>^UN:f}o.8G篯j,nJLN6xk˴ 6e?NL7F^:'ǝF:WKP44@;:ҩʮ_߹8Yqxw%@A!rp)@Ѯ=dh6fOkI4J^ۻx5[4UA1h$ ąpp;(@O}AQ"x[zJ}MExetsĦ(XUƺqaj4 &D1D4w޿wÁGl~oisɮI=ZD}4oQP" 4=]dt璪㣁^e{{aqy[_-_AOL@:lt+y-˸si9%E1͢jgmn"Ӻ-wvDh#qbt'ͺd7.rZo ҶsFùB܂% 33mx;1iDh~-?Ϙ9cuơm+[|uVtolo^rUSQ{mcpݷ~#jZOЯy /⬝nScrjaa%c494TGp@*w'px$yxOJI7bK |>YiFi軮mwoJj菨?jc~7yl!71ғ{'0jS$BPѕwc[r7̀"ET i g%al"ҵ0hkŮ Pmq[b5MC:!ʕuG!H^$#E`"-D%5*7J[{5)"~b|gwߛ&M]Mw#rþU~u?_vIU=L?hJolTr.ᄋp!ZkoWmYb\ #6ӊ1ٽe9YwYSWD*G!TN4(Q65<r]^{{'Ӌ)We6Fӳ_|秋(h#DM2fA]7Q|/m1@\-+ b7JB AV0:Y^"JB) #: u~d_|Ҋ 84/Kn=y11(@:DB,A0WVLUR&Rn4l>k뢴&!DF͚f^ZӃK OWg'Nv3r|{Iojxt<~dP@2MJGyi&ZJ)A"!"P^) e{BtIX]YE;&MS",p4pAljuX71 «gl+PڋWv4T& '-M++"c5iA4H f$QC.j㪸Lo_L"(odzV'Ea%3".NDV]^k8/νkn*w<`#d#Xå:6^ֵ9h#B~{FRJN+\K CӰwF pN9Zc97EyAF)wYHt 4Ȋq:{D_y*O@PuQxq烤B22ШDE~b#5g)?\zL,d<=_{=}eVݵ߷vl=B}Diޢؼ؏#}׶[ 켈!M&R\78̳Ξ@]^$b?z>ir4Qnڷn2 ml@33'ИsKT.EZfwVD8$͕r^?(we0B)ФT)8AT) @!UXnt_WWVEd\?bRGq]nm= BhHU2!B&!cwo>Y_*WJru[Jwa]ʻrTi&QҲ'rn0+9U՝2lEj jS&SpV&B8T^Dq[鋋^"E$qd,4^ڭh yfes_Zop]T]1Jqh?XZds;с.,':YLԔx0CUz{ٙ lp07ぇjZZsI/,nlF.&Q[ZYTWćik;ONVo@HGvr`S&{ղOg^hڸNBqL,`+N Vض-_q+m $~opiޔkWm)D4of]F{$6)p"MJIp(-="AD묃ǝ3_ק$j5ںNu!xbMRhxo#)BUE s~*y/\DCQuwh>e7Mh.}s!\bByC{tt|:{Ei*k]紱JxEpXiؑRi(bjS&da_{ʘH("X~S7wfQdzfsڜ6-7zެg>3*dCX%Y\ҳW^E!]C`5sT $i`"l bt,<2O|N_E7Ih{ٺݴ`{2L8ꍿwޠ׾ӹå6d@Bœr P|`Lx@5R{+W^}kw߽B"Х=JQX#)"Mnnt?}SݴUt遒FD)CHX 2,Wm8Ad$> *|(˘mS{wz+gCĈA;ƍb%DO%aDҠI $?v<𗏎^}y4ulwmSVW.kYXі7Ub IkǣͪjN!i60]q"Iikml ޹ymV#z8[;i6 %1)B"i#fTJC˶1c֮ Ս爞(̊fЭDA<>y<p;"Ħ,仆q*\0fmܰʍ~ ɯWld#)0k1anE[TF7m HQ4hggࡶZIDimS\{%8lWܶֆ9xuN)SUu?E}֕O?D1#Xv/X.Ntɻ& !hS&6p8dVz >(M6"!a) IR譖x>Iߪ WXt_ır$u哄]C`weOrvY#tI!0o|+/^{AuO(S<ʪ:5&f`"8qk3 8K|Em۶2,bކ:$nۮ~߮iiȧmmm>Wڦ=R¬ QQF6 .ejZWc&X罃Z#1QdIDn~Z,{ΛN\k^Fq$3:1J4p_#J3*Wͅٻz7rFKVifWK&qInD7AӉHzTvv!߻8űIfx`|}Q(%VMw#"1DU".! qG&j]?|=\ZW@(*hۖ)pU׸8zZ AC`"!B5ηB% :% ?BA;宆~84K'{.IZʎ:a(@Uyˎk̊t׶Ҥb)Юo*aIܶ.7q)DQ<.8 :',QDm'Bҗ,c"Iim6N@Z#,&@*9sڭ;9` IDAT02BVZA) RJsJyZ[kt۶ (ZAR*"MTUCj`&5x*gٛnu3u{(M\^8`״6aR\wް ! &bתDsp9:!a׭u]:&GIGN!% [ )֛^yZwͼ9:U.dWjZttl !!D6 TVW%mcCt̼sozsX|q݀bP|{.OJQ=td}踭&w?+GF%o̎d3V鼅ɳջwnzeRZ㾍NDJc{D( B K4%X;A[Q&IEfbzl9"ic-wX+uxx@M1Fkʒ֐8g"HaDG+~1}c]Y(@opi殞縌qZ,A[R) gP|Pdi:l=,Yl2uɊ]ߖfFZE5۴GۿGMVi9]5;xa+oNpڗ/qP5bdBDmhIq+j5MNxd Yc1yvtɐz_yc0d ,L2wZ-0m%.V"h`F-w]v&DYeh04ԈfJ[Ǿ}@JUcR \w7Gi@V[ i@1 vfJX>`o. 1&5'Yex5p8n,Nøl/\ny(@HeQ$,MBz6MI76:+:s35# #>,WK5$F[ɰ1<1"iI{ hPOC!Da2J&1kwEpX-V!R$I!n-J qQ*utbupx[~L6o\}SN ߴŽLO>P`s#Qa׼?[-=,|p<-@"; jO_g{VoUB8=^XbdCB `p:5b>j"C`z8jMx5?ǫzuҥ|}<"`mԋuOō Y]I @DD+("hV12ĨHxrH3NIa41]M$ f J%GGwgtd`R3]W-bs8KFŭ&^ZWN);@ }SKmbMĈ /LJ>ZЧP8ML nLQ[/ Bݤֶm^ʢt^/KE UDYԳ1s!F"dM0}zEس)pXוs> }ཏ1Iu%D'T/ހZB (ۖcex7KsY5rmDNxI0˿/oM/chЍDIyKԭ'۱8۸5ʛzfzv7O1Հ:ڄ:_% !eڿk^Ly9>}bt]7j] RB>7VY#`tbk[eOuwv`L՚bT@]-DQdf#fBIX+Vﻆ L^)2ֳXCE16QԞSΆ!(>m&TOtgk(/e'_Gv]5о;9n\HS'ظN˺Fz9phB!Ѥ V9> )x]PԾV6ۮqe.ijRQL>4M<].HTij@!("u]#R"w]@BC$M1m[5z/)Vaou]HUUyhPZC]5B^V1&Fg+{oo#SuNeSVCH%7Gl.-KV"J{tdn1|VӽW%Asʯuoݵ~MVpI٧NU7 Obzc[q6. K,JQd%Menycʧo!b`MXiy9׺r(yIek./o>ѿ-y8 m ] ͤhAsl]_A"@),WQ"sD.1T|kvm":h\{ צIJ۝;GP B "K$G;_7nN/Yu;YW{O&j>FEQ\UiT󓴲0)p,|m6QT`Љ%MfZZ}ߵ'ZC?`n %V"+ E*SJ)*; 7J]%IbyI|N8ƾ_HcD,/]3Z)ͅAycH{xZn I:MӶ=Dq1ȷfkoIzՐm-]Q,)߲Յk7ᕢbt@;Yu ~Yty/,bPJqDa›o=<9m"$ Šh6VgJu׭2.ewad~Dk($w/b7_w*n^LLVGRMT)ѦU -"(T׎8H@D$p@ $} @ AF p:1ztÁ>>qd6fqR-Q#.-A@^j'?{| S|DߦJ$7!@An ݬGޫټzѰʋlN`̷E>v-8& LˡPᎢ4EC.ncg4.HTuA) BBf$:"WW?YpaJxvth  @_Y"HP?WƗӦ@$k5XI"!n9|?4Gi`눨 H-6 m[lzF5 (]a{ɫ~&,Wt.m? DnsRU9v~u2IOeyVWs{,b^cb薫%c sӮkF]|Gf*KBd,.N4PӋn|stt4yRe?} hh|qr6Mwߵ1$ԠYVN=c$ծO?޹05x}r@d:Z?"c1z0c$9sH?vK1Gz\yۛټ !~_6ͻGQ>u Kd8(5}8Se͗bp`H4{"^x 4hd YLb0U,8FVJ E82r'n`[q4K92uA{ ڱ0.k c\ɧah# @ HyV䐫?QMb(+QNnDbI>@ίSGLk[FwXwt%mܢ}|(4"\yK=xChR<'"4>o꿠 CBFi>OlooWz,v&Ei1`_YTxq˃5OU, 'u&;\ĵ+i^ _=wrvp˜sQhu֮hBM/^yuu]lnMΥY׶̞eà2vҤ@u慆j5;"ac-2c3ÝuAu] dd? mn^ >g$<<^ L5>-ٸr]* ۮ]\A|, .d0VkHr%mMҝC|~6??h |Jڶ1͎뮺<\J[o{}8#3R |DćˣSoIض(RQ:6İ1ebcS@q I} !*d6#X5 nNEB,f̱^)8e*H{g9"[M$MRJKp,{^xP= K)%,yf>99[3*A9B>py|41Qq8\ iˮBK+6jIRŵBkPa\^aܸ[ի;ip5 a؏gaIW5TlhRZ"'I}hfQ??$T ؤ()fĦi*h\#9jspv=Vfvo˟9曻e4]z*L®@EH3#člVr9,_rh/?ܕ,(ҍV(J`I͖0L!3I듕RVv=՞Gp^xq/<`1NEb:}?1!5H<9>b_n` 破u-oW]"Q:1bEd ΅AOZ+IB6JA4Pt (.;&bmToeQ hi%&61ȶUvAm:^Pui۶/ `Tʇ' !ϲ2T@,)a& r^omN(^e5.v^jiL)T?6Ϳ1؍Z ?Otk#rO{W>eef_S'4l2A\4FbCkvsklM6;tO\3ƄɦI2d 3^ۅt[gOg}7.&pZw-\b&nMw,Ƕ%u$ |з͟,g2([3%o\o#\t8(7uV<ʭ8;=n)$3.Ȓ;xgVTB h/,c7qn$8~N~. }~h0j̦*(ъ\CYat{&lي˕ UyZ/I`#2/&ރ|QxP Il6R& Fkk ]Nȱϔ>ҋPowQbJ+@$i<f?Z@gu,5/Gf uיzMfypG~,ȱfIĄi_":fOHM"|NFQtW8.Ȧ餔~?ZwfgP$FFrcu ٓGhtu2{\D"c1f $UZ!G%kRN7Mht6_fՓ?뿾ssM/¡g4MOR(w?cvmA gCC;2ڎt6Owg[堎٠eh:X(FքI:w"b&5/ZkjۚH@|5J :D0sLtz"E |뷸 ژ`0͌R╄sm(Pw} "MvlPF/Bb Osǣ'%!bBiM]sdR4^_;iONYn]~"?? #cs h[s8EDeV-gޕ [X9ImuHm:;;Q bP@12eX.S2 =*7?zz6 u:n::gpv\ݻs/:fB_d$!M %">K3(EV)qβbt奺m|p@"©sUevWEL(b|N6f')EMg]崷Zȓ< ry|!Us2p^fwsp% %sZyu0|.b֚Ngo؎$& lR 3j%1(D+m|0_H yXI퟽ڒVҽϰIOu !j.-h:ȬA m׮VUe; KBФ8EHL8"+YXFfE:0GD!Jmu%H:O)P3YRAN믇lO 6p.c. G ZT: ʊiyqŎF';vуQ|o(SuέOBBQi5lݹ})rdR71z{ko.NN7w'[œo^gkjuU.3 v0~Gf^->a@RSrD9zo# r;YɉMmomtYd2(Axd-VdXϖ'5'dD5?HV$X;Eהy~V/ݿ+_:kBۡE ")D "yz>oHcecdHȫtuUfvb 5UY P?I@EHMzq'Ū~`#QU̓QMh4/jfYllm)DBZkn8'ڶ%ƚ`0]zStԑwm\h_DyWNRc'WoG?rON >5buo\yqwm" 7v- )_ِ*ƻAYڈM@,4F%V@4/q(>xϯvЋ>1/w}ʞR gjf$"AF,Y0( `novW6>}g$ ދw4O_rc> V%w_!x#8 B纶}x<|X)767>OQI۵{x6O6 匴GGOr20O;:6 pY\FT5<;\f.wjV>[vɥ+uQcV3ػ'?f8{o2ʌ9~pi18؃5Nz2CҺ'?JgJ Vܪ'[ +$&M8Cu4+hyeYNigjdfխ*ko|&xCiWI)/\EY%#a"ӯ^K6|xIms41A[;e AT]LH PD%]jQ#ƣ͢HH$6AAut<<ΞZ?- Bo!_8I!v]7='?T`# S!UDB%)  Z 2 k=HTj!B$  QNr?K]c#$F@kh0̓˯| JV*n^i%l|3G|bƚD:KW_z PƘjYq瑨 z5Afky3`)%blv/;GϞ^(V뙂yܘNrvhc`)N/IüG(տ; GN٣7ճ(:lzvWG9}dws>V c䮓bevM G]]MΌhWGI_,mCu> /5?Hl'D:ED$T;$+l%X 10/Y}Yg$pAXBT1 G~W iE\i7γԭH hBݻWxMOEAifQdeK<ߞE %tN>NzIm*uuT`ۆ}]0!@H=%'޹,˵VkR6w66wGQb{4ZlO^=xfT,m=v1 bnF60''o'}N4A+7ٻY_jП|ƧI*ͯBBqMw)jp``\qʬPWG'! ȵjC! h]L_o/iIumnyN4,BǒIJD u,*I20nK`OcyQDCH {]fE .Ѭ[7n2J'ρH r7i,V)Ak[TvǑo={/?)b 9\+D׵",V;~қR E{zh3ѭ@$8zK umFXDz$u]1HJ 3F3''1VDҷY+42GiaYV&RHJ)A-F|.sg JS +?}["#!04M[Eնmf9<8UU*hOhL?Ͼ;p-+j˫مvcpؼ՗Z>i`7I@[Z `T42)fZsg58!@BBulRBh墷iٓR(1K(HyFuM!z#ҶXl4+G;+f*E@!!(Rϟ[H"ܻ~#102AҐƝׯ[XD/QI-8ϑ70s(tY;Bbb$:Iz_} SӇn:6D$DED(EhBz4E^bfQJ/ Ap|>MӪ{cEw~E8Ơaf6JY&ÏysP\C8чL.w~VjYDRyJZKmR~s)y!{ZEV\/U))ΔuHQw%5g Q֘ӝ?jH&_7ސ~l5W/]woNt4WN_5PJ宫LܺF[&扝^Zub6#?AvZ ;1Fk$ H.*anD P5{DDcM! (fk2dGҞ5=OzcsNb#Wd~gl4D!.O[n{H5l\=~c!MJ%$ 0OΛ.be~!"6c:mȑϭ]RG>muӼ  u.$Ez=L郐8b۟;:9MbEQ0Fd\o*sūfskHkm}zPGJҼi—l^Jgc̕xConv, Qe>bݦĮCaUaYN&>QԴ@`_z@Kom?g>>[oLsR8i Qk 3m6M FTY5"~2檮NsAC[C/ " &%fH7 I) "7H>p^1fי {?|*ݺf$)6(R%Q=ac`0 ـ2?&3hĨD)w@2-t}zw~kô0j+䐋Sl̃hl6lu>"s:or|a?FURUacE֤:]u` 5SDId ="DL:}8蹦̃MoqfOA $ T} XءH2;at)Cx%1|Ά#ED"@Q$fd#*;P@qܷ 84""$<s25ͦ DJ>C]3饭瞗r`E/`gmJJةy@;DG*<!ウD YFƚ)M V[cK|[̶"j?se{2^J#0r\o1!hħ簇h{wh`k˱rٽb~8}BmHl ÓT?XH)C>h`iÑ1LXλ{tqӜV7{[^0n|Pedv* iՁ&C+Զ7Z5Xep̒ƒM[$5f0ì@DC)2+yDLR"IDDŽ1TC۪OF 0n|Oc$YƢEA1NQ;/[O%!$8*ϲl[''']"J")W'_l<+"eW`vzd'wnla<&W[?NG~I0 ;9Qۚ$͓3 dDKpmOzyeǞZ/iэ_d҅''~(_}}f7?}Jof3^9\a OG5ݤ>N1JY@$JdD4OONoZ\xs-0/~|y3::zpo\_zI>1ի]QDJhΫ}nw~0,ir|mikKҿ*.t1:pkV28|gʵP-fGw7weթ~{wd[8Wj`#22M̉:ig _O)+ڶ f0;] s9-f9}dHΚOà'F0-YC !Y^7"9b4[r lZ YPƚ3ճ.&*sx<=b.Y.~cfT@ԛCd;(N 1!&VwG,﹬?~:0a@9IcJ"R-O=ψ ҪHruUE#Md&&b 1UC$׋1~UUb,D - dg)޸hL|4jzuOWV0̏턕DDO'a~u;l/b{)BuN'TL <~5_3 lNm#ɧqi3->,Oǎ{-`~Pxlxlquo9IOd+)BLqPk7tON<,|:MQ5m(ƌіe8yG!(nUz}Ae%F5u+D"sb+y WG[D0#2J't(8_n΍#S91&4tA][q 5DD$jAS,Zj9h^T@hܽBLje;2ko Ku*2ҫoN{M`t[5hK>p"?:>0Scs.˲R.hϞ fYc1~pWNeWL?l$r7]VM h/hv)kÿov|U6 Ί[0YSf06a\?۾s \͘Ce:NZU S>n\$E^['юɶ|?l[Cm)6Mn ^QaFHoehdc% )6qhM+coue&Ayzug#xdM4sx֭Wۦ(04'O_]g'<}pzx=aȒ)]:1EҾ'7.m_]Ŗv+fm.vvFBBGcu.Yc%e mCLU:a{+ 9ke휈P'>q5r6$I~9` 5G;%>p6s]T yPUQJ1RIMXSDUY$$"v]rYp:t0GP9d<6ʋ/?CXE Jy*("CĔX2K tv?dorcTeSmw`>;ڶ ƽP)3:%) BHwi&ynȌ9Seտ>yr=}5WKpÞM&G"L3'2bvmb]E>=< @@lޖw;eSm{OWv̞~K={̕ ;eضiZ>Тv}߼I{4~^'re޻fD?gtz|wuu{:t|i:#)]|Kq_M_IHA(PUn֧u]'Ehc?<(Ex;|2YdaQsxBM|`wZCnmKӟ aBs +KC(MP待\m>qh &ISd < e{^2spѲa\oPVf2̖n2SUc-!(umgFW9*G pݏ;vNΪ+?1d7۪gՅ ke3"$>t{߄91m2"J^x٘tUE9_/҇wW'?ͳh›jώn=5 ;'EM4^(C{cd}{a8m N{7cx|kަۦ>8W7gj#|/ԐiQ FO=HD]D;!QEdv&sllR*)DćS UۨBrg]ggrsBP՘1*TK!1M>*&2XS,Cnlvp=ђzc~J)C(2TzL>x(\j$y%ä j jM_#+-gџbx%͟M.~#S/^(;<fԵefrWU̷h06Őɮ7G?춱4eG)J3l\RڤPRwr] n;()%h:%&J*X_j7 e/lZѻk.th`ra&wNwz7c[nYD,2mi(m>11iY<"D$B#7l x{]~bn4jLňȧ㝟=|Ô+SJ@]i$p&.6[Lj[HIG(gs`kc)3?c1z@D&Q:Wͭ DFj5V#XdD ͳ/ !1Ĩgle`K^0c"4UIRk _E$%mNߦ<SJ)x!HbpMY;;)1(ۤ8<ϲL%mudAWE?ؼf`꭛m "gBS4"4a[O KgA,Ja A,cRh"v@J=W17W'XdR"!>E;."٬$* !JG>{4 xnйzq-#?;ze_Z@![%}[<݂!D%MD fǶ 1a+h9;!BjFU P6F 35`ı>. [s~щ})P a#pK)mŲBez'ʜ%DgmR׍ܜ/Ta0\ȊkfbZN7v_rOo/:_j,٢oLج2/N':ۚ e=K,|Ӛ=b%M"jEܦ^jdFU3.V[YW@}.ofzpr}$[* e/ч|z'q~?}mr2U1[\ Vu̽Q4yhk~[y,Ĩ88*i14mQE2$c$˃~^ܨflrxtXF{ٽ6!Z{(ߚHMC:Skvba8t!DDS 6F:|< ;/C@"e6@cȹ3k)5@Mo.67i 6#Ԁ6c@k1gL,yTob8ofW4"tf1H*CKgvTr#@ 5l,~>M5;xo!eonѴ:/*SAUBre5uXG3DYϗa{ü7 %` )D>uPIQTOzUn\$mUuj/B-o ˹ZGbGw_՟41&Nƃc$v% !$g݄˼-m^xb pZ J.JZb[GV;46Š&3fGAEɌ  ۘU Ѱ3GhRB$l@RJ y[t!0Qt"R$fcY$ :D(D*o}|_N_6r%D&+1U])[Ĩx\yu"(bR0{oLfoOqo?EtX4\䋍pDPdv64ubDbf60SL< ^\u׮??*Ãpʃw~ܜP?s7ދ8W6/+as8eEI?l:Xפ1LI}Y(I|j!|8p:{vjV喲 iul5[j0yV A&o鞱E"Sv\69&:~lHo7%4C- $ZA8cpF`!9 TqEIM5['<˴4Z{ǧށ.,dYddk() r^]<)̪lޑyVf  @c u3{6䏌|E L[M"*%@E$ M7 V1DAB$yB}wzZLɂ"P7 bJ. ݐFFH &Q j ͏O,k7?7w_:~k ƓI,萯BWzt! oYkҲۤWow(zNƜMNa>Jt;Yda70 M0[63ͦj\EAL$Ihg˖#`Ԛl+]|ƍ׫e*U(l4N[r.;,JkSRePɽßUڔe?te2gPɉI &MBg.9gbe@2qҤuJc6&ls<>덨K2c躩EdZJ+"'?y}eYv{dxn*GL1R KΓH⧞y۶]Ԡw)S<Q%*Lí;6X:0+$iYl|mk@NNSJB@ܲ!|i6Mwoj?G'O9u9DkMӴaHLmY [յ"S\7-j,[ȇ:_zzɟo◼쬖K0~\|;^?\o N'ֳ+27$nL{3뚌`RXDImӴ\ WA|6[rVm`Am-XV]A1fo{W>[\<:2gYYmN]-|7;ɦ?5C`bEQ@ǶNfF6j4U7艋Nbw=WA( w|9xv" MӪ&qT-ZߢcTu]W_#cSeж O"깮#hwufP$@L[9::j+UEbU;3묨"S ra00Jff[ϮCk-A QM"j\K:{.{$ަTU@Q!PW5r Y1,ofqg?G/} %#iaR=|ڢ1Y;W :Ød* v&=TmIY0od`Z1=JA1+U蘟ID G\>mFmr*ezVYWn#6&EEis!֓_YcD+`-FhӬm M9'7?xWa`vZ>x&  ,&I<80"@T 9L$Ή,߹!#W* 4I6IOB ̦on`,B^Fi)t5xԬŰ5VTH&/s"h}RI""d""5(r˜eYoo}ϽQ_\j.6-b!I BԈl&eȊ$ kWgwK_۳Ϸǿ-H૘\v-߽Ўt_fD :靣wd|T?ǃ~Q6f<ml.rctPb·>X`qƦ H!yUlC B&b*B/trtqZ7& **6c&%pA?|j|Fش&3ҦB ] Rg٥[kH 03蹟Yt=a7M49IA}5)dbbIƀL,I5hJ~W^|4#gdYc"BRmpo4:5"#-"l}{B}uxlx}N'J IDATI/Y&iUu0|d Z&5s#ꃌN yBQ?}><=%{=B=^[ӝnJ$Sf ԦĆ'U}ϔ-7խ BGsvKW'婯ym)3K^Noiݭ ݟ1?9J8It,S'HZLSEicւ* $IxW<-*E-hr$[Wn>(M*?Y@&6!@hVu6HjgԸ_|9ͻyCo֖ttyvu%>"dS=Qs7 APg8woլȸXF.WTQS `8̆krBJ@մ\lɆQ֢ IC4B 9DJ$t"\ޯ/_{i˵uۃ~\" S8Τ s׳vΕ&^9gNkՆ@!$wXI=m>Bg;ca]iѓ%n!]xpvûHS7@(b j"\2ݿ}uŤ d:ڒbz_Fsr?\צ;OmYHzu7+i멗!}a3^m}df^/~R?ƃZo-߹8_Yy>cH2 >L_520%Ab"T-˻d=M$fׯ21u4Hi2QN<./!?qLm d 1 ") M zAN!a Yf2JH"bdf3'ot<=.|X>?U<&U1UCʊڙ&T!pef4ߞ.>ه_\rbۦnze_@RL(봺2Ng)l@"\-d |yEHZx-G/7?_kOdteW\<x}SK6I$Ar }W&Ady1dMzA )ZHfiXH&d&L2R\Rk-3@ID!9s/!rw< Ry)Ïv.ܟ|t>~.[_wd4mQ} 炯P66ɋ-oAYr_O^qw</<|`e>{RWSﴛRӸO.(;/}8دESشg^9~O? ={xw;_fv}n@Y ~ǹ9$ 4_㇥$ewdgY}8sO6G_qT\O<^\2@taw'9)jvK'_wŴHUO> >ޜݛ0wYU~Ҕ9S?T ̊z§Vߞ6hIۋn kڥ.LG:b*`Js9ٙN T TY@99E G5 rg+0OuI~Z3|Rx}ѣo;'v-FK5]Y<ݘ."QPpXju':jl؝"ӣ`YD9w:Ͼ]\C)} [vN&x3;T_^+j+0-{ "@>;n'CPt}w͗ӥs{uf_0جVyTԦ~9Mٜݹd \Yl!v8^=' hpvB]y8 ru߹VGӝ^3"pmu^?:;ꃯbQٙL&?x~{z:KTz¬ԇ;}-*-gͦoCudoЀ19T9R>F4O䂠1p "=ӝO>>_xW2W=1l @ֹʲ$013l`j hm+Ք/~]IЌ|$<9;6.Mq[~O7n9/ܞ/FSJMÐA*03d7PwPksNaj3W΅2lN waqMOh'⮗m9jk[kbrΣѱ}0 Ԅ]͍O}3?΁щɢH N;kv5n))Ӿ@5[l*tfvs\jzݝOIOf7ŵ݃Yj{%x]ʒ8]py5mV`J~󃏇 .]p1}NE~#aAd 'ۋ}Зz/}{g9iyll;8 հ@nuƅv_{gS?ޜO܌"5y/DC49Z/۽p?YaS%+U+"5Ke0~߻vM=(E Ygy4 ޾'z5Q^[ʍ{ꦎB) 13g̞9,9ȭ|RS@ٽrޜOgզ[rfWpR-ГI>}b@@,RM4ވcidhӅؐP3j Y RNh)(\_0g6P@ߞ8|sOC]-bR$ۆejO?nKF{K g7W;fNG?_N@;Bs8qҀf6_C!JР=Ԯ-L3nvI"8뎸{pw7/D@҆aR8d11FA\ LhJ Q6ge^"eմ?_W}__QUixOj]3=bHŝ.nU8߮c=S,B6|g`ں.Iny~q2?x[/_z&6OG>ͲppQgv;k_5p͵׾ߎ{}Jå6YRz@j'!#!a;̧u}^.UͣdzIz(NDs bG-mS`Tަtp2U<>;]@doո.C0HT&aYv.wOI)ۂ{}1 Z ;GRlDl*n4e+-1!R.jn\fKh2ܪN;O.N<jX,xB.9Rf #6 v9o>ʛuxge_#wn/?}ع˭+FѮJ^tqvq.hRu,9#CénSHMqq,s,%!bGܥs4##rdY?Wo[ ߜ||-vU@|Wm?>×_/_Q3\_tl~j(W:/F8]^hZ $%&DRi9!ͦždHC33 w_<(_okװ AEۙ}A;/rJ&O#8#Y1w߈ /~ڟƇ~LtbgvqgƫI[Z_kf]8?.=yv{vz&1CUQJ^J*\R1UlxwsLryxmWrgPOCUi9b@dRq $bw=TU@R2d$1]Y66z-tTWE4#}PLsFƚ2PaQ":?;c@#:n2][!qf os\ʝ߹=!mNN.6x$'y|_1W0}89S1z뛶EDY^;ߩ*z-ү3s٠y88;`/2"4|x,PF45'ZD *0 \2Z*Yyt4y|Vɟ/l_E;~8I%BqTD6!"F80Pى.h\ܿf%}2O~,1xj<5?sEw4C, .f&Z6륙sJDaRՓɤ6ۮ{r|ؽz 4=*ҺHDz:C38f7:$`g4U1ۖި1Rtg ^bFxbD(fR;&cݿ윃"*b`PMi`XLsdf4^w9gBD@! = 5x>?:tMZg'C./ Cu`/,u*_,||t|b?!׊(fi$v}mc->{?x}7ҍY|'̧=aȥumf+$އ@Ωj)λfcPիM;_#^:c d`U!uk)}Zvӗ?'o}< ȫ6V#yܺAHʇsT.>X #32ۂȀB@ђ3T1Y7o?sDh|y$@ @R$*@33Gj(:&{4*'ٽ /0tvYXᲤ 5%k0ᵛ0 . r އO_{׾^wpZ] %gv|eq|.R%Ī y2s@;_r"&&fOhVL85Bi©WХr .VG@ת(rX I@ĖJ*qö$|<>$ntjCP1+# kS0[3ƦgWʟ JO\ :v&1ijGih'& 0,f)"P2s!;h8Ξ1k"OƸ;瘹mW.3_DDھoOjɊq#¢*Yg L^=`JpuBuess)e0:U@D U_zz7vO|mϖ^s,H@Ed0(;]/T _̱Sڍ\Wʆ@bWǘ;oIurBQu}p.'Mœ"F` `$xNI̔ 'y[ $Vj (,T( !FM G|28!EBӒFĪ.8i&cR/3$R[7?z6W^/ U9'}ˀu \vh?6''ٟ-C(]}HW';#{?&Mr2R%KL"bޝxd_]\\AW}_݁Se<j9x"csAb4%YFڕ|]ϞQ+]]_⯄BW_]bf Oc'퐻"mWCJ3ST3**Fܶy\ DKe1hoCvj19l|n$_'Ğ/Erdc!BC M*.ǐю*v6N}9FJ$]@ϵqK% SIDAT-뇳뷘\e68RhUx)98v~abfSJ[fa^k.o[`s]>xwԠzR )]uT hX${6JSQ4B9[ߛ<$#4fbo󶷺^ ?[şeRȗ2gOVEFOzeLoLOatRFx]@G]m㒈s\Ϫ:͟OΚb7}SGܥL.ή_zxq`K9SL c%2D]_?Xo'FWrr)`y{᪪r`tVcʉv7M ;)_ˆG1Y+IYI2eTR(0x3F&yzR+Htfbsp:z!oҗ+oE\T͇)KƦdD";fL3ip4߈}Dw=:i0u=3m+"!Wi9;7&?ڡ}K,9{YL.:ix4})#+4!#A,d3,24_44Esck?c5˱U]zxDHMtyzWﷆB(ACWr9% M Uaq ,"ϔϟ<# ]͌&"b9cؗ<Blu=AaBǛ󝲩{v A Dѓ#ȳ6Ib^0b.Vp6^ojy jU Dsɕ  < !?10:U`d$(fb@Nͤ\!$]lP%#9B{{G'w&r|~zTŪݶ DL̜sBj\y'cD9b=v*{@M rg7*~BAbF* Qə}Q;s8"BvڕLHfdM5 ۮgq18c8󻚟z)Ru:FղY0>뭟/v\1.;gϚo~Of j#] SCnvMTXtt=fAImbw,U`0HC4R)ONdR.CCfV1`C!X#d6NJ !\+9!6F#AP[S]^/_}'VEߏ| gI`ywcv Y{#QA=3)Mw"Qs8f6K)gjTѳ͡煐L/iG}!~8CgL̀ɕT~hnA7[U>9e>JcUr~* qաD&~FC0NsD̷Uz#ȥ0݋qj<$ ^X)\ۯ}\j.CD5󧳪iCh*!6qg>rU@&\fr1P#"!8Ϊ5 fJ)PI)WS.1BT/jPe"A |jk|#)mBfuxuO&ﺎiӶ`0Lǭm-"@#$rqiƎEdL[!1|X,K).cc|wKnNڝlA +UDPCCψ zzֈR w=Λ^wnṛn{3Uɥ]@D K"V3@gb<*9+ "E\gnRSg3{*E.mYsK4gZ:#Dd8OǺ@Z]I`ڴ|V<=ܿhy}1Ṭ )% .}멖BM^^sڵ8@NE&XOD +PTDyON$بf kBS A!12IlV6n7|+xrUuUN fPjfhۖ(gAd@ج&$垐CU9Km=3HE@ݬ7lG.56y"AB)EQfqw hl^\ lwOg{k_{(bHV 8$tu_m 2h ׍ID [M@;/.P],RDAk:`\-[.vr%.Lx\07D>;~އ;\h]j `VpgWQ!Y)A)9q!7CE?Zrɛ9Ra6~ ۜq:fX)/*7 3_&;IrΡTqw%8M4&CJQ!sazz<s j8:K6 /_Ynw?Zo/\}@č$2EѰkrMh&&bEsX0tk@d3R8+ R߳{j<9%CT5U&rmn] :QkIENDB`swayimg-3.8/extra/icon_64.png000066400000000000000000000217421474536441700161630ustar00rootroot00000000000000PNG  IHDR@@% gAMA asRGB, cHRMz&u0`:pQ< IDATh5uZӟ=93Ǟv\DZ38!$1 IL"(ѿBA |B ȇ(XI86dgfNo/O~epO[{ߺu]7|-nkUwVl: 2 㑯k iyY6Y#Ks]aKɟ4ϟͲF1+ݱ3P{s/$i&5`\x17I 1D֝M1\`^ b]@ة.,bpLSH*Ͳ bct3ςq63N%DsFR#er4n4NOϥ~UWzѵߞwuW-JuHARM4#FT땄1jQ@E$M&"*+BVu,/FUYB1%bK}VWxl>~~ʉ{ {}ޙc=L1U9 |pi|4\9̛k>.v*&!Iƈ0B"IBDP@4jDp>(ڨrx8 jJ;!YXb<+5Ƨ^붶SS5ZHPZ(Zyթ"JX`ōY|]wNwͷn)NΛ/\Itvbm !WLb"e4 r &@t!fZw;FKd2$yfg/ iuP'ޕf/u·%P)A1;"DxkcyguaR b[]Kpܫao_^sFٚ9E@@:0z05"95TQ'FX\I/i$آlVF](LZCRUDP g ;3j4;ny-v[~?~lo^ŋg{9nC$Q%FH+-kD%AX|1ftڨGAlv6v`mWLeF+ .ܞdSVwo#o?/li_޻Nye\婳E#&) Ԛ#1`њs2͛rP#5,hݩP}:*Xs`ަ6 &::O3֎w;+し76'#]*Y':k뽓W ̦bR|}֐PbDFY{Mfev 04y P,\U0:XF/#z}PIX# ( $ cٱZF{G[{gߟ~m_ιYse{](=A7^*cƼӁd;~֟=y* @ "iEZ(_r oIj"`%!%STZ' :W7 \󏏖w|}щD$O&l')F^׼uqe Tutl7d0|畯}ڴQ* T) TK" j/'>u%ߌI,T͔)"zTB25Vz#z.X.>bDZI}Zy.htN+^vF˯|޵gy'¯mg*rO}5w(k͙+*zV 1gPZ9 F"e\`ԁ){re=}%GRj "QI&v9 xOm$8;NL/_RL[(ZLdVb@ňsPb^! *(b"0VPJPP3F?Mi;@}@ `fi D4ʽG~!~E0Ғ G/oIb>ALP0cQg!@ OY@)D#f @D\0nOQ%Qb rKbx8Iߩ8pjN UVV"Hh͑h} iy@O r]Զ&T 9X:?r["&({[BkGv/?S%؟ɕCWnđN=oM`4ͭ(MnJ]̄`QD%O (R#f}sp{۴vPewyl,&j IuPON Ϧv:bG˜E(*ӑ-VBIP r)03)4Z)#s1+YYA* WdW u70zgd3}?IWq/Iͣ9ߞo\^;k،.DBRS.\3J Lm~@|!MD"0"$!4,3;e*We) !d=pT(SwNʬܻ{E2=]jm߼+W.O/=7޵-t_3|WfJ@ѱȤ5+#3R5T(n9xUv:2;s?F;+ڙw*KA-1zA@"ɼ.$MJ@! i"/֘2'x6X{K2k6&J3k$t ,("FH FOA$jaOvT,:ȪiV2xbFkv A8FL iali%K,s*rFG֚^xex(-GGgamdU" :E@kR2$jVjoF_3!@Q"!( KP1w0s y#_ƚ!4 g9˪7&Yd~0],֝`09v;pe@Ln(fkV*kIs'zhr=ͬi e^!QM׉a hU(aTրG ! k[fP-thZUCiNFK}hlƨk/WۮZ>cƨ՘h1Qۘl>O~)K DP} g*,}n_kmEG ,rp!koQTb)t\^ ʚH)'nGתϱ8׼]s WuT282k+ [Wxe"'=5VA\"[5ѤtS' "@cQ/_Y\}"9>R+xq/vAm,caZ.<֪#V6|bz{4.|M,$ @J@Di,DpXL"Yp㭉|T[a!sd 13s <4=Je@RdȃCs7EhRc2oOCkd_Y=U4*SDA@fp(WFV*447Kߚ$qj@EA$8_Uܸ \Y2i1~rpK޼vW~ >k/rugAޤY~bj67Rc(e> N,TJ"3`p*!T(rT2e=Kj"Y8 "Vh 8  KgCö)-#ټ<Ȇ)l>|6kۇao¹p;cO<]#>ێpZGalYJa=6 qE*f).6bDNF)IvyF]GA lc؊0Oۧv.|doM箚qPۤ6`4}pѵʡr?a_:\*3hSNJC ǿfqr΁YI9O}L>ϏfK\'6uXBU_ӣ{BCuZGNڿ"i6fΐ=L",<[ $hLClL[,% N> 1:C/n aJ#@",@5NŅ?ī֠ht}B_WіNnسKYQM}ѥSLHB9mg.1ZHD}p5$n x! ) AMO|r" 53!" "_V Zq .JY6Nή<&l\MZVӽk]JZ[R⣔Cw>tq7[< - m,XzI.]2*_a Xk(=q4 c*d q<*FCЦW4 Z"*k{2R_qWu:6zޠ»q4&Y-nR-Z..PJTǿo蒌e5˵?rg>o(++2DL(S\u<-$Ji:-ʪ,JWtãO?7~K1^Lh l)Y{Cp¼^٣9iX8hG˘_^u77YlIV|4_Nmތ It/(* ֵsq3x.|C藪cIb֒+}Q,i5{g&HN;g wtamy>R^8s <Ok6meg.ww2 $W֙ CJ6L&" c& dEe-(Jwh2sj?}J_]pPg%Mŋ<|>;PN0jӔ]U)\]B , bJ)DJ$!(iG7yN'7uz ~t޲|J,c'S><֞ 9WC33Zi,;?qb/娜[O;D׹MoH]N„*t~Ŧ1F0ЂTEpR(1[qBY+-b`o PU|[׿7/k.W؎.ˈuF0MEښߺz7K{'isxDk/ihEӜu("X% [sHe<fT]zUniI|iQib+_-tt78Oh1c=lcl”kM$s" jP?atkq9V&%< [Z>k6I@>ܟQh<&?YXԸ)g*P4"(-o[p|GҭQ-@!҂``K ‰oB (Qd/y7SsC&'_#02oO\3 Fk7]AtG5D 8_S8|}|uPKo=?zqQ6<#*1(Ma6m}7@y֍!EDH)@|y捣MMdUB7@;uddpi3YQX{7|'%,˵N@ RqUNZL7nN7>ѽxQl.>d'BlT(D.P%A!1sOFoَI RP ^ 2`u/P@d`C@mTY{x曆}$LJ\DN%W:Zw9iǫR~f9l3-gQ 9}w{C){C쩜wBhyieQF\H3D%M4{F"#v+陰Ӗv=#bSg϶o7IB> DN0p)J4Nu=p~cjHg];of_~;mұk9hz-[TZ(:HldBN)Q'2VFTIDATab$dDx_;KZ%" P}ij3f1D\S,>i+IfFjcE!܋5凯_`UNX!AH\RcZi.oh 9&I Y:>1(e^h[-EIENDB`swayimg-3.8/extra/swayimg.1000066400000000000000000000151761474536441700157620ustar00rootroot00000000000000.\" Swayimg: image viewer for Sway/Wayland .\" Copyright (C) 2021 Artem Senichev .TH SWAYIMG 1 2021-12-28 swayimg "Swayimg manual" .SH "NAME" swayimg \- lightweight image viewer for Wayland display servers .SH "SYNOPSIS" swayimg [\fIOPTIONS\fR]... \fI[FILE]...\fR .\" **************************************************************************** .\" Description .\" **************************************************************************** .SH "DESCRIPTION" If no input files or directories are specified, the viewer will try to read all files in the current directory. .PP By default, the application generates a list of images with all files in the same directory as the image being loaded. This behavior can be changed with the \fBlist.all\fR parameter in the config file. .PP Use '-' as \fIFILE\fR to read image data from stdin. .PP Use prefix 'exec://' to get image data from stdout printed by external command. .\" **************************************************************************** .\" Options .\" **************************************************************************** .SH "OPTIONS" Mandatory arguments to long options are mandatory for short options too. .\" ---------------------------------------------------------------------------- .IP "\fB\-h\fR, \fB\-\-help\fR" Display help message. .\" ---------------------------------------------------------------------------- .IP "\fB\-v\fR, \fB\-\-version\fR" Display version information and list of supported image formats. .\" ---------------------------------------------------------------------------- .IP "\fB\-g\fR, \fB\-\-gallery\fR" Start in gallery mode. .\" ---------------------------------------------------------------------------- .IP "\fB\-r\fR, \fB\-\-recursive\fR" Read directories recursively. .\" ---------------------------------------------------------------------------- .IP "\fB\-o\fR, \fB\-\-order\fR=\fIORDER\fR:" Set order of the image list: .nf \fInone\fR: unsorted, order is system depended; \fIalpha\fR: sorted alphabetically (default); \fIreverse\fR: reversed alphabetically; \fIrandom\fR: randomize list. .\" ---------------------------------------------------------------------------- .IP "\fB\-s\fR, \fB\-\-scale\fR=\fIMODE\fR" Set the default image scale, valid modes are: .nf \fIoptimal\fR: 100% or less to fit to window (default); \fIwidth\fR: fit image width to window width; \fIheight\fR: fit image height to window height; \fIfit\fR: fit to window; \fIfill\fR: crop image to fill the window; \fIreal\fR: real size (100%). .\" ---------------------------------------------------------------------------- .IP "\fB\-l\fR, \fB\-\-slideshow\fR" Run slideshow mode on startup. .\" ---------------------------------------------------------------------------- .IP "\fB\-f\fR, \fB\-\-fullscreen\fR" Start in full screen mode. .\" ---------------------------------------------------------------------------- .IP "\fB\-p\fR, \fB\-\-position\fR=\fIPOS\fR" Set initial position of the window (Sway only): .nf \fIparent\fR: set position from parent (currently active) window (default); \fIX,Y\fR: absolute coordinates of the top left corner. .\" ---------------------------------------------------------------------------- .IP "\fB\-w\fR, \fB\-\-size\fR=\fISIZE\fR" Set initial size of the window: .nf \fIparent\fR: set size from parent (currently active) window (Sway only, default); \fIimage\fR: set size from the first loaded image; \fIWIDTH,HEIGHT\fR: absolute size of the window in pixels. .\" ---------------------------------------------------------------------------- .IP "\fB\-a\fR, \fB\-\-class\fR=\fINAME\fR" Set a constant window class/app_id. .\" ---------------------------------------------------------------------------- .IP "\fB\-c\fR, \fB\-\-config\fR=\fISECTION.KEY=VALUE\fR" Set a configuration parameter, see swayimgrc(5) for a list of sections and its parameters. .\" **************************************************************************** .\" SWAY integration .\" **************************************************************************** .SH "SWAY MODE" The Sway compatible mode is automatically enabled if the environment variable \fISWAYSOCK\fR points to a valid Sway IPC socket file. This mode provides some features such as setting the window position and getting the workspace layout. By default, the application creates an "overlay" above the currently active window, which gives the illusion that the image is opened directly inside the terminal window. .\" **************************************************************************** .\" Environment variables .\" **************************************************************************** .SH "ENVIRONMENT" .IP \fISWAYSOCK\fR Path to the socket file used for Sway IPC. .IP "\fIXDG_CONFIG_HOME\fR, \fIXDG_CONFIG_DIRS\fR, \fIHOME\fR" Prefix of the path to the application config file. .IP "\fISHELL\fR" Shell for executing an external command and loading an image from stdout. .\" **************************************************************************** .\" Signals .\" **************************************************************************** .SH "SIGNALS" .IP \fISIGUSR1\fR Perform the actions specified in the config file, \fIreload\fR by default. .IP \fISIGUSR2\fR Perform the actions specified in the config file, \fInext_file\fR by default. .\" **************************************************************************** .\" Exit status .\" **************************************************************************** .SH "EXIT STATUS" The exit status is 0 if the program completed successfully and 1 if an error occurred. .\" **************************************************************************** .\" Examples .\" **************************************************************************** .SH "EXAMPLES" .PP swayimg photo.jpg logo.png .RS 4 View multiple files. .RE .PP swayimg --slideshow --recursive --order=random .RS 4 Start slideshow for all files (recursively) in the current directory in random order. .RE .PP wget -qO- https://www.kernel.org/theme/images/logos/tux.png | swayimg - .RS 4 View using pipes. .RE .PP swayimg "exec://wget -qO- https://www.kernel.org/theme/images/logos/tux.png" .RS 4 Loading stdout from external commands. .RE .\" **************************************************************************** .\" Cross links .\" **************************************************************************** .SH SEE ALSO swayimgrc(5) .\" **************************************************************************** .\" Home page .\" **************************************************************************** .SH BUGS For suggestions, comments, bug reports etc. visit the .UR https://github.com/artemsen/swayimg project homepage .UE . swayimg-3.8/extra/swayimg.desktop000066400000000000000000000010021474536441700172520ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Swayimg GenericName=Image viewer Comment=Image viewer for Sway/Wayland Icon=swayimg Exec=swayimg %F Terminal=false Categories=Graphics;Viewer StartupNotify=false MimeType=image/avif;image/bmp;image/gif;image/heif;image/jpeg;image/jpg;image/pbm;image/pjpeg;image/png;image/svg+xml;image/tiff;image/webp;image/x-bmp;image/x-exr;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-targa;image/x-tga NoDisplay=true swayimg-3.8/extra/swayimgrc000066400000000000000000000142401474536441700161370ustar00rootroot00000000000000# Swayimg configuration file. # vim: filetype=dosini # This file contains the default configuration. # The viewer searches for the config file in the following locations: # 1. $XDG_CONFIG_HOME/swayimg/config # 2. $HOME/.config/swayimg/config # 3. $XDG_CONFIG_DIRS/swayimg/config # 4. /etc/xdg/swayimg/config # Any of these options can be overridden using the --config argument # on the command line, for instance: # $ swayimg --config="general.mode=gallery" ################################################################################ # General configuration ################################################################################ [general] # Mode at startup (viewer/gallery) mode = viewer # Window position (parent or absolute coordinates, e.g. 100,200) position = parent # Window size (fullscreen/parent/image, or absolute size, e.g. 800,600) size = parent # Use window decoration (yes/no) decoration = no # Action performed by SIGUSR1 signal (same format as for key bindings) sigusr1 = reload # Action performed by SIGUSR2 signal (same format as for key bindings) sigusr2 = next_file # Application ID and window class name app_id = swayimg ################################################################################ # Viewer mode configuration ################################################################################ [viewer] # Window background color (RGBA) window = #00000000 # Background for transparent images (grid/RGBA) transparency = grid # Default image scale (optimal/fit/width/height/fill/real) scale = optimal # Keep absolute zoom across images (yes/no) keep_zoom = no # Initial image position position = center # Fix position of the image on the window surface (yes/no) fixed = yes # Anti-aliasing mode (none/box/bilinear/bicubic/mks13) antialiasing = mks13 # Run slideshow at startup (yes/no) slideshow = no # Slideshow image display time (seconds) slideshow_time = 3 # Number of previously viewed images to store in cache history = 1 # Number of preloaded images (read ahead) preload = 1 ################################################################################ # Gallery mode configuration ################################################################################ [gallery] # Size of the thumbnail (pixels) size = 200 # Max number of thumbnails in memory cache, 0 for unlimited cache = 100 # Enable/disable storing thumbnails in persistent storage (yes/no) pstore = no # Fill the entire tile with thumbnail (yes/no) fill = yes # Anti-aliasing mode for thumbnails (none/box/bilinear/bicubic/mks13) antialiasing = mks13 # Background color of the window (RGBA) window = #00000000 # Background color of the tile (RGBA) background = #202020ff # Background color of the selected tile (RGBA) select = #404040ff # Border color of the selected tile (RGBA) border = #000000ff # Shadow color of the selected tile (RGBA) shadow = #000000ff ################################################################################ # Image list configuration ################################################################################ [list] # Default order (none/alpha/reverse/random) order = alpha # Looping list of images (yes/no) loop = yes # Read directories recursively (yes/no) recursive = no # Open all files in the directory of the specified file (yes/no) all = no ################################################################################ # Font configuration ################################################################################ [font] # Font name name = monospace # Font size (pt) size = 14 # Font color (RGBA) color = #ccccccff # Shadow color (RGBA) shadow = #000000d0 # Background color (RGBA) background = #00000000 ################################################################################ # Image meta info scheme (format, size, EXIF, etc) ################################################################################ [info] # Show on startup (yes/no) show = yes # Timeout to hide info (seconds, 0 to always show) info_timeout = 5 # Timeout to hide status message (seconds) status_timeout = 3 # Display scheme for viewer mode (position = content) [info.viewer] top_left = +name,+format,+filesize,+imagesize,+exif top_right = index bottom_left = scale,frame bottom_right = status # Display scheme for gallery mode (position = content) [info.gallery] top_left = none top_right = none bottom_left = none bottom_right = name,status ################################################################################ # Viewer mode key binding configuration: key = action [parameters] ################################################################################ [keys.viewer] F1 = help Home = first_file End = last_file Prior = prev_file Next = next_file Space = next_file Shift+r = rand_file Shift+d = prev_dir d = next_dir Shift+o = prev_frame o = next_frame c = skip_file Shift+s = slideshow s = animation f = fullscreen Return = mode Left = step_left 10 Right = step_right 10 Up = step_up 10 Down = step_down 10 Equal = zoom +10 Plus = zoom +10 Minus = zoom -10 w = zoom width Shift+w = zoom height z = zoom fit Shift+z = zoom fill 0 = zoom real BackSpace = zoom optimal Alt+s = scale Alt+z = keep_zoom bracketleft = rotate_left bracketright = rotate_right m = flip_vertical Shift+m = flip_horizontal a = antialiasing r = reload i = info Shift+Delete = exec rm "%"; skip_file Escape = exit q = exit # Mouse related ScrollLeft = step_right 5 ScrollRight = step_left 5 ScrollUp = step_up 5 ScrollDown = step_down 5 Ctrl+ScrollUp = zoom +10 Ctrl+ScrollDown = zoom -10 Shift+ScrollUp = prev_file Shift+ScrollDown = next_file Alt+ScrollUp = prev_frame Alt+ScrollDown = next_frame ################################################################################ # Gallery mode key binding configuration: key = action [parameters] ################################################################################ [keys.gallery] F1 = help Home = first_file End = last_file Left = step_left Right = step_right Up = step_up Down = step_down Prior = page_up Next = page_down c = skip_file f = fullscreen Return = mode a = antialiasing r = reload i = info Shift+Delete = exec rm "%"; skip_file Escape = exit q = exit # Mouse related ScrollLeft = step_right ScrollRight = step_left ScrollUp = step_up ScrollDown = step_down swayimg-3.8/extra/swayimgrc.5000066400000000000000000000456511474536441700163140ustar00rootroot00000000000000.\" Swayimg configuration file format. .\" Copyright (C) 2022 Artem Senichev .TH SWAYIMGRC 5 2022-02-09 swayimg "Swayimg configuration" .SH "NAME" swayimgrc \- configuration file for the Swayimg viewer .SH "SYNOPSIS" The Swayimg configuration file is a text-based INI file used to override the default settings. .\" **************************************************************************** .\" Config file location .\" **************************************************************************** .SH "LOCATION" Swayimg searches for a config file in the following locations, in this order: .nf \- $XDG_CONFIG_HOME/swayimg/config \- $HOME/.config/swayimg/config \- $XDG_CONFIG_DIRS/swayimg/config \- /etc/xdg/swayimg/config .\" **************************************************************************** .\" Format description .\" **************************************************************************** .SH "DESCRIPTION" The structure of the INI file consists of key-value pairs for properties and sections that organize properties. .PP The basic element contained in the INI file is the key or property. Every key has a name and a value, delimited by an equals sign (=). .PP The name appears to the left of the equals sign. The value can contain any characters. .PP Keys are grouped into named sections. The section name appears on a line by itself, in square brackets ([ and ]). All keys after the section declaration are associated with that section. .PP The number sign (#) at the beginning of the line indicates a comment. Empty lines and comments are ignored. .PP Any option can be overridden using the \fI--config\fR argument in the command line, for instance: `swayimg --config="general.mode=gallery"`. .\" **************************************************************************** .\" General config section .\" **************************************************************************** .SH "SECTIONS" .SS "General" The general configuration is stored in the section \fB[general]\fR. .\" ---------------------------------------------------------------------------- .IP "\fBmode\fR = \fI[viewer|gallery]\fR" Mode used at startup, \fIviewer\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBposition\fR = \fI[parent|X,Y]\fR" Set initial position of the window (Sway only): .nf \fIparent\fR: set position from parent (currently active) window (default); \fIX,Y\fR: absolute coordinates of the top left corner, e.g. 100,200. .\" ---------------------------------------------------------------------------- .IP "\fBsize\fR = \fI[fullscreen|parent|image|W,H]\fR" Set initial size of the window: .nf \fIfullscreen\fR: use fullscreen mode; \fIparent\fR: set size from parent (currently active) window (Sway only, default); \fIimage\fR: set size from the first loaded image; \fIW,H\fR: absolute size of the window in pixels. .\" ---------------------------------------------------------------------------- .IP "\fBdecoration\fR\fR = \fI[yes|no]\fR" Use window decoration (borders and title), \fIno\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBsigusr1\fR = \fIACTION\fR" Set the action to be performed when the SIGUSR1 signal is triggered. Default value is \fIreload\fR. .IP "\fBsigusr2\fR = \fIACTION\fR" Set the action to be performed when the SIGUSR2 signal is triggered. Default value is \fInext_file\fR. .\" ---------------------------------------------------------------------------- .IP "\fBapp_id\fR = \fINAME\fR" Application ID used as window class name. .\" **************************************************************************** .\" Viewer config section .\" **************************************************************************** .SS "Viewer" Configuration is defined in the \fB[viewer]\fR section. .\" ---------------------------------------------------------------------------- .IP "\fBwindow\fR = \fI#COLOR\fR" Window background color in RGB or RGBA format, default is \fI#00000000\fR. .\" ---------------------------------------------------------------------------- .IP "\fBtransparency\fR = \fI[grid|#COLOR]\fR" Background for transparent images: .nf \fIgrid\fR: draw chessboard (default); \fI#COLOR\fR: solid color in RGB or RGBA, e.g "#10ff4280". .\" ---------------------------------------------------------------------------- .IP "\fBscale\fR = \fIMODE\fR" Default image scale, valid modes are: .nf \fIoptimal\fR: 100% or less to fit to window (default); \fIwidth\fR: fit image width to window width; \fIheight\fR: fit image height to window height; \fIfit\fR: fit to window; \fIfill\fR: crop image to fill the window; \fIreal\fR: real size (100%). .\" ---------------------------------------------------------------------------- .IP "\fBkeep_zoom\fR\fR = \fI[yes|no]\fR" Keep absolute zoom across images, \fIno\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBposition\fR = \fIPOSITION\fR" Initial image position, valid modes are: .nf \fItop\fR: move image to top and center by width; \fIcenter\fR: center by width and height (default); \fIbottom\fR: move to bottom and center by width; \fIleft\fR: move to left and center by height; \fIright\fR: move to right and center by height; \fItopleft\fR: move to top and left; \fItopright\fR: move to top and right; \fIbottomleft\fR: move to bottom and left; \fIbottomright\fR: move to bottom and right. .\" ---------------------------------------------------------------------------- .IP "\fBfixed\fR = \fI[yes|no]\fR" Fix position of the image on the window surface, \fIyes\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBantialiasing\fR = \fIMETHOD\fR" Scale method to use when anti-aliasing is enabled, valid choices are: .nf \fInearest\fR: nearest-neighbor, equivalent to antialiasing = no; \fIbox\fR: nearest-neighbor on upscale, average in a box on downscale; \fIbilinear\fR: bilinear; \fIbicubic\fR: bicubic with the Catmull-Rom spline; \fImks13\fR: Magic Kernel with the 2013 Sharp approximation (default); .nf In general, the methods improve in quality and decrease in performance from top to bottom. .\" ---------------------------------------------------------------------------- .IP "\fBslideshow\fR = \fI[yes|no]\fR" Run slideshow at startup, \fIno\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBslideshow_time\fR = \fISECONDS\fR" Set slideshow image duration in seconds, default is \fI3\fR. .\" ---------------------------------------------------------------------------- .IP "\fBhistory\fR = \fISIZE\fR" Number of previously viewed images to store in cache, \fI1\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBpreload\fR = \fISIZE\fR" Number of images to preload in a separate thread, \fI1\fR by default. .\" **************************************************************************** .\" Gallery config section .\" **************************************************************************** .SS "Gallery" Configuration is defined in the \fB[gallery]\fR section. .\" ---------------------------------------------------------------------------- .IP "\fBsize\fR = \fIPIXELS\fR" Size of the thumbnail in pixels, \fI200\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBpstore\fR = \fI[yes|no]\fR" Enable/disable storing thumbnails in persistent storage, \fIno\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBfill\fR = \fI[yes|no]\fR" Fill the entire tile with thumbnail, \fIyes\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBcache\fR = \fISIZE\fR" Max number of thumbnails in cache, \fI0\fR for unlimited, \fI100\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBantialiasing\fR = \fIMETHOD\fR" Scale method to use when anti-aliasing is enabled, valid choices are: .nf \fInearest\fR: nearest-neighbor, equivalent to antialiasing = no; \fIbox\fR: nearest-neighbor on upscale, average in a box on downscale; \fIbilinear\fR: bilinear; \fIbicubic\fR: bicubic with the Catmull-Rom spline; \fImks13\fR: Magic Kernel with the 2013 Sharp approximation (default); .nf In general, the methods improve in quality and decrease in performance from top to bottom. .\" ---------------------------------------------------------------------------- .IP "\fBwindow\fR = \fI#COLOR\fR" Background color of the window, default is \fI#00000000\fR. .\" ---------------------------------------------------------------------------- .IP "\fBbackground\fR = \fI#COLOR\fR" Background color of the tile, default is \fI#202020ff\fR. .\" ---------------------------------------------------------------------------- .IP "\fBborder\fR = \fI#COLOR\fR" Border color of the selected tile, default is \fI#000000ff\fR. .\" ---------------------------------------------------------------------------- .IP "\fBshadow\fR = \fI#COLOR\fR" Shadow color of the selected tile, default is \fI#000000ff\fR. .\" **************************************************************************** .\" Image list config section .\" **************************************************************************** .SS "Image list" The image list configuration is stored in the section \fB[list]\fR. .\" ---------------------------------------------------------------------------- .IP "\fBorder\fR = \fIORDER\fR" Set order of the image list: .nf \fInone\fR: unsorted, order is system depended; \fIalpha\fR: sorted alphabetically (default); \fIreverse\fR: reversed alphabetically; \fIrandom\fR: randomize list. .\" ---------------------------------------------------------------------------- .IP "\fBloop\fR\fR = \fI[yes|no]\fR" Looping file list mode, \fIyes\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBrecursive\fR = \fI[yes|no]\fR" Read directories recursively, \fIno\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBall\fR = \fI[yes|no]\fR" Open all files in the directory of the specified file, \fIno\fR by default. .\" **************************************************************************** .\" Font config section .\" **************************************************************************** .SS "Font" The font configuration is stored in the section \fB[font]\fR. .\" ---------------------------------------------------------------------------- .IP "\fBname\fR\fR = \fINAME\fR" Set the font name used for text, default is \fImonospace\fR. .\" ---------------------------------------------------------------------------- .IP "\fBsize\fR = \fISIZE\fR" Set the font size (in pt), default is \fI14\fR. .\" ---------------------------------------------------------------------------- .IP "\fBcolor\fR = \fI#COLOR\fR" Set text color in RGBA format, default is \fI#ccccccff\fR. .\" ---------------------------------------------------------------------------- .IP "\fBbackground\fR = \fI#COLOR\fR" Text background color, default is \fI#00000000\fR (none). .\" ---------------------------------------------------------------------------- .IP "\fBshadow\fR = \fI#COLOR\fR" Draw text shadow with specified color, default is \fI#000000d0\fR. To disable shadow use fully transparent color \fI#00000000\fR. .\" **************************************************************************** .\" Text info config section .\" **************************************************************************** .SS "Text info: common configuration" The section \fB[info]\fR describes how to display image meta data (file name, size, EXIF etc). .\" ---------------------------------------------------------------------------- .IP "\fBshow\fR = \fI[yes|no]\fR" Enable or disable info text at startup, \fIyes\fR by default. .\" ---------------------------------------------------------------------------- .IP "\fBinfo_timeout\fR = \fISECONDS\fR" Timeout of image information displayed on the screen, 0 to always show, default is \fI5\fR. .\" ---------------------------------------------------------------------------- .IP "\fBstatus_timeout\fR = \fISECONDS\fR" Timeout of the status message displayed on the screen, default is \fI3\fR. .\" ---------------------------------------------------------------------------- .SS "Text info: viewer" The section \fB[info.viewer]\fR describes how to display image meta data (file name, size, EXIF etc) in viewer mode. Each key/value configures a set of fields and their format. .IP "\fBtop_left\fR = \fILIST\fR" Set the display scheme for the upper left corner of the window. .IP "\fBtop_right\fR = \fILIST\fR" Set the display scheme for the upper right corner of the window. .IP "\fBbottom_left\fR = \fILIST\fR" Set the display scheme for the lower left corner of the window. .IP "\fBbottom_right\fR = \fILIST\fR" Set the display scheme for the lower right corner of the window. .PP \fILIST\fR can contain any number of fields separated by commas. Plus at the beginning of a field name adds the field title to the display. Available fields: .IP "\fIname\fR" File name of the currently viewed/selected image. .IP "\fIdir\fR" Parent directory name the currently viewed/selected image. .IP "\fIpath\fR" Absolute path or special source string of the currently viewed/selected image. .IP "\fIfilesize\fR" File size in human readable format. .IP "\fIformat\fR" Brief image format description. .IP "\fIimagesize\fR" Size of the image (or its current frame) in pixels. .IP "\fIexif\fR" List of EXIF data. .IP "\fIframe\fR" Current and total number of frames. .IP "\fIindex\fR" Current and total index of image in the image list. .IP "\fIscale\fR" Current image scale in percent. .IP "\fIstatus\fR" Status message. .IP "\fInone\fR" Empty field (ignored). .\" ---------------------------------------------------------------------------- .SS "Text info: gallery" The section \fB[info.gallery]\fR describes how to display image meta data (file name, size, EXIF etc) in gallery mode, same format as for viewer mode. .\" **************************************************************************** .\" Key bindings config section .\" **************************************************************************** .SS "Key bindings" The key bindings are described in sections \fB[keys.viewer]\fR and \fB[keys.gallery]\fR. Each line associates a key with a list of actions and optional parameters. Actions are separated by semicolons. One or more key modifiers (\fICtrl\fR, \fIAlt\fR, \fIShift\fR) can be specified in the key name. The key name can be obtained with the \fIxkbcli\fR tool: `xkbcli interactive-wayland`. .PP Predefined names for mouse scroll: .PP .IP "\fIScrollUp\fR: Mouse wheel up;" .IP "\fIScrollDown\fR: Mouse wheel down;" .IP "\fIScrollLeft\fR: Mouse scroll left;" .IP "\fIScrollRight\fR: Mouse scroll right." .PP .\" ---------------------------------------------------------------------------- .SS "Viewer mode actions" .IP "\fBnone\fR: can be used for removing built-in action;" .IP "\fBhelp\fR: show/hide help;" .IP "\fBfirst_file\fR: jump to the first file;" .IP "\fBlast_file\fR: jump to the last file;" .IP "\fBprev_dir\fR: jump to previous directory;" .IP "\fBnext_dir\fR: jump to next directory;" .IP "\fBprev_file\fR: jump to previous file;" .IP "\fBnext_file\fR: jump to next file;" .IP "\fBrand_file\fR: jump to random file;" .IP "\fBprev_frame\fR: show previous frame;" .IP "\fBnext_frame\fR: show next frame;" .IP "\fBskip_file\fR: skip the current file (remove from the image list);" .IP "\fBanimation\fR: start/stop animation;" .IP "\fBslideshow\fR: start/stop slideshow;" .IP "\fBfullscreen\fR: switch full screen mode;" .IP "\fBmode \fI[MODE]\fR\fR: switch between viewer and gallery;" .IP "\fBstep_left\fR \fI[PERCENT]\fR: move viewport left, default is 10%;" .IP "\fBstep_right\fR \fI[PERCENT]\fR: move viewport right, default is 10%;" .IP "\fBstep_up\fR \fI[PERCENT]\fR: move viewport up, default is 10%;" .IP "\fBstep_down\fR \fI[PERCENT]\fR: move viewport down, default is 10%;" .IP "\fBzoom\fR \fI[SCALE]\fR: zoom in/out/fix, \fISCALE\fR is one of \fIviewer.scale\fR modes, or percent, e.g. \fI+10\fR;" .IP "\fBscale\fR \fI[SCALE]\fR: set default/global scale, \fISCALE\fR is one of \fIviewer.scale\fR modes, cycles through available modes by default;" .IP "\fBkeep_zoom\fR: toggle zoom keeping mode;" .IP "\fBrotate_left\fR: rotate image anticlockwise;" .IP "\fBrotate_right\fR: rotate image clockwise;" .IP "\fBflip_vertical\fR: flip image vertically;" .IP "\fBflip_horizontal\fR: flip image horizontally;" .IP "\fBreload\fR: reset cache and reload current image;" .IP "\fBantialiasing\fR: switch antialiasing mode;" .IP "\fBinfo\fR \fI[MODE]\fR: switch text info mode or set specified one (\fIoff\fR/\fIviewer\fR/\fIgallery\fR);" .IP "\fBexec\fR \fICOMMAND\fR: execute an external command, use % to substitute the path to the current image, %% to escape %;" .IP "\fBstatus\fR \fITEXT\fR: print message in the status field;" .IP "\fBexit\fR: exit the application." .\" ---------------------------------------------------------------------------- .SS "Gallery mode actions" .IP "\fBnone\fR: can be used for removing built-in action;" .IP "\fBhelp\fR: show/hide help;" .IP "\fBfirst_file\fR: jump to the first file;" .IP "\fBlast_file\fR: jump to the last file;" .IP "\fBprev_file\fR: select previous file;" .IP "\fBnext_file\fR: select next file;" .IP "\fBstep_left\fR: select previous image;" .IP "\fBstep_right\fR: select next image;" .IP "\fBstep_up\fR: select image above;" .IP "\fBstep_down\fR: select image below;" .IP "\fBpage_up\fR: scroll page up;" .IP "\fBpage_down\fR: scroll page down;" .IP "\fBskip_file\fR: skip the current file (remove from the image list);" .IP "\fBfullscreen\fR: switch full screen mode;" .IP "\fBmode\fR: switch between viewer and gallery;" .IP "\fBreload\fR: reset cache and reload current image;" .IP "\fBantialiasing\fR: switch antialiasing mode;" .IP "\fBinfo\fR \fI[MODE]\fR: switch text info mode or set specified one (\fIoff\fR/\fIviewer\fR/\fIgallery\fR);" .IP "\fBexec\fR \fICOMMAND\fR: execute an external command, use % to substitute the path to the current image, %% to escape %;" .IP "\fBstatus\fR \fITEXT\fR: print message in the status field;" .IP "\fBexit\fR: exit the application." .\" **************************************************************************** .\" Example .\" **************************************************************************** .SH EXAMPLES .EX # comment [list] order = random [font] size = 16 [keys] Ctrl+Alt+e = exec echo "%" > mylist.txt .EE .PP See `/usr/share/swayimg/swayimgrc` for full example. .\" **************************************************************************** .\" Cross links .\" **************************************************************************** .SH SEE ALSO swayimg(1) .\" **************************************************************************** .\" Home page .\" **************************************************************************** .SH BUGS For suggestions, comments, bug reports etc. visit the .UR https://github.com/artemsen/swayimg project homepage .UE . swayimg-3.8/extra/zsh.completion000066400000000000000000000020311474536441700171010ustar00rootroot00000000000000#compdef swayimg # zsh completion for the "swayimg" image viewer. # Copyright (C) 2022 Artem Senichev _arguments \ '(-g --gallery)'{-g,--gallery}'[start in gallery mode]' \ '(-r --recursive)'{-r,--recursive}'[read directories recursively]' \ '(-o --order)'{-o,--order=}'[set sort order for image list]:order:(none alpha reverse random)' \ '(-s --scale=SCALE)'{-s,--scale=}'[set initial image scale]:scale:(optimal width height fit fill real)' \ '(-l --slideshow)'{-l,--slideshow}'[activate slideshow mode on startup]' \ '(-f --fullscreen)'{-f,--fullscreen}'[show image in full screen mode]' \ '(-p --position)'{-p,--position=}'[set window position]:position:(parent)' \ '(-w --size)'{-w,--size=}'[set window size]:size:(parent image)' \ '(-a --class)'{-a,--class=}'[set window class/app_id]:class' \ '(-c --config)'{-c,--config=}'[set configuration parameter]:config' \ '(-v --version)'{-v,--version}'[print version info and exit]' \ '(-h --help)'{-h,--help}'[print help and exit]' \ '*:file:_files' swayimg-3.8/meson.build000066400000000000000000000170051474536441700152300ustar00rootroot00000000000000# Rules for building with Meson project( 'swayimg', 'c', 'cpp', default_options: [ 'c_std=c99', 'warning_level=3', 'buildtype=release', 'b_ndebug=if-release', ], license: 'MIT', version: '0.0.0', meson_version: '>=0.60.0', ) add_project_arguments( [ '-D_POSIX_C_SOURCE=200809' ], language: 'c', ) cc = meson.get_compiler('c') # version info version = get_option('version') if version == '0.0.0' git = find_program('git', native: true, required: false) if git.found() git_ver = run_command([git, 'describe', '--tags', '--long', '--always', '--dirty'], check: false) if git_ver.returncode() == 0 version = git_ver.stdout().strip().substring(1) endif endif endif # mandatory dependencies wlcln = dependency('wayland-client') json = dependency('json-c') xkb = dependency('xkbcommon') fontconfig = dependency('fontconfig') freetype = dependency('freetype2') threads = dependency('threads') rt = cc.find_library('rt') m = cc.find_library('m') # optional dependencies: file formats support exr = dependency('OpenEXR', version: '>=3.1', required: get_option('exr')) heif = dependency('libheif', required: get_option('heif')) avif = dependency('libavif', required: get_option('avif')) jpeg = dependency('libjpeg', required: get_option('jpeg')) jxl = dependency('libjxl', required: get_option('jxl')) png = dependency('libpng', required: get_option('png')) rsvg = dependency('librsvg-2.0', version: '>=2.46', required: get_option('svg')) tiff = dependency('libtiff-4', required: get_option('tiff')) sixel = dependency('libsixel', required: get_option('sixel')) webp = dependency('libwebp', required: get_option('webp')) webp_demux = dependency('libwebpdemux', required: get_option('webp')) # hack to build "pkgconfigless" gif in FreeBSD gif_opt = get_option('gif') if gif_opt.disabled() gif = cc.find_library('gif', required: gif_opt) else gif = cc.find_library('gif', required: false) if not gif.found() and gif_opt.allowed() gif = cc.find_library('gif', dirs: wlcln.get_variable(pkgconfig: 'libdir'), required: gif_opt) endif endif # optional dependencies: other features exif = dependency('libexif', required: get_option('exif')) bash = dependency('bash-completion', required: get_option('bash')) # non-Linux (BSD specific) epoll = dependency('epoll-shim', required: false) inotify = dependency('libinotify', required: false) # configuration file conf = configuration_data() conf.set('HAVE_LIBEXR', exr.found()) conf.set('HAVE_LIBGIF', gif.found()) conf.set('HAVE_LIBHEIF', heif.found()) conf.set('HAVE_LIBAVIF', avif.found()) conf.set('HAVE_LIBJPEG', jpeg.found()) conf.set('HAVE_LIBJXL', jxl.found()) conf.set('HAVE_LIBPNG', png.found()) conf.set('HAVE_LIBRSVG', rsvg.found()) conf.set('HAVE_LIBTIFF', tiff.found()) conf.set('HAVE_LIBSIXEL', sixel.found()) conf.set('HAVE_LIBWEBP', webp.found() and webp_demux.found()) conf.set('HAVE_LIBEXIF', exif.found()) conf.set('HAVE_INOTIFY', cc.has_header('sys/inotify.h', dependencies: inotify)) conf.set_quoted('APP_NAME', meson.project_name()) conf.set_quoted('APP_VERSION', version) configure_file(output: 'buildcfg.h', configuration: conf) # Wayland protocols wlproto = dependency('wayland-protocols') wlproto_dir = wlproto.get_variable(pkgconfig: 'pkgdatadir') wlscan = dependency('wayland-scanner', required: false, native: true) if wlscan.found() wl_scanner = find_program(wlscan.get_variable(pkgconfig: 'wayland_scanner'), native: true) else wl_scanner = find_program('wayland-scanner', native: true) endif protocols = [ wlproto_dir / 'stable/xdg-shell/xdg-shell.xml', wlproto_dir / 'stable/tablet/tablet-v2.xml', wlproto_dir / 'stable/viewporter/viewporter.xml', wlproto_dir / 'staging/content-type/content-type-v1.xml', wlproto_dir / 'staging/cursor-shape/cursor-shape-v1.xml', wlproto_dir / 'staging/fractional-scale/fractional-scale-v1.xml', wlproto_dir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', ] wl_protos_src = [] foreach xml : protocols wl_protos_src += custom_target( xml.underscorify() + '_c', input: xml, output: '@BASENAME@-protocol.c', command: [wl_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], ) wl_protos_src += custom_target( xml.underscorify() + '_client_h', input: xml, output: '@BASENAME@-client-protocol.h', command: [wl_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], ) endforeach # install sample config install_data('extra/swayimgrc', install_dir: get_option('datadir') / 'swayimg') # man installation if get_option('man') install_man('extra/swayimg.1') install_man('extra/swayimgrc.5') endif # desktop file + icon if get_option('desktop') install_data('extra/swayimg.desktop', install_dir: get_option('datadir') / 'applications') install_data('extra/icon_64.png', rename: 'swayimg.png', install_dir: get_option('datadir') / 'icons/hicolor/64x64/apps') install_data('extra/icon_128.png', rename: 'swayimg.png', install_dir: get_option('datadir') / 'icons/hicolor/128x128/apps') install_data('extra/icon_256.png', rename: 'swayimg.png', install_dir: get_option('datadir') / 'icons/hicolor/256x256/apps') endif # zsh completion zsh = get_option('zsh') if zsh.auto() shell = find_program('zsh', required: false) zsh = zsh.disable_auto_if(not shell.found()) endif if zsh.allowed() datadir = get_option('datadir') zsh_install_dir = datadir / 'zsh' / 'site-functions' install_data('extra/zsh.completion', install_dir: zsh_install_dir, rename: '_swayimg') endif # bash completion installation if bash.found() datadir = get_option('datadir') bash_install_dir = bash.get_variable(pkgconfig: 'completionsdir', pkgconfig_define: ['datadir', datadir]) install_data('extra/bash.completion', install_dir: bash_install_dir, rename: 'swayimg') endif # unit tests if get_option('tests').enabled() subdir('test') endif # source files sources = [ 'src/action.c', 'src/application.c', 'src/config.c', 'src/event.c', 'src/fetcher.c', 'src/font.c', 'src/gallery.c', 'src/image.c', 'src/imagelist.c', 'src/info.c', 'src/keybind.c', 'src/loader.c', 'src/main.c', 'src/memdata.c', 'src/pixmap.c', 'src/pixmap_scale.c', 'src/sway.c', 'src/thumbnail.c', 'src/ui.c', 'src/viewer.c', 'src/wndbuf.c', 'src/formats/bmp.c', 'src/formats/dicom.c', 'src/formats/farbfeld.c', 'src/formats/pnm.c', 'src/formats/qoi.c', 'src/formats/tga.c', wl_protos_src, ] if exif.found() sources += 'src/exif.c' endif if exr.found() sources += 'src/formats/exr.c' endif if gif.found() sources += 'src/formats/gif.c' endif if heif.found() sources += 'src/formats/heif.c' endif if avif.found() sources += 'src/formats/avif.c' endif if jpeg.found() sources += 'src/formats/jpeg.c' endif if jxl.found() sources += 'src/formats/jxl.c' endif if png.found() sources += 'src/formats/png.c' endif if rsvg.found() sources += 'src/formats/svg.c' endif if tiff.found() sources += 'src/formats/tiff.c' endif if sixel.found() sources += 'src/formats/sixel.c' endif if webp.found() and webp_demux.found() sources += 'src/formats/webp.c' endif executable( 'swayimg', sources, dependencies: [ # runtime m, rt, threads, wlcln, epoll, inotify, json, xkb, fontconfig, freetype, exif, # image support exr, gif, heif, avif, jpeg, jxl, png, rsvg, tiff, sixel, webp, webp_demux, ], install: true ) swayimg-3.8/meson_options.txt000066400000000000000000000040371474536441700165240ustar00rootroot00000000000000# format support option('exr', type: 'feature', value: 'auto', description: 'Enable EXR format support') option('gif', type: 'feature', value: 'auto', description: 'Enable GIF format support') option('heif', type: 'feature', value: 'auto', description: 'Enable HEIF and AVIF format support') option('avif', type: 'feature', value: 'auto', description: 'Enable AVIF and AVIFS format support') option('jpeg', type: 'feature', value: 'auto', description: 'Enable JPEG format support') option('jxl', type: 'feature', value: 'auto', description: 'Enable JPEG XL format support') option('png', type: 'feature', value: 'auto', description: 'Enable PNG format support') option('svg', type: 'feature', value: 'auto', description: 'Enable SVG format support') option('tiff', type: 'feature', value: 'auto', description: 'Enable TIFF format support') option('sixel', type: 'feature', value: 'auto', description: 'Enable Sixel format support') option('webp', type: 'feature', value: 'auto', description: 'Enable WebP format support') # EXIF support option('exif', type: 'feature', value: 'auto', description: 'Enable EXIF reader support') # extra files to install option('bash', type: 'feature', value: 'auto', description: 'Install bash completion') option('zsh', type: 'feature', value: 'auto', description: 'Install zsh completion') option('man', type: 'boolean', value: true, description: 'Install man page') option('desktop', type: 'boolean', value: true, description: 'Install desktop file with icon') # project version option('version', type : 'string', value : '0.0.0', description : 'Project version') # unit tests option('tests', type: 'feature', value: 'disabled', description: 'Build unit tests') swayimg-3.8/src/000077500000000000000000000000001474536441700136525ustar00rootroot00000000000000swayimg-3.8/src/action.c000066400000000000000000000105361474536441700153000ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Actions: set of predefined commands to execute. // Copyright (C) 2024 Artem Senichev #include "action.h" #include "memdata.h" #include #include #include // Max number of actions in sequence #define ACTION_SEQ_MAX 32 /** Action names. */ static const char* action_names[] = { [action_none] = "none", [action_help] = "help", [action_first_file] = "first_file", [action_last_file] = "last_file", [action_prev_dir] = "prev_dir", [action_next_dir] = "next_dir", [action_prev_file] = "prev_file", [action_next_file] = "next_file", [action_rand_file] = "rand_file", [action_skip_file] = "skip_file", [action_prev_frame] = "prev_frame", [action_next_frame] = "next_frame", [action_animation] = "animation", [action_slideshow] = "slideshow", [action_fullscreen] = "fullscreen", [action_mode] = "mode", [action_step_left] = "step_left", [action_step_right] = "step_right", [action_step_up] = "step_up", [action_step_down] = "step_down", [action_page_up] = "page_up", [action_page_down] = "page_down", [action_zoom] = "zoom", [action_scale] = "scale", [action_keep_zoom] = "keep_zoom", [action_rotate_left] = "rotate_left", [action_rotate_right] = "rotate_right", [action_flip_vertical] = "flip_vertical", [action_flip_horizontal] = "flip_horizontal", [action_reload] = "reload", [action_antialiasing] = "antialiasing", [action_info] = "info", [action_exec] = "exec", [action_status] = "status", [action_exit] = "exit", }; /** * Parse config line and fill the action. * @param action target action instance * @param source action command with parameters as text string * @param len length of the source string * @return true if action loaded */ static bool parse(struct action* action, const char* source, size_t len) { ssize_t action_type; const char* action_name; size_t action_len; const char* params; size_t params_len; size_t pos = 0; // skip spaces while (pos < len && isspace(source[pos])) { ++pos; } action_name = &source[pos]; // get action type action_len = pos; while (pos < len && !isspace(source[pos])) { ++pos; } action_len = pos - action_len; action_type = str_index(action_names, action_name, action_len); if (action_type < 0) { return false; } action->type = action_type; // skip spaces while (pos < len && isspace(source[pos])) { ++pos; } // rest part: parameters params = &source[pos]; params_len = len - pos; if (params_len) { action->params = str_append(params, params_len, NULL); if (!action->params) { return false; } } else { action->params = NULL; } return true; } bool action_create(const char* text, struct action_seq* actions) { struct action load[ACTION_SEQ_MAX] = { 0 }; struct str_slice slices[ARRAY_SIZE(load)]; size_t seq_len; struct action* buf; size_t buf_sz; // split line, one slice per action seq_len = str_split(text, ';', slices, ARRAY_SIZE(slices)); if (seq_len == 0) { return false; } if (seq_len > ARRAY_SIZE(slices)) { seq_len = ARRAY_SIZE(slices); } // load actions for (size_t i = 0; i < seq_len; ++i) { const struct str_slice* s = &slices[i]; if (!parse(&load[i], s->value, s->len)) { while (i) { free(load[--i].params); } return 0; } } // put loaded action to output sequence buf_sz = seq_len * sizeof(struct action); buf = realloc(actions->sequence, buf_sz); if (!buf) { for (size_t i = 0; i < ARRAY_SIZE(load); ++i) { free(load[i].params); } return 0; } memcpy(buf, load, buf_sz); actions->num = seq_len; actions->sequence = buf; return seq_len; } void action_free(struct action_seq* actions) { if (actions) { for (size_t i = 0; i < actions->num; ++i) { free(actions->sequence[i].params); } free(actions->sequence); } } const char* action_typename(const struct action* action) { if (action->type < ARRAY_SIZE(action_names)) { return action_names[action->type]; } return NULL; } swayimg-3.8/src/action.h000066400000000000000000000034051474536441700153020ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Actions: set of predefined commands to execute. // Copyright (C) 2024 Artem Senichev #pragma once #include #include /** Supported actions. */ enum action_type { action_none, action_help, action_first_file, action_last_file, action_prev_dir, action_next_dir, action_prev_file, action_next_file, action_rand_file, action_skip_file, action_prev_frame, action_next_frame, action_animation, action_slideshow, action_fullscreen, action_mode, action_step_left, action_step_right, action_step_up, action_step_down, action_page_up, action_page_down, action_zoom, action_scale, action_keep_zoom, action_rotate_left, action_rotate_right, action_flip_vertical, action_flip_horizontal, action_reload, action_antialiasing, action_info, action_exec, action_status, action_exit, }; /** Single action. */ struct action { enum action_type type; ///< Action type char* params; ///< Custom parameters for the action }; /** Action sequence. */ struct action_seq { struct action* sequence; ///< Array of actions size_t num; ///< Number of actions in array }; /** * Create action sequence from config string. * @param text source config text * @param actions destination sequence of actions * @return false if format error */ bool action_create(const char* text, struct action_seq* actions); /** * Free actions sequence. * @param actions sequence to free */ void action_free(struct action_seq* actions); /** * Get action's type name. * @param action to get type * @return type name */ const char* action_typename(const struct action* action); swayimg-3.8/src/application.c000066400000000000000000000451661474536441700163350ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image viewer application: main loop and event handler. // Copyright (C) 2024 Artem Senichev #include "application.h" #include "buildcfg.h" #include "font.h" #include "gallery.h" #include "imagelist.h" #include "info.h" #include "loader.h" #include "sway.h" #include "ui.h" #include "viewer.h" #include #include #include #include #include #include #include #include #include // Special ids for windows size and position #define SIZE_FULLSCREEN SIZE_MAX #define SIZE_FROM_IMAGE (SIZE_MAX - 1) #define SIZE_FROM_PARENT (SIZE_MAX - 2) #define POS_FROM_PARENT SSIZE_MAX /** Main loop state */ enum loop_state { loop_run, loop_stop, loop_error, }; /** File descriptor and its handler. */ struct watchfd { int fd; void* data; fd_callback callback; }; /* Application event queue (list). */ struct event_queue { struct list list; struct event event; }; /** Application context */ struct application { enum loop_state state; ///< Main loop state struct watchfd* wfds; ///< FD polling descriptors size_t wfds_num; ///< Number of polling FD struct event_queue* events; ///< Event queue pthread_mutex_t events_lock; ///< Event queue lock int event_signal; ///< Queue change notification struct action_seq sigusr1; ///< Actions applied by USR1 signal struct action_seq sigusr2; ///< Actions applied by USR2 signal event_handler ehandler; ///< Event handler for the current mode struct wndrect window; ///< Preferable window position and size bool wnd_decor; //< Window decoration: borders and title char* app_id; ///< Application id (app_id name) }; /** Global application context. */ static struct application ctx; /** * Setup window position via Sway IPC. */ static void sway_setup(void) { struct wndrect parent; bool fullscreen; bool absolute = false; int ipc; ipc = sway_connect(); if (ipc == INVALID_SWAY_IPC) { return; // sway not available } if (!sway_current(ipc, &parent, &fullscreen)) { sway_disconnect(ipc); return; } if (fullscreen) { ctx.window.width = SIZE_FULLSCREEN; ctx.window.height = SIZE_FULLSCREEN; sway_disconnect(ipc); return; } if (ctx.window.width == SIZE_FROM_PARENT) { ctx.window.width = parent.width; ctx.window.height = parent.height; } if (ctx.window.x == POS_FROM_PARENT) { absolute = false; ctx.window.x = parent.x; ctx.window.y = parent.y; } // set window position via sway rules sway_add_rules(ipc, ctx.window.x, ctx.window.y, absolute); sway_disconnect(ipc); } /** Notification callback: handle event queue. */ static void handle_event_queue(__attribute__((unused)) void* data) { notification_reset(ctx.event_signal); while (ctx.events && ctx.state == loop_run) { struct event_queue* entry = NULL; pthread_mutex_lock(&ctx.events_lock); if (ctx.events) { entry = ctx.events; ctx.events = list_remove(entry); } pthread_mutex_unlock(&ctx.events_lock); if (entry) { ctx.ehandler(&entry->event); free(entry); } } } /** * Append event to queue. * @param event pointer to the event */ static void append_event(const struct event* event) { struct event_queue* entry; // create new entry entry = malloc(sizeof(*entry)); if (!entry) { return; } memcpy(&entry->event, event, sizeof(entry->event)); // add to queue tail pthread_mutex_lock(&ctx.events_lock); ctx.events = list_append(ctx.events, entry); pthread_mutex_unlock(&ctx.events_lock); notification_raise(ctx.event_signal); } /** * Apply action. * @param action pointer to the action being performed */ static void apply_action(const struct action* action) { struct event event; switch (action->type) { case action_info: info_switch(action->params); app_redraw(); break; case action_status: info_update(info_status, "%s", action->params); app_redraw(); break; case action_fullscreen: ui_toggle_fullscreen(); break; case action_help: info_switch_help(); app_redraw(); break; case action_exit: if (info_help_active()) { info_switch_help(); // remove help overlay app_redraw(); } else { app_exit(0); } break; default: // not a general action, add to event queue event.type = event_action; event.param.action = action; append_event(&event); break; } } /** * POSIX Signal handler. * @param signum signal number */ static void on_signal(int signum) { const struct action_seq* sigact; switch (signum) { case SIGUSR1: sigact = &ctx.sigusr1; break; case SIGUSR2: sigact = &ctx.sigusr2; break; default: return; } for (size_t i = 0; i < sigact->num; ++i) { apply_action(&sigact->sequence[i]); } } /** * Load first (initial) image. * @param index initial index of image in the image list * @param force mandatory image index flag * @return image instance or NULL on errors */ static struct image* load_first_file(size_t index, bool force) { struct image* img = NULL; enum loader_status status = ldr_ioerror; if (index == IMGLIST_INVALID) { index = image_list_first(); force = false; } while (index != IMGLIST_INVALID) { status = loader_from_index(index, &img); if (force || status == ldr_success) { break; } index = image_list_skip(index); } if (status != ldr_success) { // print error message if (!force) { fprintf(stderr, "No image files was loaded, exit\n"); } else { const char* reason = "Unknown error"; switch (status) { case ldr_success: break; case ldr_unsupported: reason = "Unsupported format"; break; case ldr_fmterror: reason = "Invalid format"; break; case ldr_ioerror: reason = "I/O error"; break; } fprintf(stderr, "%s: %s\n", image_list_get(index), reason); } } return img; } /** * Load config. * @param cfg config instance */ static void load_config(const struct config* cfg) { const char* value; // startup mode static const char* modes[] = { CFG_MODE_VIEWER, CFG_MODE_GALLERY }; if (config_get_oneof(cfg, CFG_GENERAL, CFG_GNRL_MODE, modes, ARRAY_SIZE(modes)) == 1) { ctx.ehandler = gallery_handle; } else { ctx.ehandler = viewer_handle; } // initial window position ctx.window.x = POS_FROM_PARENT; ctx.window.y = POS_FROM_PARENT; value = config_get(cfg, CFG_GENERAL, CFG_GNRL_POSITION); if (strcmp(value, CFG_FROM_PARENT) != 0) { struct str_slice slices[2]; ssize_t x, y; if (str_split(value, ',', slices, 2) == 2 && str_to_num(slices[0].value, slices[0].len, &x, 0) && str_to_num(slices[1].value, slices[1].len, &y, 0)) { ctx.window.x = (ssize_t)x; ctx.window.y = (ssize_t)y; } else { config_error_val(CFG_GENERAL, CFG_GNRL_POSITION); } } // initial window size value = config_get(cfg, CFG_GENERAL, CFG_GNRL_SIZE); if (strcmp(value, CFG_FROM_PARENT) == 0) { ctx.window.width = SIZE_FROM_PARENT; ctx.window.height = SIZE_FROM_PARENT; } else if (strcmp(value, CFG_FROM_IMAGE) == 0) { ctx.window.width = SIZE_FROM_IMAGE; ctx.window.height = SIZE_FROM_IMAGE; } else if (strcmp(value, CFG_FULLSCREEN) == 0) { ctx.window.width = SIZE_FULLSCREEN; ctx.window.height = SIZE_FULLSCREEN; } else { ssize_t width, height; struct str_slice slices[2]; if (str_split(value, ',', slices, 2) == 2 && str_to_num(slices[0].value, slices[0].len, &width, 0) && str_to_num(slices[1].value, slices[1].len, &height, 0) && width > 0 && width < 100000 && height > 0 && height < 100000) { ctx.window.width = width; ctx.window.height = height; } else { ctx.window.width = SIZE_FROM_PARENT; ctx.window.height = SIZE_FROM_PARENT; config_error_val(CFG_GENERAL, CFG_GNRL_SIZE); } } ctx.wnd_decor = config_get_bool(cfg, CFG_GENERAL, CFG_GNRL_DECOR); // signal actions value = config_get(cfg, CFG_GENERAL, CFG_GNRL_SIGUSR1); if (!action_create(value, &ctx.sigusr1)) { config_error_val(CFG_GENERAL, CFG_GNRL_SIGUSR1); value = config_get_default(CFG_GENERAL, CFG_GNRL_SIGUSR1); action_create(value, &ctx.sigusr1); } value = config_get(cfg, CFG_GENERAL, CFG_GNRL_SIGUSR2); if (!action_create(value, &ctx.sigusr2)) { config_error_val(CFG_GENERAL, CFG_GNRL_SIGUSR2); value = config_get_default(CFG_GENERAL, CFG_GNRL_SIGUSR2); action_create(value, &ctx.sigusr2); } // app id value = config_get(cfg, CFG_GENERAL, CFG_GNRL_APP_ID); if (!*value) { config_error_val(CFG_GENERAL, CFG_GNRL_APP_ID); value = config_get_default(CFG_GENERAL, CFG_GNRL_APP_ID); } str_dup(value, &ctx.app_id); } bool app_init(const struct config* cfg, const char** sources, size_t num) { bool force_load = false; struct image* first_image; struct sigaction sigact; load_config(cfg); // compose image list if (num == 0) { // no input files specified, use all from the current directory static const char* current_dir = "."; sources = ¤t_dir; num = 1; } else if (num == 1) { force_load = true; if (strcmp(sources[0], "-") == 0) { // load from stdin static const char* stdin_name = LDRSRC_STDIN; sources = &stdin_name; } } image_list_init(cfg); for (size_t i = 0; i < num; ++i) { image_list_add(sources[i]); } if (image_list_size() == 0) { if (force_load) { fprintf(stderr, "%s: Unable to open\n", sources[0]); } else { fprintf(stderr, "No image files found to view, exit\n"); } return false; } image_list_reorder(); // load the first image first_image = load_first_file(image_list_find(sources[0]), force_load); if (!first_image) { return false; } // setup window position and size if (ctx.window.width != SIZE_FULLSCREEN) { sway_setup(); // try Sway integration } if (ctx.window.width == SIZE_FULLSCREEN) { ui_toggle_fullscreen(); } else if (ctx.window.width == SIZE_FROM_IMAGE || ctx.window.width == SIZE_FROM_PARENT) { // determine window size from the first image const struct pixmap* pm = &first_image->frames[0].pm; ctx.window.width = pm->width; ctx.window.height = pm->height; } // connect to wayland if (!ui_init(ctx.app_id, ctx.window.width, ctx.window.height, ctx.wnd_decor)) { return false; } // create event queue notification ctx.event_signal = notification_create(); if (ctx.event_signal != -1) { app_watch(ctx.event_signal, handle_event_queue, NULL); } else { perror("Unable to create eventfd"); return false; } pthread_mutex_init(&ctx.events_lock, NULL); // initialize other subsystems font_init(cfg); keybind_init(cfg); info_init(cfg); loader_init(); viewer_init(cfg, ctx.ehandler == viewer_handle ? first_image : NULL); gallery_init(cfg, ctx.ehandler == gallery_handle ? first_image : NULL); // set mode for info if (info_enabled()) { info_switch(ctx.ehandler == viewer_handle ? CFG_MODE_VIEWER : CFG_MODE_GALLERY); } // set signal handler sigact.sa_handler = on_signal; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGUSR1, &sigact, NULL); sigaction(SIGUSR2, &sigact, NULL); return true; } void app_destroy(void) { loader_destroy(); gallery_destroy(); viewer_destroy(); ui_destroy(); image_list_destroy(); info_destroy(); keybind_destroy(); font_destroy(); for (size_t i = 0; i < ctx.wfds_num; ++i) { close(ctx.wfds[i].fd); } free(ctx.wfds); list_for_each(ctx.events, struct event_queue, it) { if (it->event.type == event_load) { image_free(it->event.param.load.image); } free(it); } if (ctx.event_signal != -1) { notification_free(ctx.event_signal); } pthread_mutex_destroy(&ctx.events_lock); action_free(&ctx.sigusr1); action_free(&ctx.sigusr2); } void app_watch(int fd, fd_callback cb, void* data) { const size_t sz = (ctx.wfds_num + 1) * sizeof(*ctx.wfds); struct watchfd* handlers = realloc(ctx.wfds, sz); if (handlers) { ctx.wfds = handlers; ctx.wfds[ctx.wfds_num].fd = fd; ctx.wfds[ctx.wfds_num].data = data; ctx.wfds[ctx.wfds_num].callback = cb; ++ctx.wfds_num; } } bool app_run(void) { struct pollfd* fds; // file descriptors to poll fds = calloc(1, ctx.wfds_num * sizeof(struct pollfd)); if (!fds) { perror("Failed to allocate memory"); return false; } for (size_t i = 0; i < ctx.wfds_num; ++i) { fds[i].fd = ctx.wfds[i].fd; fds[i].events = POLLIN; } // main event loop ctx.state = loop_run; while (ctx.state == loop_run) { ui_event_prepare(); // poll events if (poll(fds, ctx.wfds_num, -1) < 0) { if (errno != EINTR) { perror("Error polling events"); ctx.state = loop_error; break; } } // call handlers for each active event for (size_t i = 0; i < ctx.wfds_num; ++i) { if (fds[i].revents & POLLIN) { ctx.wfds[i].callback(ctx.wfds[i].data); } } ui_event_done(); } free(fds); return ctx.state != loop_error; } void app_exit(int rc) { ctx.state = rc ? loop_error : loop_stop; } void app_switch_mode(size_t index) { const char* info_mode; const struct event event = { .type = event_activate, .param.activate.index = index, }; if (ctx.ehandler == viewer_handle) { ctx.ehandler = gallery_handle; info_mode = CFG_MODE_GALLERY; } else { ctx.ehandler = viewer_handle; info_mode = CFG_MODE_VIEWER; } ctx.ehandler(&event); if (info_enabled()) { info_switch(info_mode); } if (info_help_active()) { info_switch_help(); } app_redraw(); } bool app_is_viewer(void) { return ctx.ehandler == viewer_handle; } void app_reload(void) { static const struct action action = { .type = action_reload }; const struct event event = { .type = event_action, .param.action = &action, }; append_event(&event); } void app_redraw(void) { const struct event event = { .type = event_redraw, }; // remove the same event to append new one to tail pthread_mutex_lock(&ctx.events_lock); list_for_each(ctx.events, struct event_queue, it) { if (it->event.type == event_redraw) { if (list_is_last(it)) { pthread_mutex_unlock(&ctx.events_lock); return; } ctx.events = list_remove(it); free(it); break; } } pthread_mutex_unlock(&ctx.events_lock); append_event(&event); } void app_on_resize(void) { const struct event event = { .type = event_resize, }; append_event(&event); } void app_on_keyboard(xkb_keysym_t key, uint8_t mods) { const struct keybind* kb = keybind_find(key, mods); if (kb) { for (size_t i = 0; i < kb->actions.num; ++i) { apply_action(&kb->actions.sequence[i]); } } else { char* name = keybind_name(key, mods); if (name) { info_update(info_status, "Key %s is not bound", name); free(name); app_redraw(); } } } void app_on_drag(int dx, int dy) { const struct event event = { .type = event_drag, .param.drag.dx = dx, .param.drag.dy = dy }; // merge with existing event pthread_mutex_lock(&ctx.events_lock); list_for_each(ctx.events, struct event_queue, it) { if (it->event.type == event_drag) { it->event.param.drag.dx += dx; it->event.param.drag.dy += dy; pthread_mutex_unlock(&ctx.events_lock); return; } } pthread_mutex_unlock(&ctx.events_lock); append_event(&event); } void app_on_load(struct image* image, size_t index) { const struct event event = { .type = event_load, .param.load.image = image, .param.load.index = index, }; append_event(&event); } void app_execute(const char* expr, const char* path) { char* cmd = NULL; int rc = -1; // construct command from template while (expr && *expr) { if (*expr == '%') { ++expr; if (*expr != '%') { str_append(path, 0, &cmd); // replace % with path continue; } } str_append(expr, 1, &cmd); ++expr; } if (cmd) { rc = system(cmd); // execute if (rc != -1) { rc = WEXITSTATUS(rc); } else if (errno) { rc = errno; } } // show execution status if (!cmd) { info_update(info_status, "Error: no command to execute"); } else { size_t max_len = 30; // trim long command text if (strlen(cmd) > max_len) { const char* ellipsis = "..."; const size_t ellipsis_len = strlen(ellipsis); memcpy(&cmd[max_len - ellipsis_len], ellipsis, ellipsis_len + 1); } if (rc) { info_update(info_status, "Error %d: %s", rc, cmd); } else { info_update(info_status, "OK: %s", cmd); } } free(cmd); app_redraw(); } swayimg-3.8/src/application.h000066400000000000000000000045551474536441700163370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image viewer application: main loop and event handler. // Copyright (C) 2024 Artem Senichev #pragma once #include "image.h" #include "keybind.h" /** * Handler of the fd poll events. * @param data user data */ typedef void (*fd_callback)(void* data); /** * Initialize global application context. * @param cfg config instance * @param sources list of sources * @param num number of sources in the list * @return true if application initialized successfully */ bool app_init(const struct config* cfg, const char** sources, size_t num); /** * Destroy global application context. */ void app_destroy(void); /** * Add file descriptor for polling in main loop. * @param fd file descriptor for polling * @param cb callback function * @param data user defined data to pass to callback */ void app_watch(int fd, fd_callback cb, void* data); /** * Run application. * @return true if application was closed by user, false on errors */ bool app_run(void); /** * Handler of external event: application exit request. * @param rc result (error) code to set */ void app_exit(int rc); /** * Switch mode (viewer/gallery). * @param index index of the current image */ void app_switch_mode(size_t index); /** * Get active mode. * @return true if current mode is viewer, false for gallery */ bool app_is_viewer(void); /** * Handler of external event: reload image / reset state. */ void app_reload(void); /** * Handler of external event: redraw window. */ void app_redraw(void); /** * Handler of external event: window resized. */ void app_on_resize(void); /** * Handler of external event: key/mouse press. * @param key code of key pressed * @param mods key modifiers (ctrl/alt/shift) */ void app_on_keyboard(xkb_keysym_t key, uint8_t mods); /** * Handler of external event: mouse/touch drag. * @param dx,dy delta between old and new position */ void app_on_drag(int dx, int dy); /** * Handler of image loading completion (background thread loader). * @param image loaded image instance, NULL if load error * @param index index of the image in the image list */ void app_on_load(struct image* image, size_t index); /** * Execute system command for the specified image. * @param expr command expression * @param path file path to substitute into expression */ void app_execute(const char* expr, const char* path); swayimg-3.8/src/config.c000066400000000000000000000562671474536441700153030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Program configuration. // Copyright (C) 2022 Artem Senichev #include "config.h" #include "memdata.h" #include #include #include #include #include /** Default configuration. */ struct config_default { const char* section; const char* key; const char* value; }; // clang-format off static const struct config_default defaults[] = { { CFG_GENERAL, CFG_GNRL_MODE, "viewer" }, { CFG_GENERAL, CFG_GNRL_POSITION, "parent" }, { CFG_GENERAL, CFG_GNRL_SIZE, "parent" }, { CFG_GENERAL, CFG_GNRL_DECOR, CFG_NO }, { CFG_GENERAL, CFG_GNRL_SIGUSR1, "reload" }, { CFG_GENERAL, CFG_GNRL_SIGUSR2, "next_file" }, { CFG_GENERAL, CFG_GNRL_APP_ID, "swayimg" }, { CFG_VIEWER, CFG_VIEW_WINDOW, "#00000000" }, { CFG_VIEWER, CFG_VIEW_TRANSP, "grid" }, { CFG_VIEWER, CFG_VIEW_SCALE, "optimal" }, { CFG_VIEWER, CFG_VIEW_KEEP_ZM, CFG_NO }, { CFG_VIEWER, CFG_VIEW_POSITION, "center" }, { CFG_VIEWER, CFG_VIEW_FIXED, CFG_YES }, { CFG_VIEWER, CFG_VIEW_AA, "mks13" }, { CFG_VIEWER, CFG_VIEW_SSHOW, CFG_NO }, { CFG_VIEWER, CFG_VIEW_SSHOW_TM, "3" }, { CFG_VIEWER, CFG_VIEW_HISTORY, "1" }, { CFG_VIEWER, CFG_VIEW_PRELOAD, "1" }, { CFG_GALLERY, CFG_GLRY_SIZE, "200" }, { CFG_GALLERY, CFG_GLRY_CACHE, "100" }, { CFG_GALLERY, CFG_GLRY_PSTORE, CFG_NO }, { CFG_GALLERY, CFG_GLRY_FILL, CFG_YES }, { CFG_GALLERY, CFG_GLRY_AA, "mks13" }, { CFG_GALLERY, CFG_GLRY_WINDOW, "#00000000" }, { CFG_GALLERY, CFG_GLRY_BKG, "#202020ff" }, { CFG_GALLERY, CFG_GLRY_SELECT, "#404040ff" }, { CFG_GALLERY, CFG_GLRY_BORDER, "#000000ff" }, { CFG_GALLERY, CFG_GLRY_SHADOW, "#000000ff" }, { CFG_LIST, CFG_LIST_ORDER, "alpha" }, { CFG_LIST, CFG_LIST_LOOP, CFG_YES }, { CFG_LIST, CFG_LIST_RECURSIVE, CFG_NO }, { CFG_LIST, CFG_LIST_ALL, CFG_NO }, { CFG_FONT, CFG_FONT_NAME, "monospace" }, { CFG_FONT, CFG_FONT_SIZE, "14" }, { CFG_FONT, CFG_FONT_COLOR, "#ccccccff" }, { CFG_FONT, CFG_FONT_SHADOW, "#000000d0" }, { CFG_FONT, CFG_FONT_BKG, "#00000000" }, { CFG_INFO, CFG_INFO_SHOW, CFG_YES }, { CFG_INFO, CFG_INFO_ITIMEOUT, "5" }, { CFG_INFO, CFG_INFO_STIMEOUT, "3" }, { CFG_INFO_VIEWER, CFG_INFO_CN, "none" }, { CFG_INFO_VIEWER, CFG_INFO_TL, "+name,+format,+filesize,+imagesize,+exif" }, { CFG_INFO_VIEWER, CFG_INFO_TR, "index" }, { CFG_INFO_VIEWER, CFG_INFO_BL, "scale,frame" }, { CFG_INFO_VIEWER, CFG_INFO_BR, "status" }, { CFG_INFO_GALLERY, CFG_INFO_CN, "none" }, { CFG_INFO_GALLERY, CFG_INFO_TL, "none" }, { CFG_INFO_GALLERY, CFG_INFO_TR, "none" }, { CFG_INFO_GALLERY, CFG_INFO_BL, "none" }, { CFG_INFO_GALLERY, CFG_INFO_BR, "name,status" }, { CFG_KEYS_VIEWER, "F1", "help" }, { CFG_KEYS_VIEWER, "Home", "first_file" }, { CFG_KEYS_VIEWER, "End", "last_file" }, { CFG_KEYS_VIEWER, "Prior", "prev_file" }, { CFG_KEYS_VIEWER, "Next", "next_file" }, { CFG_KEYS_VIEWER, "Space", "next_file" }, { CFG_KEYS_VIEWER, "Shift+d", "prev_dir" }, { CFG_KEYS_VIEWER, "d", "next_dir" }, { CFG_KEYS_VIEWER, "Shift+r", "rand_file" }, { CFG_KEYS_VIEWER, "Shift+o", "prev_frame" }, { CFG_KEYS_VIEWER, "o", "next_frame" }, { CFG_KEYS_VIEWER, "c", "skip_file" }, { CFG_KEYS_VIEWER, "Shift+s", "slideshow" }, { CFG_KEYS_VIEWER, "s", "animation" }, { CFG_KEYS_VIEWER, "f", "fullscreen" }, { CFG_KEYS_VIEWER, "Return", "mode" }, { CFG_KEYS_VIEWER, "Left", "step_left 10" }, { CFG_KEYS_VIEWER, "Right", "step_right 10" }, { CFG_KEYS_VIEWER, "Up", "step_up 10" }, { CFG_KEYS_VIEWER, "Down", "step_down 10" }, { CFG_KEYS_VIEWER, "Equal", "zoom +10" }, { CFG_KEYS_VIEWER, "Plus", "zoom +10" }, { CFG_KEYS_VIEWER, "Minus", "zoom -10" }, { CFG_KEYS_VIEWER, "w", "zoom width" }, { CFG_KEYS_VIEWER, "Shift+w", "zoom height" }, { CFG_KEYS_VIEWER, "z", "zoom fit" }, { CFG_KEYS_VIEWER, "Shift+z", "zoom fill" }, { CFG_KEYS_VIEWER, "0", "zoom real" }, { CFG_KEYS_VIEWER, "BackSpace", "zoom optimal" }, { CFG_KEYS_VIEWER, "Alt+z", "keep_zoom" }, { CFG_KEYS_VIEWER, "bracketleft", "rotate_left" }, { CFG_KEYS_VIEWER, "bracketright", "rotate_right" }, { CFG_KEYS_VIEWER, "m", "flip_vertical" }, { CFG_KEYS_VIEWER, "Shift+m", "flip_horizontal" }, { CFG_KEYS_VIEWER, "a", "antialiasing" }, { CFG_KEYS_VIEWER, "r", "reload" }, { CFG_KEYS_VIEWER, "i", "info" }, { CFG_KEYS_VIEWER, "Shift+Delete", "exec rm '%'; skip_file" }, { CFG_KEYS_VIEWER, "Escape", "exit" }, { CFG_KEYS_VIEWER, "q", "exit" }, { CFG_KEYS_VIEWER, "ScrollLeft", "step_right 5" }, { CFG_KEYS_VIEWER, "ScrollRight", "step_left 5" }, { CFG_KEYS_VIEWER, "ScrollUp", "step_up 5" }, { CFG_KEYS_VIEWER, "ScrollDown", "step_down 5" }, { CFG_KEYS_VIEWER, "Ctrl+ScrollUp", "zoom +10" }, { CFG_KEYS_VIEWER, "Ctrl+ScrollDown", "zoom -10" }, { CFG_KEYS_VIEWER, "Shift+ScrollUp", "prev_file" }, { CFG_KEYS_VIEWER, "Shift+ScrollDown", "next_file" }, { CFG_KEYS_VIEWER, "Alt+ScrollUp", "prev_frame" }, { CFG_KEYS_VIEWER, "Alt+ScrollDown", "next_frame" }, { CFG_KEYS_GALLERY, "F1", "help" }, { CFG_KEYS_GALLERY, "Home", "first_file" }, { CFG_KEYS_GALLERY, "End", "last_file" }, { CFG_KEYS_GALLERY, "Left", "step_left" }, { CFG_KEYS_GALLERY, "Right", "step_right" }, { CFG_KEYS_GALLERY, "Up", "step_up" }, { CFG_KEYS_GALLERY, "Down", "step_down" }, { CFG_KEYS_GALLERY, "Prior", "page_up" }, { CFG_KEYS_GALLERY, "Next", "page_down" }, { CFG_KEYS_GALLERY, "c", "skip_file" }, { CFG_KEYS_GALLERY, "f", "fullscreen" }, { CFG_KEYS_GALLERY, "Return", "mode" }, { CFG_KEYS_GALLERY, "a", "antialiasing" }, { CFG_KEYS_GALLERY, "r", "reload" }, { CFG_KEYS_GALLERY, "i", "info" }, { CFG_KEYS_GALLERY, "Shift+Delete", "exec rm '%'; skip_file" }, { CFG_KEYS_GALLERY, "Escape", "exit" }, { CFG_KEYS_GALLERY, "q", "exit" }, { CFG_KEYS_GALLERY, "ScrollLeft", "step_right" }, { CFG_KEYS_GALLERY, "ScrollRight", "step_left" }, { CFG_KEYS_GALLERY, "ScrollUp", "step_up" }, { CFG_KEYS_GALLERY, "ScrollDown", "step_down" }, }; // clang-format on /** Config file location. */ struct location { const char* prefix; ///< Environment variable name const char* postfix; ///< Constant postfix }; static const struct location config_locations[] = { { "XDG_CONFIG_HOME", "/swayimg/config" }, { "HOME", "/.config/swayimg/config" }, { "XDG_CONFIG_DIRS", "/swayimg/config" }, { NULL, "/etc/xdg/swayimg/config" } }; /** * Load configuration from a file. * @param cfg config instance * @param path full path to the file * @return false on errors */ static bool load(struct config* cfg, const char* path) { FILE* fd = NULL; char* buff = NULL; size_t buff_sz = 0; size_t line_num = 0; ssize_t nread; char* section = NULL; fd = fopen(path, "r"); if (!fd) { return false; } while ((nread = getline(&buff, &buff_sz, fd)) != -1) { char* delim; const char* value; char* line = buff; ++line_num; // trim spaces while (nread-- && isspace(line[nread])) { line[nread] = 0; } while (*line && isspace(*line)) { ++line; } // skip empty lines and comments if (!*line || *line == '#') { continue; } // check for section beginning if (*line == '[') { ssize_t len; char* new_section; ++line; delim = strchr(line, ']'); if (!delim || line + 1 == delim) { fprintf(stderr, "WARNING: Invalid config line in %s:%zu\n", path, line_num); continue; } *delim = 0; len = delim - line + 1; new_section = realloc(section, len); if (new_section) { section = new_section; memcpy(section, line, len); } continue; } if (!section) { fprintf(stderr, "WARNING: Config parameter without section in %s:%zu\n", path, line_num); continue; } delim = strchr(line, '='); if (!delim) { fprintf(stderr, "WARNING: Invalid config line in %s:%zu\n", path, line_num); continue; } // trim spaces from start of value value = delim + 1; while (*value && isspace(*value)) { ++value; } // trim spaces from key *delim = 0; while (line != delim && isspace(*--delim)) { *delim = 0; } // save configuration parameter config_set(cfg, section, line, value); } free(buff); free(section); fclose(fd); return true; } /** * Create key/value entry. * @param key,value config param * @return key/value entry */ static struct config_keyval* create_keyval(const char* key, const char* value) { const size_t key_sz = strlen(key) + 1 /*last null*/; const size_t value_sz = strlen(value) + 1 /*last null*/; struct config_keyval* kv = calloc(1, sizeof(struct config_keyval) + key_sz + value_sz); if (kv) { kv->key = (char*)kv + sizeof(struct config_keyval); memcpy(kv->key, key, key_sz); kv->value = (char*)kv + sizeof(struct config_keyval) + key_sz; memcpy(kv->value, value, value_sz); } return kv; } /** * Create section in the config instance. * @param cfg config instance * @param name section name */ static void create_section(struct config** cfg, const char* name) { const size_t sz = strlen(name) + 1 /*last null*/; struct config* section = calloc(1, sizeof(struct config) + sz); if (section) { section->name = (char*)section + sizeof(struct config); memcpy(section->name, name, sz); *cfg = list_add(*cfg, section); } } /** * Convert text value to boolean config. * @param text source config value * @param value parsed value * @param false if text has invalid format */ static bool text_to_bool(const char* text, bool* value) { if (strcmp(text, CFG_YES) == 0) { *value = true; } else if (strcmp(text, CFG_NO) == 0) { *value = false; } else { return false; } return true; } /** * Convert text value to color. * @param text source config value * @param value parsed value * @param false if text has invalid format */ static bool text_to_color(const char* text, argb_t* value) { char* endptr; argb_t color; while (*text == '#' || isspace(*text)) { ++text; } errno = 0; color = strtoull(text, &endptr, 16); if (endptr && !*endptr && errno == 0) { if (strlen(text) > 6) { // value with alpha (RRGGBBAA) color = (color >> 8) | ARGB_SET_A(color); } else { color |= ARGB_SET_A(0xff); } *value = color; return true; } return false; } struct config* config_load(void) { struct config* cfg = NULL; // create sections create_section(&cfg, CFG_GENERAL); create_section(&cfg, CFG_VIEWER); create_section(&cfg, CFG_GALLERY); create_section(&cfg, CFG_LIST); create_section(&cfg, CFG_FONT); create_section(&cfg, CFG_INFO); create_section(&cfg, CFG_INFO_VIEWER); create_section(&cfg, CFG_INFO_GALLERY); create_section(&cfg, CFG_KEYS_VIEWER); create_section(&cfg, CFG_KEYS_GALLERY); // load default config for (size_t i = 0; i < ARRAY_SIZE(defaults); ++i) { const struct config_default* def = &defaults[i]; list_for_each(cfg, struct config, section) { if (strcmp(def->section, section->name) == 0) { struct config_keyval* kv = create_keyval(def->key, def->value); if (kv) { section->params = list_add(section->params, kv); } break; } } } // find and load first available config file for (size_t i = 0; i < ARRAY_SIZE(config_locations); ++i) { const struct location* cl = &config_locations[i]; char* path = config_expand_path(cl->prefix, cl->postfix); const bool loaded = path && load(cfg, path); free(path); if (loaded) { break; } } return cfg; } void config_free(struct config* cfg) { list_for_each(cfg, struct config, section) { list_for_each(section->params, struct config_keyval, kv) { free(kv); } free(section); } } bool config_set(struct config* cfg, const char* section, const char* key, const char* value) { struct config_keyval* kv = NULL; struct config* cs = NULL; if (!value || !*value) { fprintf(stderr, "WARNING: Empty config value for key \"%s\" in section \"%s\" " "is not alowed\n", key, section); return false; } // search for config section list_for_each(cfg, struct config, it) { if (strcmp(section, it->name) == 0) { cs = it; break; } } if (!cs) { fprintf(stderr, "WARNING: Unknown config section \"%s\"\n", section); return false; } // search for existing key/value list_for_each(cs->params, struct config_keyval, it) { if (strcmp(key, it->key) == 0) { kv = it; break; } } // check if config key is valid for the current section if (strcmp(section, CFG_KEYS_VIEWER) != 0 && strcmp(section, CFG_KEYS_GALLERY) != 0 && !kv) { fprintf(stderr, "WARNING: Unknown config key \"%s\" in section \"%s\"\n", key, section); return false; } // remove existing key/value if (kv) { cs->params = list_remove(kv); free(kv); } // add new key/value kv = create_keyval(key, value); if (kv) { cs->params = list_add(cs->params, kv); } return true; } bool config_set_arg(struct config* cfg, const char* arg) { char section[32]; char key[32]; const char* ptr; struct str_slice slices[2]; size_t size; // split section.key and value size = str_split(arg, '=', slices, ARRAY_SIZE(slices)); if (size <= 1) { fprintf(stderr, "WARNING: Invalid config argument format: \"%s\"\n", arg); return false; } // split section and key ptr = slices[0].value + slices[0].len; while (*ptr != '.') { if (--ptr < arg) { fprintf(stderr, "WARNING: Invalid config argument format: \"%s\"\n", arg); return false; } } // section name size = ptr - slices[0].value; if (size > sizeof(section) - 1) { size = sizeof(section) - 1; } memcpy(section, slices[0].value, size); section[size] = 0; // key name ++ptr; // skip dot size = slices[0].len - (ptr - slices[0].value); if (size > sizeof(key) - 1) { size = sizeof(key) - 1; } memcpy(key, ptr, size); key[size] = 0; return config_set(cfg, section, key, slices[1].value); } const char* config_get_default(const char* section, const char* key) { for (size_t i = 0; i < ARRAY_SIZE(defaults); ++i) { const struct config_default* def = &defaults[i]; if (strcmp(section, def->section) == 0 && strcmp(key, def->key) == 0) { return def->value; } } fprintf( stderr, "WARNING: Default value for key \"%s\" in section \"%s\" not found\n", key, section); return ""; } const char* config_get(const struct config* cfg, const char* section, const char* key) { list_for_each(cfg, const struct config, cs) { if (strcmp(section, cs->name) == 0) { list_for_each(cs->params, const struct config_keyval, kv) { if (strcmp(key, kv->key) == 0) { const char* value = kv->value; if (!*kv->value) { value = config_get_default(section, key); fprintf(stderr, "WARNING: " "Empty value for the key \"%s\" in section " "\"%s\" is not allowed, " "the default value \"%s\" will be used\n", key, section, value); } return value; } } } } fprintf(stderr, "WARNING: Value for key \"%s\" in section \"%s\" not found\n", key, section); return ""; } ssize_t config_get_oneof(const struct config* cfg, const char* section, const char* key, const char** array, size_t array_sz) { const char* value = config_get(cfg, section, key); ssize_t index = str_search_index(array, array_sz, value, 0); if (index == -1) { fprintf(stderr, "WARNING: " "Invalid config value \"%s = %s\" in section \"%s\": " "expected one of: ", key, value, section); for (size_t i = 0; i < array_sz; ++i) { fprintf(stderr, "%s, ", array[i]); } value = config_get_default(section, key); fprintf(stderr, "the default value \"%s\" will be used\n", value); index = str_search_index(array, array_sz, value, 0); } return index >= 0 ? index : 0; } bool config_get_bool(const struct config* cfg, const char* section, const char* key) { bool boolean = false; const char* value = config_get(cfg, section, key); if (!text_to_bool(value, &boolean)) { text_to_bool(config_get_default(section, key), &boolean); fprintf(stderr, "WARNING: " "Invalid config value \"%s = %s\" in section \"%s\": " "expected \"" CFG_YES "\" or \"" CFG_NO "\", " "the default value \"%s\" will be used\n", key, value, section, boolean ? CFG_YES : CFG_NO); } return boolean; } ssize_t config_get_num(const struct config* cfg, const char* section, const char* key, ssize_t min_val, ssize_t max_val) { ssize_t num = 0; const char* value = config_get(cfg, section, key); if (!str_to_num(value, 0, &num, 0) || num < min_val || num > max_val) { str_to_num(config_get_default(section, key), 0, &num, 0); fprintf(stderr, "WARNING: " "Invalid config value \"%s = %s\" in section \"%s\": " "expected integer in range %zd-%zd, " "the default value %zd will be used\n", key, value, section, min_val, max_val, num); } return num; } argb_t config_get_color(const struct config* cfg, const char* section, const char* key) { argb_t color = 0; const char* value = config_get(cfg, section, key); if (!text_to_color(value, &color)) { text_to_color(config_get_default(section, key), &color); fprintf(stderr, "WARNING: " "Invalid color value \"%s = %s\" in section \"%s\": " "expected RGB(A) format (e.g. #11223344), " "the default value #%08x will be used\n", key, value, section, color); } return color; } char* config_expand_path(const char* prefix_env, const char* postfix) { char* path = NULL; if (prefix_env) { const char* delim; size_t prefix_len = 0; const char* prefix = getenv(prefix_env); if (!prefix || !*prefix) { return NULL; } // use only the first directory if prefix is a list delim = strchr(prefix, ':'); prefix_len = delim ? (size_t)(delim - prefix) : strlen(prefix); str_append(prefix, prefix_len, &path); } str_append(postfix, 0, &path); return path; } void config_error_key(const char* section, const char* key) { fprintf(stderr, "WARNING: Invalid config key \"%s\" in section \"%s\"\n", key, section); } void config_error_val(const char* section, const char* value) { fprintf(stderr, "WARNING: Invalid config value \"%s\" in section \"%s\"\n", value, section); } swayimg-3.8/src/config.h000066400000000000000000000142441474536441700152750ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Program configuration. // Copyright (C) 2022 Artem Senichev #pragma once #include "memdata.h" #include "pixmap.h" /* Key/value config option. */ struct config_keyval { struct list list; ///< Links to prev/next entry char* key; ///< Key char* value; ///< Value }; /** Config instance: list of sections with key/values */ struct config { struct list list; ///< Links to prev/next entry char* name; ///< Section name struct config_keyval* params; ///< List of key/value for this section }; // Section names #define CFG_GENERAL "general" #define CFG_VIEWER "viewer" #define CFG_GALLERY "gallery" #define CFG_LIST "list" #define CFG_FONT "font" #define CFG_INFO "info" #define CFG_INFO_VIEWER "info.viewer" #define CFG_INFO_GALLERY "info.gallery" #define CFG_KEYS_VIEWER "keys.viewer" #define CFG_KEYS_GALLERY "keys.gallery" // Configuration parameters #define CFG_GNRL_MODE "mode" #define CFG_GNRL_POSITION "position" #define CFG_GNRL_SIZE "size" #define CFG_GNRL_DECOR "decoration" #define CFG_GNRL_SIGUSR1 "sigusr1" #define CFG_GNRL_SIGUSR2 "sigusr2" #define CFG_GNRL_APP_ID "app_id" #define CFG_VIEW_WINDOW "window" #define CFG_VIEW_TRANSP "transparency" #define CFG_VIEW_SCALE "scale" #define CFG_VIEW_KEEP_ZM "keep_zoom" #define CFG_VIEW_POSITION "position" #define CFG_VIEW_FIXED "fixed" #define CFG_VIEW_AA "antialiasing" #define CFG_VIEW_SSHOW "slideshow" #define CFG_VIEW_SSHOW_TM "slideshow_time" #define CFG_VIEW_HISTORY "history" #define CFG_VIEW_PRELOAD "preload" #define CFG_GLRY_SIZE "size" #define CFG_GLRY_CACHE "cache" #define CFG_GLRY_PSTORE "pstore" #define CFG_GLRY_FILL "fill" #define CFG_GLRY_AA "antialiasing" #define CFG_GLRY_WINDOW "window" #define CFG_GLRY_BKG "background" #define CFG_GLRY_SELECT "select" #define CFG_GLRY_BORDER "border" #define CFG_GLRY_SHADOW "shadow" #define CFG_LIST_ORDER "order" #define CFG_LIST_LOOP "loop" #define CFG_LIST_RECURSIVE "recursive" #define CFG_LIST_ALL "all" #define CFG_FONT_NAME "name" #define CFG_FONT_SIZE "size" #define CFG_FONT_COLOR "color" #define CFG_FONT_BKG "background" #define CFG_FONT_SHADOW "shadow" #define CFG_INFO_SHOW "show" #define CFG_INFO_ITIMEOUT "info_timeout" #define CFG_INFO_STIMEOUT "status_timeout" #define CFG_INFO_CN "center" #define CFG_INFO_TL "top_left" #define CFG_INFO_TR "top_right" #define CFG_INFO_BL "bottom_left" #define CFG_INFO_BR "bottom_right" // Some configuration values #define CFG_YES "yes" #define CFG_NO "no" #define CFG_MODE_VIEWER "viewer" #define CFG_MODE_GALLERY "gallery" #define CFG_FROM_PARENT "parent" #define CFG_FROM_IMAGE "image" #define CFG_FULLSCREEN "fullscreen" /** * Load configuration from file. * @return loaded config instance */ struct config* config_load(void); /** * Free configuration instance. * @param cfg config instance */ void config_free(struct config* cfg); /** * Set config option. * @param cfg config instance * @param section section name * @param key,value configuration parameters * @return false if section or key is unknown */ bool config_set(struct config* cfg, const char* section, const char* key, const char* value); /** * Set config option from command line argument. * @param cfg config instance * @param arg command in format: "section.key=value" * @return false if section or key is unknown or argument is invalid */ bool config_set_arg(struct config* cfg, const char* arg); /** * Get default config option. * @param section section name * @param key,value configuration parameters * @return default value for specified options */ const char* config_get_default(const char* section, const char* key); /** * Get config option. * @param cfg config instance * @param section section name * @param key,value configuration parameters * @return value from the config or default value */ const char* config_get(const struct config* cfg, const char* section, const char* key); /** * Get config parameter as string value restricted by specified array. * @param cfg config instance * @param section,key section name and key * @param array array of possible values * @param array_sz number of strings in possible values array * @return index of the value or default value */ ssize_t config_get_oneof(const struct config* cfg, const char* section, const char* key, const char** array, size_t array_sz); /** * Get config parameter as boolean value. * @param cfg config instance * @param section,key section name and key * @return value or default value if user defined value is invalid */ bool config_get_bool(const struct config* cfg, const char* section, const char* key); /** * Get config parameter as integer value. * @param cfg config instance * @param section,key section name and key * @return value or default value if user defined value is invalid */ ssize_t config_get_num(const struct config* cfg, const char* section, const char* key, ssize_t min_val, ssize_t max_val); /** * Get config parameter as ARGB color value. * @param cfg config instance * @param section,key section name and key * @return value or default value if user defined value is invalid */ argb_t config_get_color(const struct config* cfg, const char* section, const char* key); /** * Expand path from environment variable. * @param prefix_env path prefix (var name) * @param postfix constant postfix * @return allocated buffer with path, caller should free it after use */ char* config_expand_path(const char* prefix_env, const char* postfix); /** * Print error about invalid key format. * @param section section name * @param key configuration parameters */ void config_error_key(const char* section, const char* key); /** * Print error about invalid value format. * @param section section name * @param value configuration parameters */ void config_error_val(const char* section, const char* value); swayimg-3.8/src/event.c000066400000000000000000000012651474536441700151430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Events processed by the viewer and gallery. // Copyright (C) 2024 Artem Senichev #include "event.h" #include #include #include int notification_create(void) { return eventfd(0, 0); } void notification_free(int fd) { close(fd); } void notification_raise(int fd) { const uint64_t value = 1; ssize_t len; do { len = write(fd, &value, sizeof(value)); } while (len == -1 && errno == EINTR); } void notification_reset(int fd) { uint64_t value; ssize_t len; do { len = read(fd, &value, sizeof(value)); } while (len == -1 && errno == EINTR); } swayimg-3.8/src/event.h000066400000000000000000000031121474536441700151410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Events processed by the viewer and gallery. // Copyright (C) 2024 Artem Senichev #pragma once #include "action.h" #include "image.h" /** Event types. */ enum event_type { event_action, ///< Apply action event_redraw, ///< Redraw window request event_resize, ///< Window resize notification event_drag, ///< Mouse or touch drag operation event_load, ///< Image loaded (preload thread notification) event_activate, ///< The mode is activating (viewer/gallery switch) }; /** Event description. */ struct event { enum event_type type; union event_params { const struct action* action; struct drag { int dx; int dy; } drag; struct activate { size_t index; } activate; struct load { struct image* image; size_t index; } load; } param; }; /** * Event handler declaration. * @param event event to handle */ typedef void (*event_handler)(const struct event* event); /** * Create notification (eventfd descriptor). * @return file descriptor or -1 on errors */ int notification_create(void); /** * Free notification instance. * @param fd file descriptor for the notification */ void notification_free(int fd); /** * Send notification through file descriptor. * @param fd file descriptor for the notification */ void notification_raise(int fd); /** * Reset notification after raising. * @param fd file descriptor for the notification */ void notification_reset(int fd); swayimg-3.8/src/exif.c000066400000000000000000000122511474536441700147520ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // EXIT reader. // Copyright (C) 2022 Artem Senichev #include "exif.h" #include #include /** * Fix orientation from EXIF data. * @param img target image instance * @param exif instance of EXIF reader */ static void fix_orientation(struct image* img, ExifData* exif) { const ExifEntry* entry = exif_data_get_entry(exif, EXIF_TAG_ORIENTATION); if (entry) { const ExifByteOrder byte_order = exif_data_get_byte_order(exif); switch (exif_get_short(entry->data, byte_order)) { case 2: // flipped back-to-front image_flip_horizontal(img); break; case 3: // upside down image_rotate(img, 180); break; case 4: // flipped back-to-front and upside down image_flip_vertical(img); break; case 5: // flipped back-to-front and on its side image_flip_horizontal(img); image_rotate(img, 90); break; case 6: // on its side image_rotate(img, 90); break; case 7: // flipped back-to-front and on its far side image_flip_vertical(img); image_rotate(img, 270); break; case 8: // on its far side image_rotate(img, 270); break; default: break; } } } /** * Add meta info from EXIF tag. * @param img target image instance * @param exif instance of EXIF reader * @param tag EXIF tag * @param name EXIF tag name */ static void add_meta(struct image* img, ExifData* exif, ExifTag tag, const char* name) { char value[64]; ExifEntry* entry = exif_data_get_entry(exif, tag); if (entry) { exif_entry_get_value(entry, value, sizeof(value)); if (*value) { image_add_meta(img, name, "%s", value); } } } /** * Append string. * @param buffer destination buffer * @param buffer_max size of the buffer * @param value value to append */ static void append(char* buffer, size_t buffer_max, const char* value) { const size_t current_len = strlen(buffer); const size_t value_len = strlen(value); if (current_len + value_len + 1 <= buffer_max) { memcpy(buffer + current_len, value, value_len + 1); } } /** * Read coordinate to string buffer. * @param exif instance of EXIF reader * @param tag location tag * @param ref location reference * @param buffer destination buffer * @param buffer_sz size of the buffer * @return number of bytes written to the buffer */ static size_t read_coordinate(ExifData* exif, ExifTag tag, ExifTag ref, char* buffer, size_t buffer_sz) { const char* delimiters = ", "; ExifEntry* entry; char value[32]; char* token; size_t index = 0; entry = exif_content_get_entry(exif->ifd[EXIF_IFD_GPS], tag); if (!entry) { return 0; } exif_entry_get_value(entry, value, sizeof(value)); if (!*value) { return 0; } buffer[0] = 0; token = strtok(value, delimiters); while (token) { append(buffer, buffer_sz, token); switch (index++) { case 0: append(buffer, buffer_sz, "°"); break; case 1: append(buffer, buffer_sz, "'"); break; case 2: append(buffer, buffer_sz, "\""); break; } token = strtok(NULL, delimiters); } entry = exif_content_get_entry(exif->ifd[EXIF_IFD_GPS], ref); if (entry) { exif_entry_get_value(entry, value, sizeof(value)); if (*value) { append(buffer, buffer_sz, value); } } return strlen(buffer); } /** * Read GPS location and add it to meta. * @param img target image instance * @param exif instance of EXIF reader */ static void read_location(struct image* img, ExifData* exif) { char latitude[32], longitude[32]; // NOLINTBEGIN(clang-analyzer-optin.core.EnumCastOutOfRange) if (read_coordinate(exif, EXIF_TAG_GPS_LATITUDE, EXIF_TAG_GPS_LATITUDE_REF, latitude, sizeof(latitude)) && read_coordinate(exif, EXIF_TAG_GPS_LONGITUDE, EXIF_TAG_GPS_LONGITUDE_REF, longitude, sizeof(longitude))) { image_add_meta(img, "Location", "%s, %s", latitude, longitude); } // NOLINTEND(clang-analyzer-optin.core.EnumCastOutOfRange) } void process_exif(struct image* img, const uint8_t* data, size_t size) { ExifData* exif = exif_data_new_from_data(data, (unsigned int)size); if (exif) { fix_orientation(img, exif); add_meta(img, exif, EXIF_TAG_DATE_TIME, "DateTime"); add_meta(img, exif, EXIF_TAG_MAKE, "Camera"); add_meta(img, exif, EXIF_TAG_MODEL, "Model"); add_meta(img, exif, EXIF_TAG_SOFTWARE, "Software"); add_meta(img, exif, EXIF_TAG_EXPOSURE_TIME, "Exposure"); add_meta(img, exif, EXIF_TAG_FNUMBER, "F Number"); read_location(img, exif); exif_data_unref(exif); } } swayimg-3.8/src/exif.h000066400000000000000000000005501474536441700147560ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // EXIF reader. // Copyright (C) 2022 Artem Senichev #pragma once #include "image.h" /** * Read and handle EXIF data. * @param img target image context * @param data image file data * @param size size of image data in bytes */ void process_exif(struct image* img, const uint8_t* data, size_t size); swayimg-3.8/src/fetcher.c000066400000000000000000000200061474536441700154340ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Images origin for viewer mode. // Copyright (C) 2024 Artem Senichev #include "fetcher.h" #include "application.h" #include "buildcfg.h" #include "imagelist.h" #include "loader.h" #include #include #include #ifdef HAVE_INOTIFY #include #endif /** Image cache queue. */ struct image_cache { size_t capacity; ///< Max length of the queue struct image** queue; ///< Cache queue }; /** Image fetch context. */ struct fetch { struct image* current; ///< Current image struct image_cache history; ///< Least recently viewed images struct image_cache preload; ///< Preloaded images #ifdef HAVE_INOTIFY int notify; ///< inotify file handler int watch; ///< Current file watcher #endif }; /** Global image fetch context. */ static struct fetch ctx; /** * Initialize cache queue. * @param cache context * @param capacity max size of the queue */ static void cache_init(struct image_cache* cache, size_t capacity) { cache->capacity = capacity; cache->queue = NULL; if (capacity) { cache->queue = calloc(1, capacity * sizeof(*cache->queue)); if (!cache->queue) { cache->capacity = 0; } } } /** * Reset (clear) cache queue. * @param cache context */ static void cache_reset(struct image_cache* cache) { for (size_t i = 0; i < cache->capacity; ++i) { struct image* img = cache->queue[i]; if (img) { image_free(img); cache->queue[i] = NULL; } } } /** * Free cache queue. * @param cache context */ static void cache_free(struct image_cache* cache) { cache_reset(cache); free(cache->queue); } /** * Put image handle to cache queue. * @param cache context * @param image pointer to image instance */ static void cache_put(struct image_cache* cache, struct image* image) { if (cache->capacity == 0 || !image) { return; } // check for empty entry for (size_t i = 0; i < cache->capacity; ++i) { if (!cache->queue[i]) { cache->queue[i] = image; return; } } // cache is full, remove the oldest entry from head image_free(cache->queue[0]); for (size_t i = 0; i < cache->capacity - 1; ++i) { cache->queue[i] = cache->queue[i + 1]; } // add new entry to tail cache->queue[cache->capacity - 1] = image; } /** * Take out image handle from cache queue. * @param cache context * @param index index of the image in the image list * @return image instance or NULL if image not in cache */ static struct image* cache_take(struct image_cache* cache, size_t index) { struct image* img = NULL; for (size_t i = 0; i < cache->capacity; ++i) { struct image* entry = cache->queue[i]; if (!entry) { break; // last entry } if (entry->index == index) { img = entry; // move remaining entries for (; i < cache->capacity - 1; ++i) { cache->queue[i] = cache->queue[i + 1]; } // remove last entry from queue cache->queue[cache->capacity - 1] = NULL; break; } } return img; } /** * Remove oldest entries from cache. * @param cache context * @param size number of entries to preserve */ static void cache_trim(struct image_cache* cache, size_t size) { if (size == 0) { cache_reset(cache); } else { for (size_t i = cache->capacity - 1; i > size; --i) { image_free(cache->queue[i]); cache->queue[i] = NULL; } } } #ifdef HAVE_INOTIFY /** inotify handler. */ static void on_inotify(__attribute__((unused)) void* data) { while (true) { bool updated = false; uint8_t buffer[1024]; ssize_t pos = 0; const ssize_t len = read(ctx.notify, buffer, sizeof(buffer)); if (len < 0) { if (errno == EINTR) { continue; } return; // something went wrong } while (pos + sizeof(struct inotify_event) <= (size_t)len) { const struct inotify_event* event = (struct inotify_event*)&buffer[pos]; if (event->mask & IN_IGNORED) { ctx.watch = -1; } else { updated = true; } pos += sizeof(struct inotify_event) + event->len; } if (updated) { app_reload(); } } } #endif // HAVE_INOTIFY /** Reset preloader queue. */ static void reset_preloader(void) { size_t* preload; size_t preload_num = 0; size_t found = 0; size_t next; if (ctx.preload.capacity == 0) { return; } loader_queue_reset(); preload = malloc(ctx.preload.capacity * sizeof(*preload)); if (!preload) { return; } // reorder preloads and create list of preloads next = ctx.current->index; for (size_t i = 0; i < ctx.preload.capacity; ++i) { struct image* img; next = image_list_next_file(next); if (next == IMGLIST_INVALID) { break; } img = cache_take(&ctx.preload, next); if (img) { cache_put(&ctx.preload, img); ++found; } else if (next != ctx.current->index) { preload[preload_num++] = next; } } // remove images that don't fit into cache size cache_trim(&ctx.preload, found); // add preloads to queue for (size_t i = 0; i < preload_num; ++i) { loader_queue_append(preload[i]); } free(preload); } /** * Set image as the current one. * @param image pointer to the image instance */ static void set_current(struct image* image) { // put current image to history cache if (ctx.current) { if (ctx.history.capacity) { cache_put(&ctx.history, ctx.current); } else { image_free(ctx.current); } } ctx.current = image; reset_preloader(); #ifdef HAVE_INOTIFY // register inotify watcher if (ctx.notify >= 0) { if (ctx.watch != -1) { inotify_rm_watch(ctx.notify, ctx.watch); } ctx.watch = inotify_add_watch(ctx.notify, ctx.current->source, IN_CLOSE_WRITE | IN_MOVE_SELF); } #endif } void fetcher_init(struct image* image, size_t history, size_t preload) { cache_init(&ctx.history, history); cache_init(&ctx.preload, preload); #ifdef HAVE_INOTIFY ctx.notify = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (ctx.notify >= 0) { app_watch(ctx.notify, on_inotify, NULL); ctx.watch = -1; } #endif // HAVE_INOTIFY if (image) { set_current(image); } } void fetcher_destroy(void) { cache_free(&ctx.history); cache_free(&ctx.preload); image_free(ctx.current); } bool fetcher_reset(size_t index, bool force) { loader_queue_reset(); cache_reset(&ctx.history); cache_reset(&ctx.preload); image_free(ctx.current); ctx.current = NULL; if (force && index != IMGLIST_INVALID) { fetcher_open(index); } else { if (index == IMGLIST_INVALID) { index = image_list_first(); } while (index != IMGLIST_INVALID && !fetcher_open(index)) { index = image_list_skip(index); } } return !!ctx.current; } bool fetcher_open(size_t index) { struct image* img; if (ctx.current && ctx.current->index == index) { return true; } // check history and preload img = cache_take(&ctx.history, index); if (!img) { img = cache_take(&ctx.preload, index); } if (!img) { loader_from_index(index, &img); } if (img) { set_current(img); } return !!img; } void fetcher_attach(struct image* image, size_t index) { if (image) { cache_put(&ctx.preload, image); } else { loader_queue_reset(); image_list_skip(index); reset_preloader(); } } struct image* fetcher_current(void) { return ctx.current; } swayimg-3.8/src/fetcher.h000066400000000000000000000023141474536441700154430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Images origin for viewer mode. // Copyright (C) 2024 Artem Senichev #pragma once #include "image.h" /** * Initialize global fetch context. * @param image initial image * @param history max number of images in history * @param preload max number of preloaded images */ void fetcher_init(struct image* image, size_t history, size_t preload); /** * Destroy global fetch context. */ void fetcher_destroy(void); /** * Reset cache and reload current image. * @param index preferable index of image in the image list * @param force flag to fail if loading specified image failed * @return true if image was reloaded */ bool fetcher_reset(size_t index, bool force); /** * Open image and set it as the current one. * @param index index of the image to fetch * @return true if image opened */ bool fetcher_open(size_t index); /** * Attach image to preload cache. * @param image loaded image instance, NULL if load error * @param index index of the image in the image list */ void fetcher_attach(struct image* image, size_t index); /** * Get current image. * @return current image or NULL if no image loaded yet */ struct image* fetcher_current(void); swayimg-3.8/src/font.c000066400000000000000000000152111474536441700147640ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Font renderer. // Copyright (C) 2022 Artem Senichev #include "font.h" #include "memdata.h" // font related #include #include #include FT_FREETYPE_H #include FT_GLYPH_H #define POINT_FACTOR 64.0 // default points per pixel for 26.6 format #define SPACE_WH_REL 2.0 #define BACKGROUND_PADDING 5 /** Font context. */ struct font { FT_Library lib; ///< Font lib instance FT_Face face; ///< Font face instance argb_t color; ///< Font color argb_t shadow; ///< Font shadow color argb_t background; ///< Font background }; /** Global font context instance. */ static struct font ctx; /** * Get path to the font file by its name. * @param name font name * @param font_file output buffer for file path * @param len size of buffer * @return false if font not found */ static bool search_font_file(const char* name, char* font_file, size_t len) { FcConfig* fc = NULL; font_file[0] = 0; font_file[len - 1] = 0; if (FcInit()) { fc = FcInitLoadConfigAndFonts(); if (fc) { FcPattern* fc_name = NULL; fc_name = FcNameParse((const FcChar8*)name); if (fc_name) { FcPattern* fc_font = NULL; FcResult result; FcConfigSubstitute(fc, fc_name, FcMatchPattern); FcDefaultSubstitute(fc_name); fc_font = FcFontMatch(fc, fc_name, &result); if (fc_font) { FcChar8* path = NULL; if (FcPatternGetString(fc_font, FC_FILE, 0, &path) == FcResultMatch) { strncpy(font_file, (const char*)path, len - 1); } FcPatternDestroy(fc_font); } FcPatternDestroy(fc_name); } FcConfigDestroy(fc); } FcFini(); } return *font_file; } /** * Calc size of the surface and allocate memory for the mask. * @param text string to print * @param surface text surface * @return base line offset or SIZE_MAX on errors */ static size_t allocate_surface(const wchar_t* text, struct text_surface* surface) { const FT_Size_Metrics* metrics = &ctx.face->size->metrics; const size_t space_size = metrics->x_ppem / SPACE_WH_REL; const size_t height = metrics->height / POINT_FACTOR; size_t base_offset = (ctx.face->ascender * (metrics->y_scale / 65536.0)) / POINT_FACTOR; size_t width = 0; uint8_t* data = NULL; size_t data_size; // get total width while (*text) { if (*text == L' ') { width += space_size; } else if (FT_Load_Char(ctx.face, *text, FT_LOAD_RENDER) == 0) { const FT_GlyphSlot glyph = ctx.face->glyph; width += glyph->advance.x / POINT_FACTOR; if ((FT_Int)base_offset < glyph->bitmap_top) { base_offset = glyph->bitmap_top; } } ++text; } // allocate surface buffer data_size = width * height; if (data_size) { data = realloc(surface->data, data_size); if (!data) { return SIZE_MAX; } surface->width = width; surface->height = height; surface->data = data; memset(surface->data, 0, data_size); } return base_offset; } void font_init(const struct config* cfg) { char font_file[256]; const char* font_name; size_t font_size; // load font font_name = config_get(cfg, CFG_FONT, CFG_FONT_NAME); if (!search_font_file(font_name, font_file, sizeof(font_file)) || FT_Init_FreeType(&ctx.lib) != 0 || FT_New_Face(ctx.lib, font_file, 0, &ctx.face) != 0) { fprintf(stderr, "WARNING: Unable to load font %s\n", font_name); return; } // set font size font_size = config_get_num(cfg, CFG_FONT, CFG_FONT_SIZE, 1, 256); FT_Set_Char_Size(ctx.face, font_size * POINT_FACTOR, 0, 96, 0); // color/background/shadow parameters ctx.color = config_get_color(cfg, CFG_FONT, CFG_FONT_COLOR); ctx.background = config_get_color(cfg, CFG_FONT, CFG_FONT_BKG); ctx.shadow = config_get_color(cfg, CFG_FONT, CFG_FONT_SHADOW); } void font_destroy(void) { if (ctx.face) { FT_Done_Face(ctx.face); } if (ctx.lib) { FT_Done_FreeType(ctx.lib); } } bool font_render(const char* text, struct text_surface* surface) { size_t space_size; size_t base_offset; wchar_t* wide; wchar_t* it; size_t x = 0; if (!ctx.face) { return false; } space_size = ctx.face->size->metrics.x_ppem / SPACE_WH_REL; wide = str_to_wide(text, NULL); if (!wide) { return false; } base_offset = allocate_surface(wide, surface); if (base_offset == SIZE_MAX) { free(wide); return false; } // draw glyphs it = wide; while (*it) { if (*it == L' ') { x += space_size; } else if (FT_Load_Char(ctx.face, *it, FT_LOAD_RENDER) == 0) { const FT_GlyphSlot glyph = ctx.face->glyph; const FT_Bitmap* bmp = &glyph->bitmap; const size_t off_y = base_offset - glyph->bitmap_top; size_t size; // calc line width, floating point math doesn't match bmp width if (x + bmp->width < surface->width) { size = bmp->width; } else { size = surface->width - x; } // put glyph's bitmap on the surface for (size_t y = 0; y < bmp->rows; ++y) { const size_t offset = (y + off_y) * surface->width + x; uint8_t* dst = &surface->data[offset + glyph->bitmap_left]; memcpy(dst, &bmp->buffer[y * bmp->pitch], size); } x += glyph->advance.x / POINT_FACTOR; } ++it; } free(wide); return true; } void font_print(struct pixmap* wnd, ssize_t x, ssize_t y, const struct text_surface* text) { if (ARGB_GET_A(ctx.background)) { pixmap_blend(wnd, x - BACKGROUND_PADDING, y, text->width + BACKGROUND_PADDING * 2, text->height, ctx.background); } if (ARGB_GET_A(ctx.shadow)) { ssize_t shadow_offset = text->height / 16; if (shadow_offset < 1) { shadow_offset = 1; } pixmap_apply_mask(wnd, x + shadow_offset, y + shadow_offset, text->data, text->width, text->height, ctx.shadow); } pixmap_apply_mask(wnd, x, y, text->data, text->width, text->height, ctx.color); } swayimg-3.8/src/font.h000066400000000000000000000017271474536441700150000ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Font renderer. // Copyright (C) 2022 Artem Senichev #pragma once #include "config.h" /** Text surface: array of alpha pixels. */ struct text_surface { size_t width; ///< Width (px) size_t height; ///< Height (px) uint8_t* data; ///< Pixel data }; /** * Initialize global font context. * @param cfg config instance */ void font_init(const struct config* cfg); /** * Destroy global font context. */ void font_destroy(void); /** * Render single text line. * @param text string to print * @param surface text surface to reallocate * @return true if operation completed successfully */ bool font_render(const char* text, struct text_surface* surface); /** * Print surface line on the window. * @param wnd destination window * @param x,y text position * @param text text surface to draw */ void font_print(struct pixmap* wnd, ssize_t x, ssize_t y, const struct text_surface* text); swayimg-3.8/src/formats/000077500000000000000000000000001474536441700153255ustar00rootroot00000000000000swayimg-3.8/src/formats/avif.c000066400000000000000000000100471474536441700164200ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // AV1 (AVIF/AVIFS) format decoder. // Copyright (C) 2023 Artem Senichev #include "../loader.h" #include #include // AVI signature static const uint32_t signature = 'f' | 't' << 8 | 'y' << 16 | 'p' << 24; #define SIGNATURE_OFFSET 4 static int decode_frame(struct image* ctx, avifDecoder* decoder) { avifRGBImage rgb; avifResult rc; struct pixmap* pm; rc = avifDecoderNextImage(decoder); if (rc != AVIF_RESULT_OK) { goto decode_fail; } memset(&rgb, 0, sizeof(rgb)); avifRGBImageSetDefaults(&rgb, decoder->image); rgb.depth = 8; rgb.format = AVIF_RGB_FORMAT_BGRA; #if AVIF_VERSION_MAJOR > 0 rc = #endif avifRGBImageAllocatePixels(&rgb); #if AVIF_VERSION_MAJOR > 0 if (rc != AVIF_RESULT_OK) { goto decode_fail; } #endif rc = avifImageYUVToRGB(decoder->image, &rgb); if (rc != AVIF_RESULT_OK) { goto fail_pixels; } pm = image_allocate_frame(ctx, decoder->image->width, decoder->image->height); if (!pm) { goto fail_pixels; } memcpy(pm->data, rgb.pixels, rgb.width * rgb.height * sizeof(argb_t)); avifRGBImageFreePixels(&rgb); return 0; fail_pixels: avifRGBImageFreePixels(&rgb); decode_fail: return -1; } static int decode_frames(struct image* ctx, avifDecoder* decoder) { avifImageTiming timing; avifRGBImage rgb = { 0 }; avifResult rc = AVIF_RESULT_UNKNOWN_ERROR; if (!image_create_frames(ctx, decoder->imageCount)) { return AVIF_RESULT_UNKNOWN_ERROR; } for (size_t i = 0; i < ctx->num_frames; ++i) { rc = avifDecoderNthImage(decoder, i); if (rc != AVIF_RESULT_OK) { break; } avifRGBImageSetDefaults(&rgb, decoder->image); rgb.depth = 8; rgb.format = AVIF_RGB_FORMAT_BGRA; #if AVIF_VERSION_MAJOR == 0 avifRGBImageAllocatePixels(&rgb); #else rc = avifRGBImageAllocatePixels(&rgb); if (rc != AVIF_RESULT_OK) { break; } #endif rc = avifImageYUVToRGB(decoder->image, &rgb); if (rc != AVIF_RESULT_OK) { break; } if (!pixmap_create(&ctx->frames[i].pm, rgb.width, rgb.height)) { break; } rc = avifDecoderNthImageTiming(decoder, i, &timing); if (rc != AVIF_RESULT_OK) { break; } ctx->frames[i].duration = (size_t)(1000.0f / (float)timing.timescale * (float)timing.durationInTimescales); memcpy(ctx->frames[i].pm.data, rgb.pixels, rgb.width * rgb.height * sizeof(argb_t)); avifRGBImageFreePixels(&rgb); rgb.pixels = NULL; } if (rgb.pixels) { avifRGBImageFreePixels(&rgb); } return rc; } // AV1 loader implementation enum loader_status decode_avif(struct image* ctx, const uint8_t* data, size_t size) { avifResult rc; avifDecoder* decoder = NULL; int ret; // check signature if (size < SIGNATURE_OFFSET + sizeof(signature) || *(const uint32_t*)(data + SIGNATURE_OFFSET) != signature) { return ldr_unsupported; } // open file in decoder decoder = avifDecoderCreate(); if (!decoder) { return ldr_fmterror; } rc = avifDecoderSetIOMemory(decoder, data, size); if (rc != AVIF_RESULT_OK) { goto fail; } rc = avifDecoderParse(decoder); if (rc != AVIF_RESULT_OK) { goto fail; } if (decoder->imageCount > 1) { ret = decode_frames(ctx, decoder); } else { ret = decode_frame(ctx, decoder); } if (ret != 0) { goto fail; } ctx->alpha = decoder->alphaPresent; image_set_format(ctx, "AV1 %dbpc %s", decoder->image->depth, avifPixelFormatToString(decoder->image->yuvFormat)); avifDecoderDestroy(decoder); return ldr_success; fail: avifDecoderDestroy(decoder); image_free_frames(ctx); return ldr_fmterror; } swayimg-3.8/src/formats/bmp.c000066400000000000000000000323371474536441700162570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // BMP format decoder. // Copyright (C) 2020 Artem Senichev #include "../loader.h" #include // BMP type id #define BMP_TYPE ('B' | ('M' << 8)) // Compression types #define BI_RGB 0 #define BI_RLE8 1 #define BI_RLE4 2 #define BI_BITFIELDS 3 // RLE escape codes #define RLE_ESC_EOL 0 #define RLE_ESC_EOF 1 #define RLE_ESC_DELTA 2 // Default mask for 16-bit images #define MASK555_RED 0x7c00 #define MASK555_GREEN 0x03e0 #define MASK555_BLUE 0x001f #define MASK555_ALPHA 0x0000 // Sizes of DIB Headers #define BITMAPINFOHEADER_SIZE 0x28 #define BITMAPINFOV2HEADER_SIZE 0x34 #define BITMAPINFOV3HEADER_SIZE 0x38 #define BITMAPINFOV4HEADER_SIZE 0x6C #define BITMAPINFOV5HEADER_SIZE 0x7C #define BITS_PER_BYTE 8 // Bitmap file header: BITMAPFILEHEADER struct __attribute__((__packed__)) bmp_file { uint16_t type; uint32_t file_size; uint32_t reserved; uint32_t offset; }; // Bitmap info: BITMAPINFOHEADER struct __attribute__((__packed__)) bmp_info { uint32_t dib_size; int32_t width; int32_t height; uint16_t planes; uint16_t bpp; uint32_t compression; uint32_t img_size; uint32_t hres; uint32_t vres; uint32_t clr_palette; uint32_t clr_important; }; // Masks used for for 16bit images struct __attribute__((__packed__)) bmp_mask { uint32_t red; uint32_t green; uint32_t blue; uint32_t alpha; }; // Color palette struct bmp_palette { const uint32_t* table; size_t size; }; /** * Get number of the consecutive zero bits (trailing) on the right. * @param val source value * @return number of zero bits */ static inline size_t right_zeros(uint32_t val) { size_t count = sizeof(uint32_t) * BITS_PER_BYTE; val &= -(int32_t)val; if (val) { --count; } if (val & 0x0000ffff) { count -= 16; } if (val & 0x00ff00ff) { count -= 8; } if (val & 0x0f0f0f0f) { count -= 4; } if (val & 0x33333333) { count -= 2; } if (val & 0x55555555) { count -= 1; } return count; } /** * Get number of bits set. * @param val source value * @return number of bits set */ static inline size_t bits_set(uint32_t val) { val = val - ((val >> 1) & 0x55555555); val = (val & 0x33333333) + ((val >> 2) & 0x33333333); return (((val + (val >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; } /** * Get shift size for color channel. * @param val color channel mask * @return shift size: positive=right, negative=left */ static inline ssize_t mask_shift(uint32_t mask) { const ssize_t start = right_zeros(mask) + bits_set(mask); return start - BITS_PER_BYTE; } /** * Decode bitmap with masked colors. * @param img decoded image context * @param bmp bitmap info * @param mask channels mask * @param buffer input bitmap buffer * @param buffer_sz size of buffer * @return false if input buffer has errors */ static bool decode_masked(struct image* ctx, const struct bmp_info* bmp, const struct bmp_mask* mask, const uint8_t* buffer, size_t buffer_sz) { struct pixmap* pm = &ctx->frames[0].pm; const bool default_mask = !mask || (mask->red == 0 && mask->green == 0 && mask->blue == 0 && mask->alpha == 0); const uint32_t mask_r = default_mask ? MASK555_RED : mask->red; const uint32_t mask_g = default_mask ? MASK555_GREEN : mask->green; const uint32_t mask_b = default_mask ? MASK555_BLUE : mask->blue; const uint32_t mask_a = default_mask ? MASK555_ALPHA : mask->alpha; const ssize_t shift_r = mask_shift(mask_r); const ssize_t shift_g = mask_shift(mask_g); const ssize_t shift_b = mask_shift(mask_b); const ssize_t shift_a = mask_shift(mask_a); const size_t stride = 4 * ((bmp->width * bmp->bpp + 31) / 32); // check size of source buffer if (buffer_sz < pm->height * stride) { return false; } for (size_t y = 0; y < pm->height; ++y) { argb_t* dst = &pm->data[y * pm->width]; const uint8_t* src_y = buffer + y * stride; for (size_t x = 0; x < pm->width; ++x) { const uint8_t* src = src_y + x * (bmp->bpp / BITS_PER_BYTE); uint32_t m, r, g, b, a; if (bmp->bpp == 32) { m = *(uint32_t*)src; } else if (bmp->bpp == 16) { m = *(uint16_t*)src; } else { return false; } r = m & mask_r; g = m & mask_g; b = m & mask_b; r = 0xff & (shift_r > 0 ? r >> shift_r : r << -shift_r); g = 0xff & (shift_g > 0 ? g >> shift_g : g << -shift_g); b = 0xff & (shift_b > 0 ? b >> shift_b : b << -shift_b); if (mask_a) { a = m & mask_a; a = 0xff & (shift_a > 0 ? a >> shift_a : a << -shift_a); } else { a = 0xff; } dst[x] = ARGB_SET_A(a) | ARGB_SET_R(r) | ARGB_SET_G(g) | ARGB_SET_B(b); } } return true; } /** * Decode RLE compressed bitmap. * @param img decoded image context * @param bmp bitmap info * @param palette color palette * @param buffer input bitmap buffer * @param buffer_sz size of buffer * @return false if input buffer has errors */ static bool decode_rle(struct image* ctx, const struct bmp_info* bmp, const struct bmp_palette* palette, const uint8_t* buffer, size_t buffer_sz) { struct pixmap* pm = &ctx->frames[0].pm; size_t x = 0, y = 0; size_t buffer_pos = 0; while (buffer_pos + 2 <= buffer_sz) { uint8_t rle1 = buffer[buffer_pos++]; const uint8_t rle2 = buffer[buffer_pos++]; if (rle1 == 0) { // escape code if (rle2 == RLE_ESC_EOL) { x = 0; ++y; } else if (rle2 == RLE_ESC_EOF) { // remove alpha channel argb_t* ptr = pm->data; while (ptr < pm->data + pm->width * pm->height) { *ptr |= ARGB_SET_A(0xff); ++ptr; } return true; } else if (rle2 == RLE_ESC_DELTA) { if (buffer_pos + 2 >= buffer_sz) { return false; } x += buffer[buffer_pos++]; y += buffer[buffer_pos++]; } else { // absolute mode if (buffer_pos + (bmp->compression == BI_RLE4 ? rle2 / 2 : rle2) > buffer_sz) { return false; } if (x + rle2 > pm->width || y >= pm->height) { return false; } uint8_t val = 0; for (size_t i = 0; i < rle2; ++i) { uint8_t index; if (bmp->compression == BI_RLE8) { index = buffer[buffer_pos++]; } else { if (i & 1) { index = val & 0x0f; } else { val = buffer[buffer_pos++]; index = val >> 4; } } if (index >= palette->size) { return false; } pm->data[y * bmp->width + x] = palette->table[index]; ++x; } if ((bmp->compression == BI_RLE8 && rle2 & 1) || (bmp->compression == BI_RLE4 && ((rle2 & 3) == 1 || (rle2 & 3) == 2))) { ++buffer_pos; // zero-padded 16-bit } } } else { // encoded mode if (x + rle1 > pm->width) { rle1 = pm->width - x; } if (y >= pm->height) { return false; } if (bmp->compression == BI_RLE8) { // 8 bpp if (rle2 >= palette->size) { return false; } for (size_t i = 0; i < rle1; ++i) { pm->data[y * pm->width + x] = palette->table[rle2]; ++x; } } else { // 4 bpp const uint8_t index[] = { rle2 >> 4, rle2 & 0x0f }; if (index[0] >= palette->size || index[1] >= palette->size) { return false; } for (size_t i = 0; i < rle1; ++i) { pm->data[y * pm->width + x] = palette->table[index[i & 1]]; ++x; } } } } return false; } /** * Decode uncompressed bitmap. * @param img decoded image context * @param palette color palette * @param buffer input bitmap buffer * @param buffer_sz size of buffer * @param decoded output data buffer * @return false if input buffer has errors */ static bool decode_rgb(struct image* ctx, const struct bmp_info* bmp, const struct bmp_palette* palette, const uint8_t* buffer, size_t buffer_sz) { struct pixmap* pm = &ctx->frames[0].pm; const size_t stride = 4 * ((bmp->width * bmp->bpp + 31) / 32); // check size of source buffer if (buffer_sz < pm->height * stride) { return false; } for (size_t y = 0; y < pm->height; ++y) { argb_t* dst = &pm->data[y * pm->width]; const uint8_t* src_y = buffer + y * stride; for (size_t x = 0; x < pm->width; ++x) { const uint8_t* src = src_y + x * (bmp->bpp / BITS_PER_BYTE); if (bmp->bpp == 32) { dst[x] = ARGB_SET_A(0xff) | *(uint32_t*)src; } else if (bmp->bpp == 24) { dst[x] = ARGB_SET_A(0xff) | *(uint32_t*)src; } else if (bmp->bpp == 8 || bmp->bpp == 4 || bmp->bpp == 1) { // indexed colors const size_t bits_offset = x * bmp->bpp; const size_t byte_offset = bits_offset / BITS_PER_BYTE; const size_t start_bit = bits_offset - byte_offset * BITS_PER_BYTE; const uint8_t index = (*(src_y + byte_offset) >> (BITS_PER_BYTE - bmp->bpp - start_bit)) & (0xff >> (BITS_PER_BYTE - bmp->bpp)); if (index >= palette->size) { return false; } dst[x] = ARGB_SET_A(0xff) | palette->table[index]; } else { return false; } } } return true; } // BMP loader implementation enum loader_status decode_bmp(struct image* ctx, const uint8_t* data, size_t size) { const struct bmp_file* hdr; const struct bmp_info* bmp; const void* color_data; size_t color_data_sz; struct bmp_palette palette; const uint32_t* mask_location; struct bmp_mask mask; bool rc; hdr = (const struct bmp_file*)data; bmp = (const struct bmp_info*)(data + sizeof(*hdr)); // check format if (size < sizeof(*hdr) || hdr->type != BMP_TYPE) { return ldr_unsupported; } if (hdr->offset >= size || hdr->offset < sizeof(struct bmp_file) + sizeof(struct bmp_info)) { return ldr_fmterror; } if (bmp->dib_size > hdr->offset) { return ldr_fmterror; } if (!image_allocate_frame(ctx, abs(bmp->width), abs(bmp->height))) { return ldr_fmterror; } color_data = (const uint8_t*)bmp + bmp->dib_size; color_data_sz = hdr->offset - sizeof(struct bmp_file) - bmp->dib_size; palette.table = color_data; palette.size = color_data_sz / sizeof(uint32_t); // create mask if (bmp->dib_size > BITMAPINFOHEADER_SIZE) { mask_location = (const uint32_t*)(bmp + 1); } else { mask_location = (color_data_sz >= 3 * sizeof(uint32_t) ? color_data : NULL); } if (!mask_location) { mask.red = mask.green = mask.blue = mask.alpha = 0; } else { mask.red = mask_location[0]; mask.green = mask_location[1]; mask.blue = mask_location[2]; mask.alpha = bmp->dib_size > BITMAPINFOV2HEADER_SIZE ? mask_location[3] : 0; } // decode bitmap if (bmp->compression == BI_BITFIELDS || bmp->bpp == 16) { rc = decode_masked(ctx, bmp, &mask, data + hdr->offset, size - hdr->offset); image_set_format(ctx, "BMP %dbit masked", bmp->bpp); } else if (bmp->compression == BI_RLE8 || bmp->compression == BI_RLE4) { rc = decode_rle(ctx, bmp, &palette, data + hdr->offset, size - hdr->offset); image_set_format(ctx, "BMP %dbit RLE", bmp->bpp); } else if (bmp->compression == BI_RGB) { rc = decode_rgb(ctx, bmp, &palette, data + hdr->offset, size - hdr->offset); image_set_format(ctx, "BMP %dbit uncompressed", bmp->bpp); } else { rc = false; } if (rc) { if (bmp->height > 0) { image_flip_vertical(ctx); } ctx->alpha = bmp->bpp == 32; } else { image_free_frames(ctx); } return (rc ? ldr_success : ldr_fmterror); } swayimg-3.8/src/formats/dicom.c000066400000000000000000000164631474536441700165760ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // DICOM format decoder. // Copyright (C) 2024 Artem Senichev #include "../loader.h" #include #include // DICOM signature static const uint8_t signature[] = { 'D', 'I', 'C', 'M' }; #define DICOM_SIGNATURE_OFFSET 128 // DICOM tags #define TAG_SAMPLES_PER_PIXEL 0x00280002 #define TAG_ROWS 0x00280010 #define TAG_COLUMNS 0x00280011 #define TAG_BIT_ALLOCATED 0x00280100 #define TAG_SMALL_PIXEL_VAL 0x00280106 #define TAG_BIG_PIXEL_VAL 0x00280107 #define TAG_PIXEL_DATA 0x7fe00010 // DICOM element value types enum value_representation { VR_AE = 'A' | ('E' << 8), VR_AS = 'A' | ('S' << 8), VR_AT = 'A' | ('T' << 8), VR_CS = 'C' | ('S' << 8), VR_DA = 'D' | ('A' << 8), VR_DS = 'D' | ('S' << 8), VR_DT = 'D' | ('T' << 8), VR_FD = 'F' | ('D' << 8), VR_FL = 'F' | ('L' << 8), VR_IS = 'I' | ('S' << 8), VR_LO = 'L' | ('O' << 8), VR_LT = 'L' | ('T' << 8), VR_PN = 'P' | ('N' << 8), VR_SH = 'S' | ('H' << 8), VR_SL = 'S' | ('L' << 8), VR_SS = 'S' | ('S' << 8), VR_ST = 'S' | ('T' << 8), VR_TM = 'T' | ('M' << 8), VR_UI = 'U' | ('I' << 8), VR_UL = 'U' | ('L' << 8), VR_US = 'U' | ('S' << 8), VR_UT = 'U' | ('T' << 8), VR_OB = 'O' | ('B' << 8), VR_OW = 'O' | ('W' << 8), VR_SQ = 'S' | ('Q' << 8), VR_UN = 'U' | ('N' << 8), VR_QQ = 'Q' | ('Q' << 8), VR_RT = 'R' | ('T' << 8), }; // DICOM image description struct dicom_image { uint16_t spp; ///< Samples per Pixel uint16_t bpp; ///< Number of bits allocated for each pixel sample uint16_t width; ///< Image width uint16_t height; ///< Image height int16_t px_min; ///< Min pixel value encountered in the image int16_t px_max; ///< Max pixel value encountered in the image const uint8_t* data; ///< Image data size_t data_sz; ///< Size of data in bytes }; // DICOM element description struct element { uint32_t tag; uint16_t vr; const void* data; size_t size; }; // Binary stream struct stream { const uint8_t* data; size_t size; size_t pos; }; /** * Consume data from the stream. * @param stream binary stream * @param bytes number of bytes to consume * @return pointer to the data or NULL on End of stream */ static const uint8_t* consume(struct stream* stream, size_t bytes) { const uint8_t* ptr = NULL; const size_t end = stream->pos + bytes; if (end <= stream->size) { ptr = stream->data + stream->pos; stream->pos = end; } return ptr; } /** * Read next data element from the stream. * @param stream binary stream * @param element output element description * @return false if no more elements int the stream */ static bool next_element(struct stream* stream, struct element* element) { const uint8_t* data; // read tag if (!(data = consume(stream, sizeof(element->tag)))) { return false; } element->tag = (*(const uint16_t*)data) << 16; element->tag |= *(const uint16_t*)(data + sizeof(uint16_t)); // read value representation (type) if (!(data = consume(stream, sizeof(element->vr)))) { return false; } element->vr = *(const uint16_t*)data; // get payload size if (!(data = consume(stream, sizeof(uint16_t)))) { return false; } element->size = *(const uint16_t*)data; if (element->size == 0 && (element->vr == VR_OB || element->vr == VR_OW || element->vr == VR_SQ || element->vr == VR_UN || element->vr == VR_UT)) { if (!(data = consume(stream, sizeof(uint32_t)))) { return false; } element->size = *(const uint32_t*)data; } // get payload data if (element->size == 0) { element->data = NULL; } else if (!(element->data = consume(stream, element->size))) { return false; } return true; } /** * Get image description from the stream. * @param stream binary stream * @param image output image description * @return true if image description is valid */ static bool get_image(struct stream* stream, struct dicom_image* image) { struct element el; memset(image, 0, sizeof(*image)); // collect info while (next_element(stream, &el)) { if (!el.data) { continue; } if (el.tag == TAG_SAMPLES_PER_PIXEL && el.vr == VR_US) { image->spp = *(const uint16_t*)el.data; } else if (el.tag == TAG_ROWS && el.vr == VR_US) { image->height = *(const uint16_t*)el.data; } else if (el.tag == TAG_COLUMNS && el.vr == VR_US) { image->width = *(const uint16_t*)el.data; } else if (el.tag == TAG_BIT_ALLOCATED && el.vr == VR_US) { image->bpp = *(const uint16_t*)el.data; } else if (el.tag == TAG_SMALL_PIXEL_VAL && el.vr == VR_SS) { image->px_min = *(const int16_t*)el.data; } else if (el.tag == TAG_BIG_PIXEL_VAL && el.vr == VR_SS) { image->px_max = *(const int16_t*)el.data; } else if (el.tag == TAG_PIXEL_DATA && el.vr == VR_OW) { image->data = el.data; image->data_sz = el.size; } } // check if (!image->data || image->height == 0 || image->width == 0 || image->data_sz != image->width * image->height * (image->bpp / 8)) { return false; } return true; } // DICOM loader implementation enum loader_status decode_dicom(struct image* ctx, const uint8_t* data, size_t size) { struct dicom_image dicom; struct stream stream; struct pixmap* pm; size_t total_pixels; double pixel_coeff; // check signature if (size < DICOM_SIGNATURE_OFFSET + sizeof(signature) || memcmp(data + DICOM_SIGNATURE_OFFSET, signature, sizeof(signature))) { return ldr_unsupported; } // get image description stream.data = data; stream.size = size; stream.pos = DICOM_SIGNATURE_OFFSET + sizeof(signature); if (!get_image(&stream, &dicom) || dicom.spp != 1 /* monochrome */ || dicom.bpp != 16 /* 2 bytes per pixel */) { return ldr_fmterror; } // calculate min/max color value if not set yet if (dicom.px_max == 0 || dicom.px_max <= dicom.px_min) { dicom.px_min = INT16_MAX; for (size_t i = 0; i < dicom.data_sz; i += sizeof(int16_t)) { const int16_t color = *(const int16_t*)(dicom.data + i); if (dicom.px_max < color) { dicom.px_max = color; } if (dicom.px_min > color) { dicom.px_min = color; } } } // Calculate coefficient for converting 16-bit color to 8-bit if (dicom.px_max <= dicom.px_min) { pixel_coeff = 1.0; } else { pixel_coeff = 256.0 / (dicom.px_max - dicom.px_min); } // allocate image buffer pm = image_allocate_frame(ctx, dicom.width, dicom.height); if (!pm) { return ldr_fmterror; } // decode image total_pixels = dicom.width * dicom.height; for (size_t i = 0; i < total_pixels; ++i) { int16_t color = *((const int16_t*)dicom.data + i); color -= dicom.px_min; color *= pixel_coeff; pm->data[i] = ARGB(0xff, color, color, color); } image_set_format(ctx, "DICOM"); return ldr_success; } swayimg-3.8/src/formats/exr.c000066400000000000000000000261771474536441700163040ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // EXR format decoder. // Copyright (C) 2023 Artem Senichev #include "../loader.h" #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop #pragma GCC diagnostic ignored "-Wmissing-field-initializers" // EXR signature static const uint8_t signature[] = { 0x76, 0x2f, 0x31, 0x01 }; // EXR data buffer struct data_buffer { const uint8_t* data; const size_t size; }; // EXR data reader static int64_t exr_reader(__attribute__((unused)) exr_const_context_t exr, void* userdata, void* buffer, uint64_t sz, uint64_t offset, __attribute__((unused)) exr_stream_error_func_ptr_t error_cb) { const struct data_buffer* buf = userdata; if (offset + sz > buf->size) { return -1; } memcpy(buffer, buf->data + offset, sz); return sz; } /** * Decode single chunk. * @param ectx EXR context * @param chunk source chunk info * @param decoder EXR decoder instance * @param buffer data pointer for unpacked data of the current chunk * @return result code */ static exr_result_t decode_chunk(const exr_context_t ectx, const exr_chunk_info_t* chunk, exr_decode_pipeline_t* decoder, uint8_t* buffer) { exr_result_t rc; int8_t bpp = 0; // initialize decoder if (decoder->channels) { rc = exr_decoding_update(ectx, 0, chunk, decoder); } else { rc = exr_decoding_initialize(ectx, 0, chunk, decoder); if (rc == EXR_ERR_SUCCESS) { rc = exr_decoding_choose_default_routines(ectx, 0, decoder); } } if (rc != EXR_ERR_SUCCESS) { return rc; } // configure output buffer for (int16_t i = 0; i < decoder->channel_count; ++i) { bpp += decoder->channels[i].bytes_per_element; } for (int16_t i = 0; i < decoder->channel_count; ++i) { exr_coding_channel_info_t* cci = &decoder->channels[i]; cci->decode_to_ptr = buffer; cci->user_pixel_stride = bpp; cci->user_line_stride = cci->width * bpp; cci->user_bytes_per_element = decoder->channels[i].bytes_per_element; buffer += decoder->channels[i].bytes_per_element; } // decode current chunk return exr_decoding_run(ectx, 0, decoder); } /** * Decode pixel. * @param decoder EXR decoder instance * @param unpacked pointer to unpacked data * @param size max number of bytes in unpacked buffer * @return ARGB value of the pixel */ static argb_t decode_pixel(const exr_decode_pipeline_t* decoder, const uint8_t* unpacked, size_t size) { uint8_t a = 0xff, r = 0, g = 0, b = 0; for (int16_t i = 0; i < decoder->channel_count; ++i) { const exr_coding_channel_info_t* channel = &decoder->channels[i]; float intensity = 0; size_t color; // it's all a dirty hack =) switch (channel->data_type) { case EXR_PIXEL_UINT: break; // not supported case EXR_PIXEL_HALF: if (size >= sizeof(uint16_t)) { // convert half to float // todo // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Assign) const uint16_t half = *(const uint16_t*)unpacked; union { uint32_t i; float f; } hf; hf.i = (half & 0x8000) << 16; hf.i |= ((half & 0x7c00) + 0x1c000) << 13; hf.i |= (half & 0x03ff) << 13; intensity = hf.f; } break; case EXR_PIXEL_FLOAT: if (size >= sizeof(float)) { // todo // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Assign) intensity = *(const float*)unpacked; } break; default: break; // not supported } color = intensity * 0xff; if (color > 0xff) { color = 0xff; } switch (*channel->channel_name) { case 'A': a = color; break; case 'R': r = color; break; case 'G': g = color; break; case 'B': b = color; break; default: break; // not supported } unpacked += channel->bytes_per_element; if (size <= (size_t)channel->bytes_per_element) { break; } size -= channel->bytes_per_element; } return ARGB_SET_A(a) | ARGB_SET_R(r) | ARGB_SET_G(g) | ARGB_SET_B(b); } /** * Load scanlined EXR image. * @param ectx EXR context * @param pm destination pixmap * @return result code */ static exr_result_t load_scanlined(const exr_context_t ectx, struct pixmap* pm) { exr_result_t rc; int32_t scanlines; uint64_t chunk_size; uint8_t* buffer = NULL; exr_decode_pipeline_t decoder = EXR_DECODE_PIPELINE_INITIALIZER; argb_t* dst = pm->data; exr_attr_box2i_t dwnd; // temporary buffer for decoded chunk's scanlines rc = exr_get_chunk_unpacked_size(ectx, 0, &chunk_size); if (rc != EXR_ERR_SUCCESS) { return rc; } buffer = malloc(chunk_size); if (!buffer) { return EXR_ERR_OUT_OF_MEMORY; } // get image properties rc = exr_get_data_window(ectx, 0, &dwnd); if (rc != EXR_ERR_SUCCESS) { goto done; } rc = exr_get_scanlines_per_chunk(ectx, 0, &scanlines); if (rc != EXR_ERR_SUCCESS) { goto done; } // decode chunks for (int32_t y = dwnd.min.y; y <= dwnd.max.y; y += scanlines) { exr_chunk_info_t chunk; int8_t bpp = 0; rc = exr_read_scanline_chunk_info(ectx, 0, y, &chunk); if (rc != EXR_ERR_SUCCESS) { break; } rc = decode_chunk(ectx, &chunk, &decoder, buffer); if (rc != EXR_ERR_SUCCESS) { break; } // put pixels to final image for (int16_t i = 0; i < decoder.channel_count; ++i) { bpp += decoder.channels[i].bytes_per_element; } for (size_t i = 0; i < chunk_size; i += bpp) { if (dst >= pm->data + (pm->width * pm->height)) { goto done; } *dst = decode_pixel(&decoder, buffer + i, chunk_size - i); ++dst; } } done: exr_decoding_destroy(ectx, &decoder); free(buffer); return rc; } /** * Load tailed EXR image. * @param ectx EXR context * @param pm destination pixmap * @return result code */ static exr_result_t load_tailed(const exr_context_t ectx, struct pixmap* pm) { exr_result_t rc; uint64_t chunk_size; uint8_t* buffer = NULL; int32_t levels_x, levels_y; exr_decode_pipeline_t decoder = EXR_DECODE_PIPELINE_INITIALIZER; // temporary buffer for decoded chunk's scanlines rc = exr_get_chunk_unpacked_size(ectx, 0, &chunk_size); if (rc != EXR_ERR_SUCCESS) { return rc; } buffer = malloc(chunk_size); if (!buffer) { return EXR_ERR_OUT_OF_MEMORY; } rc = exr_get_tile_levels(ectx, 0, &levels_x, &levels_y); if (rc != EXR_ERR_SUCCESS) { goto done; } for (int32_t lvl_y = 0; lvl_y < levels_y; ++lvl_y) { for (int32_t lvl_x = 0; lvl_x < levels_x; ++lvl_x) { int32_t lvl_w, lvl_h; int32_t tile_w, tile_h; int tile_x, tile_y; exr_chunk_info_t chunk; exr_decode_pipeline_t decoder = EXR_DECODE_PIPELINE_INITIALIZER; rc = exr_get_level_sizes(ectx, 0, lvl_x, lvl_y, &lvl_w, &lvl_h); if (rc != EXR_ERR_SUCCESS) { goto done; } rc = exr_get_tile_sizes(ectx, 0, lvl_x, lvl_y, &tile_w, &tile_h); if (rc != EXR_ERR_SUCCESS) { goto done; } tile_y = 0; for (int64_t img_y = 0; img_y < lvl_h; img_y += tile_h) { tile_x = 0; for (int64_t img_x = 0; img_x < lvl_w; img_x += tile_w) { int8_t bpp = 0; rc = exr_read_tile_chunk_info(ectx, 0, tile_x, tile_y, lvl_x, lvl_y, &chunk); if (rc != EXR_ERR_SUCCESS) { goto done; } rc = decode_chunk(ectx, &chunk, &decoder, buffer); if (rc != EXR_ERR_SUCCESS) { goto done; } // put pixels to final image for (int16_t i = 0; i < decoder.channel_count; ++i) { bpp += decoder.channels[i].bytes_per_element; } for (int32_t y = 0; y < chunk.height; ++y) { const uint8_t* src_line = &buffer[y * chunk.width * bpp]; argb_t* dst_line = &pm->data[(img_y + y) * pm->width]; for (int32_t x = 0; x < chunk.width; ++x) { dst_line[img_x + x] = decode_pixel(&decoder, src_line + x * bpp, 42); } } ++tile_x; } ++tile_y; } } } done: exr_decoding_destroy(ectx, &decoder); free(buffer); return rc; } // EXR loader implementation enum loader_status decode_exr(struct image* ctx, const uint8_t* data, size_t size) { exr_result_t rc; exr_context_t exr; exr_context_initializer_t einit = EXR_DEFAULT_CONTEXT_INITIALIZER; struct pixmap* pm; exr_attr_box2i_t dwnd; exr_storage_t storage; struct data_buffer buf = { .data = data, .size = size, }; einit.user_data = &buf; einit.read_fn = exr_reader; // check signature if (size < sizeof(signature) || memcmp(data, signature, sizeof(signature))) { return ldr_unsupported; } // decode rc = exr_start_read(&exr, "exr", &einit); if (rc != EXR_ERR_SUCCESS) { return ldr_fmterror; } rc = exr_get_data_window(exr, 0, &dwnd); if (rc != EXR_ERR_SUCCESS) { goto done; } pm = image_allocate_frame(ctx, dwnd.max.x - dwnd.min.x + 1, dwnd.max.y - dwnd.min.y + 1); if (!pm) { rc = EXR_ERR_OUT_OF_MEMORY; goto done; } rc = exr_get_storage(exr, 0, &storage); if (rc != EXR_ERR_SUCCESS) { goto done; } image_set_format(ctx, "EXR"); ctx->alpha = true; if (storage == EXR_STORAGE_SCANLINE) { rc = load_scanlined(exr, pm); } else if (storage == EXR_STORAGE_TILED) { rc = load_tailed(exr, pm); } else { rc = EXR_ERR_FEATURE_NOT_IMPLEMENTED; } done: exr_finish(&exr); if (rc != EXR_ERR_SUCCESS) { image_free_frames(ctx); return ldr_fmterror; } return ldr_success; } swayimg-3.8/src/formats/farbfeld.c000066400000000000000000000031401474536441700172340ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Farbfeld format decoder #include "../loader.h" #include #include // Farbfeld signature static const uint8_t signature[] = { 'f', 'a', 'r', 'b', 'f', 'e', 'l', 'd' }; // Farbfeld file header struct __attribute__((__packed__)) farbfeld_header { uint8_t magic[sizeof(signature)]; uint32_t width; uint32_t height; }; // Packed Farbfeld pixel struct __attribute__((__packed__)) farbfeld_rgba { uint16_t r; uint16_t g; uint16_t b; uint16_t a; }; enum loader_status decode_farbfeld(struct image* ctx, const uint8_t* data, size_t size) { const struct farbfeld_header* header = (const struct farbfeld_header*)data; size_t width, height, total; struct farbfeld_rgba* src; argb_t* dst; // check signature if (size < sizeof(*header) || memcmp(header->magic, signature, sizeof(signature))) { return ldr_unsupported; } // create pixmap width = htonl(header->width); height = htonl(header->height); if (!image_allocate_frame(ctx, width, height)) { return ldr_fmterror; } size -= sizeof(struct farbfeld_header); data += sizeof(struct farbfeld_header); // decode image dst = ctx->frames[0].pm.data; src = (struct farbfeld_rgba*)data; total = min(width * height, size / sizeof(struct farbfeld_rgba)); for (size_t i = 0; i < total; ++i) { *dst = ARGB(src->a, src->r, src->g, src->b); ++dst; ++src; } image_set_format(ctx, "Farbfeld"); ctx->alpha = true; return ldr_success; } swayimg-3.8/src/formats/gif.c000066400000000000000000000102071474536441700162360ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // GIF format decoder. // Copyright (C) 2020 Artem Senichev #include "../loader.h" #include #include // GIF signature static const uint8_t signature[] = { 'G', 'I', 'F' }; // Buffer description for GIF reader struct buffer { const uint8_t* data; const size_t size; size_t position; }; // GIF reader callback, see `InputFunc` in gif_lib.h static int gif_reader(GifFileType* gif, GifByteType* dst, int sz) { struct buffer* buf = (struct buffer*)gif->UserData; if (sz >= 0 && buf && buf->position + sz <= buf->size) { memcpy(dst, buf->data + buf->position, sz); buf->position += sz; return sz; } return -1; } /** * Decode single GIF frame. * @param ctx image context * @param gif gif context * @param index number of the frame to load * @return true if completed successfully */ static bool decode_frame(struct image* ctx, GifFileType* gif, size_t index) { const SavedImage* img = &gif->SavedImages[index]; const GifImageDesc* desc = &img->ImageDesc; const ColorMapObject* color_map = desc->ColorMap ? desc->ColorMap : gif->SColorMap; GraphicsControlBlock ctl = { .TransparentColor = NO_TRANSPARENT_COLOR }; struct image_frame* frame = &ctx->frames[index]; const size_t width = (size_t)desc->Width > frame->pm.width - desc->Left ? frame->pm.width - desc->Left : (size_t)desc->Width; const size_t height = (size_t)desc->Height > frame->pm.height - desc->Top ? frame->pm.height - desc->Top : (size_t)desc->Height; DGifSavedExtensionToGCB(gif, index, &ctl); if (ctl.DisposalMode == DISPOSE_PREVIOUS && index < ctx->num_frames - 1) { struct pixmap* next = &ctx->frames[index + 1].pm; pixmap_copy(&frame->pm, next, 0, 0, false); } for (size_t y = 0; y < height; ++y) { const uint8_t* raster = &img->RasterBits[y * desc->Width]; argb_t* pixel = frame->pm.data + desc->Top * frame->pm.width + y * frame->pm.width + desc->Left; for (size_t x = 0; x < width; ++x) { const uint8_t color = raster[x]; if (color != ctl.TransparentColor && color < color_map->ColorCount) { const GifColorType* rgb = &color_map->Colors[color]; *pixel = ARGB_SET_A(0xff) | ARGB_SET_R(rgb->Red) | ARGB_SET_G(rgb->Green) | ARGB_SET_B(rgb->Blue); } ++pixel; } } if (ctl.DisposalMode == DISPOSE_DO_NOT && index < ctx->num_frames - 1) { struct pixmap* next = &ctx->frames[index + 1].pm; pixmap_copy(&frame->pm, next, 0, 0, false); } if (ctl.DelayTime != 0) { frame->duration = ctl.DelayTime * 10; // hundreds of second to ms } else { frame->duration = 100; } return true; } // GIF loader implementation enum loader_status decode_gif(struct image* ctx, const uint8_t* data, size_t size) { GifFileType* gif = NULL; struct buffer buf = { .data = data, .size = size, .position = 0, }; int err; // check signature if (size < sizeof(signature) || memcmp(data, signature, sizeof(signature))) { return ldr_unsupported; } // decode gif = DGifOpen(&buf, gif_reader, &err); if (!gif) { return ldr_fmterror; } if (DGifSlurp(gif) != GIF_OK) { goto fail; } // allocate frame sequence if (!image_create_frames(ctx, gif->ImageCount)) { goto fail; } for (size_t i = 0; i < ctx->num_frames; ++i) { struct pixmap* pm = &ctx->frames[i].pm; if (!pixmap_create(pm, gif->SWidth, gif->SHeight)) { goto fail; } } // decode every frame for (size_t i = 0; i < ctx->num_frames; ++i) { if (!decode_frame(ctx, gif, i)) { goto fail; } } image_set_format(ctx, "GIF%s", gif->ImageCount > 1 ? " animation" : ""); ctx->alpha = true; DGifCloseFile(gif, NULL); return ldr_success; fail: DGifCloseFile(gif, NULL); image_free_frames(ctx); return ldr_fmterror; } swayimg-3.8/src/formats/heif.c000066400000000000000000000061661474536441700164150ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // HEIF and AVIF formats decoder. // Copyright (C) 2022 Artem Senichev #include "../exif.h" #include "../loader.h" #include "buildcfg.h" #include #include #ifdef HAVE_LIBEXIF /** * Read Exif info. * @param ctx image context * @param pih handle of HEIF/AVIF image */ static void read_exif(struct image* ctx, struct heif_image_handle* pih) { heif_item_id id; const int count = heif_image_handle_get_list_of_metadata_block_IDs(pih, "Exif", &id, 1); for (int i = 0; i < count; i++) { size_t sz = heif_image_handle_get_metadata_size(pih, id); uint8_t* data = malloc(sz); if (data) { const struct heif_error err = heif_image_handle_get_metadata(pih, id, data); if (err.code == heif_error_Ok) { process_exif(ctx, data + 4 /* skip offset */, sz); } free(data); } } } #endif // HAVE_LIBEXIF // HEIF/AVIF loader implementation enum loader_status decode_heif(struct image* ctx, const uint8_t* data, size_t size) { struct heif_context* heif = NULL; struct heif_image_handle* pih = NULL; struct heif_image* img = NULL; struct heif_error err; const uint8_t* decoded; struct pixmap* pm; int stride = 0; enum loader_status status = ldr_fmterror; if (heif_check_filetype(data, size) != heif_filetype_yes_supported) { return ldr_unsupported; } heif = heif_context_alloc(); if (!heif) { goto done; } err = heif_context_read_from_memory(heif, data, size, NULL); if (err.code != heif_error_Ok) { goto done; } err = heif_context_get_primary_image_handle(heif, &pih); if (err.code != heif_error_Ok) { goto done; } err = heif_decode_image(pih, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, NULL); if (err.code != heif_error_Ok) { goto done; } decoded = heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride); if (!decoded) { goto done; } pm = image_allocate_frame(ctx, heif_image_get_primary_width(img), heif_image_get_primary_height(img)); if (!pm) { goto done; } // convert to plain image frame for (size_t y = 0; y < pm->height; ++y) { const argb_t* src = (const argb_t*)(decoded + y * stride); argb_t* dst = &pm->data[y * pm->width]; for (size_t x = 0; x < pm->width; ++x) { dst[x] = ABGR_TO_ARGB(src[x]); } } ctx->alpha = heif_image_handle_has_alpha_channel(pih); image_set_format(ctx, "HEIF/AVIF %dbpp", heif_image_handle_get_luma_bits_per_pixel(pih)); #ifdef HAVE_LIBEXIF read_exif(ctx, pih); #endif status = ldr_success; done: if (status != ldr_success) { image_free_frames(ctx); } if (img) { heif_image_release(img); } if (pih) { heif_image_handle_release(pih); } if (heif) { heif_context_free(heif); } return status; } swayimg-3.8/src/formats/jpeg.c000066400000000000000000000054621474536441700164250ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // JPEG format decoder. // Copyright (C) 2020 Artem Senichev #include "../loader.h" #include #include #include // depends on stdio.h, uses FILE but doesn't include the header #include // JPEG signature static const uint8_t signature[] = { 0xff, 0xd8 }; struct jpg_error_manager { struct jpeg_error_mgr mgr; jmp_buf setjmp; }; static void jpg_error_exit(j_common_ptr jpg) { struct jpg_error_manager* err = (struct jpg_error_manager*)jpg->err; char msg[JMSG_LENGTH_MAX] = { 0 }; (*(jpg->err->format_message))(jpg, msg); longjmp(err->setjmp, 1); } // JPEG loader implementation enum loader_status decode_jpeg(struct image* ctx, const uint8_t* data, size_t size) { struct pixmap* pm; struct jpeg_decompress_struct jpg; struct jpg_error_manager err; // check signature if (size < sizeof(signature) || memcmp(data, signature, sizeof(signature))) { return ldr_unsupported; } jpg.err = jpeg_std_error(&err.mgr); err.mgr.error_exit = jpg_error_exit; if (setjmp(err.setjmp)) { image_free_frames(ctx); jpeg_destroy_decompress(&jpg); return ldr_fmterror; } jpeg_create_decompress(&jpg); jpeg_mem_src(&jpg, data, size); jpeg_read_header(&jpg, TRUE); jpeg_start_decompress(&jpg); #ifdef LIBJPEG_TURBO_VERSION jpg.out_color_space = JCS_EXT_BGRA; #endif // LIBJPEG_TURBO_VERSION pm = image_allocate_frame(ctx, jpg.output_width, jpg.output_height); if (!pm) { jpeg_destroy_decompress(&jpg); return ldr_fmterror; } while (jpg.output_scanline < jpg.output_height) { uint8_t* line = (uint8_t*)&pm->data[jpg.output_scanline * pm->width]; jpeg_read_scanlines(&jpg, &line, 1); // convert grayscale to argb if (jpg.out_color_components == 1) { uint32_t* pixel = (uint32_t*)line; for (int x = jpg.output_width - 1; x >= 0; --x) { const uint8_t src = *(line + x); pixel[x] = ((argb_t)0xff << 24) | (argb_t)src << 16 | (argb_t)src << 8 | src; } } #ifndef LIBJPEG_TURBO_VERSION // convert rgb to argb if (jpg.out_color_components == 3) { uint32_t* pixel = (uint32_t*)line; for (int x = jpg.output_width - 1; x >= 0; --x) { const uint8_t* src = line + x * 3; pixel[x] = ((argb_t)0xff << 24) | (argb_t)src[0] << 16 | (argb_t)src[1] << 8 | src[2]; } } #endif // LIBJPEG_TURBO_VERSION } image_set_format(ctx, "JPEG %dbit", jpg.out_color_components * 8); jpeg_finish_decompress(&jpg); jpeg_destroy_decompress(&jpg); return ldr_success; } swayimg-3.8/src/formats/jxl.c000066400000000000000000000110531474536441700162660ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // JPEG XL format decoder. // Copyright (C) 2021 Artem Senichev #include "../loader.h" #include #include // JPEG XL loader implementation enum loader_status decode_jxl(struct image* ctx, const uint8_t* data, size_t size) { JxlDecoder* jxl; JxlBasicInfo info = { 0 }; JxlDecoderStatus status; size_t buffer_sz; struct image_frame* frames; size_t frame_num = 0; const JxlPixelFormat jxl_format = { .num_channels = 4, // ARBG .data_type = JXL_TYPE_UINT8, .endianness = JXL_NATIVE_ENDIAN, .align = 0 }; // check signature switch (JxlSignatureCheck(data, size)) { case JXL_SIG_NOT_ENOUGH_BYTES: case JXL_SIG_INVALID: return ldr_unsupported; default: break; } // initialize decoder jxl = JxlDecoderCreate(NULL); if (!jxl) { return ldr_fmterror; } status = JxlDecoderSetInput(jxl, data, size); if (status != JXL_DEC_SUCCESS) { goto fail; } // process decoding status = JxlDecoderSubscribeEvents( jxl, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE); if (status != JXL_DEC_SUCCESS) { goto fail; } do { JxlDecoderStatus rc; status = JxlDecoderProcessInput(jxl); switch (status) { case JXL_DEC_SUCCESS: break; // decoding complete case JXL_DEC_ERROR: goto fail; case JXL_DEC_BASIC_INFO: rc = JxlDecoderGetBasicInfo(jxl, &info); if (rc != JXL_DEC_SUCCESS) { goto fail; } break; case JXL_DEC_FULL_IMAGE: // convert ABGR -> ARGB for (size_t i = 0; i < ctx->frames[frame_num].pm.width * ctx->frames[frame_num].pm.height; ++i) { ctx->frames[frame_num].pm.data[i] = ABGR_TO_ARGB(ctx->frames[frame_num].pm.data[i]); } frame_num = ctx->num_frames; break; case JXL_DEC_FRAME: frames = realloc(ctx->frames, sizeof(*ctx->frames) * (ctx->num_frames + 1)); if (!frames) { goto fail; } ctx->frames = frames; if (!pixmap_create(&ctx->frames[frame_num].pm, info.xsize, info.ysize)) { goto fail; } ctx->num_frames += 1; if (info.have_animation) { JxlFrameHeader header; rc = JxlDecoderGetFrameHeader(jxl, &header); if (rc != JXL_DEC_SUCCESS) { goto fail; } ctx->frames[frame_num].duration = header.duration * 1000.0f * info.animation.tps_denominator / info.animation.tps_numerator; } break; case JXL_DEC_NEED_IMAGE_OUT_BUFFER: // get image buffer size rc = JxlDecoderImageOutBufferSize(jxl, &jxl_format, &buffer_sz); if (rc != JXL_DEC_SUCCESS) { goto fail; } // check buffer format if (buffer_sz != ctx->frames[frame_num].pm.width * ctx->frames[frame_num].pm.height * sizeof(argb_t)) { goto fail; } // set output buffer rc = JxlDecoderSetImageOutBuffer(jxl, &jxl_format, ctx->frames[frame_num].pm.data, buffer_sz); if (rc != JXL_DEC_SUCCESS) { goto fail; } break; default: break; } } while (status != JXL_DEC_SUCCESS); if (!ctx->frames) { goto fail; } image_set_format(ctx, "JPEG XL %ubpp", info.bits_per_sample * info.num_color_channels + info.alpha_bits); ctx->alpha = info.alpha_bits != 0; JxlDecoderDestroy(jxl); return ldr_success; fail: JxlDecoderDestroy(jxl); image_free_frames(ctx); return ldr_fmterror; } swayimg-3.8/src/formats/png.c000066400000000000000000000256341474536441700162670ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // PNG format decoder. // Copyright (C) 2020 Artem Senichev #include "png.h" #include #include #include #include // PNG memory reader struct mem_reader { const uint8_t* data; const size_t size; size_t position; }; // PNG memory writer struct mem_writer { uint8_t** data; size_t* size; }; // PNG reader callback, see `png_rw_ptr` in png.h static void png_reader(png_structp png, png_bytep buffer, size_t size) { struct mem_reader* reader = (struct mem_reader*)png_get_io_ptr(png); if (reader && reader->position + size < reader->size) { memcpy(buffer, reader->data + reader->position, size); reader->position += size; } else { png_error(png, "No data in PNG reader"); } } // PNG writer callback, see `png_rw_ptr` in png.h static void png_writer(png_structp png, png_bytep buffer, size_t size) { struct mem_writer* writer = (struct mem_writer*)png_get_io_ptr(png); const size_t old_size = *writer->size; const size_t new_size = old_size + size; uint8_t* new_ptr = realloc(*writer->data, new_size); if (new_ptr) { memcpy(new_ptr + old_size, buffer, size); *writer->data = new_ptr; *writer->size = new_size; } else { png_error(png, "No memory"); } } /** * Bind pixmap with PNG line-reading decoder. * @param pm pixmap to bind * @return array of pointers to pixmap data */ static png_bytep* bind_pixmap(const struct pixmap* pm) { png_bytep* ptr = malloc(pm->height * sizeof(*ptr)); if (ptr) { for (uint32_t i = 0; i < pm->height; ++i) { ptr[i] = (png_bytep)&pm->data[i * pm->width]; } } return ptr; } /** * Decode single framed image. * @param ctx image context * @param png png decoder * @param info png image info * @return false if decode failed */ static bool decode_single(struct image* ctx, png_struct* png, png_info* info) { const uint32_t width = png_get_image_width(png, info); const uint32_t height = png_get_image_height(png, info); struct pixmap* pm = image_allocate_frame(ctx, width, height); png_bytep* bind; if (!pm) { return false; } bind = bind_pixmap(pm); if (!bind) { return false; } if (setjmp(png_jmpbuf(png))) { free(bind); return false; } png_read_image(png, bind); free(bind); return true; } #ifdef PNG_APNG_SUPPORTED /** * Decode single PNG frame. * @param ctx image context * @param png png decoder * @param info png image info * @param index number of the frame to load * @return true if completed successfully */ static bool decode_frame(struct image* ctx, png_struct* png, png_info* info, size_t index) { png_uint_32 width = 0; png_uint_32 height = 0; png_uint_32 offset_x = 0; png_uint_32 offset_y = 0; png_uint_16 delay_num = 0; png_uint_16 delay_den = 0; png_byte dispose = 0; png_byte blend = 0; png_bytep* bind; struct pixmap frame_png; struct image_frame* frame_img = &ctx->frames[index]; // get frame params if (png_get_valid(png, info, PNG_INFO_acTL)) { png_read_frame_head(png, info); } if (png_get_valid(png, info, PNG_INFO_fcTL)) { png_get_next_frame_fcTL(png, info, &width, &height, &offset_x, &offset_y, &delay_num, &delay_den, &dispose, &blend); } // fixup frame params if (width == 0) { width = png_get_image_width(png, info); } if (height == 0) { height = png_get_image_height(png, info); } if (delay_den == 0) { delay_den = 100; } if (delay_num == 0) { delay_num = 100; } // allocate frame buffer and bind it to png reader if (!pixmap_create(&frame_png, width, height)) { return false; } bind = bind_pixmap(&frame_png); if (!bind) { pixmap_free(&frame_png); return false; } // decode frame into pixmap if (setjmp(png_jmpbuf(png))) { pixmap_free(&frame_png); free(bind); return false; } png_read_image(png, bind); // handle dispose if (dispose == PNG_DISPOSE_OP_PREVIOUS) { if (index == 0) { dispose = PNG_DISPOSE_OP_BACKGROUND; } else if (index + 1 < ctx->num_frames) { struct pixmap* next = &ctx->frames[index + 1].pm; pixmap_copy(&frame_img->pm, next, 0, 0, false); } } // put frame on final pixmap pixmap_copy(&frame_png, &frame_img->pm, offset_x, offset_y, blend == PNG_BLEND_OP_OVER); // handle dispose if (dispose == PNG_DISPOSE_OP_NONE && index + 1 < ctx->num_frames) { struct pixmap* next = &ctx->frames[index + 1].pm; pixmap_copy(&frame_img->pm, next, 0, 0, false); } // calc frame duration in milliseconds frame_img->duration = (float)delay_num * 1000 / delay_den; pixmap_free(&frame_png); free(bind); return true; } /** * Decode multi framed image. * @param ctx image context * @param png png decoder * @param info png image info * @return false if decode failed */ static bool decode_multiple(struct image* ctx, png_struct* png, png_info* info) { const uint32_t width = png_get_image_width(png, info); const uint32_t height = png_get_image_height(png, info); const uint32_t frames = png_get_num_frames(png, info); uint32_t index; // allocate frames if (!image_create_frames(ctx, frames)) { return false; } for (index = 0; index < frames; ++index) { struct image_frame* frame = &ctx->frames[index]; if (!pixmap_create(&frame->pm, width, height)) { return false; } } // decode frames for (index = 0; index < frames; ++index) { if (!decode_frame(ctx, png, info, index)) { break; } } if (index != frames) { // not all frames were decoded, leave only the first for (index = 1; index < frames; ++index) { pixmap_free(&ctx->frames[index].pm); } ctx->num_frames = 1; } if (png_get_first_frame_is_hidden(png, info) && ctx->num_frames > 1) { --ctx->num_frames; pixmap_free(&ctx->frames[0].pm); memmove(&ctx->frames[0], &ctx->frames[1], ctx->num_frames * sizeof(*ctx->frames)); } return true; } #endif // PNG_APNG_SUPPORTED // PNG loader implementation enum loader_status decode_png(struct image* ctx, const uint8_t* data, size_t size) { png_struct* png = NULL; png_info* info = NULL; png_byte color_type, bit_depth; bool rc; struct mem_reader reader = { .data = data, .size = size, .position = 0, }; // check signature if (png_sig_cmp(data, 0, size) != 0) { return ldr_unsupported; } // create decoder png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png) { return ldr_fmterror; } info = png_create_info_struct(png); if (!info) { png_destroy_read_struct(&png, NULL, NULL); return ldr_fmterror; } // setup error handling if (setjmp(png_jmpbuf(png))) { png_destroy_read_struct(&png, &info, NULL); return ldr_fmterror; } // get general image info png_set_read_fn(png, &reader, &png_reader); png_read_info(png, info); color_type = png_get_color_type(png, info); bit_depth = png_get_bit_depth(png, info); // setup decoder if (png_get_interlace_type(png, info) != PNG_INTERLACE_NONE) { png_set_interlace_handling(png); } if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png); } if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { png_set_gray_to_rgb(png); if (bit_depth < 8) { png_set_expand_gray_1_2_4_to_8(png); } } if (png_get_valid(png, info, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png); } if (bit_depth == 16) { png_set_strip_16(png); } png_set_filler(png, 0xff, PNG_FILLER_AFTER); png_set_packing(png); png_set_packswap(png); png_set_bgr(png); png_set_expand(png); png_read_update_info(png, info); #ifdef PNG_APNG_SUPPORTED if (png_get_valid(png, info, PNG_INFO_acTL) && png_get_num_frames(png, info) > 1) { rc = decode_multiple(ctx, png, info); } else { rc = decode_single(ctx, png, info); } #else rc = decode_single(ctx, png, info); #endif // PNG_APNG_SUPPORTED // read text info if (rc) { png_text* txt; int total; if (png_get_text(png, info, &txt, &total)) { for (int i = 0; i < total; ++i) { image_add_meta(ctx, txt[i].key, "%s", txt[i].text); } } } if (!rc) { image_free_frames(ctx); } else { image_set_format(ctx, "PNG %dbit", bit_depth * 4); ctx->alpha = true; } // free resources png_destroy_read_struct(&png, &info, NULL); return rc ? ldr_success : ldr_fmterror; } bool encode_png(const struct image* ctx, uint8_t** data, size_t* size) { png_struct* png = NULL; png_info* info = NULL; png_bytep* bind = NULL; struct mem_writer writer = { .data = data, .size = size, }; // create encoder png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png) { return false; } info = png_create_info_struct(png); if (!info) { png_destroy_write_struct(&png, NULL); return false; } // setup error handling if (setjmp(png_jmpbuf(png))) { free(bind); png_destroy_write_struct(&png, &info); return false; } png_set_write_fn(png, &writer, png_writer, NULL); // setup output: 8bit RGBA png_set_IHDR(png, info, ctx->frames[0].pm.width, ctx->frames[0].pm.height, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_set_bgr(png); // save meta info as text if (ctx->info) { const size_t info_sz = list_size(&ctx->info->list); png_text* txt = calloc(1, info_sz * sizeof(png_text)); if (txt) { size_t i = 0; list_for_each(ctx->info, const struct image_info, it) { txt[i].key = it->key; txt[i].text = it->value; ++i; } png_set_text(png, info, txt, info_sz); free(txt); } } png_write_info(png, info); // encode image bind = bind_pixmap(&ctx->frames[0].pm); if (!bind) { png_destroy_write_struct(&png, &info); return false; } png_write_image(png, bind); png_write_end(png, NULL); // free resources free(bind); png_destroy_write_struct(&png, &info); return true; } swayimg-3.8/src/formats/png.h000066400000000000000000000011351474536441700162620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // PNG format encoder/decoder. // Copyright (C) 2024 Artem Senichev #pragma once #include "../loader.h" // PNG decoder implementation enum loader_status decode_png(struct image* ctx, const uint8_t* data, size_t size); /** * Encode PNG to memory buffer. * @param image source image instance * @param data PNG data buffer, the caller should free it after use * @param size size of PNG data buffer * @return true if image saved successfully */ bool encode_png(const struct image* ctx, uint8_t** data, size_t* size); swayimg-3.8/src/formats/pnm.c000066400000000000000000000220551474536441700162670ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // PNM formats decoder // Copyright (C) 2023 Abe Wieland #include "../loader.h" #include // Both assume positive arguments and evaluate b more than once // Divide, rounding to nearest (up on ties) #define div_near(a, b) (((a) + (b) / 2) / (b)) // Divide, rounding up #define div_ceil(a, b) (((a) + (b) - 1) / (b)) // PNM file types enum pnm_type { pnm_pbm, // Bitmap pnm_pgm, // Grayscale pixmap pnm_ppm // Color pixmap }; // A file-like abstraction for cleaner number parsing struct pnm_iter { const uint8_t* pos; const uint8_t* end; }; // Error conditions #define PNM_EEOF -1 #define PNM_ERNG -2 #define PNM_EFMT -3 #define PNM_EOVF -4 // Digits in INT_MAX #define INT_MAX_DIGITS 10 /** * Read an integer, ignoring leading whitespace and comments * @param it image iterator * @param digits maximum number of digits to read, or 0 for no limit * @return the integer read (positive) or an error code (negative) * * Although the specification states comments may also appear in integers, this * is not supported by any known parsers at the time of writing; thus, we don't * support it either */ static int pnm_readint(struct pnm_iter* it, size_t digits) { if (!digits) { digits = INT_MAX_DIGITS; } for (; it->pos != it->end; ++it->pos) { const char c = *it->pos; if (c == '#') { while (it->pos != it->end && *it->pos != '\n' && *it->pos != '\r') { ++it->pos; } } else if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { break; } } if (it->pos == it->end) { return PNM_EEOF; } if (*it->pos < '0' || *it->pos > '9') { return PNM_EFMT; } int val = 0; size_t i = 0; do { const uint8_t d = *it->pos - '0'; if (val > INT_MAX / 10) { return PNM_ERNG; } val *= 10; if (val > INT_MAX - d) { return PNM_ERNG; } val += d; ++it->pos; ++i; } while (it->pos != it->end && *it->pos >= '0' && *it->pos <= '9' && i < digits); return val; } /** * Decode a plain/ASCII PNM file * @param pm pixel map to write data to * @param it image iterator * @param type type of PNM file * @param maxval maximum value for each sample * @return 0 on success, error code on failure */ static int decode_plain(struct pixmap* pm, struct pnm_iter* it, enum pnm_type type, int maxval) { for (size_t y = 0; y < pm->height; ++y) { argb_t* dst = pm->data + y * pm->width; for (size_t x = 0; x < pm->width; ++x) { argb_t pix = ARGB_SET_A(0xff); if (type == pnm_pbm) { const int bit = pnm_readint(it, 1); if (bit < 0) { return bit; } if (bit > maxval) { return PNM_EOVF; } pix |= bit - 1; } else if (type == pnm_pgm) { int v = pnm_readint(it, 0); if (v < 0) { return v; } if (v > maxval) { return PNM_EOVF; } if (maxval != UINT8_MAX) { v = div_near(v * UINT8_MAX, maxval); } pix |= ARGB_SET_R(v) | ARGB_SET_G(v) | ARGB_SET_B(v); } else { int r = pnm_readint(it, 0); if (r < 0) { return r; } if (r > maxval) { return PNM_EOVF; } int g = pnm_readint(it, 0); if (g < 0) { return g; } if (g > maxval) { return PNM_EOVF; } int b = pnm_readint(it, 0); if (b < 0) { return b; } if (b > maxval) { return PNM_EOVF; } if (maxval != UINT8_MAX) { r = div_near(r * UINT8_MAX, maxval); g = div_near(g * UINT8_MAX, maxval); b = div_near(b * UINT8_MAX, maxval); } pix |= ARGB_SET_R(r) | ARGB_SET_G(g) | ARGB_SET_B(b); } dst[x] = pix; } } return 0; } /** * Decode a raw/binary PNM file * @param f image frame to write data to * @param it image iterator * @param type type of PNM file * @param maxval maximum value for each sample * @return 0 on success, error code on failure */ static int decode_raw(struct pixmap* pm, struct pnm_iter* it, enum pnm_type type, int maxval) { // PGM and PPM use bpc (bytes per channel) bytes for each channel depending // on the max, with 1 channel for PGM and 3 for PPM; PBM pads each row to // the nearest whole byte size_t bpc = maxval <= UINT8_MAX ? 1 : 2; size_t rowsz = type == pnm_pbm ? div_ceil(pm->width, 8) : pm->width * bpc * (type == pnm_pgm ? 1 : 3); if (it->end < it->pos + pm->height * rowsz) { return PNM_EEOF; } for (size_t y = 0; y < pm->height; ++y) { argb_t* dst = pm->data + y * pm->width; const uint8_t* src = it->pos + y * rowsz; for (size_t x = 0; x < pm->width; ++x) { argb_t pix = ARGB_SET_A(0xff); if (type == pnm_pbm) { const int bit = (src[x / 8] >> (7 - x % 8)) & 1; pix |= bit - 1; } else if (type == pnm_pgm) { int v = bpc == 1 ? src[x] : src[x] << 8 | src[x + 1]; if (v > maxval) { return PNM_EOVF; } if (maxval != UINT8_MAX) { v = div_near(v * UINT8_MAX, maxval); } pix |= ARGB_SET_R(v) | ARGB_SET_G(v) | ARGB_SET_B(v); } else { int r, g, b; if (bpc == 1) { r = src[x * 3]; g = src[x * 3 + 1]; b = src[x * 3 + 2]; } else { r = src[x * 3] << 8 | src[x * 3 + 1]; g = src[x * 3 + 2] << 8 | src[x * 3 + 3]; b = src[x * 3 + 4] << 8 | src[x * 3 + 5]; } if (r > maxval || g > maxval || b > maxval) { return PNM_EOVF; } if (maxval != UINT8_MAX) { r = div_near(r * UINT8_MAX, maxval); g = div_near(g * UINT8_MAX, maxval); b = div_near(b * UINT8_MAX, maxval); } pix |= ARGB_SET_R(r) | ARGB_SET_G(g) | ARGB_SET_B(b); } dst[x] = pix; } } return 0; } enum loader_status decode_pnm(struct image* ctx, const uint8_t* data, size_t size) { struct pnm_iter it; bool plain; enum pnm_type type; int width, height, maxval, ret; if (size < 2 || data[0] != 'P') { return ldr_unsupported; } switch (data[1]) { case '1': plain = true; type = pnm_pbm; break; case '2': plain = true; type = pnm_pgm; break; case '3': plain = true; type = pnm_ppm; break; case '4': plain = false; type = pnm_pbm; break; case '5': plain = false; type = pnm_pgm; break; case '6': plain = false; type = pnm_ppm; break; default: return ldr_unsupported; } it.pos = data + 2; it.end = data + size; width = pnm_readint(&it, 0); if (width < 0) { return ldr_fmterror; } height = pnm_readint(&it, 0); if (height < 0) { return ldr_fmterror; } if (type == pnm_pbm) { maxval = 1; } else { maxval = pnm_readint(&it, 0); if (maxval < 0) { return ldr_fmterror; } if (!maxval || maxval > UINT16_MAX) { return ldr_fmterror; } } if (!plain) { // Again, the specifications technically allow for comments here, but no // other parsers support that (they treat that comment as image data), // so we won't allow one either const char c = *it.pos; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return ldr_fmterror; } ++it.pos; } if (!image_allocate_frame(ctx, width, height)) { return ldr_fmterror; } ret = plain ? decode_plain(&ctx->frames[0].pm, &it, type, maxval) : decode_raw(&ctx->frames[0].pm, &it, type, maxval); if (ret < 0) { image_free_frames(ctx); return ldr_fmterror; } image_set_format(ctx, "P%cM (%s)", type == pnm_pbm ? 'B' : (type == pnm_pgm ? 'G' : 'P'), plain ? "ASCII" : "raw"); return ldr_success; } swayimg-3.8/src/formats/qoi.c000066400000000000000000000100671474536441700162650ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // QOI format decoder. // Copyright (C) 2024 Artem Senichev #include "../loader.h" #include #include // Chunk tags #define QOI_OP_INDEX 0x00 #define QOI_OP_DIFF 0x40 #define QOI_OP_LUMA 0x80 #define QOI_OP_RUN 0xc0 #define QOI_OP_RGB 0xfe #define QOI_OP_RGBA 0xff // Mask of second byte in steram #define QOI_MASK_2 0xc0 // Size of color map #define QOI_CLRMAP_SIZE 64 // Calc color index in map #define QOI_CLRMAP_INDEX(r, g, b, a) \ ((r * 3 + g * 5 + b * 7 + a * 11) % QOI_CLRMAP_SIZE) // QOI signature static const uint8_t signature[] = { 'q', 'o', 'i', 'f' }; // QOI file header struct __attribute__((__packed__)) qoi_header { uint8_t magic[4]; // Magic bytes "qoif" uint32_t width; // Image width in pixels uint32_t height; // Image height in pixels uint8_t channels; // Number of color channels: 3 = RGB, 4 = RGBA uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear }; // QOI loader implementation enum loader_status decode_qoi(struct image* ctx, const uint8_t* data, size_t size) { const struct qoi_header* qoi = (const struct qoi_header*)data; argb_t color_map[QOI_CLRMAP_SIZE]; struct pixmap* pm; uint8_t a, r, g, b; size_t total_pixels; size_t rlen; size_t pos; // check signature if (size < sizeof(*qoi) || memcmp(qoi->magic, signature, sizeof(signature))) { return ldr_unsupported; } // check format if (qoi->width == 0 || qoi->height == 0 || qoi->channels < 3 || qoi->channels > 4) { return ldr_fmterror; } // allocate image buffer pm = image_allocate_frame(ctx, htonl(qoi->width), htonl(qoi->height)); if (!pm) { return ldr_fmterror; } // initialize decoder state r = 0; g = 0; b = 0; a = 0xff; rlen = 0; pos = sizeof(struct qoi_header); total_pixels = pm->width * pm->height; memset(color_map, 0, sizeof(color_map)); // decode image for (size_t i = 0; i < total_pixels; ++i) { if (rlen > 0) { --rlen; } else { uint8_t tag; if (pos >= size) { break; } tag = data[pos++]; if (tag == QOI_OP_RGB) { if (pos + 3 >= size) { goto fail; } r = data[pos++]; g = data[pos++]; b = data[pos++]; } else if (tag == QOI_OP_RGBA) { if (pos + 4 >= size) { goto fail; } r = data[pos++]; g = data[pos++]; b = data[pos++]; a = data[pos++]; } else if ((tag & QOI_MASK_2) == QOI_OP_INDEX) { const argb_t clr = color_map[tag & 0x3f]; a = ARGB_GET_A(clr); r = ARGB_GET_R(clr); g = ARGB_GET_G(clr); b = ARGB_GET_B(clr); } else if ((tag & QOI_MASK_2) == QOI_OP_DIFF) { r += (int8_t)((tag >> 4) & 3) - 2; g += (int8_t)((tag >> 2) & 3) - 2; b += (int8_t)(tag & 3) - 2; } else if ((tag & QOI_MASK_2) == QOI_OP_LUMA) { uint8_t diff; int8_t diff_green; if (pos + 1 >= size) { goto fail; } diff = data[pos++]; diff_green = (int8_t)(tag & 0x3f) - 32; r += diff_green - 8 + ((diff >> 4) & 0x0f); g += diff_green; b += diff_green - 8 + (diff & 0x0f); } else if ((tag & QOI_MASK_2) == QOI_OP_RUN) { rlen = (tag & 0x3f); } color_map[QOI_CLRMAP_INDEX(r, g, b, a)] = ARGB(a, r, g, b); } pm->data[i] = ARGB(a, r, g, b); } image_set_format(ctx, "QOI %dbpp", qoi->channels * 8); ctx->alpha = (qoi->channels == 4); return ldr_success; fail: image_free_frames(ctx); return ldr_fmterror; } swayimg-3.8/src/formats/sixel.c000066400000000000000000000027301474536441700166170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Sixel format decoder. #include "../loader.h" #include #include #include // Sixel loader implementation enum loader_status decode_sixel(struct image* ctx, const uint8_t* data, size_t size) { uint8_t* pixels = NULL; uint8_t* palette = NULL; int width, height, ncolors; SIXELSTATUS status; // sixel always starts with Esc code if (data[0] != 0x1b) { return ldr_unsupported; } // decode image status = sixel_decode_raw((uint8_t*)data, (int)size, &pixels, &width, &height, &palette, &ncolors, NULL); if (SIXEL_FAILED(status)) { return ldr_unsupported; } if (!image_allocate_frame(ctx, width, height)) { free(pixels); free(palette); return ldr_fmterror; } // convert palette to real pixels for (int y = 0; y < height; ++y) { const int y_offset = y * width; const uint8_t* src = &pixels[y_offset]; argb_t* dst = &ctx->frames[0].pm.data[y_offset]; for (int x = 0; x < width; ++x) { if (src[x] >= ncolors) { dst[x] = ARGB(0xff, 0, 0, 0); } else { const uint8_t* rgb = &palette[src[x] * 3]; dst[x] = ARGB(0xff, rgb[0], rgb[1], rgb[2]); } } } image_set_format(ctx, "Sixel"); free(pixels); free(palette); return ldr_success; } swayimg-3.8/src/formats/svg.c000066400000000000000000000074531474536441700163010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // SVG format decoder. // Copyright (C) 2020 Artem Senichev #include "../loader.h" #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wexpansion-to-defined" #include #pragma GCC diagnostic pop // SVG uses physical units to store size, // these macro defines default viewbox dimension in pixels #define RENDER_SIZE 1024 // Max offset of the root svg node in xml file #define MAX_OFFSET 1024 /** * Check if data is SVG. * @param data raw image data * @param size size of image data in bytes * @return true if it is an SVG format */ static bool is_svg(const uint8_t* data, size_t size) { const char svg_begin[] = " sizeof(svg_begin) && strncmp((const char*)data, svg_begin, sizeof(svg_begin) - 1) == 0) { return true; } if (size > sizeof(xml_begin) && strncmp((const char*)data, xml_begin, sizeof(xml_begin) - 1) == 0) { // search for svg node size_t pos = sizeof(xml_begin); while (pos < MAX_OFFSET && pos + sizeof(svg_begin) < size) { if (strncmp((const char*)&data[pos], svg_begin, sizeof(svg_begin) - 1) == 0) { return true; } ++pos; } } return false; } // SVG loader implementation enum loader_status decode_svg(struct image* ctx, const uint8_t* data, size_t size) { RsvgHandle* svg; gboolean has_vb_real; RsvgRectangle vb_real; RsvgRectangle vb_render; GError* err = NULL; cairo_surface_t* surface = NULL; cairo_t* cr = NULL; struct pixmap* pm; cairo_status_t status; if (!is_svg(data, size)) { return ldr_unsupported; } svg = rsvg_handle_new_from_data(data, size, &err); if (!svg) { return ldr_fmterror; } // define image size in pixels rsvg_handle_get_intrinsic_dimensions(svg, NULL, NULL, NULL, NULL, &has_vb_real, &vb_real); vb_render.x = 0; vb_render.y = 0; if (has_vb_real) { if (vb_real.width < vb_real.height) { vb_render.width = RENDER_SIZE * (vb_real.width / vb_real.height); vb_render.height = RENDER_SIZE; } else { vb_render.width = RENDER_SIZE; vb_render.height = RENDER_SIZE * (vb_real.height / vb_real.width); } } else { vb_render.width = RENDER_SIZE; vb_render.height = RENDER_SIZE; } // allocate and bind buffer pm = image_allocate_frame(ctx, vb_render.width, vb_render.height); if (!pm) { goto fail; } memset(pm->data, 0, pm->width * pm->height * sizeof(argb_t)); surface = cairo_image_surface_create_for_data( (uint8_t*)pm->data, CAIRO_FORMAT_ARGB32, pm->width, pm->height, pm->width * sizeof(argb_t)); status = cairo_surface_status(surface); if (status != CAIRO_STATUS_SUCCESS) { goto fail; } // render svg to surface cr = cairo_create(surface); if (!rsvg_handle_render_document(svg, cr, &vb_render, &err)) { goto fail; } image_set_format(ctx, "SVG"); if (has_vb_real) { image_add_meta(ctx, "Real size", "%0.2fx%0.2f", vb_real.width, vb_real.height); } ctx->alpha = true; cairo_destroy(cr); cairo_surface_destroy(surface); g_object_unref(svg); return ldr_success; fail: if (cr) { cairo_destroy(cr); } if (surface) { cairo_surface_destroy(surface); } image_free_frames(ctx); g_object_unref(svg); return ldr_fmterror; } swayimg-3.8/src/formats/tga.c000066400000000000000000000200121474536441700162370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Truevision TGA format decoder. // Copyright (C) 2024 Artem Senichev #include "../loader.h" #include /** TGA file header. */ struct __attribute__((__packed__)) tga_header { uint8_t id_len; uint8_t clrmap_type; uint8_t image_type; uint16_t cm_index; uint16_t cm_size; uint8_t cm_bpc; uint16_t origin_x; uint16_t origin_y; uint16_t width; uint16_t height; uint8_t bpp; uint8_t desc; }; #define TGA_COLORMAP 1 // color map present flag #define TGA_UNC_CM 1 // uncompressed color-mapped #define TGA_UNC_TC 2 // uncompressed true-color #define TGA_UNC_GS 3 // uncompressed grayscale #define TGA_RLE_CM 9 // run-length encoded color-mapped #define TGA_RLE_TC 10 // run-length encoded true-color #define TGA_RLE_GS 11 // run-length encoded grayscale #define TGA_ORDER_R2L (1 << 4) // right-to-left pixel ordering #define TGA_ORDER_T2B (1 << 5) // top-to-bottom pixel ordering #define TGA_PACKET_RLE (1 << 7) // rle/raw field #define TGA_PACKET_LEN 0x7f // length mask /** * Get pixel color from data stream. * @param data pointer to stream data * @param bpp bits per pixel * @return color */ static inline argb_t get_pixel(const uint8_t* data, size_t bpp) { argb_t pixel; switch (bpp) { case 8: pixel = ARGB_SET_A(0xff) | ARGB_SET_R(data[0]) | ARGB_SET_G(data[0]) | ARGB_SET_B(data[0]); break; case 15: case 16: pixel = ARGB_SET_A(0xff) | ARGB_SET_G(data[0] & 0xf8) | ARGB_SET_B((data[0] << 5) | ((data[1] & 0xc0) >> 2)) | ARGB_SET_R((data[1] & 0x3e) << 2); break; case 24: pixel = ARGB_SET_A(0xff) | ARGB_SET_R(data[2]) | ARGB_SET_G(data[1]) | ARGB_SET_B(data[0]); break; default: pixel = *(const argb_t*)data; break; } return pixel; } /** * Decode uncompressed image. * @param pm destination pixmap * @param tga source image descriptor * @param colormap color map * @param data pointer to image data * @param size size of image data in bytes * @return true if image decoded successfully */ static bool decode_unc(struct pixmap* pm, const struct tga_header* tga, const uint8_t* colormap, const uint8_t* data, size_t size) { const uint8_t bytes_per_pixel = tga->bpp / 8 + (tga->bpp % 8 ? 1 : 0); const size_t num_pixels = pm->width * pm->height; const size_t data_size = num_pixels * bytes_per_pixel; if (data_size > size) { return false; } if (tga->bpp == 32) { memcpy(pm->data, data, data_size); } else { const uint8_t cm_bpp = tga->cm_bpc / 8 + (tga->cm_bpc % 8 ? 1 : 0); for (size_t i = 0; i < num_pixels; ++i) { const uint8_t* src = data + i * bytes_per_pixel; if (!colormap) { pm->data[i] = get_pixel(src, tga->bpp); } else { const uint8_t* entry = colormap + cm_bpp * (*src); if (entry + cm_bpp > data) { return false; } pm->data[i] = get_pixel(entry, tga->cm_bpc); } } } return true; } /** * Decode RLE compressed image. * @param pm destination pixmap * @param tga source image descriptor * @param colormap color map * @param data pointer to image data * @param size size of image data in bytes * @return true if image decoded successfully */ static bool decode_rle(struct pixmap* pm, const struct tga_header* tga, const uint8_t* colormap, const uint8_t* data, size_t size) { const uint8_t cm_bpp = tga->cm_bpc / 8 + (tga->cm_bpc % 8 ? 1 : 0); const uint8_t bytes_per_pixel = tga->bpp / 8 + (tga->bpp % 8 ? 1 : 0); const argb_t* pm_end = pm->data + pm->width * pm->height; argb_t* pixel = pm->data; size_t pos = 0; while (pixel < pm_end) { const uint8_t pack = data[pos++]; const bool is_rle = (pack & TGA_PACKET_RLE); size_t len = (pack & TGA_PACKET_LEN) + 1; while (len--) { if (pos + bytes_per_pixel > size) { return false; } if (!colormap) { *pixel = get_pixel(data + pos, tga->bpp); } else { const uint8_t* entry = colormap + cm_bpp * data[pos]; if (entry + cm_bpp > data) { return false; } *pixel = get_pixel(entry, tga->cm_bpc); } if (pixel++ >= pm_end) { break; } if (!is_rle) { pos += bytes_per_pixel; } } if (is_rle) { pos += bytes_per_pixel; } } return true; } // TGA loader implementation enum loader_status decode_tga(struct image* ctx, const uint8_t* data, size_t size) { const struct tga_header* tga = (const struct tga_header*)data; const uint8_t* colormap = NULL; size_t colormap_sz = 0; const char* type_name = NULL; bool rc = false; size_t data_offset; struct pixmap* pm; // check type if (size < sizeof(struct tga_header) || (tga->image_type != TGA_UNC_CM && tga->image_type != TGA_UNC_TC && tga->image_type != TGA_UNC_GS && tga->image_type != TGA_RLE_CM && tga->image_type != TGA_RLE_TC && tga->image_type != TGA_RLE_GS)) { return ldr_unsupported; } // check image params if (tga->width == 0 || tga->height == 0 || (tga->bpp != 8 && tga->bpp != 15 && tga->bpp != 16 && tga->bpp != 24 && tga->bpp != 32)) { return ldr_unsupported; } // get color map switch (tga->image_type) { case TGA_UNC_CM: case TGA_RLE_CM: if (!(tga->clrmap_type & TGA_COLORMAP) || !tga->cm_size || !tga->cm_bpc) { return ldr_unsupported; } colormap_sz = tga->cm_size * (tga->cm_bpc / 8 + (tga->cm_bpc % 8 ? 1 : 0)); colormap = data + sizeof(struct tga_header) + tga->id_len; break; default: if (tga->clrmap_type & TGA_COLORMAP || tga->cm_size || tga->cm_bpc) { return ldr_unsupported; } break; } // get pixel array offset data_offset = sizeof(struct tga_header) + tga->id_len + colormap_sz; if (data_offset >= size) { return ldr_unsupported; } data += data_offset; size -= data_offset; // decode image pm = image_allocate_frame(ctx, tga->width, tga->height); if (!pm) { return ldr_fmterror; } switch (tga->image_type) { case TGA_UNC_CM: case TGA_UNC_TC: case TGA_UNC_GS: rc = decode_unc(pm, tga, colormap, data, size); break; case TGA_RLE_CM: case TGA_RLE_TC: case TGA_RLE_GS: rc = decode_rle(pm, tga, colormap, data, size); break; } if (!rc) { image_free_frames(ctx); return ldr_fmterror; } // fix orientation if (!(tga->desc & TGA_ORDER_T2B)) { pixmap_flip_vertical(pm); } if (tga->desc & TGA_ORDER_R2L) { pixmap_flip_horizontal(pm); } // set image meta data switch (tga->image_type) { case TGA_UNC_CM: type_name = "uncompressed color-mapped"; break; case TGA_UNC_TC: type_name = "uncompressed true-color"; break; case TGA_UNC_GS: type_name = "uncompressed grayscale"; break; case TGA_RLE_CM: type_name = "RLE color-mapped"; break; case TGA_RLE_TC: type_name = "RLE true-color"; break; case TGA_RLE_GS: type_name = "RLE grayscale"; break; } image_set_format(ctx, "TARGA %dbpp, %s", tga->bpp, type_name); ctx->alpha = (tga->bpp == 32); return ldr_success; } swayimg-3.8/src/formats/tiff.c000066400000000000000000000074001474536441700164220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // TIFF format decoder. // Copyright (C) 2022 Artem Senichev #include "../loader.h" #include #include // TIFF signatures static const uint8_t signature1[] = { 0x49, 0x49, 0x2a, 0x00 }; static const uint8_t signature2[] = { 0x4d, 0x4d, 0x00, 0x2a }; // Size of buffer for error messages, see libtiff for details #define LIBTIFF_ERRMSG_SZ 1024 // TIFF memory reader struct mem_reader { const uint8_t* data; size_t size; size_t position; }; // TIFF memory reader: TIFFReadWriteProc static tmsize_t tiff_read(thandle_t data, void* buffer, tmsize_t size) { struct mem_reader* mr = data; if (mr->position >= mr->size) { return 0; } if (mr->position + size > mr->size) { size = mr->size - mr->position; } memcpy(buffer, mr->data + mr->position, size); mr->position += size; return size; } // TIFF memory reader: TIFFReadWriteProc static tmsize_t tiff_write(__attribute__((unused)) thandle_t data, __attribute__((unused)) void* buffer, __attribute__((unused)) tmsize_t size) { return 0; } // TIFF memory reader: TIFFSeekProc static toff_t tiff_seek(thandle_t data, toff_t off, __attribute__((unused)) int xxx) { struct mem_reader* mr = data; if (off < mr->size) { mr->position = off; } return mr->position; } // TIFF memory reader: TIFFCloseProc static int tiff_close(__attribute__((unused)) thandle_t data) { return 0; } // TIFF memory reader: TIFFSizeProc static toff_t tiff_size(thandle_t data) { struct mem_reader* mr = data; return mr->size; } // TIFF memory reader: TIFFMapFileProc static int tiff_map(thandle_t data, void** base, toff_t* size) { struct mem_reader* mr = data; *base = (uint8_t*)mr->data; *size = mr->size; return 0; } // TIFF memory reader: TIFFUnmapFileProc static void tiff_unmap(__attribute__((unused)) thandle_t data, __attribute__((unused)) void* base, __attribute__((unused)) toff_t size) { } // TIFF loader implementation enum loader_status decode_tiff(struct image* ctx, const uint8_t* data, size_t size) { TIFF* tiff; TIFFRGBAImage timg; struct pixmap* pm; char err[LIBTIFF_ERRMSG_SZ]; struct mem_reader reader; // check signature if (size < sizeof(signature1) || size < sizeof(signature2) || (memcmp(data, signature1, sizeof(signature1)) && memcmp(data, signature2, sizeof(signature2)))) { return ldr_unsupported; } reader.data = data; reader.size = size; reader.position = 0; TIFFSetErrorHandler(NULL); TIFFSetWarningHandler(NULL); tiff = TIFFClientOpen("", "r", &reader, tiff_read, tiff_write, tiff_seek, tiff_close, tiff_size, tiff_map, tiff_unmap); if (!tiff) { return ldr_fmterror; } *err = 0; if (!TIFFRGBAImageBegin(&timg, tiff, 0, err)) { goto fail; } pm = image_allocate_frame(ctx, timg.width, timg.height); if (!pm) { goto fail; } if (!TIFFRGBAImageGet(&timg, pm->data, timg.width, timg.height)) { goto fail; } // convert ABGR -> ARGB for (size_t i = 0; i < pm->width * pm->height; ++i) { pm->data[i] = ABGR_TO_ARGB(pm->data[i]); } if (timg.orientation == ORIENTATION_TOPLEFT) { image_flip_vertical(ctx); } image_set_format(ctx, "TIFF %dbpp", timg.bitspersample * timg.samplesperpixel); ctx->alpha = true; TIFFRGBAImageEnd(&timg); TIFFClose(tiff); return ldr_success; fail: image_free_frames(ctx); TIFFRGBAImageEnd(&timg); TIFFClose(tiff); return ldr_fmterror; } swayimg-3.8/src/formats/webp.c000066400000000000000000000055341474536441700164350ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // WebP format decoder. // Copyright (C) 2020 Artem Senichev #include "../exif.h" #include "../loader.h" #include "buildcfg.h" #include #include // WebP signature static const uint8_t signature[] = { 'R', 'I', 'F', 'F' }; // WebP loader implementation enum loader_status decode_webp(struct image* ctx, const uint8_t* data, size_t size) { const WebPData raw = { .bytes = data, .size = size }; WebPAnimDecoderOptions webp_opts; WebPAnimDecoder* webp_dec = NULL; WebPAnimInfo webp_info; WebPBitstreamFeatures prop; int prev_timestamp = 0; // check signature if (size < sizeof(signature) || memcmp(data, signature, sizeof(signature))) { return ldr_unsupported; } // get image properties if (WebPGetFeatures(data, size, &prop) != VP8_STATUS_OK) { return ldr_fmterror; } // open decoder WebPAnimDecoderOptionsInit(&webp_opts); webp_opts.color_mode = MODE_BGRA; webp_dec = WebPAnimDecoderNew(&raw, &webp_opts); if (!webp_dec) { goto fail; } if (!WebPAnimDecoderGetInfo(webp_dec, &webp_info)) { goto fail; } // allocate frame sequence if (!image_create_frames(ctx, webp_info.frame_count)) { goto fail; } // decode every frame for (size_t i = 0; i < ctx->num_frames; ++i) { uint8_t* buffer; int timestamp; struct image_frame* frame = &ctx->frames[i]; struct pixmap* pm = &frame->pm; if (!pixmap_create(pm, webp_info.canvas_width, webp_info.canvas_height)) { goto fail; } if (!WebPAnimDecoderGetNext(webp_dec, &buffer, ×tamp)) { goto fail; } memcpy(pm->data, buffer, pm->width * pm->height * sizeof(argb_t)); if (ctx->num_frames > 1) { frame->duration = timestamp - prev_timestamp; prev_timestamp = timestamp; if (frame->duration <= 0) { frame->duration = 100; } } } #ifdef HAVE_LIBEXIF const WebPDemuxer* webp_dmx = WebPAnimDecoderGetDemuxer(webp_dec); if (WebPDemuxGetI(webp_dmx, WEBP_FF_FORMAT_FLAGS) & EXIF_FLAG) { WebPChunkIterator it; if (WebPDemuxGetChunk(webp_dmx, "EXIF", 1, &it)) { process_exif(ctx, it.chunk.bytes, it.chunk.size); WebPDemuxReleaseChunkIterator(&it); } } #endif // HAVE_LIBEXIF WebPAnimDecoderDelete(webp_dec); image_set_format( ctx, "WebP %s %s%s", prop.format == 1 ? "lossy" : "lossless", prop.has_alpha ? "+alpha" : "", prop.has_animation ? "+animation" : ""); ctx->alpha = prop.has_alpha; return ldr_success; fail: if (webp_dec) { WebPAnimDecoderDelete(webp_dec); } return ldr_fmterror; } swayimg-3.8/src/gallery.c000066400000000000000000000356671474536441700154760ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Gallery mode. // Copyright (C) 2024 Artem Senichev #include "gallery.h" #include "application.h" #include "imagelist.h" #include "info.h" #include "loader.h" #include "thumbnail.h" #include "ui.h" #include // Scale for selected thumbnail #define THUMB_SELECTED_SCALE 1.15f /** Gallery context. */ struct gallery { size_t thumb_size; ///< Size of thumbnail size_t thumb_cache; ///< Max number of thumbnails in cache argb_t clr_window; ///< Window background argb_t clr_background; ///< Tile background argb_t clr_select; ///< Selected tile background argb_t clr_border; ///< Selected tile border argb_t clr_shadow; ///< Selected tile shadow size_t top; ///< Index of the first displayed image size_t selected; ///< Index of the selected image }; /** Global gallery context. */ static struct gallery ctx; /** * Get thumbnail layout. * @param cols,rows,gap layout description */ static void get_layout(size_t* cols, size_t* rows, size_t* gap) { const size_t width = ui_get_width(); const size_t height = ui_get_height(); const size_t cnum = width / ctx.thumb_size; const size_t gap_px = (width - (cnum * ctx.thumb_size)) / (cnum + 1); const size_t rnum = (height - gap_px) / (ctx.thumb_size + gap_px); if (cols) { *cols = cnum; } if (rows) { *rows = rnum; } if (gap) { *gap = gap_px; } } /** Reset loader queue. */ static void reset_loader(void) { // get number of thumbnails on the screen size_t cols, rows; get_layout(&cols, &rows, NULL); ++rows; const size_t total = cols * rows; // search for nearest to selected const size_t last = image_list_jump(ctx.top, total - 1, true); const size_t max_f = image_list_distance(ctx.selected, last); const size_t max_b = image_list_distance(ctx.top, ctx.selected); size_t next_f = ctx.selected; size_t next_b = ctx.selected; loader_queue_reset(); if (!thumbnail_get(ctx.selected)) { loader_queue_append(ctx.selected); } for (size_t i = 0; i < max(max_f, max_b); ++i) { if (i < max_f) { next_f = image_list_nearest(next_f, true, false); if (!thumbnail_get(next_f)) { loader_queue_append(next_f); } } if (i < max_b) { next_b = image_list_nearest(next_b, false, false); if (!thumbnail_get(next_b)) { loader_queue_append(next_b); } } } // remove the furthest thumbnails from the cache if (ctx.thumb_cache != 0 && total < ctx.thumb_cache) { const size_t half = (ctx.thumb_cache - total) / 2; const size_t min_id = image_list_jump(ctx.top, half, false); const size_t max_id = image_list_jump(last, half, true); thumbnail_clear(min_id, max_id); } } /** Update thumbnail layout. */ static void update_layout(void) { size_t cols, rows; size_t distance; get_layout(&cols, &rows, NULL); // if selection is not visible, put it on the center distance = image_list_distance(ctx.top, ctx.selected); if (distance > cols * rows) { const size_t center_x = cols / 2; const size_t center_y = rows / 2; ctx.top = image_list_jump(ctx.selected, center_y * cols + center_x, false); } // remove gap at the bottom of the screen distance = image_list_distance(ctx.top, image_list_last()); if (distance < cols * (rows - 1)) { ctx.top = image_list_jump(image_list_last(), cols * rows - 1, false); } reset_loader(); } /** * Update info text container for currently selected image. */ static void update_info(void) { const struct thumbnail* th = thumbnail_get(ctx.selected); if (th) { info_reset(th->image); info_update(info_image_size, "%zux%zu", th->width, th->height); info_update(info_index, "%zu of %zu", th->image->index + 1, image_list_size()); } app_redraw(); } /** * Set current selection. * @param index image index to set as selected one */ static void select_thumbnail(size_t index) { ctx.selected = index; update_info(); update_layout(); app_redraw(); } /** * Skip specified image. * @param index image position in the image list * @return true if next image was loaded */ static bool skip_thumbnail(size_t index) { const size_t next = image_list_skip(index); if (next == IMGLIST_INVALID) { printf("No more images, exit\n"); app_exit(0); return false; } thumbnail_remove(index); if (index == ctx.top || ctx.top > next) { ctx.top = next; } if (index == ctx.selected) { select_thumbnail(next); } else { update_layout(); } return true; } /** * Select closest item. * @param direction next image position in list */ static void select_nearest(enum action_type direction) { size_t cols, rows; size_t index; get_layout(&cols, &rows, NULL); index = ctx.selected; switch (direction) { case action_first_file: index = image_list_first(); ctx.top = index; break; case action_last_file: index = image_list_last(); ctx.top = image_list_jump(index, cols * rows - 1, false); break; case action_prev_file: case action_step_left: if (index != image_list_first()) { index = image_list_nearest(index, false, false); } break; case action_next_file: case action_step_right: if (index != image_list_last()) { index = image_list_nearest(index, true, false); } break; case action_step_up: index = image_list_jump(index, cols, false); break; case action_step_down: index = image_list_jump(index, cols, true); break; default: break; } if (index != IMGLIST_INVALID && index != ctx.selected) { // fix up top by one line if (ctx.top > index) { ctx.top = image_list_jump(ctx.top, cols, false); } else { const size_t distance = image_list_distance(ctx.top, index); if (distance >= cols * rows) { ctx.top = image_list_jump(ctx.top, cols, true); } } select_thumbnail(index); } } /** * Scroll one page. * @param forward scroll direction */ static void scroll_page(bool forward) { size_t cols, rows; size_t distance; size_t selected; get_layout(&cols, &rows, NULL); distance = cols * rows - 1; selected = image_list_jump(ctx.selected, distance, forward); if (selected != IMGLIST_INVALID && selected != ctx.selected) { const size_t top = image_list_jump(ctx.top, distance, forward); if (top != image_list_last()) { ctx.top = top; } select_thumbnail(selected); } } /** * Draw thumbnail. * @param window destination window * @param x,y top left coordinate * @param image thumbnail image * @param selected flag to highlight current thumbnail */ static void draw_thumbnail(struct pixmap* window, ssize_t x, ssize_t y, const struct image* image, bool selected) { const struct pixmap* thumb = image ? &image->frames[0].pm : NULL; if (!selected) { pixmap_fill(window, x, y, ctx.thumb_size, ctx.thumb_size, ctx.clr_background); if (thumb) { x += ctx.thumb_size / 2 - thumb->width / 2; y += ctx.thumb_size / 2 - thumb->height / 2; pixmap_copy(thumb, window, x, y, image->alpha); } } else { // currently selected item const size_t thumb_size = THUMB_SELECTED_SCALE * ctx.thumb_size; const size_t thumb_offset = (thumb_size - ctx.thumb_size) / 2; x = max(0, x - (ssize_t)thumb_offset); y = max(0, y - (ssize_t)thumb_offset); if (x + thumb_size >= window->width) { x = window->width - thumb_size; } pixmap_fill(window, x, y, thumb_size, thumb_size, ctx.clr_select); if (thumb) { const ssize_t thumb_w = thumb->width * THUMB_SELECTED_SCALE; const ssize_t thumb_h = thumb->height * THUMB_SELECTED_SCALE; const ssize_t tx = x + thumb_size / 2 - thumb_w / 2; const ssize_t ty = y + thumb_size / 2 - thumb_h / 2; pixmap_scale(thumbnail_get_aa(), thumb, window, tx, ty, THUMB_SELECTED_SCALE, image->alpha); } // shadow if (ARGB_GET_A(ctx.clr_shadow)) { const argb_t base = ctx.clr_shadow & 0x00ffffff; const uint8_t alpha = ARGB_GET_A(ctx.clr_shadow); const size_t width = max(1, (double)thumb_size / 15.0 * ((double)alpha / 255.0)); const size_t alpha_step = alpha / width; for (size_t i = 0; i < width; ++i) { const ssize_t lx = i + x + thumb_size; const ssize_t ly = y + width; const ssize_t lh = thumb_size - (width - i); const argb_t color = base | ARGB_SET_A(alpha - i * alpha_step); pixmap_vline(window, lx, ly, lh, color); } for (size_t i = 0; i < width; ++i) { const ssize_t lx = x + width; const ssize_t ly = y + thumb_size + i; const ssize_t lw = thumb_size - (width - i) + 1; const argb_t color = base | ARGB_SET_A(alpha - i * alpha_step); pixmap_hline(window, lx, ly, lw, color); } } // border if (ARGB_GET_A(ctx.clr_border)) { pixmap_rect(window, x, y, thumb_size, thumb_size, ctx.clr_border); } } } /** * Draw thumbnails. * @param window destination window */ static void draw_thumbnails(struct pixmap* window) { size_t cols, rows, gap; size_t index = ctx.top; ssize_t select_x = 0; ssize_t select_y = 0; const struct thumbnail* select_th = NULL; // thumbnail layout get_layout(&cols, &rows, &gap); ++rows; // draw for (size_t row = 0; row < rows; ++row) { const ssize_t y = row * ctx.thumb_size + gap * (row + 1); for (size_t col = 0; col < cols; ++col) { const ssize_t x = col * ctx.thumb_size + gap * (col + 1); const struct thumbnail* th = thumbnail_get(index); // draw preview, but postpone the selected item if (index == ctx.selected) { select_x = x; select_y = y; select_th = th; } else { draw_thumbnail(window, x, y, th ? th->image : NULL, false); } // get next thumbnail index index = image_list_nearest(index, true, false); if (index == IMGLIST_INVALID) { goto done; } } } done: // draw selected thumbnail draw_thumbnail(window, select_x, select_y, select_th ? select_th->image : NULL, true); } /** * Draw gallery. */ static void redraw(void) { struct pixmap* wnd; if (image_list_first() == IMGLIST_INVALID) { printf("No more images, exit\n"); app_exit(0); return; } wnd = ui_draw_begin(); if (!wnd) { return; } pixmap_fill(wnd, 0, 0, wnd->width, wnd->height, ctx.clr_window); draw_thumbnails(wnd); info_print(wnd); ui_draw_commit(); } /** * Apply action. * @param action pointer to the action being performed */ static void apply_action(const struct action* action) { switch (action->type) { case action_antialiasing: info_update(info_status, "Anti-aliasing: %s", pixmap_aa_names[thumbnail_switch_aa()]); thumbnail_clear(IMGLIST_INVALID, IMGLIST_INVALID); reset_loader(); app_redraw(); break; case action_first_file: case action_last_file: case action_prev_file: case action_next_file: case action_step_left: case action_step_right: case action_step_up: case action_step_down: select_nearest(action->type); break; case action_page_up: case action_page_down: scroll_page(action->type == action_page_down); break; case action_skip_file: skip_thumbnail(ctx.selected); break; case action_reload: thumbnail_clear(IMGLIST_INVALID, IMGLIST_INVALID); reset_loader(); app_redraw(); break; case action_exec: app_execute(action->params, image_list_get(ctx.selected)); break; case action_status: info_update(info_status, "%s", action->params); app_redraw(); break; case action_mode: app_switch_mode(ctx.selected); break; default: break; } } /** * Background loader thread callback. * @param image loaded image instance, NULL if load error * @param index index of the image in the image list */ static void on_image_load(struct image* image, size_t index) { if (!image) { loader_queue_reset(); skip_thumbnail(index); } else { if (thumbnail_get(index)) { image_free(image); } else { thumbnail_add(image); if (index == ctx.selected) { update_info(); } } } app_redraw(); } void gallery_init(const struct config* cfg, struct image* image) { thumbnail_init(cfg); ctx.thumb_size = config_get_num(cfg, CFG_GALLERY, CFG_GLRY_SIZE, 1, 1024); ctx.thumb_cache = config_get_num(cfg, CFG_GALLERY, CFG_GLRY_CACHE, 0, 1024); ctx.clr_window = config_get_color(cfg, CFG_GALLERY, CFG_GLRY_WINDOW); ctx.clr_background = config_get_color(cfg, CFG_GALLERY, CFG_GLRY_BKG); ctx.clr_select = config_get_color(cfg, CFG_GALLERY, CFG_GLRY_SELECT); ctx.clr_border = config_get_color(cfg, CFG_GALLERY, CFG_GLRY_BORDER); ctx.clr_shadow = config_get_color(cfg, CFG_GALLERY, CFG_GLRY_SHADOW); ctx.top = image_list_first(); ctx.selected = ctx.top; if (image) { thumbnail_add(image); select_thumbnail(image->index); } } void gallery_destroy(void) { thumbnail_free(); } void gallery_handle(const struct event* event) { switch (event->type) { case event_action: apply_action(event->param.action); break; case event_redraw: redraw(); break; case event_activate: select_thumbnail(event->param.activate.index); break; case event_load: on_image_load(event->param.load.image, event->param.load.index); break; case event_resize: update_layout(); break; case event_drag: break; // unused in gallery mode } } swayimg-3.8/src/gallery.h000066400000000000000000000010121474536441700154540ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Gallery mode. // Copyright (C) 2024 Artem Senichev #pragma once #include "config.h" #include "event.h" /** * Initialize global gallery context. * @param cfg config instance * @param image initial image to open */ void gallery_init(const struct config* cfg, struct image* image); /** * Destroy global gallery context. */ void gallery_destroy(void); /** * Event handler, see `event_handler` for details. */ void gallery_handle(const struct event* event); swayimg-3.8/src/image.c000066400000000000000000000101101474536441700150710ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image instance: pixel data and meta info. // Copyright (C) 2021 Artem Senichev #include "image.h" #include #include #include #include struct image* image_alloc(void) { return calloc(1, sizeof(struct image)); } void image_free(struct image* ctx) { if (ctx) { image_free_frames(ctx); free(ctx->source); free(ctx->parent_dir); free(ctx->format); list_for_each(ctx->info, struct image_info, it) { free(it); } free(ctx); } } void image_set_source(struct image* ctx, const char* source) { ctx->source = str_dup(source, NULL); // set name and parent dir if (strcmp(source, LDRSRC_STDIN) == 0 || strncmp(source, LDRSRC_EXEC, LDRSRC_EXEC_LEN) == 0) { ctx->name = ctx->source; str_dup("", &ctx->parent_dir); } else { size_t pos = strlen(ctx->source) - 1; // get name while (pos && ctx->source[--pos] != '/') { } ctx->name = ctx->source + pos + (ctx->source[pos] == '/' ? 1 : 0); // get parent dir if (pos == 0) { str_dup("", &ctx->parent_dir); } else { const size_t end = pos; while (pos && ctx->source[--pos] != '/') { } if (ctx->source[pos] == '/') { ++pos; } str_append(ctx->source + pos, end - pos, &ctx->parent_dir); } } } void image_flip_vertical(struct image* ctx) { for (size_t i = 0; i < ctx->num_frames; ++i) { pixmap_flip_vertical(&ctx->frames[i].pm); } } void image_flip_horizontal(struct image* ctx) { for (size_t i = 0; i < ctx->num_frames; ++i) { pixmap_flip_horizontal(&ctx->frames[i].pm); } } void image_rotate(struct image* ctx, size_t angle) { for (size_t i = 0; i < ctx->num_frames; ++i) { pixmap_rotate(&ctx->frames[i].pm, angle); } } void image_set_format(struct image* ctx, const char* fmt, ...) { va_list args; int len; char* buffer; va_start(args, fmt); // NOLINTNEXTLINE(clang-analyzer-valist.Uninitialized) len = vsnprintf(NULL, 0, fmt, args); va_end(args); if (len <= 0) { return; } ++len; // last null buffer = realloc(ctx->format, len); if (!buffer) { return; } va_start(args, fmt); vsprintf(buffer, fmt, args); va_end(args); ctx->format = buffer; } void image_add_meta(struct image* ctx, const char* key, const char* fmt, ...) { va_list args; int len; struct image_info* entry; const size_t key_len = strlen(key) + 1 /* last null */; // get value string size va_start(args, fmt); // NOLINTNEXTLINE(clang-analyzer-valist.Uninitialized) len = vsnprintf(NULL, 0, fmt, args); va_end(args); if (len <= 0) { return; } // allocate new entry len += sizeof(struct image_info) + key_len + 1 /* last null of value */; entry = calloc(1, len); if (!entry) { return; } // fill entry entry->key = (char*)entry + sizeof(struct image_info); memcpy(entry->key, key, key_len); entry->value = entry->key + key_len; va_start(args, fmt); vsprintf(entry->value, fmt, args); va_end(args); ctx->info = list_append(ctx->info, entry); } struct pixmap* image_allocate_frame(struct image* ctx, size_t width, size_t height) { if (image_create_frames(ctx, 1) && pixmap_create(&ctx->frames[0].pm, width, height)) { return &ctx->frames[0].pm; } image_free_frames(ctx); return NULL; } struct image_frame* image_create_frames(struct image* ctx, size_t num) { struct image_frame* frames; frames = calloc(1, num * sizeof(struct image_frame)); if (frames) { ctx->frames = frames; ctx->num_frames = num; } return frames; } void image_free_frames(struct image* ctx) { for (size_t i = 0; i < ctx->num_frames; ++i) { pixmap_free(&ctx->frames[i].pm); } free(ctx->frames); ctx->frames = NULL; ctx->num_frames = 0; } swayimg-3.8/src/image.h000066400000000000000000000064621474536441700151150ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image instance: pixel data and meta info. // Copyright (C) 2021 Artem Senichev #pragma once #include "memdata.h" #include "pixmap.h" // File name used for image, that is read from stdin through pipe #define LDRSRC_STDIN "stdin://" #define LDRSRC_STDIN_LEN (sizeof(LDRSRC_STDIN) - 1) // Special prefix used to load images from external command output #define LDRSRC_EXEC "exec://" #define LDRSRC_EXEC_LEN (sizeof(LDRSRC_EXEC) - 1) /** Image frame. */ struct image_frame { struct pixmap pm; ///< Frame data size_t duration; ///< Frame duration in milliseconds (animation) }; /** Image meta info. */ struct image_info { struct list list; ///< Links to prev/next entry char* key; ///< Meta key name char* value; ///< Meta value }; /** Image context. */ struct image { size_t index; ///< Index of the entry in the image list char* source; ///< Image source (e.g. path to the image file) const char* name; ///< Name of the image file char* parent_dir; ///< Parent directory name size_t file_size; ///< Size of image file char* format; ///< Format description struct image_frame* frames; ///< Image frames size_t num_frames; ///< Total number of frames bool alpha; ///< Image has alpha channel struct image_info* info; ///< Image meta info }; /** * Allocate empty image instance. * @return image context or NULL on errors */ struct image* image_alloc(void); /** * Free image. * @param ctx image context to free */ void image_free(struct image* ctx); /** * Set image source (path or special prefix). * @param ctx image context * @param source image source */ void image_set_source(struct image* ctx, const char* source); /** * Flip image vertically. * @param ctx image context */ void image_flip_vertical(struct image* ctx); /** * Flip image horizontally. * @param ctx image context */ void image_flip_horizontal(struct image* ctx); /** * Rotate image. * @param ctx image context * @param angle rotation angle (only 90, 180, or 270) */ void image_rotate(struct image* ctx, size_t angle); /** * Set image format description. * @param ctx image context * @param fmt format description */ void image_set_format(struct image* ctx, const char* fmt, ...) __attribute__((format(printf, 2, 3))); /** * Add meta info property. * @param ctx image context * @param key property name * @param fmt value format */ void image_add_meta(struct image* ctx, const char* key, const char* fmt, ...) __attribute__((format(printf, 3, 4))); /** * Create single frame, allocate buffer and add frame to the image. * @param width frame width in px * @param height frame height in px * @return pointer to the pixmap associated with the frame, or NULL on errors */ struct pixmap* image_allocate_frame(struct image* ctx, size_t width, size_t height); /** * Create list of empty frames. * @param ctx image context * @param num total number of frames * @return pointer to the frame list, NULL on errors */ struct image_frame* image_create_frames(struct image* ctx, size_t num); /** * Free image frames. * @param ctx image context */ void image_free_frames(struct image* ctx); swayimg-3.8/src/imagelist.c000066400000000000000000000332101474536441700157730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // List of images. // Copyright (C) 2022 Artem Senichev #include "imagelist.h" #include "image.h" #include #include #include #include #include #include #include #include #include #include /** Context of the image list (which is actually an array). */ struct image_list { char** sources; ///< Array of entries size_t capacity; ///< Number of allocated entries (size of array) size_t size; ///< Number of entries in array enum list_order order; ///< File list order bool loop; ///< File list loop mode bool recursive; ///< Read directories recursively bool all_files; ///< Open all files from the same directory }; /** Global image list instance. */ static struct image_list ctx; /** Order names. */ static const char* order_names[] = { [order_none] = "none", [order_alpha] = "alpha", [order_reverse] = "reverse", [order_random] = "random", }; /** * Get absolute path from relative source. * @param source relative file path * @param path output absolute path buffer * @param path_max size of the buffer * @return length of the absolute path including last null */ static size_t absolute_path(const char* source, char* path, size_t path_max) { assert(source && path && path_max); char buffer[PATH_MAX]; struct str_slice dirs[1024]; size_t dirs_num; size_t pos; if (strcmp(source, LDRSRC_STDIN) == 0 || strncmp(source, LDRSRC_EXEC, LDRSRC_EXEC_LEN) == 0) { strncpy(path, source, path_max - 1); return strlen(path) + 1; } if (*source == '/') { strncpy(buffer, source, sizeof(buffer) - 1); } else { // relative to the current dir size_t len; if (!getcwd(buffer, sizeof(buffer) - 1)) { return 0; } len = strlen(buffer); if (buffer[len] != '/') { buffer[len] = '/'; ++len; } if (len >= sizeof(buffer)) { return 0; } strncpy(buffer + len, source, sizeof(buffer) - len - 1); } // split by component dirs_num = str_split(buffer, '/', dirs, ARRAY_SIZE(dirs)); // remove "/../" and "/./" for (size_t i = 0; i < dirs_num; ++i) { if (dirs[i].len == 1 && dirs[i].value[0] == '.') { dirs[i].len = 0; } else if (dirs[i].len == 2 && dirs[i].value[0] == '.' && dirs[i].value[1] == '.') { dirs[i].len = 0; for (ssize_t j = (ssize_t)i - 1; j >= 0; --j) { if (dirs[j].len != 0) { dirs[j].len = 0; break; } } } } // collect to the absolute path path[0] = '/'; pos = 1; for (size_t i = 0; i < dirs_num; ++i) { if (dirs[i].len) { if (pos + dirs[i].len + 1 >= path_max) { return 0; } memcpy(path + pos, dirs[i].value, dirs[i].len); pos += dirs[i].len; if (i < dirs_num - 1) { if (pos + 1 >= path_max) { return 0; } path[pos++] = '/'; } } } // last null if (pos + 1 >= path_max) { return 0; } path[pos++] = 0; return pos; } /** * Add new entry to the list. * @param source image data source to add */ static void add_entry(const char* source) { // check for duplicates for (size_t i = 0; i < ctx.size; ++i) { if (strcmp(ctx.sources[i], source) == 0) { return; } } // relocate array, if needed if (ctx.size + 1 >= ctx.capacity) { const size_t cap = ctx.capacity ? ctx.capacity * 2 : 4; char** ptr = realloc(ctx.sources, cap * sizeof(*ctx.sources)); if (!ptr) { return; } ctx.capacity = cap; ctx.sources = ptr; } // add new entry ctx.sources[ctx.size] = str_dup(source, NULL); if (ctx.sources[ctx.size]) { ++ctx.size; } } /** * Add file to the list. * @param file path to the file */ static void add_file(const char* path) { char abspath[PATH_MAX]; if (absolute_path(path, abspath, sizeof(abspath))) { add_entry(abspath); } } /** * Add files from the directory to the list. * @param dir full path to the directory */ static void add_dir(const char* dir) { DIR* dir_handle; struct dirent* dir_entry; char* path = NULL; dir_handle = opendir(dir); if (!dir_handle) { return; } while ((dir_entry = readdir(dir_handle))) { const char* name = dir_entry->d_name; struct stat st; if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { continue; // skip link to self/parent dirs } // compose full path if (!str_dup(dir, &path) || !str_append("/", 1, &path) || !str_append(name, 0, &path)) { continue; } if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) { if (ctx.recursive) { add_dir(path); } } else if (S_ISREG(st.st_mode)) { add_file(path); } } } free(path); closedir(dir_handle); } /** * Get next directory entry index (works only for paths as source). * @param start index of the start position * @param forward step direction * @return index of the next entry or IMGLIST_INVALID if not found */ static size_t next_dir(size_t start, bool forward) { const char* cur_path = ctx.sources[start]; size_t cur_len; size_t index = start; if (start == IMGLIST_INVALID) { return image_list_first(); } // directory part of the current file path cur_len = strlen(cur_path) - 1; while (cur_len && cur_path[cur_len] != '/') { --cur_len; } // search for another directory in file list while (true) { const char* next_path; size_t next_len; index = image_list_nearest(index, forward, ctx.loop); if (index == IMGLIST_INVALID || index == start) { break; // not found } next_path = ctx.sources[index]; next_len = strlen(next_path) - 1; while (next_len && next_path[next_len] != '/') { --next_len; } if (cur_len != next_len || strncmp(cur_path, next_path, next_len)) { return index; } }; return IMGLIST_INVALID; } /** * Compare sources callback for `qsort`. * @return negative if a < b, positive if a > b, 0 otherwise */ static int compare_alpha(const void* a, const void* b) { return strcoll(*(const char**)a, *(const char**)b); } /** * Compare sources callback for `qsort`. * @return negative if a > b, positive if a < b, 0 otherwise */ static int compare_reverse(const void* a, const void* b) { return -strcoll(*(const char**)a, *(const char**)b); } void image_list_init(const struct config* cfg) { ctx.order = config_get_oneof(cfg, CFG_LIST, CFG_LIST_ORDER, order_names, ARRAY_SIZE(order_names)); ctx.loop = config_get_bool(cfg, CFG_LIST, CFG_LIST_LOOP); ctx.recursive = config_get_bool(cfg, CFG_LIST, CFG_LIST_RECURSIVE); ctx.all_files = config_get_bool(cfg, CFG_LIST, CFG_LIST_ALL); } void image_list_destroy(void) { for (size_t i = 0; i < ctx.size; ++i) { free(ctx.sources[i]); } free(ctx.sources); ctx.sources = NULL; ctx.capacity = 0; ctx.size = 0; } void image_list_add(const char* source) { struct stat st; // special url if (strncmp(source, LDRSRC_STDIN, LDRSRC_STDIN_LEN) == 0 || strncmp(source, LDRSRC_EXEC, LDRSRC_EXEC_LEN) == 0) { add_entry(source); return; } // file from file system if (stat(source, &st) != 0) { fprintf(stderr, "Unable to query file %s: [%i] %s\n", source, errno, strerror(errno)); return; } if (S_ISDIR(st.st_mode)) { add_dir(source); } else if (S_ISREG(st.st_mode)) { if (!ctx.all_files) { add_file(source); } else { // add all files from the same directory const char* delim = strrchr(source, '/'); const size_t len = delim ? delim - source : 0; if (len == 0) { add_dir("."); } else { char* dir = str_append(source, len, NULL); if (dir) { add_dir(dir); free(dir); } } } } } void image_list_reorder(void) { assert(ctx.size); switch (ctx.order) { case order_none: break; case order_alpha: qsort(ctx.sources, ctx.size, sizeof(*ctx.sources), compare_alpha); break; case order_reverse: qsort(ctx.sources, ctx.size, sizeof(*ctx.sources), compare_reverse); break; case order_random: for (size_t i = 0; i < ctx.size; ++i) { if (i == 0) { // init random sequence struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); srand(ts.tv_nsec); } const size_t j = rand() % ctx.size; if (i != j) { char* swap = ctx.sources[i]; ctx.sources[i] = ctx.sources[j]; ctx.sources[j] = swap; } } break; } } size_t image_list_size(void) { return ctx.size; } const char* image_list_get(size_t index) { return index < ctx.size ? ctx.sources[index] : NULL; } size_t image_list_find(const char* source) { char abspath[PATH_MAX]; if (absolute_path(source, abspath, sizeof(abspath))) { for (size_t i = 0; i < ctx.size; ++i) { if (ctx.sources[i] && strcmp(ctx.sources[i], abspath) == 0) { return i; } } } return IMGLIST_INVALID; } size_t image_list_nearest(size_t start, bool forward, bool loop) { size_t index = start; if (index == IMGLIST_INVALID) { if (forward) { return image_list_first(); } if (loop && !forward) { return image_list_last(); } return IMGLIST_INVALID; } if (index >= ctx.size) { if (!forward) { return image_list_last(); } if (loop && forward) { return image_list_first(); } return IMGLIST_INVALID; } while (true) { if (forward) { if (index + 1 < ctx.size) { ++index; } else if (!loop) { index = IMGLIST_INVALID; // already at last entry break; } else { index = 0; } } else { if (index > 0) { --index; } else if (!loop) { index = IMGLIST_INVALID; // already at first entry break; } else { index = ctx.size - 1; } } if (index == start) { index = IMGLIST_INVALID; // only one valid entry in the list break; } if (ctx.sources[index]) { break; } } return index; } size_t image_list_jump(size_t start, size_t distance, bool forward) { size_t index = start; if (index == IMGLIST_INVALID || index >= ctx.size) { return IMGLIST_INVALID; } while (distance) { const size_t next = image_list_nearest(index, forward, false); if (next == IMGLIST_INVALID) { break; } index = next; --distance; } return index; } size_t image_list_distance(size_t start, size_t end) { size_t distance = 0; size_t index; if (start == IMGLIST_INVALID) { start = image_list_first(); } if (end == IMGLIST_INVALID) { end = image_list_last(); } if (start <= end) { index = start; } else { index = end; end = start; } while (index != IMGLIST_INVALID && index != end) { ++distance; index = image_list_nearest(index, true, false); } return distance; } size_t image_list_next_file(size_t start) { return image_list_nearest(start, true, ctx.loop); } size_t image_list_prev_file(size_t start) { return image_list_nearest(start, false, ctx.loop); } size_t image_list_rand_file(size_t exclude) { size_t index = image_list_nearest(rand() % ctx.size, true, true); if (index != IMGLIST_INVALID && index == exclude) { index = image_list_nearest(exclude, true, true); } return index; } size_t image_list_next_dir(size_t start) { return next_dir(start, true); } size_t image_list_prev_dir(size_t start) { return next_dir(start, false); } size_t image_list_first(void) { if (ctx.size == 0) { return IMGLIST_INVALID; } return ctx.sources[0] ? 0 : image_list_nearest(0, true, false); } size_t image_list_last(void) { const size_t index = ctx.size - 1; if (ctx.size == 0) { return IMGLIST_INVALID; } return ctx.sources[index] ? index : image_list_nearest(index, false, false); } size_t image_list_skip(size_t index) { size_t next; // remove current entry from list if (index < ctx.size && ctx.sources[index]) { free(ctx.sources[index]); ctx.sources[index] = NULL; } // get next entry next = image_list_nearest(index, true, false); if (next == IMGLIST_INVALID) { next = image_list_nearest(index, false, false); } return next; } swayimg-3.8/src/imagelist.h000066400000000000000000000071721474536441700160100ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // List of images. // Copyright (C) 2022 Artem Senichev #pragma once #include "config.h" // Invalid index of the entry #define IMGLIST_INVALID SIZE_MAX /** Order of file list. */ enum list_order { order_none, ///< Unsorted (system depended) order_alpha, ///< Alphanumeric sort order_reverse, ///< Reversed alphanumeric sort order_random ///< Random order }; /** * Initialize global image list context. * @param cfg config instance */ void image_list_init(const struct config* cfg); /** * Destroy global image list context. */ void image_list_destroy(void); /** * Add image source to the list. * @param source image source to add (file path or special prefix) */ void image_list_add(const char* source); /** * Reorder image list (sort/random/...). */ void image_list_reorder(void); /** * Get image list size. * @return total number of entries in the list include non-image files */ size_t image_list_size(void); /** * Get image source for specified index. * @param index index of the image list entry * @return image data source description (path, ...) or NULL if no source */ const char* image_list_get(size_t index); /** * Find index for specified source. * @param source image data source * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_find(const char* source); /** * Get index of nearest entry in the list. * @param start start position * @param forward direction(forward/backward) * @param loop enable/disable loop mode * @return index of the entry or IMGLIST_INVALID */ size_t image_list_nearest(size_t start, bool forward, bool loop); /** * Get distance between two indexes. * @param start,end entry indexes * @return number of image entries between indexes */ size_t image_list_distance(size_t start, size_t end); /** * Get index of the entry in specified distance from start. * @param start start position * @param distance number entries to skip * @param forward direction(forward/backward) * @return index of the entry or IMGLIST_INVALID */ size_t image_list_jump(size_t start, size_t distance, bool forward); /** * Get next entry index. * @param start index of the start position * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_next_file(size_t start); /** * Get previous entry index. * @param start index of the start position * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_prev_file(size_t start); /** * Get random entry index. * @param exclude index of the excluded position (currently viewed image) * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_rand_file(size_t exclude); /** * Get next directory entry index (works only for paths as source). * @param start index of the start position * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_next_dir(size_t start); /** * Get previous directory entry index (works only for paths as source). * @param start index of the start position * @return index of the entry or IMGLIST_INVALID if not found */ size_t image_list_prev_dir(size_t start); /** * Get the first entry index. * @return index of the entry or IMGLIST_INVALID if image list is empty */ size_t image_list_first(void); /** * Get the first entry index. * @return index of the entry or IMGLIST_INVALID if image list is empty */ size_t image_list_last(void); /** * Skip entry (remove from the image list). * @param index entry to remove * @return next valid index of the entry or IMGLIST_INVALID if list is empty */ size_t image_list_skip(size_t index); swayimg-3.8/src/info.c000066400000000000000000000453361474536441700147640ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image info: text blocks with image meta data. // Copyright (C) 2023 Artem Senichev #include "info.h" #include "application.h" #include "font.h" #include "keybind.h" #include "loader.h" #include "ui.h" #include #include #include #include #include #include /** Display modes. */ enum info_mode { mode_viewer, mode_gallery, mode_off, }; static const char* mode_names[] = { [mode_viewer] = CFG_MODE_VIEWER, [mode_gallery] = CFG_MODE_GALLERY, [mode_off] = "off", }; #define MODES_NUM 2 // clang-format off /** Field names. */ static const char* field_names[] = { [info_file_name] = "name", [info_file_dir] = "dir", [info_file_path] = "path", [info_file_size] = "filesize", [info_image_format] = "format", [info_image_size] = "imagesize", [info_exif] = "exif", [info_frame] = "frame", [info_index] = "index", [info_scale] = "scale", [info_status] = "status", }; #define FIELDS_NUM ARRAY_SIZE(field_names) /** Positions of text info block. */ enum block_position { pos_center, pos_top_left, pos_top_right, pos_bottom_left, pos_bottom_right, }; /** Block position names. */ static const char* position_names[] = { [pos_center] = CFG_INFO_CN, [pos_top_left] = CFG_INFO_TL, [pos_top_right] = CFG_INFO_TR, [pos_bottom_left] = CFG_INFO_BL, [pos_bottom_right] = CFG_INFO_BR, }; #define POSITION_NUM ARRAY_SIZE(position_names) // clang-format on // Max number of lines in one positioned block #define MAX_LINES (FIELDS_NUM + 10 /* EXIF and duplicates */) // Space between text layout and window edge #define TEXT_PADDING 10 /** Scheme of displayed field (line(s) of text). */ struct field_scheme { enum info_field type; ///< Field type bool title; ///< Print/hide field title }; /** Key/value text surface. */ struct keyval { struct text_surface key; struct text_surface value; }; /** Info scheme: set of fields in one of screen positions. */ struct block_scheme { struct field_scheme* fields; ///< Array of fields size_t fields_num; ///< Size of array }; /** Info timeout description. */ struct info_timeout { int fd; ///< Timer FD size_t timeout; ///< Timeout duration in seconds bool active; ///< Current state }; /** Info data context. */ struct info_context { enum info_mode mode; ///< Currently active mode struct info_timeout info; ///< Text info timeout struct info_timeout status; ///< Status message timeout struct text_surface* help; ///< Help layer lines size_t help_num; ///< Number of lines in help struct keyval* exif_lines; ///< EXIF data lines size_t exif_num; ///< Number of lines in EXIF data struct keyval fields[FIELDS_NUM]; ///< Info data struct block_scheme scheme[MODES_NUM][POSITION_NUM]; ///< Info scheme }; /** Global info context. */ static struct info_context ctx; /** Notification callback: handle timer event. */ static void on_timeout(void* data) { struct info_timeout* timeout = data; struct itimerspec ts = { 0 }; timeout->active = false; timerfd_settime(timeout->fd, 0, &ts, NULL); app_redraw(); } /** * Initialize timer. * @param timeout timer instance */ static void timeout_init(struct info_timeout* timeout) { timeout->fd = -1; timeout->active = true; if (timeout->timeout != 0) { timeout->fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (timeout->fd != -1) { app_watch(timeout->fd, on_timeout, timeout); } } } /** * Reset/restart timer. * @param timeout timer instance */ static void timeout_reset(struct info_timeout* timeout) { timeout->active = true; if (timeout->fd != -1) { struct itimerspec ts = { .it_value.tv_sec = timeout->timeout }; timerfd_settime(timeout->fd, 0, &ts, NULL); } } /** * Close timer FD. * @param timeout timer instance */ static void timeout_close(struct info_timeout* timeout) { if (timeout->fd != -1) { close(timeout->fd); } } /** * Print centered text block. * @param wnd destination window */ static void print_help(struct pixmap* window) { const size_t line_height = ctx.help[0].height; const size_t row_max = (window->height - TEXT_PADDING * 2) / line_height; const size_t columns = (ctx.help_num / row_max) + (ctx.help_num % row_max ? 1 : 0); const size_t rows = (ctx.help_num / columns) + (ctx.help_num % columns ? 1 : 0); const size_t col_space = line_height; size_t total_width = 0; size_t top = 0; size_t left = 0; // calculate total width for (size_t col = 0; col < columns; ++col) { size_t max_width = 0; for (size_t row = 0; row < rows; ++row) { const size_t index = row + col * rows; if (index >= ctx.help_num) { break; } if (max_width < ctx.help[index].width) { max_width = ctx.help[index].width; } } total_width += max_width; } total_width += col_space * (columns - 1); // top left corner of the centered text block if (total_width < ui_get_width()) { left = window->width / 2 - total_width / 2; } if (rows * line_height < ui_get_height()) { top = window->height / 2 - (rows * line_height) / 2; } // put text on window for (size_t col = 0; col < columns; ++col) { size_t y = top; size_t col_width = 0; for (size_t row = 0; row < rows; ++row) { const size_t index = row + col * rows; if (index >= ctx.help_num) { break; } font_print(window, left, y, &ctx.help[index]); if (col_width < ctx.help[index].width) { col_width = ctx.help[index].width; } y += line_height; } left += col_width + col_space; } } /** * Print info block with key/value text. * @param wnd destination window * @param pos block position * @param lines array of key/value lines to print * @param lines_num total number of lines */ static void print_keyval(struct pixmap* wnd, enum block_position pos, const struct keyval* lines, size_t lines_num) { size_t max_key_width = 0; const size_t height = lines[0].value.height; // calc max width of keys, used if block on the left side for (size_t i = 0; i < lines_num; ++i) { if (lines[i].key.width > max_key_width) { max_key_width = lines[i].key.width; } } max_key_width += height / 2; // draw info block for (size_t i = 0; i < lines_num; ++i) { const struct text_surface* key = &lines[i].key; const struct text_surface* value = &lines[i].value; size_t y = 0; size_t x_key = 0; size_t x_val = 0; // calculate line position switch (pos) { case pos_center: return; // not supported (not used anywhere) case pos_top_left: y = TEXT_PADDING + i * height; if (key->data) { x_key = TEXT_PADDING; x_val = TEXT_PADDING + max_key_width; } else { x_val = TEXT_PADDING; } break; case pos_top_right: y = TEXT_PADDING + i * height; x_val = wnd->width - TEXT_PADDING - value->width; if (key->data) { x_key = x_val - key->width - TEXT_PADDING; } break; case pos_bottom_left: y = wnd->height - TEXT_PADDING - height * lines_num + i * height; if (key->data) { x_key = TEXT_PADDING; x_val = TEXT_PADDING + max_key_width; } else { x_val = TEXT_PADDING; } break; case pos_bottom_right: y = wnd->height - TEXT_PADDING - height * lines_num + i * height; x_val = wnd->width - TEXT_PADDING - value->width; if (key->data) { x_key = x_val - key->width - TEXT_PADDING; } break; } if (key->data) { font_print(wnd, x_key, y, key); } font_print(wnd, x_val, y, value); } } /** * Import meta data from image. * @param image source image */ static void import_exif(const struct image* image) { struct keyval* lines; const size_t num_entries = list_size(&image->info->list); const size_t buf_size = num_entries * sizeof(*lines); // free previous lines for (size_t i = 0; i < ctx.exif_num; ++i) { free(ctx.exif_lines[i].key.data); free(ctx.exif_lines[i].value.data); } ctx.exif_num = 0; if (num_entries == 0) { return; } lines = realloc(ctx.exif_lines, buf_size); if (!lines) { return; } memset(lines, 0, buf_size); ctx.exif_num = num_entries; ctx.exif_lines = lines; list_for_each(image->info, const struct image_info, it) { char key[64]; snprintf(key, sizeof(key), "%s:", it->key); font_render(key, &lines->key); font_render(it->value, &lines->value); ++lines; } } /** * Parse and load scheme from config line. * @param config line to parse * @param scheme destination scheme description * @return true if config parsed successfully */ static bool parse_scheme(const char* config, struct block_scheme* scheme) { struct str_slice slices[MAX_LINES]; size_t slices_num; struct field_scheme* fields; // split into fields slices slices_num = str_split(config, ',', slices, ARRAY_SIZE(slices)); if (slices_num > ARRAY_SIZE(slices)) { slices_num = ARRAY_SIZE(slices); } fields = realloc(scheme->fields, slices_num * sizeof(*fields)); if (!fields) { return false; } scheme->fields = fields; scheme->fields_num = 0; for (size_t i = 0; i < slices_num; ++i) { struct field_scheme* field = &scheme->fields[scheme->fields_num]; ssize_t field_idx; struct str_slice* sl = &slices[i]; // title show/hide ('+' at the beginning) field->title = (sl->len > 0 && *sl->value == '+'); if (field->title) { ++sl->value; --sl->len; } // field type field_idx = str_index(field_names, sl->value, sl->len); if (field_idx >= 0) { field->type = field_idx; scheme->fields_num++; } else if (sl->len == 4 && strncmp(sl->value, "none", sl->len) == 0) { continue; // special value, just skip } else { return false; // invalid field name } } if (scheme->fields_num == 0) { free(scheme->fields); scheme->fields = NULL; } return true; } void info_init(const struct config* cfg) { for (size_t i = 0; i < MODES_NUM; ++i) { const char* section; section = (i == mode_viewer ? CFG_INFO_VIEWER : CFG_INFO_GALLERY); for (size_t j = 0; j < POSITION_NUM; ++j) { const char* position = position_names[j]; const char* format = config_get(cfg, section, position); if (!parse_scheme(format, &ctx.scheme[i][j])) { config_error_val(section, format); format = config_get_default(section, position); parse_scheme(format, &ctx.scheme[i][j]); } } } ctx.mode = config_get_bool(cfg, CFG_INFO, CFG_INFO_SHOW) ? mode_viewer : mode_off; ctx.info.timeout = config_get_num(cfg, CFG_INFO, CFG_INFO_ITIMEOUT, 0, 1024); timeout_init(&ctx.info); ctx.status.timeout = config_get_num(cfg, CFG_INFO, CFG_INFO_STIMEOUT, 0, 1024); timeout_init(&ctx.status); font_render("File name:", &ctx.fields[info_file_name].key); font_render("Directory:", &ctx.fields[info_file_dir].key); font_render("File path:", &ctx.fields[info_file_path].key); font_render("File size:", &ctx.fields[info_file_size].key); font_render("Image format:", &ctx.fields[info_image_format].key); font_render("Image size:", &ctx.fields[info_image_size].key); font_render("Frame:", &ctx.fields[info_frame].key); font_render("Index:", &ctx.fields[info_index].key); font_render("Scale:", &ctx.fields[info_scale].key); font_render("Status:", &ctx.fields[info_status].key); } void info_destroy(void) { timeout_close(&ctx.info); timeout_close(&ctx.status); for (size_t i = 0; i < ctx.exif_num; ++i) { free(ctx.exif_lines[i].key.data); free(ctx.exif_lines[i].value.data); } for (size_t i = 0; i < MODES_NUM; ++i) { for (size_t j = 0; j < POSITION_NUM; ++j) { free(ctx.scheme[i][j].fields); } } for (size_t i = 0; i < FIELDS_NUM; ++i) { free(ctx.fields[i].key.data); free(ctx.fields[i].value.data); } for (size_t i = 0; i < ctx.help_num; i++) { free(ctx.help[i].data); } free(ctx.help); } void info_switch(const char* mode) { timeout_reset(&ctx.info); if (!ctx.info.active) { return; } if (mode && *mode) { const ssize_t mode_num = str_index(mode_names, mode, 0); if (mode_num >= 0) { ctx.mode = mode_num; } } else { ++ctx.mode; if (ctx.mode > mode_off) { ctx.mode = mode_viewer; } } } void info_switch_help(void) { if (ctx.help) { for (size_t i = 0; i < ctx.help_num; i++) { free(ctx.help[i].data); } free(ctx.help); ctx.help = NULL; ctx.help_num = 0; } else { // get number of bindings size_t num = 0; list_for_each(keybind_get(), struct keybind, it) { if (it->help) { ++num; } } if (num == 0) { return; } // create help layer in reverse order ctx.help = calloc(1, num * sizeof(*ctx.help)); if (!ctx.help) { return; } ctx.help_num = num; list_for_each(keybind_get(), struct keybind, it) { if (it->help) { font_render(it->help, &ctx.help[--num]); } } } } bool info_help_active(void) { return !!ctx.help_num; } bool info_enabled(void) { return (ctx.mode != mode_off); } void info_reset(const struct image* image) { const size_t mib = 1024 * 1024; const char unit = image->file_size >= mib ? 'M' : 'K'; const float sz = (float)image->file_size / (image->file_size >= mib ? mib : 1024); font_render(image->name, &ctx.fields[info_file_name].value); font_render(image->parent_dir, &ctx.fields[info_file_dir].value); font_render(image->source, &ctx.fields[info_file_path].value); font_render(image->format, &ctx.fields[info_image_format].value); info_update(info_file_size, "%.02f %ciB", sz, unit); info_update(info_image_size, "%zux%zu", image->frames[0].pm.width, image->frames[0].pm.height); import_exif(image); info_update(info_frame, NULL); info_update(info_scale, NULL); timeout_reset(&ctx.info); } void info_update(enum info_field field, const char* fmt, ...) { struct text_surface* surface = &ctx.fields[field].value; va_list args; int len; char* text; if (!fmt) { free(surface->data); memset(surface, 0, sizeof(*surface)); return; } va_start(args, fmt); // NOLINTNEXTLINE(clang-analyzer-valist.Uninitialized) len = vsnprintf(NULL, 0, fmt, args); va_end(args); if (len <= 0) { return; } text = malloc(len + 1 /* last null */); if (!text) { return; } va_start(args, fmt); vsprintf(text, fmt, args); va_end(args); font_render(text, surface); free(text); if (field == info_status) { timeout_reset(&ctx.status); } } void info_print(struct pixmap* window) { if (info_help_active()) { print_help(window); } if (ctx.mode == mode_off || !ctx.info.active) { // print only status if (ctx.fields[info_status].value.width && ctx.status.active) { const size_t btype = app_is_viewer() ? mode_viewer : mode_gallery; for (size_t i = 0; i < POSITION_NUM; ++i) { const struct block_scheme* block = &ctx.scheme[btype][i]; for (size_t j = 0; j < block->fields_num; ++j) { const struct field_scheme* field = &block->fields[j]; if (field->type == info_status) { struct keyval status = ctx.fields[info_status]; if (!field->title) { memset(&status.key, 0, sizeof(status.key)); } print_keyval(window, i, &status, 1); break; } } } } return; } for (size_t i = 0; i < POSITION_NUM; ++i) { struct keyval lines[MAX_LINES] = { 0 }; const struct block_scheme* block = &ctx.scheme[ctx.mode][i]; size_t lnum = 0; for (size_t j = 0; j < block->fields_num; ++j) { const struct field_scheme* field = &block->fields[j]; const struct keyval* origin = &ctx.fields[field->type]; switch (field->type) { case info_exif: for (size_t n = 0; n < ctx.exif_num; ++n) { if (lnum < ARRAY_SIZE(lines)) { if (field->title) { lines[lnum].key = ctx.exif_lines[n].key; } lines[lnum++].value = ctx.exif_lines[n].value; } } break; case info_status: if (origin->value.width && ctx.status.active) { if (field->title) { lines[lnum].key = origin->key; } lines[lnum++].value = origin->value; } break; default: if (origin->value.width) { if (field->title) { lines[lnum].key = origin->key; } lines[lnum++].value = origin->value; } break; } if (lnum >= ARRAY_SIZE(lines)) { break; } } if (lnum) { print_keyval(window, i, lines, lnum); } } } swayimg-3.8/src/info.h000066400000000000000000000026611474536441700147630ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image info: text blocks with image meta data. // Copyright (C) 2023 Artem Senichev #pragma once #include "config.h" #include "image.h" /** Available info fields. */ enum info_field { info_file_name, info_file_dir, info_file_path, info_file_size, info_image_format, info_image_size, info_exif, info_frame, info_index, info_scale, info_status, }; /** * Initialize global info context. * @param cfg config instance */ void info_init(const struct config* cfg); /** * Destroy global info context. */ void info_destroy(void); /** * Switch display mode. * @param mode display mode name */ void info_switch(const char* mode); /** * Enable/disable help layer. */ void info_switch_help(void); /** * Check if help layer is enabled. * @return true if help layer is visible */ bool info_help_active(void); /** * Check if info enabled. * @param true if info text layer is enabled */ bool info_enabled(void); /** * Compose info data from image. * @param image image instance */ void info_reset(const struct image* image); /** * Update info text. * @param field info field id * @param fmt text string to set, NULL to clear */ void info_update(enum info_field field, const char* fmt, ...) __attribute__((format(printf, 2, 3))); /** * Print info text. * @param window target window surface */ void info_print(struct pixmap* window); swayimg-3.8/src/keybind.c000066400000000000000000000205661474536441700154540ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Keyboard bindings. // Copyright (C) 2023 Artem Senichev #include "keybind.h" #include "application.h" #include #include // Names of virtual keys struct virtual_keys { xkb_keysym_t key; const char* name; }; static const struct virtual_keys virtual_keys[] = { { VKEY_SCROLL_UP, "ScrollUp" }, { VKEY_SCROLL_DOWN, "ScrollDown" }, { VKEY_SCROLL_LEFT, "ScrollLeft" }, { VKEY_SCROLL_RIGHT, "ScrollRight" }, { VKEY_MOUSE_LEFT, "MouseLeft" }, { VKEY_MOUSE_RIGHT, "MouseRight" }, { VKEY_MOUSE_MIDDLE, "MouseMiddle" }, { VKEY_MOUSE_SIDE, "MouseSide" }, { VKEY_MOUSE_EXTRA, "MouseExtra" }, }; /** Head of global key binding list. */ static struct keybind* kb_viewer; static struct keybind* kb_gallery; /** * Convert text name to key code with modifiers. * @param name key text name * @param key output keyboard key * @param mods output key modifiers (ctrl/alt/shift) * @return false if name is invalid */ static bool parse_keymod(const char* name, xkb_keysym_t* key, uint8_t* mods) { struct str_slice slices[4]; // mod[alt+ctrl+shift]+key const size_t snum = str_split(name, '+', slices, ARRAY_SIZE(slices)); if (snum == 0) { return false; } // get modifiers *mods = 0; for (size_t i = 0; i < snum - 1; ++i) { const char* mod_names[] = { "Ctrl", "Alt", "Shift" }; const ssize_t index = str_index(mod_names, slices[i].value, slices[i].len); if (index < 0) { return false; } *mods |= 1 << index; } // get key *key = xkb_keysym_from_name(slices[snum - 1].value, XKB_KEYSYM_CASE_INSENSITIVE); // check for virtual keys if (*key == XKB_KEY_NoSymbol) { for (size_t i = 0; i < ARRAY_SIZE(virtual_keys); ++i) { if (strcmp(slices[snum - 1].value, virtual_keys[i].name) == 0) { *key = virtual_keys[i].key; break; } } } // check for international symbols if (*key == XKB_KEY_NoSymbol) { wchar_t* wide = str_to_wide(slices[snum - 1].value, NULL); *key = xkb_utf32_to_keysym(wide[0]); free(wide); } return (*key != XKB_KEY_NoSymbol); } /** * Allocate new key binding. * @param key keyboard key * @param mods key modifiers (ctrl/alt/shift) * @param actions sequence of actions */ static struct keybind* create_binding(xkb_keysym_t key, uint8_t mods, struct action_seq* actions) { struct keybind* kb = calloc(1, sizeof(struct keybind)); if (!kb) { return NULL; } kb->key = key; kb->mods = mods; kb->actions = *actions; // construct help description if (kb->actions.sequence[0].type != action_none) { size_t max_len = 30; char* key_name = keybind_name(kb->key, kb->mods); if (key_name) { const struct action* action = &kb->actions.sequence[0]; // first only str_append(key_name, 0, &kb->help); str_append(": ", 0, &kb->help); str_append(action_typename(action), 0, &kb->help); if (action->params) { str_append(" ", 0, &kb->help); str_append(action->params, 0, &kb->help); } if (kb->actions.num > 1) { str_append("; ...", 0, &kb->help); } if (strlen(kb->help) > max_len) { const char* ellipsis = "..."; const size_t ellipsis_len = strlen(ellipsis); memcpy(&kb->help[max_len - ellipsis_len], ellipsis, ellipsis_len + 1); } free(key_name); } } return kb; } /** * Free key binding. * @param kb key binding */ static void free_binding(struct keybind* kb) { if (kb) { action_free(&kb->actions); free(kb->help); free(kb); } } /** * Put key binding to the global scheme. * @param kb head of binding list * @param key keyboard key * @param mods key modifiers (ctrl/alt/shift) * @param actions sequence of actions */ static void set_binding(struct keybind** head, xkb_keysym_t key, uint8_t mods, struct action_seq* actions) { struct keybind* kb; // remove existing binding list_for_each(*head, struct keybind, it) { if (it->key == key && it->mods == mods) { *head = list_remove(&it->list); free_binding(it); break; } } // create new binding kb = create_binding(key, mods, actions); if (kb) { *head = list_add(*head, kb); } else { action_free(actions); } } /** * Load binding from config parameters. * @param kb head of binding list * @param cfg config instance * @param section name of the section * @param value actions */ static void load_binding(struct keybind** head, const struct config* cfg, const char* section) { list_for_each(cfg, const struct config, cs) { if (strcmp(section, cs->name) == 0) { list_for_each(cs->params, const struct config_keyval, kv) { struct action_seq actions = { 0 }; xkb_keysym_t keysym; uint8_t mods; // parse keyboard shortcut if (!parse_keymod(kv->key, &keysym, &mods)) { config_error_key(section, kv->key); continue; } // parse actions if (!action_create(kv->value, &actions)) { config_error_val(section, kv->value); continue; } set_binding(head, keysym, mods, &actions); } break; } } } void keybind_init(const struct config* cfg) { load_binding(&kb_viewer, cfg, CFG_KEYS_VIEWER); load_binding(&kb_gallery, cfg, CFG_KEYS_GALLERY); } void keybind_destroy(void) { list_for_each(kb_viewer, struct keybind, it) { free_binding(it); } kb_viewer = NULL; list_for_each(kb_gallery, struct keybind, it) { free_binding(it); } kb_gallery = NULL; } struct keybind* keybind_get(void) { return app_is_viewer() ? kb_viewer : kb_gallery; } struct keybind* keybind_find(xkb_keysym_t key, uint8_t mods) { // we always use lowercase + Shift modifier key = xkb_keysym_to_lower(key); list_for_each(keybind_get(), struct keybind, it) { if (it->key == key && it->mods == mods) { return it; } } return NULL; } char* keybind_name(xkb_keysym_t key, uint8_t mods) { char key_name[32]; char* name = NULL; // skip modifiers switch (key) { case XKB_KEY_Super_L: case XKB_KEY_Super_R: case XKB_KEY_Shift_L: case XKB_KEY_Shift_R: case XKB_KEY_Control_L: case XKB_KEY_Control_R: case XKB_KEY_Meta_L: case XKB_KEY_Meta_R: case XKB_KEY_Alt_L: case XKB_KEY_Alt_R: return NULL; } // modifiers if (mods & KEYMOD_CTRL) { str_append("Ctrl+", 0, &name); } if (mods & KEYMOD_ALT) { str_append("Alt+", 0, &name); } if (mods & KEYMOD_SHIFT) { str_append("Shift+", 0, &name); } // key name if (xkb_keysym_get_name(key, key_name, sizeof(key_name)) > 0) { str_append(key_name, 0, &name); } else { // search in virtual keys bool found = false; for (size_t i = 0; !found && i < ARRAY_SIZE(virtual_keys); ++i) { found = (virtual_keys[i].key == key); if (found) { str_append(virtual_keys[i].name, 0, &name); } } if (!found) { str_append("", 0, &name); } } return name; } uint8_t keybind_mods(struct xkb_state* state) { uint8_t mods = 0; if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) { mods |= KEYMOD_CTRL; } if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0) { mods |= KEYMOD_ALT; } if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) { mods |= KEYMOD_SHIFT; } return mods; } swayimg-3.8/src/keybind.h000066400000000000000000000036721474536441700154600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Keyboard bindings. // Copyright (C) 2023 Artem Senichev #pragma once #include "action.h" #include "config.h" #include // Key modifiers #define KEYMOD_CTRL (1 << 0) #define KEYMOD_ALT (1 << 1) #define KEYMOD_SHIFT (1 << 2) // Virtual keys used for scrolling (mouse wheel, touchpad etc) #define VKEY_SCROLL_UP 0x42000001 #define VKEY_SCROLL_DOWN 0x42000002 #define VKEY_SCROLL_LEFT 0x42000003 #define VKEY_SCROLL_RIGHT 0x42000004 #define VKEY_MOUSE_LEFT 0x42000005 #define VKEY_MOUSE_RIGHT 0x42000006 #define VKEY_MOUSE_MIDDLE 0x42000007 #define VKEY_MOUSE_SIDE 0x42000008 #define VKEY_MOUSE_EXTRA 0x42000009 /** Key binding list entry. */ struct keybind { struct list list; ///< Links to prev/next entry xkb_keysym_t key; ///< Keyboard key uint8_t mods; ///< Key modifiers struct action_seq actions; ///< Sequence of action char* help; ///< Help line with binding description }; /** * Initialize global default key binding scheme. * @param cfg config instance */ void keybind_init(const struct config* cfg); /** * Destroy global key binding scheme. */ void keybind_destroy(void); /** * Get head of the global binding list. * @return pointer to the list head */ struct keybind* keybind_get(void); /** * Find binding for the key. * @param key keyboard key * @param mods key modifiers (ctrl/alt/shift) * @return pointer to key binding or NULL if not found */ struct keybind* keybind_find(xkb_keysym_t key, uint8_t mods); /** * Get key name. * @param key keyboard key * @param mods key modifiers (ctrl/alt/shift) * @return text name of key, caller should free the buffer */ char* keybind_name(xkb_keysym_t key, uint8_t mods); /** * Get current key modifiers state. * @param state XKB handle * @return active key modifiers (ctrl/alt/shift) */ uint8_t keybind_mods(struct xkb_state* state); swayimg-3.8/src/loader.c000066400000000000000000000242621474536441700152720ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image loader. // Copyright (C) 2024 Artem Senichev #include "loader.h" #include "application.h" #include "buildcfg.h" #include "exif.h" #include "imagelist.h" #include #include #include #include #include #include #include #include #include // Construct function name of loader #define LOADER_FUNCTION(name) decode_##name // Declaration of loader function #define LOADER_DECLARE(name) \ enum loader_status LOADER_FUNCTION(name)(struct image * ctx, \ const uint8_t* data, size_t size) const char* supported_formats = "bmp, pnm, farbfeld, tga, dicom" #ifdef HAVE_LIBJPEG ", jpeg" #endif #ifdef HAVE_LIBPNG ", png" #endif #ifdef HAVE_LIBGIF ", gif" #endif #ifdef HAVE_LIBWEBP ", webp" #endif #ifdef HAVE_LIBRSVG ", svg" #endif #ifdef HAVE_LIBHEIF ", heif, avif" #endif #ifdef HAVE_LIBAVIF #ifndef HAVE_LIBHEIF ", avif" #endif ", avifs" #endif #ifdef HAVE_LIBJXL ", jxl" #endif #ifdef HAVE_LIBEXR ", exr" #endif #ifdef HAVE_LIBTIFF ", tiff" #endif #ifdef HAVE_LIBSIXEL ", sixel" #endif ; // declaration of loaders LOADER_DECLARE(bmp); LOADER_DECLARE(dicom); LOADER_DECLARE(farbfeld); LOADER_DECLARE(pnm); LOADER_DECLARE(qoi); LOADER_DECLARE(tga); #ifdef HAVE_LIBEXR LOADER_DECLARE(exr); #endif #ifdef HAVE_LIBGIF LOADER_DECLARE(gif); #endif #ifdef HAVE_LIBHEIF LOADER_DECLARE(heif); #endif #ifdef HAVE_LIBAVIF LOADER_DECLARE(avif); #endif #ifdef HAVE_LIBJPEG LOADER_DECLARE(jpeg); #endif #ifdef HAVE_LIBJXL LOADER_DECLARE(jxl); #endif #ifdef HAVE_LIBPNG LOADER_DECLARE(png); #endif #ifdef HAVE_LIBRSVG LOADER_DECLARE(svg); #endif #ifdef HAVE_LIBTIFF LOADER_DECLARE(tiff); #endif #ifdef HAVE_LIBSIXEL LOADER_DECLARE(sixel); #endif #ifdef HAVE_LIBWEBP LOADER_DECLARE(webp); #endif // clang-format off // list of available decoders static const image_decoder decoders[] = { #ifdef HAVE_LIBJPEG &LOADER_FUNCTION(jpeg), #endif #ifdef HAVE_LIBPNG &LOADER_FUNCTION(png), #endif #ifdef HAVE_LIBGIF &LOADER_FUNCTION(gif), #endif &LOADER_FUNCTION(bmp), &LOADER_FUNCTION(pnm), &LOADER_FUNCTION(dicom), #ifdef HAVE_LIBWEBP &LOADER_FUNCTION(webp), #endif #ifdef HAVE_LIBHEIF &LOADER_FUNCTION(heif), #endif #ifdef HAVE_LIBAVIF &LOADER_FUNCTION(avif), #endif #ifdef HAVE_LIBRSVG &LOADER_FUNCTION(svg), #endif #ifdef HAVE_LIBJXL &LOADER_FUNCTION(jxl), #endif #ifdef HAVE_LIBEXR &LOADER_FUNCTION(exr), #endif #ifdef HAVE_LIBTIFF &LOADER_FUNCTION(tiff), #endif #ifdef HAVE_LIBSIXEL &LOADER_FUNCTION(sixel), #endif &LOADER_FUNCTION(qoi), &LOADER_FUNCTION(farbfeld), &LOADER_FUNCTION(tga) // should be the last one }; // clang-format on /** Background thread loader queue. */ struct loader_queue { struct list list; ///< Links to prev/next entry size_t index; ///< Index of the image to load }; /** Loader context. */ struct loader { pthread_t tid; ///< Background loader thread id struct loader_queue* queue; ///< Background thread loader queue pthread_mutex_t lock; ///< Queue access lock pthread_cond_t signal; ///< Queue notification pthread_cond_t ready; ///< Thread ready signal }; /** Global loader context instance. */ static struct loader ctx; /** * Load image from memory buffer. * @param img destination image * @param data raw image data * @param size size of image data in bytes * @return loader status */ static enum loader_status image_from_memory(struct image* img, const uint8_t* data, size_t size) { enum loader_status status = ldr_unsupported; size_t i; for (i = 0; i < ARRAY_SIZE(decoders) && status == ldr_unsupported; ++i) { status = decoders[i](img, data, size); } img->file_size = size; #ifdef HAVE_LIBEXIF process_exif(img, data, size); #endif return status; } /** * Load image from file. * @param img destination image * @param file path to the file to load * @return loader status */ static enum loader_status image_from_file(struct image* img, const char* file) { enum loader_status status = ldr_ioerror; void* data = MAP_FAILED; struct stat st; int fd; // check file type if (stat(file, &st) == -1 || !S_ISREG(st.st_mode)) { return ldr_ioerror; } // open file and map it to memory fd = open(file, O_RDONLY); if (fd == -1) { return ldr_ioerror; } data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (data == MAP_FAILED) { close(fd); return ldr_ioerror; } // load from mapped memory status = image_from_memory(img, data, st.st_size); munmap(data, st.st_size); close(fd); return status; } /** * Load image from stream file (stdin). * @param img destination image * @param fd file descriptor for read * @return loader status */ static enum loader_status image_from_stream(struct image* img, int fd) { enum loader_status status = ldr_ioerror; uint8_t* data = NULL; size_t size = 0; size_t capacity = 0; while (true) { ssize_t rc; if (size == capacity) { const size_t new_capacity = capacity + 256 * 1024; uint8_t* new_buf = realloc(data, new_capacity); if (!new_buf) { break; } data = new_buf; capacity = new_capacity; } rc = read(fd, data + size, capacity - size); if (rc == 0) { status = image_from_memory(img, data, size); break; } if (rc == -1 && errno != EAGAIN) { break; } size += rc; } free(data); return status; } /** * Load image from stdout printed by external command. * @param img destination image * @param cmd execution command to get stdout data * @return loader status */ static enum loader_status image_from_exec(struct image* img, const char* cmd) { enum loader_status status = ldr_ioerror; int pfd[2]; pid_t pid; if (pipe(pfd) == -1) { return ldr_ioerror; } pid = fork(); if (pid == -1) { close(pfd[1]); close(pfd[0]); return ldr_ioerror; } if (pid) { // parent close(pfd[1]); status = image_from_stream(img, pfd[0]); close(pfd[0]); waitpid(pid, NULL, 0); } else { // child const char* shell = getenv("SHELL"); if (!shell || !*shell) { shell = "/bin/sh"; } dup2(pfd[1], STDOUT_FILENO); close(pfd[1]); close(pfd[0]); execlp(shell, shell, "-c", cmd, NULL); exit(1); } return status; } enum loader_status loader_from_source(const char* source, struct image** image) { enum loader_status status; struct image* img; // create image instance img = image_alloc(); if (!img) { return ldr_ioerror; } // decode image if (strcmp(source, LDRSRC_STDIN) == 0) { status = image_from_stream(img, STDIN_FILENO); } else if (strncmp(source, LDRSRC_EXEC, LDRSRC_EXEC_LEN) == 0) { status = image_from_exec(img, source + LDRSRC_EXEC_LEN); } else { status = image_from_file(img, source); } if (status == ldr_success) { image_set_source(img, source); *image = img; } else { image_free(img); } return status; } enum loader_status loader_from_index(size_t index, struct image** image) { enum loader_status status = ldr_ioerror; const char* source = image_list_get(index); if (source) { status = loader_from_source(source, image); if (status == ldr_success) { (*image)->index = index; } } return status; } /** Image loader executed in background thread. */ static void* loading_thread(__attribute__((unused)) void* data) { struct loader_queue* entry; struct image* image; do { pthread_mutex_lock(&ctx.lock); pthread_cond_signal(&ctx.ready); while (!ctx.queue) { pthread_cond_wait(&ctx.signal, &ctx.lock); if (!ctx.queue) { pthread_cond_signal(&ctx.ready); } } entry = ctx.queue; ctx.queue = list_remove(entry); pthread_mutex_unlock(&ctx.lock); if (entry->index == IMGLIST_INVALID) { free(entry); return NULL; } image = NULL; loader_from_index(entry->index, &image); app_on_load(image, entry->index); free(entry); } while (true); return NULL; } void loader_init(void) { pthread_cond_init(&ctx.signal, NULL); pthread_cond_init(&ctx.ready, NULL); pthread_mutex_init(&ctx.lock, NULL); pthread_create(&ctx.tid, NULL, loading_thread, NULL); pthread_mutex_lock(&ctx.lock); pthread_cond_wait(&ctx.ready, &ctx.lock); pthread_mutex_unlock(&ctx.lock); } void loader_destroy(void) { if (ctx.tid) { loader_queue_reset(); loader_queue_append(IMGLIST_INVALID); // send stop signal pthread_join(ctx.tid, NULL); pthread_mutex_destroy(&ctx.lock); pthread_cond_destroy(&ctx.signal); pthread_cond_destroy(&ctx.ready); } } void loader_queue_append(size_t index) { struct loader_queue* entry = malloc(sizeof(*entry)); if (entry) { entry->index = index; pthread_mutex_lock(&ctx.lock); ctx.queue = list_append(ctx.queue, entry); pthread_cond_signal(&ctx.signal); pthread_mutex_unlock(&ctx.lock); } } void loader_queue_reset(void) { pthread_mutex_lock(&ctx.lock); list_for_each(ctx.queue, struct loader_queue, it) { free(it); } ctx.queue = NULL; pthread_cond_signal(&ctx.signal); pthread_cond_wait(&ctx.ready, &ctx.lock); pthread_mutex_unlock(&ctx.lock); } swayimg-3.8/src/loader.h000066400000000000000000000033101474536441700152660ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Image loader. // Copyright (C) 2024 Artem Senichev #pragma once #include "image.h" /** Loader status. */ enum loader_status { ldr_success, ///< Image was decoded successfully ldr_unsupported, ///< Unsupported format ldr_fmterror, ///< Invalid data format ldr_ioerror ///< IO errors }; /** Contains string with the names of the supported image formats. */ extern const char* supported_formats; /** * Image loader function prototype, implemented by decoders. * @param image target image instance * @param data raw image data * @param size size of image data in bytes * @return loader status */ typedef enum loader_status (*image_decoder)(struct image* image, const uint8_t* data, size_t size); /** * Initialize background thread loader. */ void loader_init(void); /** * Destroy background thread loader. */ void loader_destroy(void); /** * Load image from specified source. * @param source image data source: path to the file, exec command, etc * @param image pointer to output image instance * @return loading status */ enum loader_status loader_from_source(const char* source, struct image** image); /** * Load image with specified index in the image list. * @param index index of the entry in the image list * @param image pointer to output image instance * @return loading status */ enum loader_status loader_from_index(size_t index, struct image** image); /** * Append image to background loader queue. * @param index index of the image in the image list */ void loader_queue_append(size_t index); /** * Reset background loader queue. */ void loader_queue_reset(void); swayimg-3.8/src/main.c000066400000000000000000000130651474536441700147470ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Program entry point. // Copyright (C) 2020 Artem Senichev #include "application.h" #include "buildcfg.h" #include "config.h" #include "imagelist.h" #include "loader.h" #include "viewer.h" #include #include #include #include #include /** Command line arguments. */ struct cmdarg { const char short_opt; ///< Short option character const char* long_opt; ///< Long option name const char* format; ///< Format description const char* help; ///< Help string }; // clang-format off static const struct cmdarg arguments[] = { { 'g', "gallery", NULL, "start in gallery mode" }, { 'r', "recursive", NULL, "read directories recursively" }, { 'o', "order", "ORDER", "set sort order for image list: none/[alpha]/reverse/random" }, { 's', "scale", "SCALE", "set initial image scale: [optimal]/fit/width/height/fill/real" }, { 'l', "slideshow", NULL, "activate slideshow mode on startup" }, { 'p', "position", "POS", "set window position [parent]/X,Y" }, { 'w', "size", "SIZE", "set window size: fullscreen/[parent]/image/W,H" }, { 'f', "fullscreen", NULL, "show image in full screen mode" }, { 'a', "class", "NAME", "set window class/app_id" }, { 'c', "config", "S.K=V", "set configuration parameter: section.key=value" }, { 'v', "version", NULL, "print version info and exit" }, { 'h', "help", NULL, "print this help and exit" }, }; // clang-format on /** * Print usage info. */ static void print_help(void) { puts("Usage: " APP_NAME " [OPTION]... [FILE]..."); puts("Show images from FILE(s)."); puts("If FILE is -, read standard input."); puts("If no FILE specified - read all files from the current directory.\n"); puts("Mandatory arguments to long options are mandatory for short options " "too."); for (size_t i = 0; i < ARRAY_SIZE(arguments); ++i) { const struct cmdarg* arg = &arguments[i]; char lopt[32]; if (arg->format) { snprintf(lopt, sizeof(lopt), "%s=%s", arg->long_opt, arg->format); } else { strncpy(lopt, arg->long_opt, sizeof(lopt) - 1); } printf(" -%c, --%-14s %s\n", arg->short_opt, lopt, arg->help); } } /** * Print version info. */ static void print_version(void) { puts(APP_NAME " version " APP_VERSION "."); puts("https://github.com/artemsen/swayimg"); printf("Supported formats: %s.\n", supported_formats); } /** * Parse command line arguments. * @param argc number of arguments to parse * @param argv arguments array * @return index of the first non option argument */ static int parse_cmdargs(int argc, char* argv[], struct config* cfg) { struct option options[1 + ARRAY_SIZE(arguments)]; char short_opts[ARRAY_SIZE(arguments) * 2]; char* short_opts_ptr = short_opts; int opt; // compose array of option structs for (size_t i = 0; i < ARRAY_SIZE(arguments); ++i) { const struct cmdarg* arg = &arguments[i]; options[i].name = arg->long_opt; options[i].has_arg = arg->format ? required_argument : no_argument; options[i].flag = NULL; options[i].val = arg->short_opt; // compose short options string *short_opts_ptr++ = arg->short_opt; if (arg->format) { *short_opts_ptr++ = ':'; } } // add terminations *short_opts_ptr = 0; memset(&options[ARRAY_SIZE(arguments)], 0, sizeof(struct option)); // parse arguments while ((opt = getopt_long(argc, argv, short_opts, options, NULL)) != -1) { switch (opt) { case 'g': config_set(cfg, CFG_GENERAL, CFG_GNRL_MODE, CFG_MODE_GALLERY); break; case 'r': config_set(cfg, CFG_LIST, CFG_LIST_RECURSIVE, CFG_YES); break; case 'o': config_set(cfg, CFG_LIST, CFG_LIST_ORDER, optarg); break; case 's': config_set(cfg, CFG_VIEWER, CFG_VIEW_SCALE, optarg); break; case 'l': config_set(cfg, CFG_VIEWER, CFG_VIEW_SSHOW, CFG_YES); break; case 'p': config_set(cfg, CFG_GENERAL, CFG_GNRL_POSITION, optarg); break; case 'w': config_set(cfg, CFG_GENERAL, CFG_GNRL_SIZE, optarg); break; case 'f': config_set(cfg, CFG_GENERAL, CFG_GNRL_SIZE, CFG_FULLSCREEN); break; case 'a': config_set(cfg, CFG_GENERAL, CFG_GNRL_APP_ID, optarg); break; case 'c': if (!config_set_arg(cfg, optarg)) { exit(EXIT_FAILURE); } break; case 'v': print_version(); exit(EXIT_SUCCESS); break; case 'h': print_help(); exit(EXIT_SUCCESS); break; default: exit(EXIT_FAILURE); } } return optind; } /** * Application entry point. */ int main(int argc, char* argv[]) { bool rc; struct config* cfg; int argn; setlocale(LC_ALL, ""); cfg = config_load(); argn = parse_cmdargs(argc, argv, cfg); rc = app_init(cfg, (const char**)&argv[argn], argc - argn); config_free(cfg); if (rc) { rc = app_run(); app_destroy(); } return rc ? EXIT_SUCCESS : EXIT_FAILURE; } swayimg-3.8/src/memdata.c000066400000000000000000000110451474536441700154270ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Manipulating data structures: arrays, strings and lists. // Copyright (C) 2024 Artem Senichev #include "memdata.h" #include #include #include #include #include struct list* list_add_head(struct list* head, struct list* entry) { entry->next = head; entry->prev = NULL; if (head) { head->prev = entry; } return entry; } struct list* list_append_tail(struct list* head, struct list* entry) { struct list* last = NULL; if (!head) { head = entry; } else { last = head; while (last && last->next) { last = last->next; } last->next = entry; } entry->prev = last; entry->next = NULL; return head; } struct list* list_remove_entry(struct list* entry) { struct list* prev = entry->prev; struct list* next = entry->next; struct list* head = prev ? prev : next; if (prev) { prev->next = next; } if (next) { next->prev = prev; } while (head && head->prev) { head = head->prev; } return head; } size_t list_size(const struct list* head) { size_t sz = 0; list_for_each(head, const struct list, it) { ++sz; } return sz; } char* str_dup(const char* src, char** dst) { const size_t sz = strlen(src) + 1; char* buffer = realloc(dst ? *dst : NULL, sz); if (buffer) { memcpy(buffer, src, sz); if (dst) { *dst = buffer; } } return buffer; } char* str_append(const char* src, size_t len, char** dst) { const size_t src_len = len ? len : strlen(src); const size_t dst_len = dst && *dst ? strlen(*dst) : 0; const size_t buf_len = dst_len + src_len + 1 /* last null */; char* buffer = realloc(dst ? *dst : NULL, buf_len); if (buffer) { memcpy(buffer + dst_len, src, src_len); buffer[buf_len - 1] = 0; if (dst) { *dst = buffer; } } return buffer; } bool str_to_num(const char* text, size_t len, ssize_t* value, int base) { char* endptr; long long num; char buffer[32]; const char* ptr; if (!text) { return false; } if (!*text) { return false; } if (len == 0) { ptr = text; } else { if (len >= sizeof(buffer)) { len = sizeof(buffer) - 1; } memcpy(buffer, text, len); buffer[len] = 0; ptr = buffer; } errno = 0; num = strtoll(ptr, &endptr, base); if (!*endptr && errno == 0) { *value = num; return true; } return false; } wchar_t* str_to_wide(const char* src, wchar_t** dst) { wchar_t* buffer; size_t len; len = mbstowcs(NULL, src, 0); if (len == (size_t)-1) { return NULL; } ++len; // last null buffer = realloc(dst ? *dst : NULL, len * sizeof(wchar_t)); if (!buffer) { return NULL; } mbstowcs(buffer, src, len); if (dst) { *dst = buffer; } return buffer; } size_t str_split(const char* text, char delimeter, struct str_slice* slices, size_t max_slices) { size_t slice_num = 0; while (*text) { struct str_slice slice; // skip spaces while (*text && isspace(*text)) { ++text; } if (!*text) { break; } // construct slice if (*text == delimeter) { // empty value slice.value = ""; slice.len = 0; } else { slice.value = text; while (*text && *text != delimeter) { ++text; } slice.len = text - slice.value; // trim spaces while (slice.len && isspace(slice.value[slice.len - 1])) { --slice.len; } } // add to output array if (slices && slice_num < max_slices) { memcpy(&slices[slice_num], &slice, sizeof(slice)); } ++slice_num; if (*text) { ++text; // skip delimiter } } return slice_num; } ssize_t str_search_index(const char** array, size_t array_sz, const char* value, size_t value_len) { if (value_len == 0) { value_len = strlen(value); } for (size_t i = 0; i < array_sz; ++i) { const char* check = array[i]; if (strlen(check) == value_len && strncmp(value, check, value_len) == 0) { return i; } } return -1; } swayimg-3.8/src/memdata.h000066400000000000000000000077131474536441700154430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Manipulating data structures: arrays, strings and lists. // Copyright (C) 2024 Artem Senichev #pragma once #include #include #include #ifndef ARRAY_SIZE #define ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a))) #endif /** String slice. */ struct str_slice { const char* value; size_t len; }; /** Double linked list. */ struct list { struct list* next; struct list* prev; }; /** * Add new entry to the head. * @param head pointer to the list head * @param entry pointer to entry * @return new head pointer */ struct list* list_add_head(struct list* head, struct list* entry); #define list_add(head, entry) \ (void*)list_add_head((struct list*)head, (struct list*)entry) /** * Append new entry to the tail. * @param head pointer to the list head * @param entry pointer to entry * @return new head pointer */ struct list* list_append_tail(struct list* head, struct list* entry); #define list_append(head, entry) \ (void*)list_append_tail((struct list*)head, (struct list*)entry) /** * Remove entry from the list. * @param entry pointer to entry * @return new head pointer */ struct list* list_remove_entry(struct list* entry); #define list_remove(entry) (void*)list_remove_entry((struct list*)entry) /** * Check if entry is the last one. * @param entry pointer to entry * @return true if this is the last entry in list */ #define list_is_last(entry) (((struct list*)entry)->next == NULL) /** * List iterator. * @param head pointer to the list head * @param type iterator type name * @param it iterator variable name */ #define list_for_each(head, type, it) \ for (type* it = head, \ *it_next = head ? (type*)((struct list*)head)->next : NULL; \ it; \ it = it_next, it_next = it ? (type*)((struct list*)it)->next : NULL) /** * Get number of entries in the list. * @param head pointer to the list head * @return length of the list */ size_t list_size(const struct list* head); /** * Duplicate string. * @param src source string to duplicate * @param dst pointer to destination buffer * @return pointer to new string, caller must free it */ char* str_dup(const char* src, char** dst); /** * Append string. * @param src source string to append * @param dst pointer to destination buffer * @return pointer to merged string, caller must free it */ char* str_append(const char* src, size_t len, char** dst); /** * Convert text string to number. * @param text text to convert * @param len length of the source string (0=auto) * @param value output variable * @param base numeric base * @return false if text has invalid format */ bool str_to_num(const char* text, size_t len, ssize_t* value, int base); /** * Convert ASCII string to wide char format. * @param src source string to encode * @param dst pointer to destination buffer * @return pointer to wide string, caller must free it */ wchar_t* str_to_wide(const char* src, wchar_t** dst); /** * Split string ("abc,def" -> "abc", "def"). * @param text source string to split * @param delimiter delimiter character * @param slices output array of slices * @param max_slices max number of slices (size of array) * @return real number of slices in source string */ size_t str_split(const char* text, char delimeter, struct str_slice* slices, size_t max_slices); /** * Search for value in string array. * @param array source array of strings * @param array_sz number of strings in array * @param value text to search * @param value_len length of the value (0=auto) * @return index of value in array or -1 if not found */ ssize_t str_search_index(const char** array, size_t array_sz, const char* value, size_t value_len); #define str_index(a, v, s) str_search_index((a), ARRAY_SIZE(a), v, s) swayimg-3.8/src/pixmap.c000066400000000000000000000223471474536441700153240ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Pixel map. // Copyright (C) 2024 Artem Senichev #include "pixmap.h" #include "memdata.h" #include "pixmap_ablend.h" #include #include bool pixmap_create(struct pixmap* pm, size_t width, size_t height) { argb_t* data = calloc(1, height * width * sizeof(argb_t)); if (data) { pm->width = width; pm->height = height; pm->data = data; } return !!data; } void pixmap_free(struct pixmap* pm) { free(pm->data); } void pixmap_fill(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)pm->width, (ssize_t)width + x); const ssize_t bottom = min((ssize_t)pm->height, (ssize_t)height + y); const ssize_t fill_width = right - left; const ssize_t fill_height = bottom - top; const size_t template_sz = fill_width * sizeof(argb_t); argb_t* template = &pm->data[top * pm->width + left]; if (right < 0 || bottom < 0 || fill_width <= 0 || fill_height <= 0) { return; } // compose and copy template line for (x = 0; x < fill_width; ++x) { template[x] = color; } for (y = top + 1; y < bottom; ++y) { memcpy(&pm->data[y * pm->width + left], template, template_sz); } } void pixmap_inverse_fill(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)pm->width, (ssize_t)width + x); const ssize_t bottom = min((ssize_t)pm->height, (ssize_t)height + y); if (left > 0) { pixmap_fill(pm, 0, top, left, bottom - top, color); } if (right < (ssize_t)pm->width) { pixmap_fill(pm, right, top, pm->width - right, bottom - top, color); } if (top > 0) { pixmap_fill(pm, 0, 0, pm->width, top, color); } if (bottom < (ssize_t)pm->height) { pixmap_fill(pm, 0, bottom, pm->width, pm->height - bottom, color); } } void pixmap_blend(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)pm->width, x + (ssize_t)width); const ssize_t bottom = min((ssize_t)pm->height, y + (ssize_t)height); for (y = top; y < bottom; ++y) { argb_t* line = &pm->data[y * pm->width]; for (x = left; x < right; ++x) { alpha_blend(color, &line[x]); } } } void pixmap_hline(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, argb_t color) { if (y >= 0 && y < (ssize_t)pm->height) { const ssize_t begin = max(0, x); const ssize_t end = min((ssize_t)pm->width, x + (ssize_t)width); const size_t offset = y * pm->width; for (ssize_t i = begin; i < end; ++i) { alpha_blend(color, &pm->data[offset + i]); } } } void pixmap_vline(struct pixmap* pm, ssize_t x, ssize_t y, size_t height, argb_t color) { if (x >= 0 && x < (ssize_t)pm->width) { const ssize_t begin = max(0, y); const ssize_t end = min((ssize_t)pm->height, y + (ssize_t)height); for (ssize_t i = begin; i < end; ++i) { alpha_blend(color, &pm->data[i * pm->width + x]); } } } void pixmap_rect(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color) { pixmap_hline(pm, x, y, width, color); pixmap_hline(pm, x, y + height - 1, width, color); pixmap_vline(pm, x, y + 1, height - 1, color); pixmap_vline(pm, x + width - 1, y + 1, height - 1, color); } void pixmap_grid(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, size_t tail_sz, argb_t color1, argb_t color2) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)pm->width, (ssize_t)width + x); const ssize_t bottom = min((ssize_t)pm->height, (ssize_t)height + y); const ssize_t grid_width = right - left; const ssize_t grid_height = bottom - top; const size_t template_sz = grid_width * sizeof(argb_t); argb_t* templates[] = { &pm->data[top * pm->width + left], &pm->data[(top + tail_sz) * pm->width + left] }; if (right < 0 || bottom < 0 || grid_width <= 0 || grid_height <= 0) { return; } for (y = 0; y < grid_height; ++y) { const size_t shift = (y / tail_sz) % 2; argb_t* line = &pm->data[(y + top) * pm->width + left]; if (line != templates[0] && line != templates[1]) { // put template line memcpy(line, templates[shift], template_sz); } else { // compose template line for (x = 0; x < grid_width; ++x) { const size_t tail = x / tail_sz; line[x] = (tail % 2) ^ shift ? color1 : color2; } } } } void pixmap_apply_mask(struct pixmap* pm, ssize_t x, ssize_t y, const uint8_t* mask, size_t width, size_t height, argb_t color) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)pm->width, x + (ssize_t)width); const ssize_t bottom = min((ssize_t)pm->height, y + (ssize_t)height); const ssize_t dst_width = right - left; const ssize_t delta_x = left - x; const ssize_t delta_y = top - y; for (ssize_t dst_y = top; dst_y < bottom; ++dst_y) { const size_t src_y = dst_y - top + delta_y; const uint8_t* mask_line = &mask[src_y * width + delta_x]; argb_t* dst_line = &pm->data[dst_y * pm->width + left]; for (x = 0; x < dst_width; ++x) { const uint8_t alpha_mask = mask_line[x]; if (alpha_mask != 0) { const uint8_t alpha_color = ARGB_GET_A(color); const uint8_t alpha = (alpha_mask * alpha_color) / 255; const argb_t clr = ARGB_SET_A(alpha) | (color & 0x00ffffff); alpha_blend(clr, &dst_line[x]); } } } } void pixmap_copy(const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, bool alpha) { const ssize_t left = max(0, x); const ssize_t top = max(0, y); const ssize_t right = min((ssize_t)dst->width, x + (ssize_t)src->width); const ssize_t bottom = min((ssize_t)dst->height, y + (ssize_t)src->height); const ssize_t dst_width = right - left; const ssize_t delta_x = left - x; const ssize_t delta_y = top - y; const size_t line_sz = dst_width * sizeof(argb_t); for (ssize_t dst_y = top; dst_y < bottom; ++dst_y) { const size_t src_y = dst_y - top + delta_y; const argb_t* src_line = &src->data[src_y * src->width + delta_x]; argb_t* dst_line = &dst->data[dst_y * dst->width + left]; if (alpha) { for (x = 0; x < dst_width; ++x) { alpha_blend(src_line[x], &dst_line[x]); } } else { memcpy(dst_line, src_line, line_sz); } } } void pixmap_flip_vertical(struct pixmap* pm) { void* buffer; const size_t stride = pm->width * sizeof(argb_t); buffer = malloc(stride); if (buffer) { for (size_t y = 0; y < pm->height / 2; ++y) { argb_t* src = &pm->data[y * pm->width]; argb_t* dst = &pm->data[(pm->height - y - 1) * pm->width]; memcpy(buffer, dst, stride); memcpy(dst, src, stride); memcpy(src, buffer, stride); } free(buffer); } } void pixmap_flip_horizontal(struct pixmap* pm) { for (size_t y = 0; y < pm->height; ++y) { argb_t* line = &pm->data[y * pm->width]; for (size_t x = 0; x < pm->width / 2; ++x) { argb_t* left = &line[x]; argb_t* right = &line[pm->width - x - 1]; const argb_t swap = *left; *left = *right; *right = swap; } } } void pixmap_rotate(struct pixmap* pm, size_t angle) { const size_t pixels = pm->width * pm->height; if (angle == 180) { for (size_t i = 0; i < pixels / 2; ++i) { argb_t* color1 = &pm->data[i]; argb_t* color2 = &pm->data[pixels - i - 1]; const argb_t swap = *color1; *color1 = *color2; *color2 = swap; } } else if (angle == 90 || angle == 270) { argb_t* data = malloc(pm->height * pm->width * sizeof(*data)); if (data) { const size_t width = pm->height; const size_t height = pm->width; for (size_t y = 0; y < pm->height; ++y) { for (size_t x = 0; x < pm->width; ++x) { size_t pos; if (angle == 90) { pos = x * width + (width - y - 1); } else { pos = (height - x - 1) * width + y; } data[pos] = pm->data[y * pm->width + x]; } } free(pm->data); pm->width = width; pm->height = height; pm->data = data; } } } swayimg-3.8/src/pixmap.h000066400000000000000000000116541474536441700153300ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Pixel map. // Copyright (C) 2024 Artem Senichev #pragma once #include #include #include #include /** ARGB color. */ typedef uint32_t argb_t; // shifts for each channel in argb_t #define ARGB_A_SHIFT 24 #define ARGB_R_SHIFT 16 #define ARGB_G_SHIFT 8 #define ARGB_B_SHIFT 0 // get channel value from argb_t #define ARGB_GET_A(c) (((c) >> ARGB_A_SHIFT) & 0xff) #define ARGB_GET_R(c) (((c) >> ARGB_R_SHIFT) & 0xff) #define ARGB_GET_G(c) (((c) >> ARGB_G_SHIFT) & 0xff) #define ARGB_GET_B(c) (((c) >> ARGB_B_SHIFT) & 0xff) // create argb_t from channel value #define ARGB_SET_A(a) (((argb_t)(a) & 0xff) << ARGB_A_SHIFT) #define ARGB_SET_R(r) (((argb_t)(r) & 0xff) << ARGB_R_SHIFT) #define ARGB_SET_G(g) (((argb_t)(g) & 0xff) << ARGB_G_SHIFT) #define ARGB_SET_B(b) (((argb_t)(b) & 0xff) << ARGB_B_SHIFT) #define ARGB(a, r, g, b) \ (ARGB_SET_A(a) | ARGB_SET_R(r) | ARGB_SET_G(g) | ARGB_SET_B(b)) // convert ABGR to ARGB #define ABGR_TO_ARGB(c) \ ((c & 0xff00ff00) | ARGB_SET_R(ARGB_GET_B(c)) | ARGB_SET_B(ARGB_GET_R(c))) #define max(a, b) ((a) > (b) ? (a) : (b)) #define min(a, b) ((a) < (b) ? (a) : (b)) /** Pixel map. */ struct pixmap { size_t width; ///< Width (px) size_t height; ///< Height (px) argb_t* data; ///< Pixel data }; /** * Allocate/reallocate pixel map. * @param pm pixmap context to create * @param width,height pixmap size * @return true pixmap was allocated */ bool pixmap_create(struct pixmap* pm, size_t width, size_t height); /** * Free pixel map created with `pixmap_create`. * @param pm pixmap context to free */ void pixmap_free(struct pixmap* pm); /** * Fill area with specified color. * @param pm pixmap context * @param x,y start coordinates, left top point * @param width,height region size * @param color color to set */ void pixmap_fill(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color); /** * Fill whole pixmap except specified area. * @param pm pixmap context * @param x,y top left corner of excluded area * @param width,height excluded area size * @param color color to set */ void pixmap_inverse_fill(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color); /** * Blend area with specified color. * @param pm pixmap context * @param x,y start coordinates, left top point * @param width,height region size * @param color color to set */ void pixmap_blend(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color); /** * Draw horizontal line. * @param pm pixmap context * @param x,y start coordinates, left top point * @param width line size * @param color color to use */ void pixmap_hline(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, argb_t color); /** * Draw vertical line. * @param pm pixmap context * @param x,y start coordinates, left top point * @param height line size * @param color color to use */ void pixmap_vline(struct pixmap* pm, ssize_t x, ssize_t y, size_t height, argb_t color); /** * Draw rectangle with 1px lines. * @param pm pixmap context * @param x,y start coordinates, left top point * @param width,height rectangle size * @param color color to use */ void pixmap_rect(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, argb_t color); /** * Fill pixmap with grid. * @param pm pixmap context * @param x,y start coordinates, left top point * @param width,height region size * @param tail_sz size of a single tail * @param color0 first grid color * @param color1 second grid color */ void pixmap_grid(struct pixmap* pm, ssize_t x, ssize_t y, size_t width, size_t height, size_t tail_sz, argb_t color0, argb_t color1); /** * Apply mask to pixmap: change color according alpha channel. * @param pm destination pixmap * @param x,y destination left top point * @param mask array with alpha channel mask * @param width,height mask size * @param color color to set */ void pixmap_apply_mask(struct pixmap* pm, ssize_t x, ssize_t y, const uint8_t* mask, size_t width, size_t height, argb_t color); /** * Draw one pixmap on another. * @param src source pixmap * @param dst destination pixmap * @param x,y destination left top coordinates * @param alpha flag to use alpha blending */ void pixmap_copy(const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, bool alpha); /** * Flip pixel map vertically. * @param pm pixmap context */ void pixmap_flip_vertical(struct pixmap* pm); /** * Flip pixel map horizontally. * @param pm pixmap context */ void pixmap_flip_horizontal(struct pixmap* pm); /** * Rotate pixel map. * @param pm pixmap context * @param angle rotation angle (only 90, 180, or 270) */ void pixmap_rotate(struct pixmap* pm, size_t angle); swayimg-3.8/src/pixmap_ablend.h000066400000000000000000000021431474536441700166260ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Alpha blending. // Copyright (C) 2024 Abe Wieland #pragma once #include "pixmap.h" /** * Alpha blending. * @param src top pixel * @param dst bottom pixel */ static inline void alpha_blend(argb_t src, argb_t* dst) { const uint8_t a1 = ARGB_GET_A(src); if (a1 == 255) { *dst = src; } else if (a1 != 0) { // if all quantities are in [0, 1] range, the formulas are: // a_out = a_top + (1 - a_top) * a_bot // c_out = a_top * c_top + (1 - a_top) * a_bot * c_bot // this integer math does the same, avoiding some division const argb_t dp = *dst; const uint32_t c1 = a1 * 255; const uint32_t c2 = (255 - a1) * ARGB_GET_A(dp); // guaranteed to be non-zero because a1 is nonzero const uint32_t alpha = c1 + c2; *dst = ARGB(alpha / 255, (ARGB_GET_R(src) * c1 + ARGB_GET_R(dp) * c2) / alpha, (ARGB_GET_G(src) * c1 + ARGB_GET_G(dp) * c2) / alpha, (ARGB_GET_B(src) * c1 + ARGB_GET_B(dp) * c2) / alpha); } } swayimg-3.8/src/pixmap_scale.c000066400000000000000000000502571474536441700164740ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Scaling pixmaps. // Copyright (C) 2024 Abe Wieland #include "pixmap_scale.h" #include "loader.h" #include "memdata.h" #include "pixmap_ablend.h" #include #include #include #include #include #include #ifdef __FreeBSD__ #include #endif #define clamp(a, low, high) (min((high), max((a), (low)))) // Except for nearest-neighbor, scaling is done via 1D convolution kernels, in // which each output is the weighted sum of a set of inputs. Weights are // stored contiguously in fixed point to limit memory consumption and improve // performance when applying. Outside of nearest-neighbor, scales are // implemented using a horizontal then vertical pass of a 1D kernel. Each // kernel is defined mathematically by a window (beyond which it's zero) and a // symmetric window function defining its weight within that window. // 14-bit fixed point means we still comfortably fit within a 16-bit signed // integer, including those weights which are slightly negative or a little // over 1 #define FIXED_BITS 14 /** The description of a single output in a kernel. */ struct output { size_t first; ///< First input for this output size_t n; ///< Number of inputs for this output size_t index; ///< Index of first weight in weights array }; /** A 1D convolution kernel. */ struct kernel { size_t start_out; ///< First output size_t n_out; ///< Number of outputs size_t start_in; ///< First input size_t n_in; ///< Number of inputs struct output* outputs; ///< Outputs int16_t* weights; ///< Weights }; /** Window function type. */ typedef double (*window_fn)(double); /** Input bounds for a given output. */ struct bounds { ssize_t first; ssize_t last; }; /** Values shared by all threads in a nearest-neighbor scale. */ struct task_nn_shared { const struct pixmap* src; ///< Source pixmap struct pixmap* dst; ///< Destination pixmap size_t x_low; ///< x start (left) size_t x_high; ///< x end (right) size_t num; ///< Numerator in fixed-point uint8_t den_bits; ///< Amount to shift for denominator ssize_t x; ///< x offset in destination ssize_t y; ///< y offset in destination bool alpha; ///< Use alpha blending? }; /** Per-thread information for a nearest-neighbor scale. */ struct task_nn_priv { struct task_nn_shared* shared; ///< Shared information size_t y_low; ///< Start row size_t y_high; ///< One beyond end row pthread_t id; ///< Thread id }; /** Values shared by all threads in all other scales. */ struct task_sc_shared { const struct pixmap* src; ///< Source pixmap struct pixmap in; ///< Intermediate pixmap struct pixmap* dst; ///< Destination pixmap struct kernel hk; ///< Horizontal kernel struct kernel vk; ///< Vertical kernel size_t yoff; ///< y offset (for horizontal kernel) size_t xoff; ///< x offset (for vertical kernel) bool alpha; ///< Use alpha blending? size_t threads; ///< Total number of threads size_t sync; ///< Number of threads done with horizontal pass pthread_mutex_t m; ///< Mutex to protect sync pthread_cond_t cv; ///< CV for the same }; /** Per-thread information for all other scales. */ struct task_sc_priv { struct task_sc_shared* shared; ///< Shared information size_t hy_low; ///< Start row for horizontal pass size_t hy_high; ///< One beyond end row for horizontal pass size_t vy_low; ///< Start row for vertical pass size_t vy_high; ///< One beyond end row for vertical pass pthread_t id; ///< Thread id }; // clang-format off /** Names of supported anti-aliasing modes. */ const char* pixmap_aa_names[5] = { [pixmap_nearest] = "none", [pixmap_box] = "box", [pixmap_bilinear] = "bilinear", [pixmap_bicubic] = "bicubic", [pixmap_mks13] = "mks13", }; // clang-format on // Get the first and last input for a given output static inline void get_bounds(size_t out, double scale, double window, struct bounds* bounds) { // Adjust by 0.5 to ensure sampling from the centers of pixels, // not their edges const double c = (out + 0.5) / scale - 0.5; const double d = window / fmin(scale, 1.0); bounds->first = floor(c - d); bounds->last = ceil(c + d); } // Get the weight for a given input/output pair static double get_weight(size_t in, size_t out, double scale, double window, window_fn wnd_fn) { double c, x; if (scale >= 1.0) { c = (out + 0.5) / scale - 0.5; x = fabs(in - c); } else { c = (in + 0.5) * scale - 0.5; x = fabs(out - c); } return x > window ? 0.0 : wnd_fn(x); } // Build a new fixed point kernel from its mathematical description static void new_kernel(struct kernel* kernel, size_t nin, size_t nout, ssize_t offset, double scale, double window, window_fn wnd_fn) { // Store weights locally first, for normalization and zero detection double* weights; int16_t* int_weights; // Output bounds const size_t start = max(0, offset); const size_t end = min(nout, (size_t)(offset + nin * scale)); kernel->start_out = start; kernel->n_out = end - start; // Estimate space needed for weights struct bounds bounds; get_bounds(0, scale, window, &bounds); // Due to floor and ceil, we need at least 2 extra to be safe, so 3 // certainly suffices const size_t n_per = bounds.last - bounds.first + 3; // The estimation overallocates, but kernels are only live for a short time weights = malloc(n_per * sizeof(*weights)); int_weights = malloc(n_per * sizeof(*int_weights)); kernel->weights = malloc(n_per * kernel->n_out * sizeof(*kernel->weights)); kernel->outputs = malloc(kernel->n_out * sizeof(*kernel->outputs)); // Track min and max input across all outputs size_t min_in = SIZE_MAX; size_t max_in = 0; size_t index = 0; for (size_t out = start; out < end; ++out) { double sum, norm; size_t tfirst, tlast; // Input bounds for this output struct output* output = &kernel->outputs[out - start]; get_bounds(out - offset, scale, window, &bounds); const size_t first = max(0, bounds.first); const size_t last = min(nin - 1, (size_t)bounds.last); sum = 0; for (size_t in = first; in <= last; ++in) { double w = get_weight(in, out - offset, scale, window, wnd_fn); weights[in - first] = w; sum += w; } norm = 1.0 / sum; for (size_t in = first; in <= last; ++in) { // TODO if more accuracy is needed, round may help int_weights[in - first] = weights[in - first] * norm * (1 << FIXED_BITS); } // Ignore leading or trailing zeros for (tfirst = first; tfirst < last && int_weights[tfirst - first] == 0; ++tfirst) { } for (tlast = last; tlast > tfirst && int_weights[tlast - first] == 0; --tlast) { } if (tfirst < min_in) { min_in = tfirst; } if (tlast > max_in) { max_in = tlast; } output->n = tlast - tfirst + 1; output->first = tfirst; output->index = index; memcpy(&kernel->weights[index], &int_weights[tfirst - first], output->n * sizeof(*kernel->weights)); index += output->n; } kernel->start_in = min_in; kernel->n_in = max_in - min_in + 1; free(weights); free(int_weights); } static void free_kernel(struct kernel* kernel) { free(kernel->outputs); free(kernel->weights); } static double box(__attribute__((unused)) double x) { return 1.0; } static double lin(double x) { return 1.0 - x; } static double cub(double x) { if (x <= 1.0) { return 3.0 / 2.0 * x * x * x - 5.0 / 2.0 * x * x + 1.0; } return -1.0 / 2.0 * x * x * x + 5.0 / 2.0 * x * x - 4.0 * x + 2.0; } static double mks13(double x) { if (x <= 0.5) { return 17.0 / 16.0 - 7.0 / 4.0 * x * x; } if (x <= 1.5) { return x * x - 11.0 / 4.0 * x + 7.0 / 4.0; } return -1.0 / 8.0 * x * x + 5.0 / 8.0 * x - 25.0 / 32.0; } static void new_named_kernel(enum pixmap_aa_mode scaler, struct kernel* kernel, size_t in, size_t out, ssize_t offset, double scale) { switch (scaler) { case pixmap_nearest: // We shouldn't ever get here break; case pixmap_box: new_kernel(kernel, in, out, offset, scale, 0.5, box); break; case pixmap_bilinear: new_kernel(kernel, in, out, offset, scale, 1.0, lin); break; case pixmap_bicubic: new_kernel(kernel, in, out, offset, scale, 2.0, cub); break; case pixmap_mks13: new_kernel(kernel, in, out, offset, scale, 2.5, mks13); break; } } // Apply a horizontal kernel; the output pixmap is assumed to be only as tall as // needed by the vertical pass - yoff indicates where it begins in the source static void apply_hk(const struct pixmap* src, struct pixmap* dst, const struct kernel* kernel, size_t y_low, size_t y_high, size_t yoff, bool alpha) { for (size_t y = y_low; y < y_high; ++y) { argb_t* dst_line = &dst->data[y * dst->width]; for (size_t x = 0; x < dst->width; ++x) { const struct output* output = &kernel->outputs[x]; int64_t a = 0; int64_t r = 0; int64_t g = 0; int64_t b = 0; for (size_t i = 0; i < output->n; ++i) { const argb_t c = src->data[(y + yoff) * src->width + output->first + i]; const int64_t wa = (int64_t)ARGB_GET_A(c) * kernel->weights[output->index + i]; a += wa; r += ARGB_GET_R(c) * wa; g += ARGB_GET_G(c) * wa; b += ARGB_GET_B(c) * wa; } // TODO the result would likely be more accurate (without // sacrificing speed) if we saved more than 8 bits between // the passes const uint8_t ua = clamp(a >> FIXED_BITS, 0, 255); if (a == 0) { a = (1 << FIXED_BITS); } // TODO irrespective of the above, saving the intermediate with // premultiplied alpha would almost certainly improve performance const uint8_t ur = clamp(r / a, 0, 255); const uint8_t ug = clamp(g / a, 0, 255); const uint8_t ub = clamp(b / a, 0, 255); const argb_t color = ARGB(ua, ur, ug, ub); if (alpha) { alpha_blend(color, &dst_line[x]); } else { dst_line[x] = color; } } } } // Apply a vertical kernel; the input pixmap is assumed to be only as tall as // needed - xoff indicates where it should go in the destination static void apply_vk(const struct pixmap* src, struct pixmap* dst, const struct kernel* kernel, size_t y_low, size_t y_high, size_t xoff, bool alpha) { for (size_t y = y_low; y < y_high; ++y) { argb_t* dst_line = &dst->data[(y + kernel->start_out) * dst->width]; for (size_t x = 0; x < src->width; ++x) { const struct output* output = &kernel->outputs[y]; int64_t a = 0; int64_t r = 0; int64_t g = 0; int64_t b = 0; for (size_t i = 0; i < output->n; ++i) { const argb_t c = src->data[(output->first + i - kernel->start_in) * src->width + x]; const int64_t wa = (int64_t)ARGB_GET_A(c) * kernel->weights[output->index + i]; a += wa; r += ARGB_GET_R(c) * wa; g += ARGB_GET_G(c) * wa; b += ARGB_GET_B(c) * wa; } const uint8_t ua = clamp(a >> FIXED_BITS, 0, 255); if (a == 0) { a = (1 << FIXED_BITS); } const uint8_t ur = clamp(r / a, 0, 255); const uint8_t ug = clamp(g / a, 0, 255); const uint8_t ub = clamp(b / a, 0, 255); const argb_t color = ARGB(ua, ur, ug, ub); if (alpha) { alpha_blend(color, &dst_line[x + xoff]); } else { dst_line[x + xoff] = color; } } } } // See pixmap_scale for more details (also uses fixed point arithmetic) static void scale_nearest(const struct pixmap* src, struct pixmap* dst, size_t y_low, size_t y_high, size_t x_low, size_t x_high, size_t num, uint8_t den_bits, ssize_t x, ssize_t y, bool alpha) { for (size_t dst_y = y_low; dst_y < y_high; ++dst_y) { const size_t src_y = ((dst_y - y) * num) >> den_bits; const argb_t* src_line = &src->data[src_y * src->width]; argb_t* dst_line = &dst->data[dst_y * dst->width]; for (size_t dst_x = x_low; dst_x < x_high; ++dst_x) { const size_t src_x = ((dst_x - x) * num) >> den_bits; const argb_t color = src_line[src_x]; if (alpha) { alpha_blend(color, &dst_line[dst_x]); } else { dst_line[dst_x] = ARGB_SET_A(0xff) | color; } } } } static void* nn_task(void* arg) { // Each thread simply handles a consecutive block of rows struct task_nn_priv* priv = arg; struct task_nn_shared* shared = priv->shared; scale_nearest(shared->src, shared->dst, priv->y_low, priv->y_high, shared->x_low, shared->x_high, shared->num, shared->den_bits, shared->x, shared->y, shared->alpha); return NULL; } static void* sc_task(void* arg) { // Each thread first handles a consecutive block of rows for the horizontal // scale, synchronizes with the others, then handles a consecutive block of // rows for the vertical scale struct task_sc_priv* priv = arg; struct task_sc_shared* shared = priv->shared; apply_hk(shared->src, &shared->in, &shared->hk, priv->hy_low, priv->hy_high, shared->yoff, false); pthread_mutex_lock(&shared->m); ++shared->sync; pthread_cond_broadcast(&shared->cv); while (shared->sync != shared->threads) { pthread_cond_wait(&shared->cv, &shared->m); } pthread_mutex_unlock(&shared->m); apply_vk(&shared->in, shared->dst, &shared->vk, priv->vy_low, priv->vy_high, shared->xoff, shared->alpha); return NULL; } static void pixmap_scale_nn(size_t threads, const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, float scale, bool alpha) { const size_t left = max(0, x); const size_t top = max(0, y); const size_t right = min(dst->width, (size_t)(x + scale * src->width)); const size_t bottom = min(dst->height, (size_t)(y + scale * src->height)); const size_t len = (bottom - top) / (threads + 1); // Use fixed-point for efficiency (floating-point division becomes an // addition and a shift, since it's used in a loop anyway). The choices // (32 and 25) ensure we have as much precision as floats, but still // support large downscales of large images (the largest supported image // at minimum scale would need 2^48 bytes of memory) const uint8_t den_bits = scale > 1.0 ? 32 : 25; const size_t num = (1.0 / scale) * (1UL << den_bits); struct task_nn_shared task_shared = { .src = src, .dst = dst, .x_low = left, .x_high = right, .num = num, .den_bits = den_bits, .x = x, .y = y, .alpha = alpha, }; struct task_nn_priv* task_priv = NULL; if (threads) { task_priv = malloc(threads * sizeof(*task_priv)); } size_t row = top; for (size_t i = 0; i < threads; ++i) { task_priv[i].shared = &task_shared; task_priv[i].y_low = row; row += len; task_priv[i].y_high = row; pthread_create(&task_priv[i].id, NULL, nn_task, &task_priv[i]); } struct task_nn_priv task_first = { .shared = &task_shared, .y_low = row, .y_high = bottom, }; nn_task(&task_first); for (size_t i = 0; i < threads; ++i) { pthread_join(task_priv[i].id, NULL); } free(task_priv); } static void pixmap_scale_aa(enum pixmap_aa_mode scaler, size_t threads, const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, float scale, bool alpha) { struct task_sc_shared task_shared = { .src = src, .dst = dst, .alpha = alpha, .threads = threads + 1, .sync = 0, .m = PTHREAD_MUTEX_INITIALIZER, .cv = PTHREAD_COND_INITIALIZER, }; new_named_kernel(scaler, &task_shared.hk, src->width, dst->width, x, scale); new_named_kernel(scaler, &task_shared.vk, src->height, dst->height, y, scale); pixmap_create(&task_shared.in, task_shared.hk.n_out, task_shared.vk.n_in); task_shared.yoff = task_shared.vk.start_in; task_shared.xoff = task_shared.hk.start_out; struct task_sc_priv* task_priv = NULL; if (threads) { task_priv = malloc(threads * sizeof(*task_priv)); } const size_t hlen = task_shared.vk.n_in / (threads + 1); const size_t vlen = task_shared.vk.n_out / (threads + 1); size_t hrow = 0; size_t vrow = 0; for (size_t i = 0; i < threads; ++i) { task_priv[i].shared = &task_shared; task_priv[i].hy_low = hrow; task_priv[i].vy_low = vrow; hrow += hlen; vrow += vlen; task_priv[i].hy_high = hrow; task_priv[i].vy_high = vrow; pthread_create(&task_priv[i].id, NULL, sc_task, &task_priv[i]); } struct task_sc_priv task_first = { .shared = &task_shared, .hy_low = hrow, .vy_low = vrow, .hy_high = task_shared.vk.n_in, .vy_high = task_shared.vk.n_out, }; sc_task(&task_first); for (size_t i = 0; i < threads; ++i) { pthread_join(task_priv[i].id, NULL); } free(task_priv); free_kernel(&task_shared.hk); free_kernel(&task_shared.vk); pixmap_free(&task_shared.in); } void pixmap_scale(enum pixmap_aa_mode scaler, const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, float scale, bool alpha) { // Do nothing if the scaled image won't appear on the destination pixmap if (x >= (ssize_t)dst->width || (ssize_t)(x + src->width * scale) <= 0 || y >= (ssize_t)dst->height || (ssize_t)(y + src->height * scale) <= 0) { return; } // TODO in some cases (especially when scaling to small outputs), using // threads is actually slower - it may be worth implementing some better // heuristics here to avoid using as many (or any) threads in those cases. // This is especially an issue with the gallery, which produces a lot of // small images - it would probably be more efficient to spin up threads // which each handle some thumbnails (on their own), rather than using // multiple threads for each thumbnail // get active CPUs #ifdef __FreeBSD__ uint32_t cpus = 0; size_t cpus_len = sizeof(cpus); sysctlbyname("hw.ncpu", &cpus, &cpus_len, 0, 0); #else const long cpus = sysconf(_SC_NPROCESSORS_ONLN); #endif // but limit background threads to at most 15 const size_t bthreads = clamp(cpus, 1, 16) - 1; if (scaler == pixmap_nearest) { pixmap_scale_nn(bthreads, src, dst, x, y, scale, alpha); } else { pixmap_scale_aa(scaler, bthreads, src, dst, x, y, scale, alpha); } } swayimg-3.8/src/pixmap_scale.h000066400000000000000000000020201474536441700164620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Scaling pixmaps. // Copyright (C) 2024 Abe Wieland #pragma once #include "pixmap.h" /** Scale filters. */ enum pixmap_aa_mode { pixmap_nearest, ///< Nearest neighbor on up- and downscale pixmap_box, ///< Nearest neighbor on upscale, average in a box on downscale pixmap_bilinear, ///< Bilinear scaling pixmap_bicubic, ///< Bicubic scaling with the Catmull-Rom spline pixmap_mks13, ///< Magic Kernel with 2013 Sharp approximation }; /** Names of supported anti-aliasing modes. */ extern const char* pixmap_aa_names[5]; /** * Draw scaled pixmap. * @param scaler scale filter to use * @param src source pixmap * @param dst destination pixmap * @param x,y destination left top coordinates * @param scale scale of source pixmap * @param alpha flag to use alpha blending */ void pixmap_scale(enum pixmap_aa_mode scaler, const struct pixmap* src, struct pixmap* dst, ssize_t x, ssize_t y, float scale, bool alpha); swayimg-3.8/src/sway.c000066400000000000000000000224521474536441700150060ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Integration with Sway WM. // Copyright (C) 2020 Artem Senichev #include "sway.h" #include #include #include #include #include #include #include #include /** IPC magic header value */ static const uint8_t ipc_magic[] = { 'i', '3', '-', 'i', 'p', 'c' }; /** IPC message types (used only) */ enum ipc_msg_type { IPC_COMMAND = 0, IPC_GET_WORKSPACES = 1, IPC_GET_TREE = 4 }; /** IPC header */ struct __attribute__((__packed__)) ipc_header { uint8_t magic[sizeof(ipc_magic)]; uint32_t len; uint32_t type; }; /** * Read exactly specified number of bytes from socket. * @param fd socket descriptor * @param buf buffer for destination data * @param len number of bytes to read * @return true if operation completed successfully */ static bool sock_read(int fd, void* buf, size_t len) { while (len) { const ssize_t rcv = recv(fd, buf, len, 0); if (rcv == 0) { fprintf(stderr, "IPC error: no data\n"); return false; } if (rcv == -1) { const int ec = errno; fprintf(stderr, "IPC read error: [%i] %s\n", ec, strerror(ec)); return false; } len -= rcv; buf = ((uint8_t*)buf) + rcv; } return true; } /** * Write data to the socket. * @param fd socket descriptor * @param buf buffer of data of send * @param len number of bytes to write * @return true if operation completed successfully */ static bool sock_write(int fd, const void* buf, size_t len) { while (len) { const ssize_t rcv = write(fd, buf, len); if (rcv == -1) { const int ec = errno; fprintf(stderr, "IPC write error: [%i] %s\n", ec, strerror(ec)); return false; } len -= rcv; buf = ((uint8_t*)buf) + rcv; } return true; } /** * IPC message exchange. * @param ipc IPC context (socket file descriptor) * @param type message type * @param payload payload data * @return IPC response as json object, NULL on errors */ static struct json_object* ipc_message(int ipc, enum ipc_msg_type type, const char* payload) { struct ipc_header hdr; memcpy(hdr.magic, ipc_magic, sizeof(ipc_magic)); hdr.len = payload ? strlen(payload) : 0; hdr.type = type; // send request if (!sock_write(ipc, &hdr, sizeof(hdr)) || !sock_write(ipc, payload, hdr.len)) { return NULL; } // receive response if (!sock_read(ipc, &hdr, sizeof(hdr))) { return NULL; } char* raw = malloc(hdr.len + 1); if (!raw) { fprintf(stderr, "Not enough memory\n"); return NULL; } if (!sock_read(ipc, raw, hdr.len)) { free(raw); return NULL; } raw[hdr.len] = 0; struct json_object* response = json_tokener_parse(raw); if (!response) { fprintf(stderr, "Invalid IPC response\n"); } free(raw); return response; } /** * Send command for specified application. * @param ipc IPC context (socket file descriptor) * @param command command to send * @return true if operation completed successfully */ static bool ipc_command(int ipc, const char* command) { bool rc = false; char cmd[128]; snprintf(cmd, sizeof(cmd), "for_window [pid=%d] %s", getpid(), command); json_object* response = ipc_message(ipc, IPC_COMMAND, cmd); if (response) { struct json_object* val = json_object_array_get_idx(response, 0); if (val) { rc = json_object_object_get_ex(val, "success", &val) && json_object_get_boolean(val); } if (!rc) { fprintf(stderr, "Bad IPC response\n"); } json_object_put(response); } return rc; } /** * Read numeric value from JSON node. * @param node JSON parent node * @param name name of the rect node * @param value value from JSON field * @return true if operation completed successfully */ static bool read_int(json_object* node, const char* name, int* value) { struct json_object* val; if (!json_object_object_get_ex(node, name, &val)) { fprintf(stderr, "JSON scheme error: field %s not found\n", name); return false; } *value = json_object_get_int(val); if (*value == 0 && errno == EINVAL) { fprintf(stderr, "JSON scheme error: field %s not a number\n", name); return false; } return true; } /** * Read rectangle geometry from JSON node. * @param node JSON parent node * @param name name of the rect node * @param rect rectangle geometry * @return true if operation completed successfully */ static bool read_rect(json_object* node, const char* name, struct wndrect* rect) { int x, y, width, height; struct json_object* rn; if (!json_object_object_get_ex(node, name, &rn)) { fprintf(stderr, "Failed to read rect: node %s not found\n", name); return false; } if (read_int(rn, "x", &x) && read_int(rn, "y", &y) && read_int(rn, "width", &width) && width > 0 && read_int(rn, "height", &height) && height > 0) { rect->x = (ssize_t)x; rect->y = (ssize_t)y; rect->width = (size_t)width; rect->height = (size_t)height; return true; } return false; } /** * Get currently focused workspace. * @param node parent JSON node * @return pointer to focused workspace node or NULL if not found */ static struct json_object* current_workspace(json_object* node) { int idx = json_object_array_length(node); while (--idx >= 0) { struct json_object* focused; struct json_object* wks = json_object_array_get_idx(node, idx); if (json_object_object_get_ex(wks, "focused", &focused) && json_object_get_boolean(focused)) { return wks; } } return NULL; } /** * Get currently focused window node. * @param node parent JSON node * @return pointer to focused window node or NULL if not found */ static struct json_object* current_window(json_object* node) { static const char* nnames[] = { "nodes", "floating_nodes" }; struct json_object* focused; if (json_object_object_get_ex(node, "focused", &focused) && json_object_get_boolean(focused)) { return node; } for (size_t i = 0; i < sizeof(nnames) / sizeof(nnames[0]); ++i) { struct json_object* nodes; if (json_object_object_get_ex(node, nnames[i], &nodes)) { int idx = json_object_array_length(nodes); while (--idx >= 0) { struct json_object* sub = json_object_array_get_idx(nodes, idx); struct json_object* focus = current_window(sub); if (focus) { return focus; } } } } return NULL; } int sway_connect(void) { struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); const char* path = getenv("SWAYSOCK"); if (!path) { return INVALID_SWAY_IPC; } size_t len = strlen(path); if (!len || len > sizeof(sa.sun_path)) { fprintf(stderr, "Invalid SWAYSOCK variable\n"); return INVALID_SWAY_IPC; } int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd == -1) { const int ec = errno; fprintf(stderr, "Failed to create IPC socket: [%i] %s\n", ec, strerror(ec)); return INVALID_SWAY_IPC; } sa.sun_family = AF_UNIX; memcpy(sa.sun_path, path, len); len += sizeof(sa) - sizeof(sa.sun_path); if (connect(fd, (struct sockaddr*)&sa, len) == -1) { const int ec = errno; fprintf(stderr, "Failed to connect IPC socket: [%i] %s\n", ec, strerror(ec)); close(fd); return INVALID_SWAY_IPC; } return fd; } void sway_disconnect(int ipc) { if (ipc != INVALID_SWAY_IPC) { close(ipc); } } bool sway_current(int ipc, struct wndrect* wnd, bool* fullscreen) { bool rc = false; // get currently focused window json_object* tree = ipc_message(ipc, IPC_GET_TREE, NULL); if (!tree) { return false; } json_object* cur_wnd = current_window(tree); if (!cur_wnd || !read_rect(cur_wnd, "window_rect", wnd)) { goto done; } // get full screen mode flag int fs_mode; *fullscreen = read_int(cur_wnd, "fullscreen_mode", &fs_mode) && fs_mode; if (*fullscreen) { rc = true; goto done; } // if we are not in the full screen mode - calculate client area offset json_object* workspaces = ipc_message(ipc, IPC_GET_WORKSPACES, NULL); if (!workspaces) { goto done; } json_object* cur_wks = current_workspace(workspaces); if (cur_wks) { struct wndrect workspace; struct wndrect global; rc = read_rect(cur_wks, "rect", &workspace) && read_rect(cur_wnd, "rect", &global); if (rc) { wnd->x += global.x - workspace.x; wnd->y += global.y - workspace.y; } } json_object_put(workspaces); done: json_object_put(tree); return rc; } bool sway_add_rules(int ipc, int x, int y, bool absolute) { char move[64]; snprintf(move, sizeof(move), "move %s position %i %i", absolute ? "absolute" : "", x, y); return ipc_command(ipc, "floating enable") && ipc_command(ipc, move); } swayimg-3.8/src/sway.h000066400000000000000000000024101474536441700150030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Integration with Sway WM. // Copyright (C) 2020 Artem Senichev #pragma once #include #include #include #define INVALID_SWAY_IPC -1 /** Position and size of a window. */ struct wndrect { ssize_t x; ssize_t y; size_t width; size_t height; }; /** * Connect to Sway. * @return IPC context, INVALID_SWAY_IPC if error */ int sway_connect(void); /** * Disconnect IPC channel. * @param ipc IPC context */ void sway_disconnect(int ipc); /** * Get geometry for currently focused window. * @param ipc IPC context * @param wnd geometry of currently focused window * @param fullscreen current full screen mode * @return true if operation completed successfully */ bool sway_current(int ipc, struct wndrect* wnd, bool* fullscreen); /** * Add rules for Sway for application's window: * 1. Enable floating mode; * 2. Set initial position. * * @param ipc IPC context * @param x horizontal window position * @param v vertical window position * @param absolute flag to use absolute position instead of relative to the * current workspace * @return true if operation completed successfully */ bool sway_add_rules(int ipc, int x, int y, bool absolute); swayimg-3.8/src/thumbnail.c000066400000000000000000000251041474536441700160030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Create/load/store thumbnails. // Copyright (C) 2024 Artem Senichev #include "thumbnail.h" #include "buildcfg.h" #include "imagelist.h" #include #ifdef HAVE_LIBPNG #define THUMBNAIL_PSTORE #include "formats/png.h" #include "loader.h" #include #include #include #include #include #include #include #endif // HAVE_LIBPNG /** Thumbnail context. */ struct thumbnail_context { size_t size; ///< Size of thumbnail bool fill; ///< Scale mode (fill/fit) enum pixmap_aa_mode aa_mode; ///< Anti-aliasing mode struct thumbnail* thumbs; ///< List of thumbnails bool pstore; ///< Use persistent storage for thumbnails pthread_t tid; ///< Background loader thread id struct thumbnail* queue; ///< Background thread loader queue pthread_cond_t signal; ///< Queue notification pthread_mutex_t lock; ///< Queue access lock }; /** Global thumbnail context. */ static struct thumbnail_context ctx; /** * Allocate new new entry. * @param image thumbnail image * @param width,height real image size * @return created entry or NULL on error */ static struct thumbnail* allocate_entry(struct image* image, size_t width, size_t height) { struct thumbnail* entry; entry = malloc(sizeof(*entry)); if (entry) { entry->image = image; entry->width = width; entry->height = height; } return entry; } #ifdef THUMBNAIL_PSTORE /** * Get path for the thumbnail on persistent storage. * @param source original image source * @return path or NULL if not applicable or in case of errors */ static char* pstore_path(const char* source) { char* path = NULL; if (strcmp(source, LDRSRC_STDIN) == 0 || strncmp(source, LDRSRC_EXEC, LDRSRC_EXEC_LEN) == 0) { return NULL; } path = config_expand_path("XDG_CACHE_HOME", "/swayimg"); if (!path) { path = config_expand_path("HOME", "/.cache/swayimg"); } if (path) { char state[16]; snprintf(state, sizeof(state), ".%04x%d%d", (uint16_t)ctx.size, ctx.fill ? 1 : 0, ctx.aa_mode); str_append(source, 0, &path); str_append(state, 0, &path); } return path; } /** * Write thumbnail on persistent storage. * @param thumb thumbnail instance to save */ static void pstore_save(const struct thumbnail* thumb) { char* th_path; uint8_t* th_data = NULL; size_t th_size = 0; char* delim; th_path = pstore_path(thumb->image->source); if (!th_path) { return; } // create path delim = th_path; while (true) { delim = strchr(delim + 1, '/'); if (!delim) { break; } *delim = '\0'; if (mkdir(th_path, S_IRWXU | S_IRWXG) && errno != EEXIST) { free(th_path); return; } *delim = '/'; } // save thumbnail if (encode_png(thumb->image, &th_data, &th_size)) { const int fd = creat(th_path, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); if (fd != -1) { size_t pos = 0; while (pos < th_size) { const ssize_t written = write(fd, th_data + pos, th_size - pos); if (written == -1) { break; } pos += written; } close(fd); } free(th_data); } free(th_path); } /** * Load thumbnail from persistent storage. * @param index image position in the image list * @return thumbnail instance or NULL if not found */ const struct thumbnail* pstore_load(size_t index) { struct thumbnail* entry; struct image* thumb; struct stat st_origin; struct stat st_thumb; char* path_thumb; const char* path_origin; path_origin = image_list_get(index); if (!path_origin) { return NULL; } path_thumb = pstore_path(path_origin); if (!path_thumb) { return NULL; } // check modification time if (stat(path_origin, &st_origin) == -1 || stat(path_thumb, &st_thumb) == -1 || st_origin.st_mtim.tv_sec > st_thumb.st_mtim.tv_sec) { free(path_thumb); return NULL; } if (loader_from_source(path_thumb, &thumb) != ldr_success) { free(path_thumb); return NULL; } // TODO: move to image instance thumb->index = index; str_dup(path_origin, &thumb->source); thumb->name = strrchr(thumb->source, '/'); if (!thumb->name || strcmp(thumb->name, "/") == 0) { thumb->name = thumb->source; } else { ++thumb->name; // skip slash } entry = allocate_entry(thumb, 0, 0); ctx.thumbs = list_append(ctx.thumbs, entry); free(path_thumb); return entry; } /** * Reset pstore saving queue. * @param stop flag to stop pstore thread */ static void pstore_reset(bool stop) { pthread_mutex_lock(&ctx.lock); list_for_each(ctx.queue, struct thumbnail, it) { free(it); } if (stop) { ctx.queue = list_append(NULL, allocate_entry(NULL, 0, 0)); pthread_cond_signal(&ctx.signal); } else { ctx.queue = NULL; } pthread_mutex_unlock(&ctx.lock); } /** Thumbnail saver executed in background thread. */ static void* pstore_saver_thread(__attribute__((unused)) void* data) { struct thumbnail* entry; while (true) { pthread_mutex_lock(&ctx.lock); while (!ctx.queue) { pthread_cond_wait(&ctx.signal, &ctx.lock); } entry = ctx.queue; ctx.queue = list_remove(entry); if (!entry->image) { free(entry); pthread_mutex_unlock(&ctx.lock); break; } pstore_save(entry); free(entry); pthread_mutex_unlock(&ctx.lock); } return NULL; } #endif // THUMBNAIL_PSTORE void thumbnail_init(const struct config* cfg) { ctx.size = config_get_num(cfg, CFG_GALLERY, CFG_GLRY_SIZE, 1, 1024); ctx.fill = config_get_bool(cfg, CFG_GALLERY, CFG_GLRY_FILL); ctx.aa_mode = config_get_oneof(cfg, CFG_GALLERY, CFG_GLRY_AA, pixmap_aa_names, ARRAY_SIZE(pixmap_aa_names)); #ifdef THUMBNAIL_PSTORE ctx.pstore = config_get_bool(cfg, CFG_GALLERY, CFG_GLRY_PSTORE); if (ctx.pstore) { pthread_mutex_init(&ctx.lock, NULL); pthread_cond_init(&ctx.signal, NULL); pthread_create(&ctx.tid, NULL, pstore_saver_thread, NULL); } #endif // THUMBNAIL_PSTORE } void thumbnail_free(void) { #ifdef THUMBNAIL_PSTORE if (ctx.pstore) { if (ctx.tid) { pstore_reset(true); pthread_join(ctx.tid, NULL); } pthread_mutex_destroy(&ctx.lock); pthread_cond_destroy(&ctx.signal); } #endif // THUMBNAIL_PSTORE list_for_each(ctx.thumbs, struct thumbnail, it) { image_free(it->image); free(it); } } enum pixmap_aa_mode thumbnail_get_aa(void) { return ctx.aa_mode; } enum pixmap_aa_mode thumbnail_switch_aa(void) { if (++ctx.aa_mode >= ARRAY_SIZE(pixmap_aa_names)) { ctx.aa_mode = 0; } return ctx.aa_mode; } void thumbnail_add(struct image* image) { struct thumbnail* entry; struct pixmap thumb; struct image_frame* frame; ssize_t offset_x, offset_y; const struct pixmap* full = &image->frames[0].pm; const size_t real_width = full->width; const size_t real_height = full->height; const float scale_width = 1.0 / ((float)real_width / ctx.size); const float scale_height = 1.0 / ((float)real_height / ctx.size); const float scale = ctx.fill ? max(scale_width, scale_height) : min(scale_width, scale_height); size_t thumb_width = scale * real_width; size_t thumb_height = scale * real_height; if (ctx.fill) { offset_x = ctx.size / 2 - thumb_width / 2; offset_y = ctx.size / 2 - thumb_height / 2; thumb_width = ctx.size; thumb_height = ctx.size; } else { offset_x = 0; offset_y = 0; } // create thumbnail from image (replace the first frame) if (!pixmap_create(&thumb, thumb_width, thumb_height)) { image_free(image); return; } pixmap_scale(ctx.aa_mode, full, &thumb, offset_x, offset_y, scale, image->alpha); image_free_frames(image); frame = image_create_frames(image, 1); if (!frame) { pixmap_free(&thumb); image_free(image); return; } frame->pm = thumb; // add entry to the list entry = allocate_entry(image, real_width, real_height); ctx.thumbs = list_append(ctx.thumbs, entry); #ifdef THUMBNAIL_PSTORE if (entry && ctx.pstore && (real_width > ctx.size || real_height > ctx.size)) { // save thumbnail to persistent storage struct thumbnail* save_entry = allocate_entry(entry->image, entry->width, entry->height); pthread_mutex_lock(&ctx.lock); ctx.queue = list_append(ctx.queue, save_entry); pthread_cond_signal(&ctx.signal); pthread_mutex_unlock(&ctx.lock); } #endif // THUMBNAIL_PSTORE } const struct thumbnail* thumbnail_get(size_t index) { const struct thumbnail* thumb = NULL; list_for_each(ctx.thumbs, struct thumbnail, it) { if (it->image->index == index) { thumb = it; break; } } #ifdef THUMBNAIL_PSTORE if (!thumb && ctx.pstore) { thumb = pstore_load(index); } #endif // THUMBNAIL_PSTORE return thumb; } void thumbnail_remove(size_t index) { #ifdef THUMBNAIL_PSTORE pstore_reset(false); #endif // THUMBNAIL_PSTORE list_for_each(ctx.thumbs, struct thumbnail, it) { if (it->image->index == index) { ctx.thumbs = list_remove(it); image_free(it->image); free(it); break; } } } void thumbnail_clear(size_t min_id, size_t max_id) { #ifdef THUMBNAIL_PSTORE pstore_reset(false); #endif // THUMBNAIL_PSTORE if (min_id == IMGLIST_INVALID && max_id == IMGLIST_INVALID) { list_for_each(ctx.thumbs, struct thumbnail, it) { ctx.thumbs = list_remove(it); image_free(it->image); free(it); } } else { list_for_each(ctx.thumbs, struct thumbnail, it) { if ((min_id != IMGLIST_INVALID && it->image->index < min_id) || (max_id != IMGLIST_INVALID && it->image->index > max_id)) { ctx.thumbs = list_remove(it); image_free(it->image); free(it); } } } } swayimg-3.8/src/thumbnail.h000066400000000000000000000026561474536441700160170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Create/load/store thumbnails. // Copyright (C) 2024 Artem Senichev #pragma once #include "config.h" #include "image.h" #include "pixmap_scale.h" /** List of thumbnails. */ struct thumbnail { struct list list; ///< Links to prev/next entry struct image* image; ///< Thumbnail image size_t width, height; ///< Real image size }; /** * Initialize global gallery context. * @param cfg config instance */ void thumbnail_init(const struct config* cfg); /** * Destroy/reset global thumbnail context. */ void thumbnail_free(void); /** * Get current anti-aliasing mode for thumbnails. * @return current mode */ enum pixmap_aa_mode thumbnail_get_aa(void); /** * Switch anti-aliasing mode. * @return new mode */ enum pixmap_aa_mode thumbnail_switch_aa(void); /** * Create new thumbnail from the image. * @param image original image, this instance will be replaced by thumbnail */ void thumbnail_add(struct image* image); /** * Get thumbnail. * @param index image position in the image list * @return thumbnail instance or NULL if not found */ const struct thumbnail* thumbnail_get(size_t index); /** * Remove thumbnail from the cache. * @param index image position in the image list */ void thumbnail_remove(size_t index); /** * Clear memory cache. * @param min_id,max_id range of ids to save in cache */ void thumbnail_clear(size_t min_id, size_t max_id); swayimg-3.8/src/ui.c000066400000000000000000000633101474536441700144360ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // User interface: Window management, keyboard input, etc. // Copyright (C) 2020 Artem Senichev #include "ui.h" #include "application.h" #include "buildcfg.h" #include "wndbuf.h" // autogenerated wayland headers #include "content-type-v1-client-protocol.h" #include "cursor-shape-v1-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" #include "viewporter-client-protocol.h" #include "xdg-decoration-unstable-v1-client-protocol.h" #include "xdg-shell-client-protocol.h" #include #include #include #include #include // Window size #define WINDOW_MIN 10 #define WINDOW_MAX 100000 #define WINDOW_DEFAULT_WIDTH 800 #define WINDOW_DEFAULT_HEIGHT 600 // Mouse buttons, from #ifndef BTN_LEFT #define BTN_LEFT 0x110 #define BTN_RIGHT 0x111 #define BTN_MIDDLE 0x112 #define BTN_SIDE 0x113 #define BTN_EXTRA 0x114 #endif // Fractional scale denominator #define FRACTION_SCALE_DEN 120 // Uncomment the following line to enable printing draw time // #define TRACE_DRAW_TIME /** UI context */ struct ui { // wayland objects struct wl { struct wl_display* display; struct wl_registry* registry; struct wl_shm* shm; struct wl_compositor* compositor; struct wl_seat* seat; struct wl_keyboard* keyboard; struct wl_pointer* pointer; struct wl_surface* surface; struct wl_output* output; } wl; // wayland protocols objects struct wp { struct wp_viewporter* viewporter; struct wp_viewport* viewport; struct wp_cursor_shape_manager_v1* cursor; struct wp_content_type_manager_v1* ctype_manager; struct wp_content_type_v1* ctype; struct wp_fractional_scale_manager_v1* scale_manager; struct wp_fractional_scale_v1* scale; struct zxdg_decoration_manager_v1* decor_manager; struct zxdg_toplevel_decoration_v1* decor; } wp; // window buffers struct wnd { struct wl_buffer* buffer0; struct wl_buffer* buffer1; struct wl_buffer* current; size_t width; size_t height; size_t scale; #ifdef TRACE_DRAW_TIME struct timespec draw_time; #endif } wnd; // cross-desktop struct xdg { bool initialized; struct xdg_wm_base* base; struct xdg_surface* surface; struct xdg_toplevel* toplevel; } xdg; // keyboard struct xkb { struct xkb_context* context; struct xkb_keymap* keymap; struct xkb_state* state; } xkb; // key repeat data struct repeat { int fd; xkb_keysym_t key; uint32_t rate; uint32_t delay; } repeat; // mouse drag struct mouse { bool active; int x; int y; } mouse; // fullscreen mode bool fullscreen; // flag to cancel event queue bool event_handled; }; /** Global UI context instance. */ static struct ui ctx = { .wnd.scale = FRACTION_SCALE_DEN, .repeat.fd = -1, }; /** * Fill timespec structure. * @param ts destination structure * @param ms time in milliseconds */ static inline void set_timespec(struct timespec* ts, uint32_t ms) { ts->tv_sec = ms / 1000; ts->tv_nsec = (ms % 1000) * 1000000; } /** * Recreate window buffers. * @return true if operation completed successfully */ static bool recreate_buffers(void) { const size_t width = ui_get_width(); const size_t height = ui_get_height(); // recreate buffers ctx.wnd.current = NULL; wndbuf_free(ctx.wnd.buffer0); wndbuf_free(ctx.wnd.buffer1); ctx.wnd.buffer0 = wndbuf_create(ctx.wl.shm, width, height); ctx.wnd.buffer1 = wndbuf_create(ctx.wl.shm, width, height); if (!ctx.wnd.buffer0 || !ctx.wnd.buffer1) { return false; } ctx.wnd.current = ctx.wnd.buffer0; return true; } // suppress unused parameter warnings #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" /******************************************************************************* * Keyboard handlers ******************************************************************************/ static void on_keyboard_enter(void* data, struct wl_keyboard* wl_keyboard, uint32_t serial, struct wl_surface* surface, struct wl_array* keys) { } static void on_keyboard_leave(void* data, struct wl_keyboard* wl_keyboard, uint32_t serial, struct wl_surface* surface) { // reset keyboard repeat timer struct itimerspec ts = { 0 }; timerfd_settime(ctx.repeat.fd, 0, &ts, NULL); } static void on_keyboard_modifiers(void* data, struct wl_keyboard* wl_keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { xkb_state_update_mask(ctx.xkb.state, mods_depressed, mods_latched, mods_locked, 0, 0, group); } static void on_keyboard_repeat_info(void* data, struct wl_keyboard* wl_keyboard, int32_t rate, int32_t delay) { // save keyboard repeat preferences ctx.repeat.rate = rate; ctx.repeat.delay = delay; } static void on_keyboard_keymap(void* data, struct wl_keyboard* wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { char* keymap; xkb_state_unref(ctx.xkb.state); xkb_keymap_unref(ctx.xkb.keymap); keymap = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); ctx.xkb.keymap = xkb_keymap_new_from_string(ctx.xkb.context, keymap, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); ctx.xkb.state = xkb_state_new(ctx.xkb.keymap); munmap(keymap, size); close(fd); } static void on_keyboard_key(void* data, struct wl_keyboard* wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { struct itimerspec ts = { 0 }; if (state == WL_KEYBOARD_KEY_STATE_RELEASED) { // stop key repeat timer timerfd_settime(ctx.repeat.fd, 0, &ts, NULL); } else if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { xkb_keysym_t keysym; key += 8; keysym = xkb_state_key_get_one_sym(ctx.xkb.state, key); if (keysym != XKB_KEY_NoSymbol) { app_on_keyboard(keysym, keybind_mods(ctx.xkb.state)); // handle key repeat if (ctx.repeat.rate && xkb_keymap_key_repeats(ctx.xkb.keymap, key)) { // start key repeat timer ctx.repeat.key = keysym; set_timespec(&ts.it_value, ctx.repeat.delay); set_timespec(&ts.it_interval, 1000 / ctx.repeat.rate); timerfd_settime(ctx.repeat.fd, 0, &ts, NULL); } } } } /** * Set mouse pointer shape. * @param wl_pointer wayland pointer instance * @param grabbing true to set grabbing shape, false to use default */ static void set_pointer_shape(struct wl_pointer* wl_pointer, bool grabbing) { if (ctx.wp.cursor) { const uint32_t shape = grabbing ? WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING : WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; struct wp_cursor_shape_device_v1* cursor_device = wp_cursor_shape_manager_v1_get_pointer(ctx.wp.cursor, wl_pointer); wp_cursor_shape_device_v1_set_shape(cursor_device, 0, shape); wp_cursor_shape_device_v1_destroy(cursor_device); } } static void on_pointer_enter(void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { set_pointer_shape(wl_pointer, false); } static void on_pointer_leave(void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface) { } static void on_pointer_motion(void* data, struct wl_pointer* wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { const int x = wl_fixed_to_int(surface_x); const int y = wl_fixed_to_int(surface_y); if (ctx.mouse.active) { const int dx = x - ctx.mouse.x; const int dy = y - ctx.mouse.y; if (dx || dy) { app_on_drag(dx, dy); } } ctx.mouse.x = x; ctx.mouse.y = y; } static void on_pointer_button(void* data, struct wl_pointer* wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { const bool pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED); if (button == BTN_LEFT) { ctx.mouse.active = pressed; set_pointer_shape(wl_pointer, pressed); } if (pressed) { xkb_keysym_t key; switch (button) { // TODO: Configurable drag // case BTN_LEFT: // key = VKEY_MOUSE_LEFT; // break; case BTN_RIGHT: key = VKEY_MOUSE_RIGHT; break; case BTN_MIDDLE: key = VKEY_MOUSE_MIDDLE; break; case BTN_SIDE: key = VKEY_MOUSE_SIDE; break; case BTN_EXTRA: key = VKEY_MOUSE_EXTRA; break; default: key = XKB_KEY_NoSymbol; break; } if (key != XKB_KEY_NoSymbol) { app_on_keyboard(key, keybind_mods(ctx.xkb.state)); } } } static void on_pointer_axis(void* data, struct wl_pointer* wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { xkb_keysym_t key; if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { key = value > 0 ? VKEY_SCROLL_RIGHT : VKEY_SCROLL_LEFT; } else { key = value > 0 ? VKEY_SCROLL_DOWN : VKEY_SCROLL_UP; } app_on_keyboard(key, keybind_mods(ctx.xkb.state)); } static const struct wl_keyboard_listener keyboard_listener = { .keymap = on_keyboard_keymap, .enter = on_keyboard_enter, .leave = on_keyboard_leave, .key = on_keyboard_key, .modifiers = on_keyboard_modifiers, .repeat_info = on_keyboard_repeat_info, }; static const struct wl_pointer_listener pointer_listener = { .enter = on_pointer_enter, .leave = on_pointer_leave, .motion = on_pointer_motion, .button = on_pointer_button, .axis = on_pointer_axis, }; /******************************************************************************* * Seat handlers ******************************************************************************/ static void on_seat_name(void* data, struct wl_seat* seat, const char* name) { } static void on_seat_capabilities(void* data, struct wl_seat* seat, uint32_t cap) { if (cap & WL_SEAT_CAPABILITY_KEYBOARD) { ctx.wl.keyboard = wl_seat_get_keyboard(seat); wl_keyboard_add_listener(ctx.wl.keyboard, &keyboard_listener, NULL); } else if (ctx.wl.keyboard) { wl_keyboard_destroy(ctx.wl.keyboard); ctx.wl.keyboard = NULL; } if (cap & WL_SEAT_CAPABILITY_POINTER) { ctx.wl.pointer = wl_seat_get_pointer(seat); wl_pointer_add_listener(ctx.wl.pointer, &pointer_listener, NULL); } else if (ctx.wl.pointer) { wl_pointer_destroy(ctx.wl.pointer); ctx.wl.pointer = NULL; } } static const struct wl_seat_listener seat_listener = { .capabilities = on_seat_capabilities, .name = on_seat_name, }; /******************************************************************************* * XDG handlers ******************************************************************************/ static void on_xdg_surface_configure(void* data, struct xdg_surface* surface, uint32_t serial) { xdg_surface_ack_configure(surface, serial); if (ctx.xdg.initialized) { app_redraw(); } else { wl_surface_attach(ctx.wl.surface, ctx.wnd.current, 0, 0); wl_surface_commit(ctx.wl.surface); } } static const struct xdg_surface_listener xdg_surface_listener = { .configure = on_xdg_surface_configure }; static void on_xdg_ping(void* data, struct xdg_wm_base* base, uint32_t serial) { xdg_wm_base_pong(base, serial); } static const struct xdg_wm_base_listener xdg_base_listener = { .ping = on_xdg_ping }; static void handle_xdg_toplevel_configure(void* data, struct xdg_toplevel* lvl, int32_t width, int32_t height, struct wl_array* states) { bool reset_buffers = (ctx.wnd.current == NULL); if (width > 0 && height > 0) { if (ctx.wnd.width != (size_t)width || ctx.wnd.height != (size_t)height) { ctx.wnd.width = (size_t)width; ctx.wnd.height = (size_t)height; reset_buffers = true; } if (ctx.wp.viewport) { wp_viewport_set_destination(ctx.wp.viewport, width, height); } ctx.xdg.initialized = true; } if (reset_buffers) { if (recreate_buffers()) { app_on_resize(); } else { app_exit(1); } } } static void handle_xdg_toplevel_close(void* data, struct xdg_toplevel* top) { app_exit(0); } static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = handle_xdg_toplevel_configure, .close = handle_xdg_toplevel_close, }; /******************************************************************************* * Fractional scale handlers ******************************************************************************/ static void handle_scale(void* data, struct wp_fractional_scale_v1* scaler, uint32_t factor) { if (ctx.wnd.scale != factor) { ctx.wnd.scale = factor; if (recreate_buffers()) { app_on_resize(); app_redraw(); } else { app_exit(1); } } } static const struct wp_fractional_scale_v1_listener scale_listener = { .preferred_scale = handle_scale, }; /******************************************************************************* * Registry handlers ******************************************************************************/ static void on_registry_global(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { if (strcmp(interface, wl_compositor_interface.name) == 0) { // wayland compositor ctx.wl.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION); } else if (strcmp(interface, wl_shm_interface.name) == 0) { // wayland shared memory ctx.wl.shm = wl_registry_bind(registry, name, &wl_shm_interface, WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION); } else if (strcmp(interface, wp_viewporter_interface.name) == 0) { // viewport (fractional scale output) ctx.wp.viewporter = wl_registry_bind(registry, name, &wp_viewporter_interface, WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION); } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { // xdg (app window) ctx.xdg.base = wl_registry_bind(registry, name, &xdg_wm_base_interface, XDG_WM_BASE_PING_SINCE_VERSION); xdg_wm_base_add_listener(ctx.xdg.base, &xdg_base_listener, data); } else if (strcmp(interface, wl_seat_interface.name) == 0) { // seat (keayboard and mouse) ctx.wl.seat = wl_registry_bind(registry, name, &wl_seat_interface, WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION); wl_seat_add_listener(ctx.wl.seat, &seat_listener, data); } else if (strcmp(interface, wp_content_type_manager_v1_interface.name) == 0) { // content type ctx.wp.ctype_manager = wl_registry_bind( registry, name, &wp_content_type_manager_v1_interface, WP_CONTENT_TYPE_V1_SET_CONTENT_TYPE_SINCE_VERSION); } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { // cursor shape ctx.wp.cursor = wl_registry_bind( registry, name, &wp_cursor_shape_manager_v1_interface, WP_CURSOR_SHAPE_MANAGER_V1_GET_POINTER_SINCE_VERSION); } else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) { // fractional scale manager ctx.wp.scale_manager = wl_registry_bind( registry, name, &wp_fractional_scale_manager_v1_interface, WP_FRACTIONAL_SCALE_V1_PREFERRED_SCALE_SINCE_VERSION); } else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { // server side window decoration ctx.wp.decor_manager = wl_registry_bind( registry, name, &zxdg_decoration_manager_v1_interface, ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION); } } static void on_registry_remove(void* data, struct wl_registry* registry, uint32_t name) { } static const struct wl_registry_listener registry_listener = { .global = on_registry_global, .global_remove = on_registry_remove, }; #pragma GCC diagnostic pop // "-Wunused-parameter" // Key repeat handler static void on_key_repeat(__attribute__((unused)) void* data) { uint64_t repeats; const ssize_t sz = sizeof(repeats); if (read(ctx.repeat.fd, &repeats, sz) == sz) { app_on_keyboard(ctx.repeat.key, keybind_mods(ctx.xkb.state)); } } // Wayland event handler static void on_wayland_event(__attribute__((unused)) void* data) { wl_display_read_events(ctx.wl.display); wl_display_dispatch_pending(ctx.wl.display); ctx.event_handled = true; } bool ui_init(const char* app_id, size_t width, size_t height, bool decor) { ctx.wnd.width = width; ctx.wnd.height = height; if (ctx.wnd.width < WINDOW_MIN || ctx.wnd.height < WINDOW_MIN || ctx.wnd.width > WINDOW_MAX || ctx.wnd.height > WINDOW_MAX) { ctx.wnd.width = WINDOW_DEFAULT_WIDTH; ctx.wnd.height = WINDOW_DEFAULT_HEIGHT; } ctx.wl.display = wl_display_connect(NULL); if (!ctx.wl.display) { fprintf(stderr, "Failed to open display\n"); return false; } ctx.xkb.context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); ctx.wl.registry = wl_display_get_registry(ctx.wl.display); if (!ctx.wl.registry) { fprintf(stderr, "Failed to open registry\n"); ui_destroy(); return false; } wl_registry_add_listener(ctx.wl.registry, ®istry_listener, NULL); wl_display_roundtrip(ctx.wl.display); ctx.wl.surface = wl_compositor_create_surface(ctx.wl.compositor); if (!ctx.wl.surface) { fprintf(stderr, "Failed to create surface\n"); ui_destroy(); return false; } ctx.xdg.surface = xdg_wm_base_get_xdg_surface(ctx.xdg.base, ctx.wl.surface); if (!ctx.xdg.surface) { fprintf(stderr, "Failed to create xdg surface\n"); ui_destroy(); return false; } xdg_surface_add_listener(ctx.xdg.surface, &xdg_surface_listener, NULL); ctx.xdg.toplevel = xdg_surface_get_toplevel(ctx.xdg.surface); xdg_toplevel_add_listener(ctx.xdg.toplevel, &xdg_toplevel_listener, NULL); xdg_toplevel_set_app_id(ctx.xdg.toplevel, app_id); if (ctx.fullscreen) { xdg_toplevel_set_fullscreen(ctx.xdg.toplevel, NULL); } if (ctx.wp.scale_manager) { ctx.wp.scale = wp_fractional_scale_manager_v1_get_fractional_scale( ctx.wp.scale_manager, ctx.wl.surface); wp_fractional_scale_v1_add_listener(ctx.wp.scale, &scale_listener, NULL); } if (ctx.wp.viewporter) { ctx.wp.viewport = wp_viewporter_get_viewport(ctx.wp.viewporter, ctx.wl.surface); } if (ctx.wp.ctype_manager) { ctx.wp.ctype = wp_content_type_manager_v1_get_surface_content_type( ctx.wp.ctype_manager, ctx.wl.surface); wp_content_type_v1_set_content_type(ctx.wp.ctype, WP_CONTENT_TYPE_V1_TYPE_PHOTO); } if (ctx.wp.decor_manager && decor) { ctx.wp.decor = zxdg_decoration_manager_v1_get_toplevel_decoration( ctx.wp.decor_manager, ctx.xdg.toplevel); if (ctx.wp.decor) { zxdg_toplevel_decoration_v1_set_mode( ctx.wp.decor, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); } } wl_surface_commit(ctx.wl.surface); app_watch(wl_display_get_fd(ctx.wl.display), on_wayland_event, NULL); ctx.repeat.fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); app_watch(ctx.repeat.fd, on_key_repeat, NULL); return true; } void ui_destroy(void) { // free protocols if (ctx.wp.scale_manager) { if (ctx.wp.scale) { wp_fractional_scale_v1_destroy(ctx.wp.scale); } wp_fractional_scale_manager_v1_destroy(ctx.wp.scale_manager); } if (ctx.wp.viewporter) { if (ctx.wp.viewport) { wp_viewport_destroy(ctx.wp.viewport); } wp_viewporter_destroy(ctx.wp.viewporter); } if (ctx.wp.ctype_manager) { if (ctx.wp.ctype) { wp_content_type_v1_destroy(ctx.wp.ctype); } wp_content_type_manager_v1_destroy(ctx.wp.ctype_manager); } if (ctx.wp.cursor) { wp_cursor_shape_manager_v1_destroy(ctx.wp.cursor); } if (ctx.wp.decor_manager) { if (ctx.wp.decor) { zxdg_toplevel_decoration_v1_destroy(ctx.wp.decor); } zxdg_decoration_manager_v1_destroy(ctx.wp.decor_manager); } // keyboard related if (ctx.xkb.state) { xkb_state_unref(ctx.xkb.state); } if (ctx.xkb.keymap) { xkb_keymap_unref(ctx.xkb.keymap); } if (ctx.xkb.context) { xkb_context_unref(ctx.xkb.context); } if (ctx.repeat.fd != -1) { close(ctx.repeat.fd); } // xdg if (ctx.xdg.toplevel) { xdg_toplevel_destroy(ctx.xdg.toplevel); } if (ctx.xdg.surface) { xdg_surface_destroy(ctx.xdg.surface); } if (ctx.xdg.base) { xdg_wm_base_destroy(ctx.xdg.base); } // window buffers wndbuf_free(ctx.wnd.buffer0); wndbuf_free(ctx.wnd.buffer1); // base wayland if (ctx.wl.seat) { wl_seat_destroy(ctx.wl.seat); } if (ctx.wl.keyboard) { wl_keyboard_destroy(ctx.wl.keyboard); } if (ctx.wl.pointer) { wl_pointer_destroy(ctx.wl.pointer); } if (ctx.wl.shm) { wl_shm_destroy(ctx.wl.shm); } if (ctx.wl.output) { wl_output_destroy(ctx.wl.output); } if (ctx.wl.surface) { wl_surface_destroy(ctx.wl.surface); } if (ctx.wl.compositor) { wl_compositor_destroy(ctx.wl.compositor); } if (ctx.wl.registry) { wl_registry_destroy(ctx.wl.registry); } if (ctx.wl.display) { wl_display_disconnect(ctx.wl.display); } } void ui_event_prepare(void) { ctx.event_handled = false; while (wl_display_prepare_read(ctx.wl.display) != 0) { wl_display_dispatch_pending(ctx.wl.display); } wl_display_flush(ctx.wl.display); } void ui_event_done(void) { if (!ctx.event_handled) { wl_display_cancel_read(ctx.wl.display); } } struct pixmap* ui_draw_begin(void) { if (!ctx.wnd.current) { return NULL; // not yet initialized } // switch buffers if (ctx.wnd.current == ctx.wnd.buffer0) { ctx.wnd.current = ctx.wnd.buffer1; } else { ctx.wnd.current = ctx.wnd.buffer0; } #ifdef TRACE_DRAW_TIME clock_gettime(CLOCK_MONOTONIC, &ctx.wnd.draw_time); #endif return wndbuf_pixmap(ctx.wnd.current); } void ui_draw_commit(void) { const struct pixmap* pm = wndbuf_pixmap(ctx.wnd.current); #ifdef TRACE_DRAW_TIME struct timespec curr; clock_gettime(CLOCK_MONOTONIC, &curr); const double ns = (curr.tv_sec * 1000000000 + curr.tv_nsec) - (ctx.wnd.draw_time.tv_sec * 1000000000 + ctx.wnd.draw_time.tv_nsec); printf("Rendered in %.6f sec\n", ns / 1000000000); #endif wl_surface_attach(ctx.wl.surface, ctx.wnd.current, 0, 0); wl_surface_damage_buffer(ctx.wl.surface, 0, 0, pm->width, pm->height); wl_surface_commit(ctx.wl.surface); } void ui_set_title(const char* name) { char* title = NULL; str_append(APP_NAME ": ", 0, &title); str_append(name, 0, &title); if (title) { xdg_toplevel_set_title(ctx.xdg.toplevel, title); free(title); } } void ui_set_content_type_animated(bool animated) { if (ctx.wp.ctype) { if (animated) { wp_content_type_v1_set_content_type(ctx.wp.ctype, WP_CONTENT_TYPE_V1_TYPE_VIDEO); } else { wp_content_type_v1_set_content_type(ctx.wp.ctype, WP_CONTENT_TYPE_V1_TYPE_PHOTO); } } } size_t ui_get_width(void) { const double scale = (double)ctx.wnd.scale / FRACTION_SCALE_DEN; return scale * ctx.wnd.width; } size_t ui_get_height(void) { const double scale = (double)ctx.wnd.scale / FRACTION_SCALE_DEN; return scale * ctx.wnd.height; } void ui_toggle_fullscreen(void) { ctx.fullscreen = !ctx.fullscreen; if (ctx.xdg.toplevel) { if (ctx.fullscreen) { xdg_toplevel_set_fullscreen(ctx.xdg.toplevel, NULL); } else { xdg_toplevel_unset_fullscreen(ctx.xdg.toplevel); } } } swayimg-3.8/src/ui.h000066400000000000000000000027011474536441700144400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // User interface: Window management, keyboard input, etc. // Copyright (C) 2020 Artem Senichev #pragma once #include "pixmap.h" /** * Create global UI context. */ void ui_create(void); /** * Initialize global UI context: create window, register handlers etc. * @param app_id application id, used as window class * @param width,height initial window size in pixels * @param decor flag to use server-side window decoration * @return true if window created */ bool ui_init(const char* app_id, size_t width, size_t height, bool decor); /** * Destroy global UI context. */ void ui_destroy(void); /** * Prepare the window system to read events. */ void ui_event_prepare(void); /** * Event handler complete notification. */ void ui_event_done(void); /** * Begin window redraw procedure. * @return window pixmap */ struct pixmap* ui_draw_begin(void); /** * Finish window redraw procedure. */ void ui_draw_commit(void); /** * Set window title. * @param name file name of the current image */ void ui_set_title(const char* name); /** * Change surface content type to photo or animation */ void ui_set_content_type_animated(bool animated); /** * Get window width. * @return window width in pixels */ size_t ui_get_width(void); /** * Get window height. * @return window height in pixels */ size_t ui_get_height(void); /** * Toggle full screen mode. */ void ui_toggle_fullscreen(void); swayimg-3.8/src/viewer.c000066400000000000000000000574661474536441700153410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Business logic of application and UI event handlers. // Copyright (C) 2020 Artem Senichev #include "viewer.h" #include "application.h" #include "buildcfg.h" #include "fetcher.h" #include "imagelist.h" #include "info.h" #include "pixmap_scale.h" #include "ui.h" #include #include #include #include #include // Background grid parameters #define GRID_NAME "grid" #define GRID_BKGID 0x00f1f2f3 #define GRID_STEP 10 #define GRID_COLOR1 0xff333333 #define GRID_COLOR2 0xff4c4c4c // Scale thresholds #define MIN_SCALE 10 // pixels #define MAX_SCALE 100.0 // factor /** Scaling operations. */ enum fixed_scale { scale_fit_optimal, ///< Fit to window, but not more than 100% scale_fit_window, ///< Fit to window size scale_fit_width, ///< Fit width to window width scale_fit_height, ///< Fit height to window height scale_fill_window, ///< Fill the window scale_real_size, ///< Real image size (100%) }; // clang-format off static const char* scale_names[] = { [scale_fit_optimal] = "optimal", [scale_fit_window] = "fit", [scale_fit_width] = "width", [scale_fit_height] = "height", [scale_fill_window] = "fill", [scale_real_size] = "real", }; // clang-format on enum position { position_top, position_center, position_bottom, position_left, position_right, position_top_left, position_top_right, position_bottom_left, position_bottom_right, }; // clang-format off static const char* position_names[] = { [position_top] = "top", [position_center] = "center", [position_bottom] = "bottom", [position_left] = "left", [position_right] = "right", [position_top_left] = "topleft", [position_top_right] = "topright", [position_bottom_left] = "bottomleft", [position_bottom_right] = "bottomright", }; // clang-format on /** Viewer context. */ struct viewer { ssize_t img_x, img_y; ///< Top left corner of the image ssize_t img_w, img_h; ///< Image width and height size_t frame; ///< Index of the current frame argb_t image_bkg; ///< Image background mode/color argb_t window_bkg; ///< Window background mode/color enum pixmap_aa_mode aa_mode; ///< Anti-aliasing mode bool fixed; ///< Fix image position enum fixed_scale scale_init; ///< Initial scale bool keep_zoom; ///< Keep absolute zoom across images enum position position; ///< Initial position double scale; ///< Current scale factor of the image bool animation_enable; ///< Animation enable/disable int animation_fd; ///< Animation timer bool slideshow_enable; ///< Slideshow enable/disable int slideshow_fd; ///< Slideshow timer size_t slideshow_time; ///< Slideshow image display time (seconds) }; /** Global viewer context. */ static struct viewer ctx; /** * Fix up image position. * @param force true to ignore current config setting */ static void fixup_position(bool force) { const ssize_t wnd_width = ui_get_width(); const ssize_t wnd_height = ui_get_height(); const struct pixmap* img = &fetcher_current()->frames[ctx.frame].pm; const ssize_t img_width = ctx.scale * img->width; const ssize_t img_height = ctx.scale * img->height; if (force || ctx.fixed) { // bind to window border if (ctx.img_x > 0 && ctx.img_x + img_width > wnd_width) { ctx.img_x = 0; } if (ctx.img_y > 0 && ctx.img_y + img_height > wnd_height) { ctx.img_y = 0; } if (ctx.img_x < 0 && ctx.img_x + img_width < wnd_width) { ctx.img_x = wnd_width - img_width; } if (ctx.img_y < 0 && ctx.img_y + img_height < wnd_height) { ctx.img_y = wnd_height - img_height; } // centering small image if (img_width <= wnd_width) { ctx.img_x = wnd_width / 2 - img_width / 2; } if (img_height <= wnd_height) { ctx.img_y = wnd_height / 2 - img_height / 2; } } // don't let canvas to be far out of window if (ctx.img_x + img_width < 0) { ctx.img_x = -img_width; } if (ctx.img_x > wnd_width) { ctx.img_x = wnd_width; } if (ctx.img_y + img_height < 0) { ctx.img_y = -img_height; } if (ctx.img_y > wnd_height) { ctx.img_y = wnd_height; } } /** Set image position. */ static void set_position(void) { const struct image* img = fetcher_current(); const struct pixmap* pm = &img->frames[ctx.frame].pm; const size_t wnd_width = ui_get_width(); const size_t wnd_height = ui_get_height(); switch (ctx.position) { case position_top: ctx.img_y = 0; ctx.img_x = wnd_width / 2 - (ctx.scale * pm->width) / 2; break; case position_center: ctx.img_y = wnd_height / 2 - (ctx.scale * pm->height) / 2; ctx.img_x = wnd_width / 2 - (ctx.scale * pm->width) / 2; break; case position_bottom: ctx.img_y = wnd_height - ctx.scale * pm->height; ctx.img_x = wnd_width / 2 - (ctx.scale * pm->width) / 2; break; case position_left: ctx.img_y = wnd_height / 2 - (ctx.scale * pm->height) / 2; ctx.img_x = 0; break; case position_right: ctx.img_y = wnd_height / 2 - (ctx.scale * pm->height) / 2; ctx.img_x = wnd_width - ctx.scale * pm->width; break; case position_top_left: ctx.img_y = 0; ctx.img_x = 0; break; case position_top_right: ctx.img_y = 0; ctx.img_x = wnd_width - ctx.scale * pm->width; break; case position_bottom_left: ctx.img_y = wnd_height - ctx.scale * pm->height; ctx.img_x = 0; break; case position_bottom_right: ctx.img_y = wnd_height - ctx.scale * pm->height; ctx.img_x = wnd_width - ctx.scale * pm->width; break; } fixup_position(true); } /** * Move image (viewport). * @param horizontal axis along which to move (false for vertical) * @param positive direction (increase/decrease) * @param params optional move step in percents */ static void move_image(bool horizontal, bool positive, const char* params) { const ssize_t old_x = ctx.img_x; const ssize_t old_y = ctx.img_y; ssize_t step = 10; // in % if (params) { ssize_t val; if (str_to_num(params, 0, &val, 0) && val > 0 && val <= 1000) { step = val; } else { fprintf(stderr, "Invalid move step: \"%s\"\n", params); } } if (!positive) { step = -step; } if (horizontal) { ctx.img_x += (ui_get_width() / 100) * step; } else { ctx.img_y += (ui_get_height() / 100) * step; } fixup_position(false); if (ctx.img_x != old_x || ctx.img_y != old_y) { app_redraw(); } } /** * Rotate image 90 degrees. * @param clockwise rotation direction */ static void rotate_image(bool clockwise) { struct image* img = fetcher_current(); const struct pixmap* pm = &img->frames[ctx.frame].pm; const ssize_t diff = (ssize_t)pm->width - pm->height; const ssize_t shift = (ctx.scale * diff) / 2; image_rotate(img, clockwise ? 90 : 270); ctx.img_x += shift; ctx.img_y -= shift; fixup_position(false); app_redraw(); } /** * Set fixed scale for the image. * @param sc scale to set */ static void scale_image(enum fixed_scale sc) { const struct image* img = fetcher_current(); const struct pixmap* pm = &img->frames[ctx.frame].pm; const size_t wnd_width = ui_get_width(); const size_t wnd_height = ui_get_height(); const float scale_w = 1.0 / ((float)pm->width / wnd_width); const float scale_h = 1.0 / ((float)pm->height / wnd_height); switch (sc) { case scale_fit_optimal: ctx.scale = min(scale_w, scale_h); if (ctx.scale > 1.0) { ctx.scale = 1.0; } break; case scale_fit_window: ctx.scale = min(scale_w, scale_h); break; case scale_fit_width: ctx.scale = scale_w; break; case scale_fit_height: ctx.scale = scale_h; break; case scale_fill_window: ctx.scale = max(scale_w, scale_h); break; case scale_real_size: ctx.scale = 1.0; // 100 % break; } set_position(); info_update(info_scale, "%.0f%%", ctx.scale * 100); } /** * Zoom in/out. * @param params zoom operation */ static void zoom_image(const char* params) { ssize_t percent = 0; ssize_t fixed_scale; if (!params || !*params) { return; } // check for fixed scale type fixed_scale = str_index(scale_names, params, 0); if (fixed_scale >= 0) { scale_image(fixed_scale); } else if (str_to_num(params, 0, &percent, 0) && percent != 0 && percent > -1000 && percent < 1000) { // zoom in % const double wnd_half_w = (double)ui_get_width() / 2; const double wnd_half_h = (double)ui_get_height() / 2; const float step = (ctx.scale / 100) * percent; const double center_x = wnd_half_w / ctx.scale - ctx.img_x / ctx.scale; const double center_y = wnd_half_h / ctx.scale - ctx.img_y / ctx.scale; if (percent > 0) { ctx.scale += step; if (ctx.scale > MAX_SCALE) { ctx.scale = MAX_SCALE; } } else { const struct image* img = fetcher_current(); const struct pixmap* pm = &img->frames[ctx.frame].pm; const float scale_w = (float)MIN_SCALE / pm->width; const float scale_h = (float)MIN_SCALE / pm->height; const float scale_min = max(scale_w, scale_h); ctx.scale += step; if (ctx.scale < scale_min) { ctx.scale = scale_min; } } // restore center ctx.img_x = wnd_half_w - center_x * ctx.scale; ctx.img_y = wnd_half_h - center_y * ctx.scale; fixup_position(false); } else { fprintf(stderr, "Invalid zoom operation: \"%s\"\n", params); } info_update(info_scale, "%.0f%%", ctx.scale * 100); app_redraw(); } /** * Set default scale to a fixed scale value and apply it. * @param params fixed scale to set */ static void scale_global(const char* params) { if (params && *params) { ssize_t fixed_scale = str_index(scale_names, params, 0); if (fixed_scale >= 0) { ctx.scale_init = fixed_scale; } else { fprintf(stderr, "Invalid scale operation: \"%s\"\n", params); return; } } else { // toggle to the next scale ctx.scale_init++; if (ctx.scale_init >= ARRAY_SIZE(scale_names)) { ctx.scale_init = 0; } } info_update(info_status, "Scale %s", scale_names[ctx.scale_init]); scale_image(ctx.scale_init); app_redraw(); } /** * Toggle zoom keeping mode. */ static void toggle_keep_zoom(void) { ctx.keep_zoom = !ctx.keep_zoom; info_update(info_status, "Keep zoom %s", ctx.keep_zoom ? "ON" : "OFF"); app_redraw(); } /** * Start/stop animation if image supports it. * @param enable state to set */ static void animation_ctl(bool enable) { struct itimerspec ts = { 0 }; if (enable) { const struct image* img = fetcher_current(); const size_t duration = img->frames[ctx.frame].duration; enable = (img->num_frames > 1 && duration); if (enable) { ts.it_value.tv_sec = duration / 1000; ts.it_value.tv_nsec = (duration % 1000) * 1000000; } } ctx.animation_enable = enable; timerfd_settime(ctx.animation_fd, 0, &ts, NULL); } /** * Start/stop slide show. * @param enable state to set */ static void slideshow_ctl(bool enable) { struct itimerspec ts = { 0 }; ctx.slideshow_enable = enable; if (enable) { ts.it_value.tv_sec = ctx.slideshow_time; } timerfd_settime(ctx.slideshow_fd, 0, &ts, NULL); } /** * Reset state to defaults. */ static void reset_state(void) { const struct image* img = fetcher_current(); const size_t total_img = image_list_size(); ctx.frame = 0; if (!ctx.keep_zoom || ctx.scale == 0) { scale_image(ctx.scale_init); set_position(); } else { const ssize_t diff_w = ctx.img_w - img->frames[0].pm.width; const ssize_t diff_h = ctx.img_h - img->frames[0].pm.height; ctx.img_x += floor(ctx.scale * diff_w) / 2.0; ctx.img_y += floor(ctx.scale * diff_h) / 2.0; fixup_position(true); } ctx.img_w = img->frames[0].pm.width; ctx.img_h = img->frames[0].pm.height; ui_set_title(img->name); animation_ctl(true); slideshow_ctl(ctx.slideshow_enable); info_reset(img); info_update(info_scale, "%.0f%%", ctx.scale * 100); if (total_img) { info_update(info_index, "%zu of %zu", img->index + 1, total_img); } ui_set_content_type_animated(ctx.animation_enable); app_redraw(); } /** * Skip current image. * @return true if next image was loaded */ static bool skip_image(void) { size_t index; const size_t current = fetcher_current()->index; index = image_list_skip(current); while (index != IMGLIST_INVALID && !fetcher_open(index)) { index = image_list_skip(index); } return (index != IMGLIST_INVALID); } /** * Switch to the next image. * @param direction next image position * @return true if next image was loaded */ static bool next_image(enum action_type direction) { size_t index = fetcher_current()->index; do { switch (direction) { case action_first_file: index = image_list_first(); // look forward in case the first file fails to load direction = action_next_file; break; case action_last_file: index = image_list_last(); // look backward in case the last file fails to load direction = action_prev_file; break; case action_prev_dir: index = image_list_prev_dir(index); break; case action_next_dir: index = image_list_next_dir(index); break; case action_prev_file: index = image_list_prev_file(index); break; case action_next_file: index = image_list_next_file(index); break; case action_rand_file: index = image_list_rand_file(index); break; default: break; } } while (index != IMGLIST_INVALID && !fetcher_open(index)); if (index == IMGLIST_INVALID) { return false; } reset_state(); return true; } /** * Switch to the next or previous frame. * @param forward switch direction */ static void next_frame(bool forward) { size_t index = ctx.frame; const struct image* img = fetcher_current(); if (forward) { if (++index >= img->num_frames) { index = 0; } } else { if (index-- == 0) { index = img->num_frames - 1; } } if (index != ctx.frame) { ctx.frame = index; info_update(info_frame, "%zu of %zu", ctx.frame + 1, img->num_frames); info_update(info_image_size, "%zux%zu", img->frames[ctx.frame].pm.width, img->frames[ctx.frame].pm.height); app_redraw(); } } /** * Animation timer event handler. */ static void on_animation_timer(__attribute__((unused)) void* data) { next_frame(true); animation_ctl(true); } /** * Slideshow timer event handler. */ static void on_slideshow_timer(__attribute__((unused)) void* data) { slideshow_ctl(next_image(action_next_file)); } /** * Draw image. * @param wnd pixel map of target window */ static void draw_image(struct pixmap* wnd) { const struct image* img = fetcher_current(); const struct pixmap* img_pm = &img->frames[ctx.frame].pm; const size_t width = ctx.scale * img_pm->width; const size_t height = ctx.scale * img_pm->height; // clear window background pixmap_inverse_fill(wnd, ctx.img_x, ctx.img_y, width, height, ctx.window_bkg); // clear image background if (img->alpha) { if (ctx.image_bkg == GRID_BKGID) { pixmap_grid(wnd, ctx.img_x, ctx.img_y, width, height, GRID_STEP, GRID_COLOR1, GRID_COLOR2); } else { pixmap_fill(wnd, ctx.img_x, ctx.img_y, width, height, ctx.image_bkg); } } // put image on window surface if (ctx.scale == 1.0) { pixmap_copy(img_pm, wnd, ctx.img_x, ctx.img_y, img->alpha); } else { pixmap_scale(ctx.aa_mode, img_pm, wnd, ctx.img_x, ctx.img_y, ctx.scale, img->alpha); } } /** * Reload image file and reset state (position, scale, etc). */ static void reload(void) { const size_t index = fetcher_current()->index; if (fetcher_reset(index, false)) { if (index == fetcher_current()->index) { info_update(info_status, "Image reloaded"); } else { info_update(info_status, "Unable to update, open next file"); } reset_state(); } else { printf("No more images to view, exit\n"); app_exit(0); } } /** * Redraw handler. */ static void redraw(void) { struct pixmap* window = ui_draw_begin(); if (window) { draw_image(window); info_print(window); ui_draw_commit(); } } /** * Window resize handler. */ static void on_resize(void) { fixup_position(false); reset_state(); } /** * Apply action. * @param action pointer to the action being performed */ static void apply_action(const struct action* action) { switch (action->type) { case action_first_file: case action_last_file: case action_prev_dir: case action_next_dir: case action_prev_file: case action_next_file: case action_rand_file: next_image(action->type); break; case action_skip_file: if (skip_image()) { reset_state(); } else { printf("No more images, exit\n"); app_exit(0); } break; case action_prev_frame: case action_next_frame: animation_ctl(false); next_frame(action->type == action_next_frame); break; case action_animation: animation_ctl(!ctx.animation_enable); break; case action_slideshow: slideshow_ctl(!ctx.slideshow_enable && next_image(action_next_file)); break; case action_mode: app_switch_mode(fetcher_current()->index); break; case action_step_left: move_image(true, true, action->params); break; case action_step_right: move_image(true, false, action->params); break; case action_step_up: move_image(false, true, action->params); break; case action_step_down: move_image(false, false, action->params); break; case action_zoom: zoom_image(action->params); break; case action_scale: scale_global(action->params); break; case action_keep_zoom: toggle_keep_zoom(); break; case action_rotate_left: rotate_image(false); break; case action_rotate_right: rotate_image(true); break; case action_flip_vertical: image_flip_vertical(fetcher_current()); app_redraw(); break; case action_flip_horizontal: image_flip_horizontal(fetcher_current()); app_redraw(); break; case action_antialiasing: if (++ctx.aa_mode >= ARRAY_SIZE(pixmap_aa_names)) { ctx.aa_mode = 0; } info_update(info_status, "Anti-aliasing: %s", pixmap_aa_names[ctx.aa_mode]); app_redraw(); break; case action_reload: reload(); break; case action_exec: app_execute(action->params, fetcher_current()->source); break; default: break; } } /** * Image drag handler. * @param dx,dy delta to move viewpoint */ static void on_drag(int dx, int dy) { const ssize_t old_x = ctx.img_x; const ssize_t old_y = ctx.img_y; ctx.img_x += dx; ctx.img_y += dy; if (ctx.img_x != old_x || ctx.img_y != old_y) { fixup_position(false); app_redraw(); } } void viewer_init(const struct config* cfg, struct image* image) { size_t history; size_t preload; const char* value; ctx.fixed = config_get_bool(cfg, CFG_VIEWER, CFG_VIEW_FIXED); ctx.aa_mode = config_get_oneof(cfg, CFG_VIEWER, CFG_VIEW_AA, pixmap_aa_names, ARRAY_SIZE(pixmap_aa_names)); ctx.window_bkg = config_get_color(cfg, CFG_VIEWER, CFG_VIEW_WINDOW); // background for transparent images value = config_get(cfg, CFG_VIEWER, CFG_VIEW_TRANSP); if (strcmp(value, GRID_NAME) == 0) { ctx.image_bkg = GRID_BKGID; } else { ctx.image_bkg = config_get_color(cfg, CFG_VIEWER, CFG_VIEW_TRANSP); } // initial scale and position ctx.scale_init = config_get_oneof(cfg, CFG_VIEWER, CFG_VIEW_SCALE, scale_names, ARRAY_SIZE(scale_names)); ctx.keep_zoom = config_get_bool(cfg, CFG_VIEWER, CFG_VIEW_KEEP_ZM); ctx.position = config_get_oneof(cfg, CFG_VIEWER, CFG_VIEW_POSITION, position_names, ARRAY_SIZE(position_names)); // cache and preloads history = config_get_num(cfg, CFG_VIEWER, CFG_VIEW_HISTORY, 0, 1024); preload = config_get_num(cfg, CFG_VIEWER, CFG_VIEW_PRELOAD, 0, 1024); // setup animation timer ctx.animation_enable = true; ctx.animation_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (ctx.animation_fd != -1) { app_watch(ctx.animation_fd, on_animation_timer, NULL); } // setup slideshow timer ctx.slideshow_enable = config_get_bool(cfg, CFG_VIEWER, CFG_VIEW_SSHOW); ctx.slideshow_time = config_get_num(cfg, CFG_VIEWER, CFG_VIEW_SSHOW_TM, 1, 86400); ctx.slideshow_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (ctx.slideshow_fd != -1) { app_watch(ctx.slideshow_fd, on_slideshow_timer, NULL); } fetcher_init(image, history, preload); } void viewer_destroy(void) { fetcher_destroy(); if (ctx.animation_fd != -1) { close(ctx.animation_fd); } if (ctx.slideshow_fd != -1) { close(ctx.slideshow_fd); } } void viewer_handle(const struct event* event) { switch (event->type) { case event_action: apply_action(event->param.action); break; case event_redraw: redraw(); break; case event_resize: on_resize(); break; case event_drag: on_drag(event->param.drag.dx, event->param.drag.dy); break; case event_activate: if (fetcher_reset(event->param.activate.index, false)) { reset_state(); } else { app_exit(0); } break; case event_load: fetcher_attach(event->param.load.image, event->param.load.index); break; } } swayimg-3.8/src/viewer.h000066400000000000000000000010771474536441700153310ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Business logic of application and UI event handlers. // Copyright (C) 2020 Artem Senichev #pragma once #include "config.h" #include "event.h" #include "image.h" /** * Initialize global viewer context. * @param cfg config instance * @param image initial image to open */ void viewer_init(const struct config* cfg, struct image* image); /** * Destroy global viewer context. */ void viewer_destroy(void); /** * Event handler, see `event_handler` for details. */ void viewer_handle(const struct event* event); swayimg-3.8/src/wndbuf.c000066400000000000000000000051621474536441700153070ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Wayland window surface buffer. // Copyright (C) 2024 Artem Senichev #include "wndbuf.h" #include "buildcfg.h" #include #include #include #include #include #include #include #include struct wl_buffer* wndbuf_create(struct wl_shm* shm, size_t width, size_t height) { assert(shm); assert(width > 0); assert(height > 0); const size_t stride = width * sizeof(argb_t); const size_t data_sz = stride * height; const size_t buffer_sz = data_sz + sizeof(struct pixmap); struct pixmap* pm; struct wl_buffer* buffer; struct wl_shm_pool* pool; int fd = -1; char path[64]; struct timespec ts; void* data; // generate unique file name clock_gettime(CLOCK_MONOTONIC, &ts); snprintf(path, sizeof(path), "/" APP_NAME "_%" PRIx64, ((uint64_t)ts.tv_sec << 32) | ts.tv_nsec); // open shared mem fd = shm_open(path, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd == -1) { fprintf(stderr, "Unable to create shared file: [%i] %s\n", errno, strerror(errno)); return NULL; } shm_unlink(path); // set shared memory size if (ftruncate(fd, buffer_sz) == -1) { fprintf(stderr, "Unable to truncate shared file: [%i] %s\n", errno, strerror(errno)); close(fd); return NULL; } // get data pointer of the shared mem data = mmap(NULL, buffer_sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { fprintf(stderr, "Unable to map shared file: [%i] %s\n", errno, strerror(errno)); close(fd); return NULL; } // fill buffer user data pm = (struct pixmap*)((uint8_t*)data + data_sz); pm->width = width; pm->height = height; pm->data = data; // create wayland buffer pool = wl_shm_create_pool(shm, fd, buffer_sz); buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); wl_buffer_set_user_data(buffer, pm); wl_shm_pool_destroy(pool); close(fd); return buffer; } struct pixmap* wndbuf_pixmap(struct wl_buffer* buffer) { assert(buffer); return wl_buffer_get_user_data(buffer); } void wndbuf_free(struct wl_buffer* buffer) { if (buffer) { struct pixmap* pm = wl_buffer_get_user_data(buffer); const size_t sz = pm->width * pm->height * sizeof(argb_t) + sizeof(struct pixmap); munmap(pm->data, sz); wl_buffer_destroy(buffer); } } swayimg-3.8/src/wndbuf.h000066400000000000000000000014341474536441700153120ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Wayland window surface buffer. // Copyright (C) 2024 Artem Senichev #pragma once #include "pixmap.h" #include /** * Create window buffer. * @param shm wayland shared memory interface * @param width,height buffer size in pixels * @return wayland buffer on NULL on errors */ struct wl_buffer* wndbuf_create(struct wl_shm* shm, size_t width, size_t height); /** * Get pixel map assiciated with the buffer. * @param buffer wayland buffer * @return pointer to pixel map assiciated with the buffer */ struct pixmap* wndbuf_pixmap(struct wl_buffer* buffer); /** * Free window buffer. * @param buffer wayland buffer to free */ void wndbuf_free(struct wl_buffer* buffer); swayimg-3.8/test/000077500000000000000000000000001474536441700140425ustar00rootroot00000000000000swayimg-3.8/test/action_test.cpp000066400000000000000000000042211474536441700170610ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "action.h" } #include class Action : public ::testing::Test { protected: void TearDown() override { action_free(&actions); } struct action_seq actions = { nullptr, 0 }; }; TEST_F(Action, Create) { ASSERT_TRUE(action_create("info", &actions)); ASSERT_EQ(actions.num, static_cast(1)); ASSERT_NE(actions.sequence, nullptr); ASSERT_EQ(actions.sequence[0].type, action_info); ASSERT_EQ(actions.sequence[0].params, nullptr); } TEST_F(Action, Fail) { ASSERT_FALSE(action_create("", &actions)); ASSERT_EQ(actions.num, static_cast(0)); ASSERT_EQ(actions.sequence, nullptr); ASSERT_FALSE(action_create("invalid", &actions)); ASSERT_EQ(actions.num, static_cast(0)); ASSERT_EQ(actions.sequence, nullptr); ASSERT_FALSE(action_create("info123 exec", &actions)); ASSERT_EQ(actions.num, static_cast(0)); ASSERT_EQ(actions.sequence, nullptr); } TEST_F(Action, Params) { ASSERT_TRUE(action_create("exec \t param 123 ", &actions)); ASSERT_EQ(actions.num, static_cast(1)); ASSERT_NE(actions.sequence, nullptr); ASSERT_EQ(actions.sequence[0].type, action_exec); ASSERT_STREQ(actions.sequence[0].params, "param 123"); } TEST_F(Action, Sequence) { ASSERT_TRUE(action_create("exec cmd;\nreload;\t exit;status ok", &actions)); ASSERT_EQ(actions.num, static_cast(4)); ASSERT_NE(actions.sequence, nullptr); ASSERT_EQ(actions.sequence[0].type, action_exec); ASSERT_STREQ(actions.sequence[0].params, "cmd"); ASSERT_EQ(actions.sequence[1].type, action_reload); ASSERT_EQ(actions.sequence[1].params, nullptr); ASSERT_EQ(actions.sequence[2].type, action_exit); ASSERT_EQ(actions.sequence[2].params, nullptr); ASSERT_EQ(actions.sequence[3].type, action_status); ASSERT_STREQ(actions.sequence[3].params, "ok"); } TEST_F(Action, FailSequence) { ASSERT_FALSE(action_create("exec cmd;\nreload;invalid", &actions)); ASSERT_EQ(actions.num, static_cast(0)); ASSERT_EQ(actions.sequence, nullptr); } swayimg-3.8/test/config_test.cpp000066400000000000000000000133231474536441700170540ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "config.h" } #include class Config : public ::testing::Test { protected: void SetUp() override { unsetenv("XDG_CONFIG_HOME"); unsetenv("XDG_CONFIG_DIRS"); unsetenv("HOME"); config = config_load(); } void TearDown() override { config_free(config); } struct config* config = nullptr; }; TEST(ConfigLoader, Load) { testing::internal::CaptureStderr(); setenv("XDG_CONFIG_HOME", TEST_DATA_DIR, 1); struct config* config = config_load(); ASSERT_NE(config, nullptr); EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_MODE), "s p a c e s"); EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_APP_ID), "my_ap_id"); config_free(config); unsetenv("XDG_CONFIG_HOME"); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, Set) { EXPECT_TRUE(config_set(config, CFG_GENERAL, CFG_GNRL_APP_ID, "test123")); EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_APP_ID), "test123"); testing::internal::CaptureStderr(); EXPECT_FALSE(config_set(config, CFG_GENERAL, CFG_GNRL_APP_ID, "")); EXPECT_FALSE(config_set(config, CFG_GENERAL, "unknown", "test123")); EXPECT_FALSE(config_set(config, "unknown", "unknown", "test123")); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, SetArg) { EXPECT_TRUE( config_set_arg(config, CFG_GENERAL "." CFG_GNRL_APP_ID "=test123")); EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_APP_ID), "test123"); EXPECT_TRUE(config_set_arg( config, "\t\n" CFG_GENERAL "." CFG_GNRL_APP_ID " = \ttest321")); EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_APP_ID), "test321"); testing::internal::CaptureStderr(); EXPECT_FALSE(config_set_arg(config, "")); EXPECT_FALSE(config_set_arg(config, "abc=1")); EXPECT_FALSE(config_set_arg(config, "abc.def")); EXPECT_FALSE(config_set_arg(config, "abc.def=")); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, Add) { testing::internal::CaptureStderr(); EXPECT_STREQ(config_get(config, CFG_KEYS_VIEWER, "F12"), ""); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); EXPECT_TRUE(config_set(config, CFG_KEYS_VIEWER, "F12", "quit")); EXPECT_STREQ(config_get(config, CFG_KEYS_VIEWER, "F12"), "quit"); } TEST_F(Config, Replace) { EXPECT_STREQ(config_get(config, CFG_KEYS_VIEWER, "F1"), "help"); EXPECT_TRUE(config_set(config, CFG_KEYS_VIEWER, "F1", "quit")); EXPECT_STREQ(config_get(config, CFG_KEYS_VIEWER, "F1"), "quit"); } TEST_F(Config, GetDefault) { EXPECT_TRUE(config_set(config, CFG_GENERAL, CFG_GNRL_APP_ID, "test123")); EXPECT_STREQ(config_get_default(CFG_GENERAL, CFG_GNRL_APP_ID), "swayimg"); testing::internal::CaptureStderr(); EXPECT_STREQ(config_get_default(CFG_GENERAL, "unknown"), ""); EXPECT_STREQ(config_get_default("unknown", "unknown"), ""); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, Get) { EXPECT_STREQ(config_get(config, CFG_GENERAL, CFG_GNRL_APP_ID), "swayimg"); testing::internal::CaptureStderr(); EXPECT_STREQ(config_get(config, CFG_GENERAL, "unknown"), ""); EXPECT_STREQ(config_get(config, "unknown", "unknown"), ""); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, GetOneOf) { const char* possible[] = { "one", "two", "three" }; EXPECT_TRUE(config_set(config, CFG_LIST, CFG_LIST_ORDER, "two")); EXPECT_EQ(config_get_oneof(config, CFG_LIST, CFG_LIST_ORDER, possible, 3), 1); testing::internal::CaptureStderr(); EXPECT_TRUE(config_set(config, CFG_LIST, CFG_LIST_ORDER, "four")); EXPECT_EQ(config_get_oneof(config, CFG_LIST, CFG_LIST_ORDER, possible, 3), 0); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, GetBool) { EXPECT_TRUE(config_set(config, CFG_GALLERY, CFG_GLRY_FILL, CFG_YES)); EXPECT_TRUE(config_get_bool(config, CFG_GALLERY, CFG_GLRY_FILL)); EXPECT_TRUE(config_set(config, CFG_GALLERY, CFG_GLRY_FILL, CFG_NO)); EXPECT_FALSE(config_get_bool(config, CFG_GALLERY, CFG_GLRY_FILL)); } TEST_F(Config, GetNum) { EXPECT_TRUE(config_set(config, CFG_FONT, CFG_FONT_SIZE, "123")); EXPECT_EQ(config_get_num(config, CFG_FONT, CFG_FONT_SIZE, 0, 1024), 123); testing::internal::CaptureStderr(); EXPECT_EQ(config_get_num(config, CFG_FONT, CFG_FONT_SIZE, 0, -1), 14); EXPECT_EQ(config_get_num(config, CFG_FONT, CFG_FONT_SIZE, 0, 1), 14); EXPECT_EQ(config_get_num(config, CFG_FONT, CFG_FONT_SIZE, -1, 0), 14); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } TEST_F(Config, GetColor) { config_set(config, CFG_VIEWER, CFG_VIEW_WINDOW, "#010203"); EXPECT_EQ(config_get_color(config, CFG_VIEWER, CFG_VIEW_WINDOW), static_cast(0xff010203)); config_set(config, CFG_VIEWER, CFG_VIEW_WINDOW, "#010203aa"); EXPECT_EQ(config_get_color(config, CFG_VIEWER, CFG_VIEW_WINDOW), static_cast(0xaa010203)); config_set(config, CFG_VIEWER, CFG_VIEW_WINDOW, "010203aa"); EXPECT_EQ(config_get_color(config, CFG_VIEWER, CFG_VIEW_WINDOW), static_cast(0xaa010203)); config_set(config, CFG_VIEWER, CFG_VIEW_WINDOW, "# 010203aa"); EXPECT_EQ(config_get_color(config, CFG_VIEWER, CFG_VIEW_WINDOW), static_cast(0xaa010203)); testing::internal::CaptureStderr(); config_set(config, CFG_VIEWER, CFG_VIEW_WINDOW, "invalid"); config_get_color(config, CFG_VIEWER, CFG_VIEW_WINDOW); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } swayimg-3.8/test/data/000077500000000000000000000000001474536441700147535ustar00rootroot00000000000000swayimg-3.8/test/data/exif.jpg000066400000000000000000000032721474536441700164140ustar00rootroot00000000000000JFIF,,BExifII* (1 2i%lGooglePixel 7,,GIMP 2.99.162024:07:06 12:31:44("'0232    "*  27767767760100:   3B4"J` N@Bd2024:05:30 21:18:482024:05:30 21:18:48+03:00+03:00+03:004ddddGooglePixel 7 back camera 6.81mm f/1.85 NEM& .7, d%% d+2024:05:30C      C   }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?^yswayimg-3.8/test/data/image.avif000066400000000000000000000010301474536441700166760ustar00rootroot00000000000000ftypavifavifmif1miafmeta!hdlrpictpitm4ilocD@=8iinfinfeav01infeav01iprpipcocolrnclx av1Cispepixi8auxCurn:mpeg:mpegB:cicp:systems:auxiliary:alpha av1C ispepixiipmairefauxldmdat 62[=q5ƿ{ 8620@D tKY8Rm.nE(ozlD0R+Kswayimg-3.8/test/data/image.bmp000066400000000000000000000001261474536441700165340ustar00rootroot00000000000000BMVF8 #.#.swayimg-3.8/test/data/image.dcm000066400000000000000000000447661474536441700165430ustar00rootroot00000000000000DICMULOBUI1.2.840.10008.5.1.4.1.1.4UI:1.2.276.0.7230010.3.1.4.2831162880.23968.1733046846.730393UI1.2.840.10008.1.2.1UI1.2.276.0.7230010.3.0.3.6.8SHOFFIS_DCMTK_368 CSDERIVED\PRIMARY\GDC DA20241115TM 095700.000UI1.2.392.200036.9116.4.2.8833UI1.2.840.10008.5.1.4.1.1.4UI:1.2.276.0.7230010.3.1.4.2831162880.23968.1733046846.730393 DA20241115!DA20241115"DA20241115#DA202411150TM0956171TM0957002TM 095700.0003TM 095700.000PSH`CSMRpLOTOSHIBA LOScandinavian MC ST------------- PNSH Vantage Elan0LO>LO Locator SG@LO DepartmentHPN`PNLOLO Vantage ElanSQh`PUI1.2.840.10008.3.1.2.3.3UUI81.2.392.200036.9116.4.2.8833.1041.20241115005558363.1.3!ST Scaled image!SQ^VUI1.2.840.10008.5.1.4.1.1.4UI,1.2.392.200036.9116.4.2.8833.15270.8.2001.1PNSENICHEV^A.A.  LO978194!LO0DA197711052TM@CSM LOPNPNAS047Y DS1.750DS70.00 @LO`PNLOLOLO LO!LOP!LOR!LOT!SH`!SH!SH!CS!LT!DA!LO@LTCSCSPINE CSGR!CSNONE"CS#CS2D$SHFE_sltPDS8.0000DS55.0DS4.000 DS1.0000DS 63.79229879 IS1 DS1.5 DS10.0000 IS128 IS1 DS100.0000DS488 LO8833 LO V4.0SP0053* 0LO Locator SGCSN DS330.0000PSH Octave Head QSHQD Whole Body USCSROW DS25DS0.005000DS3.187368QCSHFS sFD @ UI81.2.392.200036.9116.4.2.8833.1467.20241115005558338.3.8 UI(1.2.392.200036.9116.4.2.8833.15270.2002 SH15270 IS2001 IS2 IS1 2DS(-20.00000191\-165.00000000\165.00000000 7DSB0.00000000\1.00000000\0.00000000\0.00000000\0.00000000\-1.00000000 RUI$1.2.392.200036.9116.4.2.8833.15270.4 @LO ADS -20.00000142 VSH1 WUL (UL(US(CS MONOCHROME2 (IS1 (US(US(0DS65.996800\65.996800 (US(US (US (US(PDS8191.50 (QDS16383.00)LOTOSHIBA_MEC_MR3 )SQ?)LOTOSHIBA_MEC_MR3 )OB BO SU01^3RM_CEM_ABIHSOTOLSU01^3RM_CEM_ABIHSOTOLydutSsulPM OL) 3RM_CEM_ABIHSOTOL)NP )LOTOSHIBA_MEC_MR3 )OBdradnats enips-C\ENIPSC\RESUOLp 01^3RM_CEM_ABIHSOTOLp 3388.07251nuR/yduts/setis/pg/OLP) ydutSRMOL)01^3RM_CEM_ABIHSOTOL) 3RM_CEM_ABIHSOTOL)LSLLU00000000.7611-SD11^3RM_CEM_ABIHSOTOL01^3RM_CEM_ABIHSOTOL 3AM002TRM HS SUSUSU01^3RM_CEM_ABIHSOTOL$SU01^3RM_CEM_ABIHSOTOL$XQSSU01^3RM_CEM_ABIHSOTOL3388HS 01^3RM_CEM_ABIHSOTOL )LOTOSHIBA_MEC_MR3 )OB SU01^3RM_CEM_ABIHSOTOLGS rotacoL HS01^3RM_CEM_ABIHSOTOL seireSsulPM OL) 3RM_CEM_ABIHSOTOL)1002SI @ DFs8!)LOTOSHIBA_MEC_MR3 )OB!SSp LSp 01^3RM_CEM_ABIHSOTOLp 7.1/:3388.07251nuR/yduts/setis/pg/"OLT)1.7/7.1/:3388.07251nuR/yduts/setis/pg/&OLQ)LSN) LS!)EmULF)LF)01^3RM_CEM_ABIHSOTOL)2:QS)seireSRMOL)01^3RM_CEM_ABIHSOTOL) 3RM_CEM_ABIHSOTOL)SU!SU!LS!LS!01^3RM_CEM_ABIHSOTOL!LSSUOLOLSUSUSUSUSSLULF?LSLSLFSUOL?LF?wLF?wE?xN4E?{"E?|E?qE\?\E`?(HD@?D@@LF?jLFBzLFLFLFLF?jLF??? LF07251.3388.2.4.6119.630002.293.2.1"IUBOSUSULSLF01^3RM_CEM_ABIHSOTOL DFg)EmULF)LF)01^3RM_CEM_ABIHSOTOL)1.1002.07251.3388.2.4.6119.630002.293.2.1*IUd LSSU21^3RM_CEM_ABIHSOTOLQSOLSU@ DF21^3RM_CEM_ABIHSOTOL*2QSyBOvLS4EFHS2CCLF!@LF!00000000.0\00000000.0-\00000000.0-"SD!SU!ALF !SU !SU ! 00000000.1-\00000000.0\00000000.0"SD ! 00000000.0\00000000.1\00000000.0-"SD ! 00000000.0\00000000.0-\00000000.1"SD!01^3RM_CEM_ABIHSOTOL! 0\0\0SD*21^3RM_CEM_ABIHSOTOL.6QS+00000000.8 SD(LUSULS@ @ @  LF?'NxLF[d2SUC C ;n; LF@LF??? LFSULLFgLS`LS] LS\SUYSUXLFWPR:1LNOLULSNCLFFLSDLSCLSBSUASU@0.0.0.0IU?$LF>@_w@_w@_w LF<=aGLF:BLF9LS8LS7LF6LS5LF4LS2LS1LS0LS. LS- LS,LF+LF*LS)LS(LS&LS%LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSdLSLS21^3RM_CEM_ABIHSOTOL>^QSLSdLS11^3RM_CEM_ABIHSOTOLQSLS ~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#"!  BO  LS LS 51711761HSLSLSLS LSLSđLFLFLFLSLS@Oj DFLSB\LFF4LFLSApLF?(HLFSULS0.0.0.0IULSLSLSLSLSLSLS21^3RM_CEM_ABIHSOTOL11^3RM_CEM_ABIHSOTOL|QSLSLS?LLF?LLFKLSLSLSLSLS11^3RM_CEM_ABIHSOTOLQSLSLSLSLSSULSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSdLSLS21^3RM_CEM_ABIHSOTOL>^QSLSdLS11^3RM_CEM_ABIHSOTOLQSSULSLS LSLSSULSLSLULUtrc_EFOLLS;oLFLSGALSLSACLSCCLF;oLF<oLFCCLFLSLSLSLSLSSU??@ LFLS=aGLFLSLF LF LSLSLSLS??@ LFSU??? LFLSLSLU0LSniaGxROLApLFLSLFLSLS~B@LF}JCPLF| LFROL{ 1EpefOLx GR1EpefOLvlfRpefOLuGR1EknilOLsGR1EknilOLq lfRknilOLpLSo?LFnLSmLSkSUjLFiLSeLFdLFcSUbSUaSU`LF_LS^?????LF]LFZLSY? LFX? LFWSUUSUT PR:*8OLRLSQLSPSUOSUNLSMSULSUKSUISUHLFGSUFSSELSDSUCSUBLS?LS>LS=LS<LS;LS:LS8LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSLSLS21^3RM_CEM_ABIHSOTOL>LSdLSLS21^3RM_CEM_ABIHSOTOL>^QSLSdLS11^3RM_CEM_ABIHSOTOLQS6LS5LS4 LS3LF2LU0<# LF/SU.LS-41711761HS+LS(LS'LS&=LLF%LF$>LF#LS"SU!?=LLF LSSULSLSLSSULSSULSLFLSSULSSULSSULSLSLS SU LF LF LF SU LFLS?LFLSSULSLU31^3RM_CEM_ABIHSOTOL21^3RM_CEM_ABIHSOTOL11^3RM_CEM_ABIHSOTOL01^3RM_CEM_ABIHSOTOLz)LOTOSHIBA_MEC_MR3 )OBVBBH0BO?HBOBOWEIVHS31^3RM_CEM_ABIHSOTOLQS01^3RM_CEM_ABIHSOTOLBO$HS#cdg.RMOL"01^3RM_CEM_ABIHSOTOL<BO$ *3500PS0.4V HS#nocer.RMOL"01^3RM_CEM_ABIHSOTOLJQS!      >e[FE$/082    gf_6<>5$  '  W{P^fX0 '   KڼdXoVC+  "    ɞldJsPD%&   ˣqJo^=  ' ˣuFW`@,  "'   }T^zMG  $#  y鬜|Xohgf-      t֡YF[jL (& !$cטTT`N3  #%  U^Pjk70  *"   (l\^lhU$ )   ުxYiMdP &#!    ٣xRK_I( $, %  ОsVHJU,-   ),$# }ִscYT^fS/ -/WᡈvkUYSM^: )$'"ϔxkXJFNF)  ""(Φ~bbAGLE&# %$& ّj`J>L=UM2 "&" 0cړkYMWQDFFH (( ,#˕t_SSR'.<) ',!$  ܘtbSMI*2,@>*30**%($#k~pcWGG3C865!<)('0$#&  ԧ}cXLH0!'(6%$,(-'&* ̶bPHCD/*7(*&$*+)&- ij^VIEA:!%.*(%'.1+'0zȺaUKF@=B)&,"(*,/-$/:ﵪVVNG?>.(8 %)+.+(*'VMF?=:'**')(((%+* z]NJA?4.("'-'!&!(+-+d^QJC>:*()&'%%!*++- aTKEA<12"*((%3)+-+,;WNKG@>5'$*.)*(&*30-0@ORIH?A4(3*-/--/00*-\MVEIAB8 .&)2,-5/3@BED5_RXGIE?==, 8#0/.25*=G>69FB+ tSWEJFB>A?3001--736++,0:+5!~SWGNDE@?((812002322//6)"&5, "YUOKHC:V,0-)2,62/2/3,+"$&;,5\[QNFAGX:?02/2=45335*(8<K]WSJKTDf*%752+7:23432%95 h\ZTIGTLM4%):-+525134, -0'~}V_RKHJEC2310../2511+ ..$Nj]\UWIHFBB3,.-/-,3/cc\VLfHGB;<&.#,)'22+!%YcXVbWWK9%&1-1!/.-011 (!6ůfX[STOY_;O3()29/*7/ "&ȤjaYRV_W/796$&(141+(2 ## & 1¦gZ]WE!Q,"%-66(,+.(,"  &qRl>UIJE@>"'&/$-,,),%$!~s\=%NLKFFAA=:($$(+*&!!5sX$QLIECA=@98+$ *$.$"`bQ )GAAA@?=8:2++#'' H')K7<=9:9:7220" +.!# -*220.4451/.*  #   $%*'-)-,,+*+ !"!!& ,! !"$""""   '     ~~~}}}|||{{{zzzyyyxxxwwwvvvuuutttsssrrrqqqpppooonnnmmmlllkkkjjjiiihhhgggfffeeedddcccbbbaaa```___^^^]]]\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::999888777666555444333222111000///...---,,,+++***)))((('''&&&%%%$$$###"""!!!  @((BOLF@KLFBOSUSUSUSUSU01^3RM_CEM_ABIHSOTOLegamIsulPM OL) 3RM_CEM_ABIHSOTOL)0000.38361 SDQ(00005.1918 SDP(SU(SU(LU( LUW 1HSV 24100000.02- SDA 00000000.1-\00000000.0\00000000.0\00000000.0\00000000.1\00000000.0BSD7 00000000.561\00000000.561-\19100000.02-(SD2 ENONSC!)LOTOSHIBA_MEC_MR3 )OBBBH0BOn)SSh)0.9/0.8/1.7/7.1/:3388.07251nuR/yduts/setis/pg/.OLR)CCLF)LS) egamIRMOL)01^3RM_CEM_ABIHSOTOL) 3RM_CEM_ABIHSOTOL) 1SI! 00000000.0\00000000.0-\19100000.02-$SD! 00000000.1-\00000000.0\00000000.0"SD ! 00000000.0\00000000.1\00000000.0-"SD ! 00000000.0\00000000.0-\00000000.1"SD!01^3RM_CEM_ABIHSOTOL!QS!01^3RM_CEM_ABIHSOTOL!000500.0SD@ DF0000.8SD 2SDtls_EFOLLSSU daeH evatcO TLI ydoB elohW DQTLH21^3RM_CEM_ABIHSOTOL11^3RM_CEM_ABIHSOTOL2DA202411152TM09450022PN23LO2@LT8PLO8LO@DDA20241115@ETM 095616.014@SSHMR20241115095558@TLO@0LO pLOTOSHIBA_MEC_MR3 pLOTOSHIBA_MEC_MR3^10 pDS 4.31436681 pDS0.330\0.330\0.330\0.330 p CSY pSL pLOUSER\CSPINE\C-spine standard pSSOW2 ." >Ar 9swayimg-3.8/test/data/image.ff000066400000000000000000000000601474536441700163460ustar00rootroot00000000000000farbfeldswayimg-3.8/test/data/image.gif000066400000000000000000000001051474536441700165200ustar00rootroot00000000000000GIF89a! NETSCAPE2.0!,;swayimg-3.8/test/data/image.heif000066400000000000000000000010111474536441700166630ustar00rootroot00000000000000ftypheicmif1heicmiafmeta!hdlrpictpitmidat8ilocD@;8iinfinfehvc1infegridiprpipcoshvcCp @ p@!'Bp ꮚ"Dsispe@@ispepixiipmairefdimgCmdat7(!trڱJ2`~'F<V[ы!¥T"swayimg-3.8/test/data/image.jpg000066400000000000000000000015441474536441700165430ustar00rootroot00000000000000JFIF++CC }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ??> x_ csZuk߄5]gZu_5m[TԵ=Jy/'in.%i>?G'+-r<[evN p a0-<> ӧCNi•(FWv7 8Ӎ1,N>**:8sAfi93晎+bkkի/swayimg-3.8/test/data/image.jxl000066400000000000000000000004361474536441700165570ustar00rootroot00000000000000 )4@[c!%L2СnhCCZfeǓIAJVpe_uۘm%li+2 h_ql#9aB/, S]'JX0*P6TYC?\eYZQԱ@T{eHzTϟ'tH#ۅu!({#P[Z,-,e&t NJ0ssU(+HɃLL x¹|6yGW?hKQ'>^fK?{K?swayimg-3.8/test/data/image.png000066400000000000000000000002301474536441700165360ustar00rootroot00000000000000PNG  IHDRr $gAMA asRGB, cHRMz&u0`:pQ<IDATO" ?}l IENDB`swayimg-3.8/test/data/image.pnm000066400000000000000000000000451474536441700165500ustar00rootroot00000000000000P6 # Simple pnm 2 2 255 swayimg-3.8/test/data/image.qoi000066400000000000000000000000321474536441700165420ustar00rootroot00000000000000qoifZvmswayimg-3.8/test/data/image.six000066400000000000000000000001271474536441700165620ustar00rootroot00000000000000Pq #0;2;0;0;0#1;2;100;100;0#2;2;0;100;0 #1~~@@vv@@~~@@~~$ #2??}}GG}}??}}??- #1!14@ \ swayimg-3.8/test/data/image.svg000066400000000000000000000003731474536441700165610ustar00rootroot00000000000000 swayimg-3.8/test/data/image.tga000066400000000000000000000000761474536441700165350ustar00rootroot00000000000000  TRUEVISION-XFILE.swayimg-3.8/test/data/image.tiff000066400000000000000000000004251474536441700167100ustar00rootroot00000000000000II* (RS,,Backgroundswayimg-3.8/test/data/image.webp000066400000000000000000000000621474536441700167120ustar00rootroot00000000000000RIFF*WEBPVP8L/@ Hz?`"swayimg-3.8/test/data/swayimg/000077500000000000000000000000001474536441700164335ustar00rootroot00000000000000swayimg-3.8/test/data/swayimg/config000066400000000000000000000002011474536441700176140ustar00rootroot00000000000000# Swayimg test configuration file. [general] mode = s p a c e s app_id=my_ap_id sigusr1= [unknown_section] some_param = 1 swayimg-3.8/test/exif_test.cpp000066400000000000000000000043131474536441700165410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "exif.h" } #include #include class Exif : public ::testing::Test { protected: void SetUp() override { image = image_alloc(); } void TearDown() override { image_free(image); } struct image* image; }; TEST_F(Exif, Read) { std::ifstream file(TEST_DATA_DIR "/exif.jpg", std::ios::binary); const std::vector data((std::istreambuf_iterator(file)), (std::istreambuf_iterator())); process_exif(image, data.data(), data.size()); ASSERT_EQ(list_size(&image->info->list), static_cast(7)); size_t i = 0; list_for_each(image->info, const struct image_info, it) { const char* expect_key; const char* expect_val; switch (i) { case 0: expect_key = "DateTime"; expect_val = "2024:07:06 12:31:44"; break; case 1: expect_key = "Camera"; expect_val = "Google"; break; case 2: expect_key = "Model"; expect_val = "Pixel 7"; break; case 3: expect_key = "Software"; expect_val = "GIMP 2.99.16"; break; case 4: expect_key = "Exposure"; expect_val = "1/50 sec."; break; case 5: expect_key = "F Number"; expect_val = "f/1.9"; break; case 6: expect_key = "Location"; expect_val = "55°44'28.41\"N, 37°37'25.46\"E"; break; default: GTEST_FAIL(); break; } EXPECT_STREQ(it->key, expect_key); EXPECT_STREQ(it->value, expect_val); ++i; } } TEST_F(Exif, Fail) { process_exif(image, nullptr, 0); EXPECT_EQ(list_size(&image->info->list), static_cast(0)); process_exif(image, reinterpret_cast("abcd"), 4); EXPECT_EQ(list_size(&image->info->list), static_cast(0)); } swayimg-3.8/test/image_test.cpp000066400000000000000000000032671474536441700166770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "image.h" } #include class Image : public ::testing::Test { protected: void SetUp() override { image = image_alloc(); ASSERT_TRUE(image); } void TearDown() override { image_free(image); } struct image* image; }; TEST_F(Image, SetSourcePathLong) { image_set_source(image, "/home/user/image.jpg"); EXPECT_STREQ(image->source, "/home/user/image.jpg"); EXPECT_STREQ(image->name, "image.jpg"); EXPECT_STREQ(image->parent_dir, "user"); } TEST_F(Image, SetSourcePathShort) { image_set_source(image, "/image.jpg"); EXPECT_STREQ(image->source, "/image.jpg"); EXPECT_STREQ(image->name, "image.jpg"); EXPECT_STREQ(image->parent_dir, ""); } TEST_F(Image, SetSourcePathNameOnly) { image_set_source(image, "image.jpg"); EXPECT_STREQ(image->source, "image.jpg"); EXPECT_STREQ(image->name, "image.jpg"); EXPECT_STREQ(image->parent_dir, ""); } TEST_F(Image, SetSourcePathRelative) { image_set_source(image, "user/image.jpg"); EXPECT_STREQ(image->source, "user/image.jpg"); EXPECT_STREQ(image->name, "image.jpg"); EXPECT_STREQ(image->parent_dir, "user"); } TEST_F(Image, SetSourceStdin) { image_set_source(image, LDRSRC_STDIN); EXPECT_STREQ(image->source, LDRSRC_STDIN); EXPECT_STREQ(image->name, LDRSRC_STDIN); EXPECT_STREQ(image->parent_dir, ""); } TEST_F(Image, SetSourceExec) { const char* src = LDRSRC_EXEC "cat image.txt"; image_set_source(image, src); EXPECT_STREQ(image->source, src); EXPECT_STREQ(image->name, src); EXPECT_STREQ(image->parent_dir, ""); } swayimg-3.8/test/imagelist_test.cpp000066400000000000000000000226231474536441700175700ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "imagelist.h" } #include class ImageList : public ::testing::Test { protected: void SetUp() override { unsetenv("XDG_CONFIG_HOME"); unsetenv("XDG_CONFIG_DIRS"); unsetenv("HOME"); config = config_load(); ASSERT_TRUE(config); } void TearDown() override { image_list_destroy(); config_free(config); } struct config* config; }; TEST_F(ImageList, Add) { image_list_init(config); ASSERT_EQ(image_list_size(), static_cast(0)); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_size(), static_cast(3)); } TEST_F(ImageList, Find) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); const size_t idx = image_list_find("exec://cmd2"); ASSERT_EQ(idx, static_cast(1)); } TEST_F(ImageList, Skip) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_skip(2), static_cast(1)); ASSERT_EQ(image_list_skip(0), static_cast(1)); ASSERT_EQ(image_list_skip(1), static_cast(IMGLIST_INVALID)); } TEST_F(ImageList, NearestFwdNoLoop) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_nearest(IMGLIST_INVALID, true, false), static_cast(0)); ASSERT_EQ(image_list_nearest(0, true, false), static_cast(1)); ASSERT_EQ(image_list_nearest(1, true, false), static_cast(2)); ASSERT_EQ(image_list_nearest(2, true, false), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_nearest(42, true, false), static_cast(IMGLIST_INVALID)); image_list_skip(1); ASSERT_EQ(image_list_nearest(0, true, false), static_cast(2)); ASSERT_EQ(image_list_nearest(1, true, false), static_cast(2)); } TEST_F(ImageList, NearestFwdLoop) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_nearest(IMGLIST_INVALID, true, true), static_cast(0)); ASSERT_EQ(image_list_nearest(0, true, true), static_cast(1)); ASSERT_EQ(image_list_nearest(1, true, true), static_cast(2)); ASSERT_EQ(image_list_nearest(2, true, true), static_cast(0)); ASSERT_EQ(image_list_nearest(42, true, true), static_cast(0)); image_list_skip(0); ASSERT_EQ(image_list_nearest(0, true, true), static_cast(1)); ASSERT_EQ(image_list_nearest(2, true, true), static_cast(1)); } TEST_F(ImageList, NearestBackNoLoop) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_nearest(IMGLIST_INVALID, false, false), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_nearest(0, false, false), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_nearest(1, false, false), static_cast(0)); ASSERT_EQ(image_list_nearest(2, false, false), static_cast(1)); ASSERT_EQ(image_list_nearest(42, false, false), static_cast(2)); image_list_skip(1); ASSERT_EQ(image_list_nearest(2, false, false), static_cast(0)); ASSERT_EQ(image_list_nearest(1, false, false), static_cast(0)); } TEST_F(ImageList, NearestBackLoop) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_nearest(IMGLIST_INVALID, false, true), static_cast(2)); ASSERT_EQ(image_list_nearest(0, false, true), static_cast(2)); ASSERT_EQ(image_list_nearest(1, false, true), static_cast(0)); ASSERT_EQ(image_list_nearest(2, false, true), static_cast(1)); ASSERT_EQ(image_list_nearest(42, false, true), static_cast(2)); image_list_skip(1); ASSERT_EQ(image_list_nearest(2, false, true), static_cast(0)); ASSERT_EQ(image_list_nearest(1, false, true), static_cast(0)); } TEST_F(ImageList, JumpFwd) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_jump(IMGLIST_INVALID, 1, true), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_jump(42, 1, true), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_jump(0, 42, true), static_cast(2)); ASSERT_EQ(image_list_jump(0, 0, true), static_cast(0)); ASSERT_EQ(image_list_jump(0, 1, true), static_cast(1)); ASSERT_EQ(image_list_jump(0, 2, true), static_cast(2)); ASSERT_EQ(image_list_jump(0, 42, true), static_cast(2)); ASSERT_EQ(image_list_jump(2, 1, true), static_cast(2)); image_list_skip(1); ASSERT_EQ(image_list_jump(0, 1, true), static_cast(2)); } TEST_F(ImageList, JumpBack) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_jump(IMGLIST_INVALID, 1, false), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_jump(42, 1, false), static_cast(IMGLIST_INVALID)); ASSERT_EQ(image_list_jump(2, 42, false), static_cast(0)); ASSERT_EQ(image_list_jump(2, 0, false), static_cast(2)); ASSERT_EQ(image_list_jump(2, 1, false), static_cast(1)); ASSERT_EQ(image_list_jump(2, 2, false), static_cast(0)); ASSERT_EQ(image_list_jump(2, 42, false), static_cast(0)); ASSERT_EQ(image_list_jump(0, 1, false), static_cast(0)); image_list_skip(1); ASSERT_EQ(image_list_jump(2, 1, false), static_cast(0)); } TEST_F(ImageList, Distance) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_distance(IMGLIST_INVALID, IMGLIST_INVALID), static_cast(2)); ASSERT_EQ(image_list_distance(0, IMGLIST_INVALID), static_cast(2)); ASSERT_EQ(image_list_distance(IMGLIST_INVALID, 2), static_cast(2)); ASSERT_EQ(image_list_distance(0, 0), static_cast(0)); ASSERT_EQ(image_list_distance(0, 1), static_cast(1)); ASSERT_EQ(image_list_distance(0, 2), static_cast(2)); ASSERT_EQ(image_list_distance(2, 1), static_cast(1)); ASSERT_EQ(image_list_distance(2, 0), static_cast(2)); ASSERT_EQ(image_list_distance(2, 2), static_cast(0)); image_list_skip(1); ASSERT_EQ(image_list_distance(0, 2), static_cast(1)); ASSERT_EQ(image_list_distance(2, 0), static_cast(1)); } TEST_F(ImageList, Get) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); const char* src = image_list_get(1); ASSERT_STREQ(src, "exec://cmd2"); } TEST_F(ImageList, GetFirst) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_first(), static_cast(0)); } TEST_F(ImageList, GetLast) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_last(), static_cast(2)); } TEST_F(ImageList, GetNextFile) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_next_file(0), static_cast(1)); ASSERT_EQ(image_list_next_file(1), static_cast(2)); ASSERT_EQ(image_list_next_file(2), static_cast(0)); } TEST_F(ImageList, GePrevFile) { image_list_init(config); image_list_add("exec://cmd1"); image_list_add("exec://cmd2"); image_list_add("exec://cmd3"); ASSERT_EQ(image_list_prev_file(0), static_cast(2)); ASSERT_EQ(image_list_prev_file(1), static_cast(0)); } TEST_F(ImageList, GetNextDir) { image_list_init(config); image_list_add("exec://cmd1/dir1/image1"); image_list_add("exec://cmd1/dir1/image2"); image_list_add("exec://cmd1/dir2/image3"); image_list_add("exec://cmd1/dir2/image4"); image_list_add("exec://cmd1/dir3/image5"); ASSERT_EQ(image_list_next_dir(0), static_cast(2)); ASSERT_EQ(image_list_next_dir(2), static_cast(4)); ASSERT_EQ(image_list_next_dir(4), static_cast(0)); } TEST_F(ImageList, GetPrevDir) { image_list_init(config); image_list_add("exec://cmd1/dir1/image1"); image_list_add("exec://cmd1/dir1/image2"); image_list_add("exec://cmd1/dir2/image3"); image_list_add("exec://cmd1/dir2/image4"); image_list_add("exec://cmd1/dir3/image5"); ASSERT_EQ(image_list_prev_dir(0), static_cast(4)); ASSERT_EQ(image_list_prev_dir(2), static_cast(1)); ASSERT_EQ(image_list_prev_dir(3), static_cast(1)); } swayimg-3.8/test/keybind_test.cpp000066400000000000000000000063031474536441700172340ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "config.h" #include "keybind.h" } #include class Keybind : public ::testing::Test { protected: void SetUp() override { unsetenv("XDG_CONFIG_HOME"); unsetenv("XDG_CONFIG_DIRS"); unsetenv("HOME"); config = config_load(); } void TearDown() override { keybind_destroy(); config_free(config); } struct config* config; }; TEST_F(Keybind, Add) { config_set(config, CFG_KEYS_VIEWER, "a", "exit"); keybind_init(config); const struct keybind* kb = keybind_find('a', 0); ASSERT_NE(kb, nullptr); EXPECT_EQ(kb->key, static_cast('a')); EXPECT_EQ(kb->mods, static_cast(0)); ASSERT_EQ(kb->actions.num, static_cast(1)); EXPECT_EQ(kb->actions.sequence[0].type, action_exit); EXPECT_STREQ(kb->help, "a: exit"); } TEST_F(Keybind, Replace) { config_set(config, CFG_KEYS_VIEWER, "Escape", "info"); keybind_init(config); const struct keybind* kb = keybind_find(XKB_KEY_Escape, 0); ASSERT_NE(kb, nullptr); EXPECT_EQ(kb->key, static_cast(XKB_KEY_Escape)); EXPECT_EQ(kb->mods, static_cast(0)); ASSERT_EQ(kb->actions.num, static_cast(1)); EXPECT_EQ(kb->actions.sequence[0].type, action_info); } TEST_F(Keybind, Mods) { config_set(config, CFG_KEYS_VIEWER, "Ctrl+a", "exit"); config_set(config, CFG_KEYS_VIEWER, "Alt+b", "exit"); config_set(config, CFG_KEYS_VIEWER, "Shift+c", "exit"); config_set(config, CFG_KEYS_VIEWER, "Alt+Ctrl+d", "exit"); config_set(config, CFG_KEYS_VIEWER, "Ctrl+Shift+Alt+e", "exit"); keybind_init(config); EXPECT_NE(keybind_find('a', KEYMOD_CTRL), nullptr); EXPECT_NE(keybind_find('b', KEYMOD_ALT), nullptr); EXPECT_NE(keybind_find('c', KEYMOD_SHIFT), nullptr); EXPECT_NE(keybind_find('d', KEYMOD_CTRL | KEYMOD_ALT), nullptr); EXPECT_NE(keybind_find('e', KEYMOD_CTRL | KEYMOD_ALT | KEYMOD_SHIFT), nullptr); } TEST_F(Keybind, ActionParams) { config_set(config, CFG_KEYS_VIEWER, "a", "status \t params 1 2 3\t"); keybind_init(config); const struct keybind* kb = keybind_find('a', 0); ASSERT_NE(kb, nullptr); EXPECT_EQ(kb->key, static_cast('a')); EXPECT_EQ(kb->mods, static_cast(0)); ASSERT_EQ(kb->actions.num, static_cast(1)); EXPECT_EQ(kb->actions.sequence[0].type, action_status); EXPECT_STREQ(kb->actions.sequence[0].params, "params 1 2 3"); EXPECT_STREQ(kb->help, "a: status params 1 2 3"); } TEST_F(Keybind, Multiaction) { config_set(config, CFG_KEYS_VIEWER, "a", "exec cmd;reload;exit"); keybind_init(config); const struct keybind* kb = keybind_find('a', 0); ASSERT_NE(kb, nullptr); EXPECT_EQ(kb->key, static_cast('a')); EXPECT_EQ(kb->mods, static_cast(0)); ASSERT_EQ(kb->actions.num, static_cast(3)); EXPECT_EQ(kb->actions.sequence[0].type, action_exec); EXPECT_EQ(kb->actions.sequence[1].type, action_reload); EXPECT_EQ(kb->actions.sequence[2].type, action_exit); EXPECT_STREQ(kb->help, "a: exec cmd; ..."); } swayimg-3.8/test/loader_test.cpp000066400000000000000000000041211474536441700170510ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "application.h" #include "buildcfg.h" #include "loader.h" #include "ui.h" #include "viewer.h" } #include // stubs for linker (application and ui are not included to tests) extern "C" { void app_watch(int, fd_callback, void*) { } void app_reload() { } void app_redraw() { } void app_on_resize() { } void app_on_keyboard(xkb_keysym_t, uint8_t) { } void app_on_drag(int, int) { } void app_exit(int) { } void app_on_load(struct image*, size_t) { } bool app_is_viewer() { return true; } } class Loader : public ::testing::Test { protected: void TearDown() override { image_free(image); } void Load(const char* file) { EXPECT_EQ(loader_from_source(file, &image), ldr_success); ASSERT_NE(image, nullptr); EXPECT_NE(image->frames[0].pm.width, static_cast(0)); EXPECT_NE(image->frames[0].pm.height, static_cast(0)); EXPECT_NE(image->frames[0].pm.data[0], static_cast(0)); } struct image* image = nullptr; }; TEST_F(Loader, External) { const enum loader_status status = loader_from_source( LDRSRC_EXEC "cat " TEST_DATA_DIR "/image.bmp", &image); EXPECT_EQ(status, ldr_success); ASSERT_NE(image, nullptr); } #define TEST_LOADER(n) \ TEST_F(Loader, n) \ { \ Load(TEST_DATA_DIR "/image." #n); \ } TEST_LOADER(bmp); TEST_LOADER(dcm); TEST_LOADER(ff); TEST_LOADER(pnm); TEST_LOADER(qoi); TEST_LOADER(tga); #ifdef HAVE_LIBEXR // TEST_LOADER(exr); #endif #ifdef HAVE_LIBGIF TEST_LOADER(gif); #endif #ifdef HAVE_LIBHEIF TEST_LOADER(heif); #endif #ifdef HAVE_LIBAVIF TEST_LOADER(avif); #endif #ifdef HAVE_LIBJPEG TEST_LOADER(jpg); #endif #ifdef HAVE_LIBJXL TEST_LOADER(jxl); #endif #ifdef HAVE_LIBPNG TEST_LOADER(png); #endif #ifdef HAVE_LIBRSVG TEST_LOADER(svg); #endif #ifdef HAVE_LIBTIFF TEST_LOADER(tiff); #endif #ifdef HAVE_LIBSIXEL TEST_LOADER(six); #endif #ifdef HAVE_LIBWEBP TEST_LOADER(webp); #endif swayimg-3.8/test/memdata_test.cpp000066400000000000000000000116771474536441700172310ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "memdata.h" } #include #include TEST(List, Add) { struct list entry[2]; struct list* head = NULL; memset(entry, 0xff, sizeof(entry)); // poison head = list_add_head(head, &entry[0]); ASSERT_EQ(head, &entry[0]); ASSERT_EQ(head->next, nullptr); ASSERT_EQ(head->prev, nullptr); ASSERT_TRUE(list_is_last(head)); ASSERT_EQ(list_size(head), static_cast(1)); head = list_add_head(head, &entry[1]); ASSERT_EQ(head, &entry[1]); ASSERT_EQ(head->next, &entry[0]); ASSERT_EQ(head->prev, nullptr); ASSERT_FALSE(list_is_last(head)); ASSERT_EQ(entry[0].next, nullptr); ASSERT_EQ(entry[0].prev, &entry[1]); ASSERT_EQ(list_size(head), static_cast(2)); } TEST(List, Append) { struct list entry[2]; struct list* head = NULL; memset(entry, 0xff, sizeof(entry)); // poison head = list_append_tail(head, &entry[0]); ASSERT_EQ(head, &entry[0]); ASSERT_EQ(head->next, nullptr); ASSERT_EQ(head->prev, nullptr); head = list_append_tail(head, &entry[1]); ASSERT_EQ(head, &entry[0]); ASSERT_EQ(head->next, &entry[1]); ASSERT_EQ(head->prev, nullptr); ASSERT_EQ(entry[1].next, nullptr); ASSERT_EQ(entry[1].prev, &entry[0]); } TEST(List, Remove) { struct list entry[3]; struct list* head = NULL; memset(entry, 0xff, sizeof(entry)); // poison for (auto& it : entry) { head = list_add_head(head, &it); } head = list_remove_entry(&entry[1]); ASSERT_EQ(head, &entry[2]); ASSERT_EQ(head->next, &entry[0]); ASSERT_EQ(head->prev, nullptr); ASSERT_EQ(entry[0].next, nullptr); ASSERT_EQ(entry[0].prev, &entry[2]); head = list_remove_entry(&entry[0]); ASSERT_EQ(head, &entry[2]); ASSERT_EQ(head->next, nullptr); ASSERT_EQ(head->prev, nullptr); head = list_remove_entry(&entry[2]); ASSERT_EQ(head, nullptr); } TEST(List, ForEach) { struct list entry[3]; struct list* head = NULL; for (auto& it : entry) { head = list_add_head(head, &it); } size_t i = 0; list_for_each(head, struct list, it) { switch (i) { case 0: ASSERT_EQ(it, &entry[2]); break; case 1: ASSERT_EQ(it, &entry[1]); break; case 2: ASSERT_EQ(it, &entry[0]); break; default: GTEST_FAIL(); } ++i; } ASSERT_EQ(list_size(head), static_cast(3)); ASSERT_EQ(i, static_cast(3)); } TEST(Str, Duplicate) { char* str = str_dup("Test123", NULL); ASSERT_NE(str, nullptr); EXPECT_STREQ(str, "Test123"); EXPECT_NE(str_dup("NewTest123", &str), nullptr); EXPECT_NE(str, nullptr); EXPECT_STREQ(str, "NewTest123"); free(str); } TEST(Str, Append) { char* str = str_dup("Test", NULL); ASSERT_NE(str, nullptr); EXPECT_NE(str_append("123", 0, &str), nullptr); EXPECT_STREQ(str, "Test123"); EXPECT_NE(str_append("ABCD", 2, &str), nullptr); EXPECT_STREQ(str, "Test123AB"); free(str); } TEST(Str, ToNum) { ssize_t num; ASSERT_TRUE(str_to_num("1234", 0, &num, 0)); ASSERT_EQ(num, 1234); ASSERT_TRUE(str_to_num("1234", 2, &num, 0)); ASSERT_EQ(num, 12); ASSERT_TRUE(str_to_num("0x1234", 0, &num, 0)); ASSERT_EQ(num, 0x1234); ASSERT_TRUE(str_to_num("1234", 0, &num, 16)); ASSERT_EQ(num, 0x1234); } TEST(Str, ToWide) { wchar_t* str = str_to_wide("Test", NULL); ASSERT_NE(str, nullptr); EXPECT_STREQ(str, L"Test"); EXPECT_NE(str_to_wide("NewTest123", &str), nullptr); EXPECT_NE(str, nullptr); EXPECT_STREQ(str, L"NewTest123"); free(str); } TEST(Str, Split) { struct str_slice slices[4]; ASSERT_EQ(str_split("a,bc,def", ',', slices, ARRAY_SIZE(slices)), static_cast(3)); ASSERT_EQ(slices[0].len, static_cast(1)); ASSERT_EQ(strncmp(slices[0].value, "a", slices[0].len), 0); ASSERT_EQ(slices[1].len, static_cast(2)); ASSERT_EQ(strncmp(slices[1].value, "bc", slices[1].len), 0); ASSERT_EQ(slices[2].len, static_cast(3)); ASSERT_EQ(strncmp(slices[2].value, "def", slices[2].len), 0); ASSERT_EQ(str_split("", ';', slices, ARRAY_SIZE(slices)), static_cast(0)); ASSERT_EQ(str_split("a", ';', slices, ARRAY_SIZE(slices)), static_cast(1)); ASSERT_EQ(str_split("a;b;c;", ';', slices, ARRAY_SIZE(slices)), static_cast(3)); ASSERT_EQ(str_split("a,b,c,d,e,f", ',', slices, ARRAY_SIZE(slices)), static_cast(6)); } TEST(Str, SearchIndex) { const char* array[] = { "param1", "param2", "param3" }; ASSERT_EQ(str_index(array, "param2", 0), 1); ASSERT_EQ(str_index(array, "param22", 0), -1); ASSERT_EQ(str_index(array, "param22", 6), 1); } swayimg-3.8/test/meson.build000066400000000000000000000034721474536441700162120ustar00rootroot00000000000000# Rules for building tests sources = [ 'action_test.cpp', 'config_test.cpp', 'image_test.cpp', 'imagelist_test.cpp', 'keybind_test.cpp', 'loader_test.cpp', 'memdata_test.cpp', 'pixmap_test.cpp', '../src/action.c', '../src/config.c', '../src/event.c', '../src/image.c', '../src/imagelist.c', '../src/keybind.c', '../src/loader.c', '../src/memdata.c', '../src/pixmap.c', '../src/pixmap_scale.c', '../src/formats/bmp.c', '../src/formats/dicom.c', '../src/formats/farbfeld.c', '../src/formats/pnm.c', '../src/formats/qoi.c', '../src/formats/tga.c', ] if exif.found() sources += ['exif_test.cpp', '../src/exif.c'] endif if exr.found() sources += '../src/formats/exr.c' endif if gif.found() sources += '../src/formats/gif.c' endif if heif.found() sources += '../src/formats/heif.c' endif if avif.found() sources += '../src/formats/avif.c' endif if jpeg.found() sources += '../src/formats/jpeg.c' endif if jxl.found() sources += '../src/formats/jxl.c' endif if png.found() sources += '../src/formats/png.c' endif if rsvg.found() sources += '../src/formats/svg.c' endif if tiff.found() sources += '../src/formats/tiff.c' endif if sixel.found() sources += '../src/formats/sixel.c' endif if webp.found() and webp_demux.found() sources += '../src/formats/webp.c' endif test( 'swayimg', executable( 'swayimg_test', sources, dependencies: [ dependency('gtest', main: true, disabler: true, required: true), xkb, exif, exr, gif, heif, inotify, avif, jpeg, jxl, png, rsvg, tiff, sixel, webp, webp_demux, ], include_directories: '../src', cpp_args : '-DTEST_DATA_DIR="' + meson.current_source_dir() + '/data"', ) ) configure_file(output: 'buildcfg.h', configuration: conf) swayimg-3.8/test/pixmap_test.cpp000066400000000000000000000400541474536441700171060ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2024 Artem Senichev extern "C" { #include "pixmap.h" #include "pixmap_scale.h" } #include class Pixmap : public ::testing::Test { protected: void Compare(const struct pixmap& pm, const argb_t* expect) const { for (size_t y = 0; y < pm.height; ++y) { for (size_t x = 0; x < pm.width; ++x) { char expected[32], real[32]; snprintf(expected, sizeof(expected), "y=%ld,x=%ld,c=%08x", y, x, expect[y * pm.height + x]); snprintf(real, sizeof(real), "y=%ld,x=%ld,c=%08x", y, x, pm.data[y * pm.height + x]); EXPECT_STREQ(expected, real); } } } void ScaleCopy(enum pixmap_aa_mode scaler, const struct pixmap& src, size_t w, size_t h, float scale, ssize_t x, ssize_t y) { struct pixmap full, dst1, dst2; pixmap_create(&full, src.width * scale, src.height * scale); pixmap_create(&dst1, w, h); pixmap_create(&dst2, w, h); pixmap_scale(scaler, &src, &full, 0, 0, scale, false); pixmap_copy(&full, &dst1, x, y, false); pixmap_scale(scaler, &src, &dst2, x, y, scale, false); Compare(dst2, dst1.data); pixmap_free(&full); pixmap_free(&dst1); pixmap_free(&dst2); } }; TEST_F(Pixmap, Create) { struct pixmap pm; ASSERT_TRUE(pixmap_create(&pm, 123, 456)); EXPECT_NE(pm.data, nullptr); EXPECT_EQ(pm.data[0], static_cast(0)); EXPECT_EQ(pm.width, static_cast(123)); EXPECT_EQ(pm.height, static_cast(456)); pixmap_free(&pm); } TEST_F(Pixmap, Fill) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, 0x02, 0x03, 0x10, clr, clr, 0x13, 0x20, clr, clr, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_fill(&pm, 1, 1, 2, 2, clr); Compare(pm, expect); } TEST_F(Pixmap, FillOutsideTL) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { clr, clr, 0x02, 0x03, clr, clr, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_fill(&pm, -2, -2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, FillOutsideBR) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, clr, clr, 0x30, 0x31, clr, clr, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_fill(&pm, 2, 2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, InverseFill) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { clr, clr, clr, clr, clr, 0x11, 0x12, clr, clr, 0x21, 0x22, clr, clr, clr, clr, clr, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_inverse_fill(&pm, 1, 1, 2, 2, clr); Compare(pm, expect); } TEST_F(Pixmap, InverseOutsideTL) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, clr, clr, 0x10, 0x11, clr, clr, clr, clr, clr, clr, clr, clr, clr, clr, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_inverse_fill(&pm, -2, -2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, InverseOutsideBR) { const argb_t clr = 0x12345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { clr, clr, clr, clr, clr, clr, clr, clr, clr, clr, 0x22, 0x23, clr, clr, 0x32, 0x33, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_inverse_fill(&pm, 2, 2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, Grid) { const argb_t clr1 = 0x12345678; const argb_t clr2 = 0x87654321; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { clr2, clr2, clr1, clr1, clr2, clr2, clr1, clr1, clr1, clr1, clr2, clr2, clr1, clr1, clr2, clr2, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_grid(&pm, -10, -10, 20, 20, 2, clr1, clr2); Compare(pm, expect); } TEST_F(Pixmap, Mask) { const argb_t clr = 0xffaaaaaa; // clang-format off argb_t src[] = { 0xdddddddd, 0xcccccccc, 0xbbbbbbbb, 0xaaaaaaaa, 0x11111111, 0xff000000, 0x80000000, 0x22222222, 0x33333333, 0xaa111111, 0x00000000, 0x44444444, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, }; const uint8_t mask[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, }; const argb_t expect[] = { 0xdddddddd, 0xcccccccc, 0xbbbbbbbb, 0xaaaaaaaa, 0x11111111, 0xffaaaaaa, 0xffaaaaaa, 0x22222222, 0x33333333, 0xd46d6d6d, 0x40aaaaaa, 0x44444444, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_apply_mask(&pm, 0, 0, mask, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, MaskOutsideTL) { const argb_t clr = 0xffaaaaaa; // clang-format off argb_t src[] = { 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, }; const uint8_t mask[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, }; const argb_t expect[] = { 0xff000000, 0xffaaaaaa, 0xff000000, 0xff000000, 0xffaaaaaa, 0xffaaaaaa, 0x00000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_apply_mask(&pm, -2, -2, mask, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, MaskOutsideBR) { const argb_t clr = 0xffaaaaaa; // clang-format off argb_t src[] = { 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, }; const uint8_t mask[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, }; const argb_t expect[] = { 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000, 0x00000000, 0xff000000, 0xff000000, 0x00000000, 0xffaaaaaa, 0xffaaaaaa, 0xff000000, 0xff000000, 0xffaaaaaa, 0xff000000, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_apply_mask(&pm, 2, 2, mask, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, Copy) { // clang-format off argb_t src[] = { 0xaa, 0xbb, 0xcc, 0xdd, }; argb_t dst[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0xaa, 0xbb, 0x13, 0x20, 0xcc, 0xdd, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, 1, 1, false); Compare(pm_dst, expect); } TEST_F(Pixmap, CopyOutsideTL) { // clang-format off argb_t src[] = { 0xaa, 0xbb, 0xcc, 0xdd, }; argb_t dst[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0xdd, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, -1, -1, false); Compare(pm_dst, expect); } TEST_F(Pixmap, CopyOutsideBR) { // clang-format off argb_t src[] = { 0xaa, 0xbb, 0xcc, 0xdd, }; argb_t dst[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0xaa, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, 3, 3, false); Compare(pm_dst, expect); } TEST_F(Pixmap, CopyAlpha) { // clang-format off argb_t src[] = { 0xffaaaaaa, 0x80aaaaaa, 0x40aaaaaa, 0x00aaaaaa, }; argb_t dst[] = { 0x00000000, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffffffff, }; const argb_t expect[] = { 0x00000000, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0xffaaaaaa, 0xb2969696, 0x77777777, 0x88888888, 0xb29f9f9f, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffffffff, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, 1, 1, true); Compare(pm_dst, expect); } TEST_F(Pixmap, CopyAlphaOutsideTL) { // clang-format off argb_t src[] = { 0x00aaaaaa, 0x40bbbbbb, 0x80cccccc, 0xffdddddd, }; argb_t dst[] = { 0x00000000, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffffffff, }; const argb_t expect[] = { 0xffdddddd, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffffffff, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, -1, -1, false); Compare(pm_dst, expect); } TEST_F(Pixmap, CopyAlphaOutsideBL) { // clang-format off argb_t src[] = { 0xffaaaaaa, 0x80aaaaaa, 0x40aaaaaa, 0x00aaaaaa, }; argb_t dst[] = { 0x00000000, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffffffff, }; const argb_t expect[] = { 0x00000000, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0xeeeeeeee, 0xffaaaaaa, }; // clang-format on const struct pixmap pm_src = { 2, 2, src }; struct pixmap pm_dst = { 4, 4, dst }; pixmap_copy(&pm_src, &pm_dst, 3, 3, true); Compare(pm_dst, expect); } TEST_F(Pixmap, Rect) { const argb_t clr = 0xff345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { clr, clr, clr, clr, clr, 0x11, 0x12, clr, clr, 0x21, 0x22, clr, clr, clr, clr, clr, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_rect(&pm, 0, 0, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, RectOutsideTL) { const argb_t clr = 0xff345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, clr, 0x02, 0x03, clr, clr, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_rect(&pm, -2, -2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, RectOutsideBR) { const argb_t clr = 0xff345678; // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; const argb_t expect[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, clr, clr, 0x30, 0x31, clr, 0x33, }; // clang-format on struct pixmap pm = { 4, 4, src }; pixmap_rect(&pm, 2, 2, 4, 4, clr); Compare(pm, expect); } TEST_F(Pixmap, ScaleCopyUp) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 2.0, 0, 0); } TEST_F(Pixmap, ScaleCopyUpNeg) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 2.0, -1, -1); } TEST_F(Pixmap, ScaleCopyUpPos) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 2.0, 1, 1); } TEST_F(Pixmap, ScaleCopyDown) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 0.5, 0, 0); } TEST_F(Pixmap, ScaleCopyDownNeg) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 0.5, -1, -1); } TEST_F(Pixmap, ScaleCopyDownPos) { // clang-format off argb_t src[] = { 0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33, }; // clang-format on const struct pixmap pm = { 4, 4, src }; ScaleCopy(pixmap_bilinear, pm, 2, 2, 0.5, 1, 1); }