From a2e50ca9a02f6063f3a14850e1fc0c987b780386 Mon Sep 17 00:00:00 2001 From: Stephen Baynham Date: Wed, 13 Nov 2024 19:44:44 -0800 Subject: [PATCH 1/4] add sixels --- examples/go.mod | 3 +- examples/go.sum | 7 + examples/sixels/LICENSE.md | 7 + examples/sixels/figure.png | Bin 0 -> 85832 bytes examples/sixels/main.go | 56 ++++++ examples/sixels/snake.jpg | Bin 0 -> 39948 bytes go.mod | 5 +- go.sum | 13 +- sixel.go | 235 +++++++++++++++++++++++++ sixel_palette.go | 346 +++++++++++++++++++++++++++++++++++++ style.go | 85 +++++++++ 11 files changed, 750 insertions(+), 7 deletions(-) create mode 100644 examples/sixels/LICENSE.md create mode 100644 examples/sixels/figure.png create mode 100644 examples/sixels/main.go create mode 100644 examples/sixels/snake.jpg create mode 100644 sixel.go create mode 100644 sixel_palette.go diff --git a/examples/go.mod b/examples/go.mod index a878cbb0..8b11d53f 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -18,11 +18,12 @@ require ( require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bits-and-blooms/bitset v1.14.3 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/keygen v0.5.0 // indirect github.com/charmbracelet/lipgloss v0.13.1-0.20240822211938-b89f1a3db2a4 // indirect github.com/charmbracelet/log v0.4.0 // indirect - github.com/charmbracelet/x/ansi v0.4.2 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect github.com/charmbracelet/x/input v0.2.0 // indirect diff --git a/examples/go.sum b/examples/go.sum index 86f3ffff..1b07d9f4 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,8 +1,11 @@ +github.com/CannibalVox/bitset v1.0.0 h1:fXgciTVIjZFPsV1Amr6mhkYcHBg0aQhYYMQZ3E2J27M= +github.com/CannibalVox/bitset v1.0.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241031200731-4f70d4c680b8 h1:qm5hqKutTe3DcJHlEghLlZxBoKv+hqa8avqEUiSKIZY= @@ -21,6 +24,8 @@ github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvom github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk= github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI= github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= @@ -37,6 +42,8 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= diff --git a/examples/sixels/LICENSE.md b/examples/sixels/LICENSE.md new file mode 100644 index 00000000..692cbe70 --- /dev/null +++ b/examples/sixels/LICENSE.md @@ -0,0 +1,7 @@ +[figure.png](https://commons.wikimedia.org/wiki/File:3D_chess_moves_basic_dodeca_(color).png) was + created by Tilman Piesk for the Wikipedia Commons and is licensed under the Creative Commons + Attribution 4.0 International. + +[snake.jpg](https://commons.wikimedia.org/wiki/File:Green,_yellow_snake.jpg) was created by "Whkoh" + for Wikipedia English and is released to the public domain. + diff --git a/examples/sixels/figure.png b/examples/sixels/figure.png new file mode 100644 index 0000000000000000000000000000000000000000..f8f651c8081e2ca275e78ff18406591dcefbab6f GIT binary patch literal 85832 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%0DKWj)$uzprdD4HI*W zl6dm@r%w3Oia%ZYeAns)u5d-fiU?DF_t__c!Qhc$=h8t~|WCcip@zV`1}iA?w070V;l<4~dAP>lR-JOX9UKcZ+p(^^82v;yD*)CN`BvfqLbk$ijO= z)f5~$A~;_VKpP3bw*c-~al#2>g8*Pb&a`bO%{#x21p+8(25e}nnvD`2l&Hxl?|~1U+e6mOdGCm75{Y^MfTHi0gCHr_cONJl@!kemdX>nOJWK z7D6tcGhf*F!t5tsdh*N-9UJF#WII~YUWU*7h@_hlCMGvABPQyNI=OR*7(}$xUh=#r zKmb3rv-V%NJ+zH4>K`^h4}Ba2@OgC&z`gzMyN^-=Anv+!TDaNTbia|IhTna!ztH^H4<6phb?h;Ll`Efopr@zrtHVhEhY1^ea3+-f>vS0b_k&W|D!r(_?Mh1|NGEZ?hhEo8>2xX%g%bvGc*UX}fmJfD6b`$`tzCQE zoaa`bVueFh1VHeYb@MkM0Hx>$6M)pvbU`^fgqbeO`7?j%5SB-a>bWQGMeYwj$QE8- zzlgv29x?8!D|k`Kc3qFR_R5!UAq?Zelgn2i78eR4t&S1e#CEbs;{|gNpNA~BplQ0h zXHWZr6)T>Z_uO-<#O=4=O$%VBZ$fblJx_$g##x?c-_YIN zTgTUZW8#vpt7~`=%6}QGTXgdwz44WDTe3`fXQ4=>646~9ZR5AKZ=Kqc>}?KP<^`~} z8v0XxeNSwE>N^FuxLGf0Ydpu@u=%%JdSwM8fM=dr_R(xM_eGxo?zsEzdw=R@4GV@! zU|$x`tzL0cI+gxa+pcy_xjuC3Ew_Kw&l)pWwd$$+AZXth>jW?q&jIX@C>S0bFGV6n z^S8f!P3+-EZVpExaw%`XH2}XfReM?S`{6*jQ=!YSEU(f6OQBq*>KXv!*f*DKc@R=a zs|D5TPYC_(ZxW$}rwhy}{pBp>Xth)vkF&C*Mfm5(2zQOPC27!NN|~5(i+DVqhJ}@c zaw+gIFbF-@En(J&Bj<5CZU#OMQIbdzgksCHXQB+NbHqPR5x)Zv9K(Rq!68*g)v+lH zpa=`VKI#d;(@eLicg3mk{v|gWrF4^_n`klKD$cg|gw5B&6m5|yxTSRHxF{)<) zt-RxoyU$dmMjq7!fb+cmo_p>c);A&_Q6nBKo_cD@9hg!6q^)f?hJ^R=JMOsWi+3Y8DjTIC+4-r#++puysL@Nh*%&; z5&=tNk{Dc6-`Aa4l{>UlIZMU~x&M8T2!{)g1Nk6IUObl<)a6BWgmF6Z`3`u9czl4c zWcy)^kI#2GbKd9*V_c8IrZX`A`B<<-Bvxo{o8mL}FD#L)|&oUALHhGU8aV;)#!DGFcG7 zZca|lg05WcXAKKRL{M;2m*jH!zu}}XP{uX^^z`(4)2Gk)RwNSLih($02q2%&zaFzx zQCC<0Z*_HbFOAr-S(aN`yLM*u2mf_n1X{_2Ql##YiV%NP`%p|}5*R%Dryp1V!R(&p zU=x5uWE<6S84ky7N28HdNbiN~=cXhIINtgT z-sD|d18(nSb8|_IRkE#oMaFn}pEwTX$zSQKl&W;Pfg2qnJl%9-$<5OzcJT>vHv&qR zL@$pFKeGRMt2nFx`N|j&0B;xXOZ19lSZ{mlQgh~v zUTAd`tt?#;gW^)*@)%{rUp~Mg$(Vl;c){AWYs7We-SqtY`SW+9y~Dgyiwc~`WYTrm z9Qxhh?Y&{l%;iOO$_PnBSr8_;$Ocau@=0~<)4(7$)SX!*24!)OPzIAoKpx7GFe*gC zW2aMOqFMw#tq>nt24QJM!0}e!G*1=h$rI-7y=Y!Z6nt`>WTAs3`G^G{I+Q>jmzdBc zl6?4#h_qe+c-HbLsm|STimr!Pks^FJhGqZz1R!4# z!{>zxumUm#k+X$}obty1oV?(Sl@8k5uXDJ76CFDQkWOd6*xtU|_6gwVw*a1he#ObD zRBFZ8CV(eF0R7+S=;#CiO#N14W78NVfO-%>{R?QvkvTSuXlNSG{mysa64|olM1H1G zUI>o%3dC84qR4u=(jt`|eg65r0u=|ga~$DJ z7@v*2_05m#(P$BgL4kvY+$8{o%e#Q&a{w>C_=33g);pi!T|k(5514II;rcdWPEw$3VNE_0!Z??+(tex|7V^3VtPM#L z$SZ*~1TV59&&wkW%S1i7P7*;`bSK(G7@1IV`^53P|K|f?vn0O`NWzuZd7$v6oXEIR z{=q9__>xFk0j`r1;c2gaX#6?1{?W4w!#&wS-;NCec=p-lA5En)#~J}V0|H1N6$H@P z+38K5Jk|-I4g@fA%Tro7Z0G*?$3^D<{r;A4BwBeG40(Z896$hmI8g420F(v5`}^rD z<*$f>^(f`S0q`L(ap}d|W3PMdbC^O7P}Bzt)PS2f=uZ z=#L=m*Bpc^tyG=I^7xmVl0fMEdic~iZ_7OyX;U$A;$434ntiR)8b{gjtT-dw}w%Lz+}BRX(Bi zfjWw|h`jC1%foZ$c0zj&1p%B00vOc?!FZlLH~2g<7ss^_N}aEQNVT+#`!W={ybH+B zmwg8cmMlhM>?rE>~r<*=CRkm#-|4$i#pQ3WaVxM+5C_qy*PR3;p|)c<@GE4Z!(s_{trm?PzZRxdFOP$=5xgFTPFe$!0AK;w z>KXvw8XylFa^jiso{5*-|8*-Ghoy1&?|V83;Cx8{dq4n55P$^&xN{T@8UgGLS^@q4^V8Rfzdd?+I0`F(in&U9s8FdjBmuxl0`Oh+?4M4);0=SH z%AWV@D1RUcuxfblD?1?pc=>|x=FjVozwz};HPckAbBL(kWULUtAdmpg@Co3cZUe0* z2BJESy8KzX1ZL_g5A|_LM>a6q1p6b2p!qfV>Ju(g`DL|4qQ4ns4AjM^)8mCgAr58l zIgTAgIlk_MiXYZXZk(K@ zAtOe-chaTze?3&!m;+l?1YzjGBYWrW-FxOGldmiSIIP*z;bA!Y@09_m5J#&SF}sXh zv}k#@E|$vKMZTg`6fk&;frC;nAdTzzM;!Nvs(7Oa~$Z{GS-Pd)AFbIv*EFAEng zeE96M&-uee7hU|r^Ugc}hi9F2_J5yq&UxQH^USlpamK>4zK%)MUo&|}z6`SbB|tdF z`GKfR-WKVc1E+jG4-hO9Igk?(YU;D14N(52VU>CkUNr{CM;5dMl|K>~F#;IXHa$p; zs)8Nk;GH(nsZ)Ez1?NAW%w$B?#lUvpe5EcGJQRK&N;**VB#0o7j%8^9KtV~zyLqH? z$HZ>{380MfRhW)TMn&vbJLl~7=yAvGKHR0B{h-$CKsABT?Vm*K{g8-f!V>UEAYsca zm73;kP322&77CshcDcS^c8F8yCo)CpxcpP@Z_Em?524pj-KJY1%*4kaKMy-b=A1{+ z0w8MR?8q>>?FSMtCtY*RW5p9rSes0x2a$jy^1P5oKT!HI4#kf$Bn5f#m-YCLcLK?o zJsk%>mA>!fb%QEr^CK{mTXPMUy=sldPeCH>aB!HL)o_p~lZR^?q4BfZ)7s=siA+|c z$}|0nOG5GYrwio^zYebr(8`0{6g|tSpK<#0sBnaJl;CrQuNaJY{5O^tmC%*ZnsEp%B7+ANg3^kp0lWI$go`9SkRF-XnN**zWOdntUsteW-n+5r?o zu_y|e@#!I+{S!4>7;1n7j8NFor(gU?cf$7i)4B3&KannqJ}CY~mNS1? zg;e}h`taq&;Cxz`aVLy^rliGW!uuF;Q~USqh&y)URL3DZk6h+~Q+Egj3g5q248>1Z zz|6m(2`_&4ozKcFQo?SF2&OdX#Gx3r??%dNYI{GrAE0Gu7$vkHN zwy>e>x#lmGJ{`y)lTC?I{qY;8oqN-Rv?TnhMi^Dky^SI;6owr&3t*q9>4TSpTP&K> zPCtF?+;_e6KN90wy1RONMSrm%l0FG=)=woKtmW_1fd+ywA4v`F5DFYGEax*>QHVsG zrhD#Q)qMJCJC1bbkKNQT!th+jIPT(`9*)dk{J-secD}Q_N2IfPxy&Q)0Kz1pK)K1J zMKYZb#<+#gFL=#c|2rCQ$&|byxAYExSDt{3XagS|7g>X=@HM;~?0nvuaq6kt&->WN z{>3(OL<4nYw_bZ zO*{XsKRWaJ_xyLPu_ZIYOF!)Ys)dSI&IGV#0aPI%r?F2`V*?H>fpJr(^u7ASANr5E z?|jEEz2>Gwu8$w+Ew27sS06hs;`#GGL`@!N0RIPYz5Sr(r$ zqjxBjKX&zUsP*_Tf+S!U3)a**$G2a0`+I(R=^H-%)eG+W(%KWQ`KXw-=#!O|bmm|3JU4;~NuwVhbyQ?^_~#`fN&p8Kgf$$6aG{(H%ks{; z>Bc1&{r#uD*?Pxqe<`)LcI8qjkxM4!8on);gk1H~ufQQ*L^|{H{Rxr7jNfi)E;QVD z!}bLad|=t^tFC;>s;fh>kzNKETi}>ERwg7aX7!G3oAvRQ^rY#t^UbX@M8o(gZp)-; zeS9*RBryu>{+#*qQ>}cRxP&{ZmjFig^haoLRW$$DC4t%`@NmJqgNpgQ)jV}---Y+y z`}p^)`^{Hh`Gh`uR$IX`bNOP?%l7n&9F;xEA(s?1c4d14r13zm;|7Yd7Xf7%ky(b9w6BA z&edUz>aj?SRCA070R$)GpWNuR?g3FFfg=Hww$5t<(ZGWN_s=eaRj}yu zpZnTrAOF}tpZ!n&*m2z7e_Twt?_JJ<4}WmQX`lGmqYK~n-X*8re%p%aXD-|kZEDIx zF&INV>pzIWRO|Rrz$ohcy-;ms@r8G<6pV&>J>vKE9vrHraSU_RQ2@@|?LxtHibczc zMxAlfr}xZSxN!62;}&F^X3P-r>C@c_^X7FoOqh_iA`#njJ)KKQBRJ!)p@uE@CpQldL#w=5K&{NQN!44dUtg1107L}I05X)Pjx1)~OMc|t9KT^~ha2PRW8~4~BH)HVx zt3RQ9Y-2<%fU&K_j=5=84K>to7~q%5#sWsd5EwBFV62*ZM+0hYcW5|__NujT4Q23E z<*yWsm<4cjn6k&byQV^fCPNk`c22juwnnqB?fgf@`|AhH8Z{ z=9EL{JKuT#C^CSC2I2Rz{pc`b1g`;%?SWrgW*I3AZ$lquKh}DGL^0OBx>wOnnx=R7 z!BK(hI#YtS8Hu}rM$`fr+q2jK%TzUmFg!pDk4YgI(~^1YVr&gFrKd*s<%FK+P4j~z z(l{eR0KBMDA+s?*)-*7$>4$3=QjEZqriQT&3@^{}Jhxftg0Z9zMqfNNO$)9-Qx6S$ z}3y( z!des9V-_`PtpOOhw{VUXjL;I$(blm0KiJVdkD8vU#xXot+0nuKsCbsg=$VeC(;9nC zJQDIfVgx{p z0LlnjTfV0R{>!)iD3ieG?tKhkh}(ZGD^~~d==$LrUNJbvo&!)PGu9_DO7Rzqg#%gv z#Bh-Sc3sl-l4}~CO{Z&~s}2Vh{Jk)gfk79|lFx~R<(4<9uW9Z`%;J{x-^F#HEpTLKz{!x4fb6&%d!Jwx~GWI8ly z$kSoz2~JZ zb@#utC31UrzdnNmqQ3%&U>F8!ZH8ULD-t6@0Kr9#V;MZf!#PbZ<)m{~%eoyci#N12 zzppnPyTWlplQhjRblreeV8Albh3A>V6BfdVL(A*%g>Gt&XSJlV;qx}_ioExwO`*Gc z6Jn}q2)nj4RKr+DFlNS%dfy|(u{%crk zI1r9$nXKVZ@X8W}5hMUAE-FZGybO#{Nd0<*I1mYFTFIkTv|;DC8+P}`?{Z6Kqh40} zf#R<@{zCv zMuY&=5>WYUrhfnX|2$^C2RdBXU+2j$;gy88ao4yTG3&pC%Q>9IE9JjGpt5p(1KP4G zq!186I%k~n;)dee(;2tP&}+95)=sH$Mm{637>gnVYs3Zz&YL2zX(PEC-Fr$@UnP#~NOF7}@bL zA_Nd1xVK)dS-&zs5(tPuH#}#1N8^RbY~-Sf@()~qYF)%ID^M|sa@Y@4=M91mt0JA# zPTaVya2=ZdWlA0$7)+=`h>;z2HTXE{Me?Y}h*|(;MacE;F^Az5Mn1sRlZejfPR3p< zmwfgI9!9_#L0ARkUl5jFIZg!rKe=f9o%{<(foZ<42!TQ7%Yckl`~aG0?{uyy6uj8cY6+cGVq?foPK29axP?01D@L$)YzSh|bhknbH4Fu)4@MvIAO@{e zk0ycx8U6zrAx&I;BEzC!hXlhVjE3&HUH!57xuSJKWgpc$^V>&CV4&EQFT;Mibk$|5 z)49aYxO7ZX>AY2xRzT4ft!*9lLPOUaWE{)DnfuBbJ*t)L`jDHM9<}>Usw;GyU7u^e zs3E`W(uVBzi|cc{E~wA$SQHnj>G6;dGnx!BwY1~7#`M}njoB?1z+D{AZo4F&*|rGf z&WdJtpAahaP1f9kyorJPz%I(xLk))xDW;^!b6 z-+oR*X4@rosVx^oGP{q{?Q9eFuBm<3EQ3RVFY==dd0So>4pV@!g^_px>V&G* z;IR(A<{_Q4#^sCVoPD1*+QwJxdxfR*NkA%jU)fhH|0;5*CW4An%HOvFrgZf>b3wqP za$6|-{c!sJWfTR*h|V>9VY<2Ix?KCoO{onRHKx~JSfATI4~o7iQ~GR-JuQD>J37dDG%W|fE~pA^yL3K7X{6lS3-6mrw! zB6iOSb?NOF#gbbt2!RYVCs$7*LdEQe8jcP?9HdY}%Jwg4BMq)c(G537ga8Jg$1BMK z;QM)cli~T$QZdDUlruUiazE@VYt`X&%nO`~Q@NM({X9hme!NTs)g(~`Ddd{yPrAn= z#EOn;Q1Rne4;A|-#?zb5ZA!0Sgc<(4keiH~!qF&W@~LVndn$cMI-6jCLo>un?INUQ zP#^D1Q{=$Ar}(N3`5F%0f)9tuz3KJ~E5A`hQ%{L-|5GBA-Y6`$U$?w`OQhIyLNvAY z;&5vFnOZU3EE~8+1T~Ctj1U1-kAtzPd!%kv8hS{9dbM=Um>)zG0-x=omTd+qf2NTT z0>!V&sXT_O(yHUtPL}8SRL%+NP2}>W8Mfnvkjer_9n_3pl){Ccsg23?iyAW<&yPBN z6Aj@&-$QXz=~MYr!3Pf81&#twpT7~21S|I-XjYY1B%lbx56g7cOHv>q7{c^Y!pdzB zp}yrJlz5I-fiQ(@hMe@YNNW3{Q2&;5wR~!Pi6l@Xf-w*F@mK(kLt5uJoYL%xKP$?_ zVZ;a#K((%6OOMEI4+}#n2<$}L)kC(U`LDn5m3s9b+eWqzbp(}nkmko_TE$fe!B2x< zt^9*)0LoPZM}vRc70rcWiPlJUr;J%tL`UOtj~gl`$2X-moEOh-J}cy2E)AOj;LiNh$i3oG%ou=-XA z14Lj54^}~LVkou!e5-%^nZnLBU;_^9qoU164r8G?cnlZ}<@NTFKc>sy`Kmt&~1+1*ncnk+F573V^7Ppv)>#X+>m2L34nt~`NMWTV+)Wg+twnEE1Hxh z^If3eWf~QAkmf7@K!FEJT{>4f!3jb+ooQ9&f^Fn}*v9nfD)78gUB2MeqYnmVjA3xr zuX%Q;A+zn|#>|HE!)|h-A(v^m#G~X&I+T^J@>BH}NpjG9TLo zaaEdWm2^K>`YMl0O|%M3VK`mF>VHz0eXE3Fr-Xq6H0|UxtAFc-T58WcbQTYo9aMu4 z*-c{va_Ef^uK!?L?529LwIe_A zuQMBs7tXx&bIY!KV8@qU`~FRrUiPM?mS4q?U_=N&-36ptcPw@I$bsih4D;}q*Yv{EC~TbAc-Mxs;vh?rTH@*PI>r3I(t9PK^4-WtiL1aSV`ax8%hQa zTm?$tEOj?r?3>V>UUy-v*g4OTOZyeYFPCmmRyzNuteoNY1ohyWIW(Ys6-$&GSfhE@uj*^ZO7zAfj8Vm9tu1N#GdNb^viY#@eB z-6(=f*#5^Tj!2x2D%|klJ@q$#yzd=WxZx?o2tA@3;rk3TbQY}1NtPKhSGPBbCw9&j zTaq(iVTMDDBL9^PX3~dD)QMwmL!tP?24wn>c#L7WSz# zhYG&J9<1P{kGRU?0r+_s5Ch9nU#Qu>bXCeeNK;`F0smOVpcC?=ryLhES|`Tt3P(b3 zjm09jMWf+6B9YLIa2LT{91dF-hC0IjI9XEu@K-7Rmjmw@ik4Es6d8p5 z**~D_kU_u?R9A*@C8|Q)8$;z7jzRu`1o$RyJ^hlxI0KFIZ_8Q zoX2zX$NTRvLv_zU*?$M_WZh7fBjtrlioDJgHgyp(HDOWC*3jf*@mmH1oIUs^> zg`!PME`R&8SNK&QHYzebjClO@66yq{sX0^(iV0@faxsT;eKue`>80&$Q6$-ud>tE>C0zP`Rn7$07wdlLBd_B(*E}F}$KM`&g!k}`byOEa7dgGL}7}*|az!RR4 zcG7cmrQB@P5SGdxe-4_{b3rDao^8+@o|)A+^Dog*G~;^i(fUFyoUJf1Pk8_C-Fqgd z)0zI+v*&y*6bfh1ma0QW7LZqOrfnDQ+qJ7rFBA$tzWeSs-tFfN32yvoS3Q*WH=)Sy z4RnAUb8!5t1|&ubnv^+za#!;Fi5_H$&U0(&=nnCX;DE`TATgHyQq{LZLXLShP zR4PnvO0PdZTBZEiJ8I0xpMj2f^7rLIS`CT32$hlu`^sPbR{X(WdUf8u_9F+e`Y(rN zI)j|Sp$0ASoDeq{D-!A z(HR60>nrwMvZiCrTN9bY1Xx25fEva+ZvJ@RWW$R63A6WmIg6JweJVioX<1f$8J5_W zAo}@=EVvCVVR;jR4)@_`{gj_yc>8Y;Ye{HCRPp4=C*rg;%p}J$sF?iV5CJ+^3x!HK zOLx1=EG`wfufSzoIR_jOvRtuXMnTM<-Y;5PdSqF?L`eBi6ZA`J79i^ZIT+9bB#UrZ z6S0Uc8tY9lzR46*CRk$nA%uYWWa zi#-~RMwdb%FAImm%S_XH4wCf(l+0=<_D!zq?Qk4-yKOsr^7%qPT%RHkNgf3f$zTFe zB$B0zp$rHl*Ptva*~nZ%nt%qFh44%$`8gnmQw`HPHLN+O$Ftkc4SBgnWdftHbZWqG z>%bijB2fOCT$%DA2b@3Z0xuYZfe$ckAISqzB|!3+%V8Wh2Y+shD0MysLnwp6YYM%X zp5*pyUzo7_PAdv~q9iu}%3j#`tKJcFNCbJeFmqGa=38yYCQvq#!vZ`;aMQq2eAqi&s93eJKA4O^!HePDVsS{UV)8LYaGF`Xo(E zoun(*s*7pxr%f`&v{v6u4wQTZ@d(N&Wk0Uj5)DxJ;V=Y*S4kl{%A^#+FtiM2=tT&S z3#FTO;=Y}w++Ow8UB`dJpN*1W8+HrtKgnScqx235J)`<bPei&_3)6;@_LaZ9<#IoL za;QDUd330hKx?Qis|VCTl!xLV>^ohi@yE}s92btfeEbZDgjVbprH*Ano~nnJVF zt>NB|DLSu{@q|$a`^uHFf;i_2LEUhyD`)5CZR^^;NZ0f#COcYDz7|qxwqpR-esG5c z<^NxrVVy0%-%W+hQ)D0er&=vdGUP{Z9Qj zoKP;JTm)gdFy_++WD81NDjK;zU(qYay$9#T^b$#-3rZD(PD)|fA&+bMfiw2ySdOz! zsnj&h1##?)K)OH*TR;jVkj;&a@zp+oJZo8IH|ii*s(<{ zk%Y2W-9qIr@0^oMg1%28U^b#`oBmwiIR(2AV}G#fqY*qe$W9MrATQ7J7(zJG{AnoV z%XzD^>@X<-QVJA!x^R>kdG3Das&Mxu80ZM|Cmz_KWU*=zV_bL1Bb7 zu{vVe9aVF-jo2~s6q?U)B$;l-jxCt^d<1FWO$Q2)yk2+AP6EV4Kj&4Y#D zXu44}yu21qZD_H)EXE6!KUKe96dvV8?u5=y4iH4;X&(R~7$CtXl4beI&t4UjqioXv z`ztdDOt0QdqUyLc4TIIo3AbapDCK&D1{Mz&3ZkwrAqB=|AG_!!9;)w`BnhBWIJS@N z+7o)yy%W(+RG#~|3v2jp;~h#+4(J$EDI*WIC!zZ6$Y-ZFf4uJkO*1|uFZd1?*lKWx za;M5izxh6z!}paNm@l2&UU`A9u9q2~zUZ!pj#nuM#;8oiU#?A#HK4EbSWHjUMV%Kt z&laxh2)>bd&NN3Xn2{5X-7nY54I>8CMCFgEz3k-5xMj!7&X;ki4OlZAG0;azRhCid zRGcnkX}#f)kwkD*`h76u5qq{7O>CNKy6H$o`72OP4kRk-tQD%mdjsb$Qtf3oBi^9?aqoH(~6#x-W;tlx29B3O;0 z=*B?~ykjKjSmi1m4acyIE1{6SM76KN6dCoWVss7l<`#dpHR=eAfk<-uB+KrP$-5(} z6#l^~iNVE!mZ%2z#k+4Tk(J>}U;6=gmF6USb0U(+2;NxBB|;DKA)O7&fIj-4R$x=KQG|5sILC1=gp6Dc#c&(;a;stJ zcNvE9UJ$^2AcFhC;m`wgk%;xdXf*tuSSg{*T! zAtNA=nLdHkqdx)y;i(*eKwj1YVSDb?Ok)MGO1JsJ!@^A;>$w6QdAaOic5?Yl8G!SJ z32h|9fijolyeh2n2d7oy(&r>WhUwEXggRI5j*IU85jFOLx=fU(W|!w0VCHaA-B z$Ir30A3uj{At`y6jA4XDNox{CJEA(WYOItyoN?L73_~a+e4j!xAZ>71ISNNOq;=QD ztPX@nefF=!K=Z6jM{_8*yIGpbObK!hBqs;RA9(i4g`oU(QAAq=8~a)aKhkHg9Prc1 z2>`;3NjD(ya;R*xUtpr>hNuarNWG z)RrltE?h5C>5RiWuwX&DRd-!cwzO0#qajbvWHR;HY&M?Dz?1F%0cakO&EcxA95>0Wt74UJ}TCAdYu~Ku9ESgj+E9TlvFy+wQymWF;R3PAP07sQWQZa zD&Kd$uv#v6wrfqoZSHUw^`$GA`a8PpqA4 z2*;E{D+QOu_I(!BkgZYGB>QWf0BnIquT9jIb|Y-S`CGsXww2B7=cq7!#Dg+b;UHZ; z^u@FR@k&@Zgz+OCEmGAil{kN7WfjPDPmJ?62}`69Rn5+HnZ=;Lt?x-+4N8?K z2kZXAz7HMk`zlTPRPugaKmdW_--`fTJ`3WC@YG9%+0cp&9O2Y83$KAMl0^l!-8AgNg0~0CKh=ODpvIlQEq>_x3PR=uaX+aXrVsrsw+!O+rNWovSt)!)1S(xBeVJde z0{9FFeMedWn8le5A~y3?LJP%Y9d;9}fG|HntPX&`(8l(p=7~$gp>Psqd`3HD$lH0j z5BRF`3I@knPTuK-pzI;YvxPVbpoDP}!Z~3!k5lzZCMG<=Q>i-g8sMWCmA*_jNE8?w z_3iDu4V3p`fs4L_vlJ=h4Yn;PYjA1nSkY2(#Il(vL8gm;wSxBOT}B zMlzZ7Svz*@(&O=l|2*fMGry5eXF})?*RrfOZiC6229ijebkbQ|mf9z-zQ6s8(QxFW zx)H+A<~1$uQXL4v^Wq^%pGsOPYWkE*B|q|zWgg>IBvDrWiU2CJe;aXW6qVr<0Ls`O z0s(y0Pu?dgM>otEXaS5x0$2tDC`tmTudjb%%9QCpI}8FK4SR;6(T>NgKRYRx%kf@k zufD!vV=NZi2%%tai~j!1oc=`a6iO`r_JS0igT>xuDM0mgp-tnI@+U^DulJ2A0_g1) z^Fy7&(j0Z}vJL!F2XT~YpeU4s958;c8XWh0`nTEJh;T($mIs?34z^W;w30R;j-TrE z>NKh7%RUvo%A>MZN?+wkhT z-m`Iphsa~X?*LTCWsjejGHeQ?3q1#&I@RVFzQ2* z608wvfv^q|2rUrof&}6*-2^#sIg8)$D4okg^y+YLkSAmKsb_CzB3#((_p=!drS#!jlNiON>P3=%*`2d@Dn*M!5NGjG0mY!Lt~luHW*8wAh>0%%w^Wy-XlA0h$3 zloU}Y|0^+bxEM1C-L@Uf`lAWd_aaEI8=cAamTe9}@{-BS^u9#)Wb`f9LW3;)2HgTb z2o8oXPzsfD6c_|c4GrN9%}ufODs{iLj3BBBpbG@hr3k=3@8AG~@v=V*IBg(^W0=VJ zV6ZhBybe)c+KFL;aa&bEWm^R~@T4E)aqy|I9Ed?)kX}wx@c|?t6@QSX6h7lr_Ou3} z?|okp0SSP>76X|EDSL#rI{PJE*9!u7&5j7(F!>Bah3{ce;S#pS#s0-H?F<m~wbhGQNWI-t z9%>-aIDX|RMX!{!?>MWMM8Gm|>Re?WGF9GS3xLZ%nE5lz{qR^6f^8)MJPZPOji0ek zs52a4`0Y9-QEtbB2iuz$dS}C|eS0pKy|lkSp>x1Pn6@@GH81m(e_415#v9{xJ0?wN zd?Fk&kgtS6cA)>qFcSCeBKOThPmkav{q5~a&_@ zemb0j8pxc~7P?;qTNGv7$n=#OqOlFF6DBW#^527cj#lMQRCRs%%(=WIuOs-b`XNG@ zhBCYk!S zfWHiGiFyxK!(+&Qw63oHZ`gj7It(09sk91p{#w(F-(n0un9moUrORgXs}hOij{g2+ z8{Dq$?%qUKS5FRpwzIR_X>acqyL)Xg3Kh8;i-F=+3f@=rawcCLXV~{SvzLmUGkmqQ zLnW`2y`SgL`h#?A%%g@^ElKcI@PHH8QBuXhfsP%p0S`m@b2&%1Ed4Tc&^sX@v-mPJ z2voMErR6Ei{8vL*c?ZKj+ZnS%BN7QECQoX95(@h{%hVI-btN3~FYx#MCv#LrBu@gdEECR3qwQsvL?Y2`^XAR_ z%gmWG9+@?3)}Q9jpZ~+TbLakW_Ut+T1^-*KXV3n|%$YO4Ic?gsf0{C7%GX+3Tff%Q zGVYsAjg9{?zClYCZCGq>pwKZk10@fqUgpRp9LAy0%ZlI6N1Aj}$oMyO@%0Yzcw<|0a|>UJ zwj0IRzAsxF1r#Mpum-kGncTc|{J4gvVo__SY4D{r$PEZ#kYk>yi(E8fwvTJBe|Ac1 z)8j48^&4Q4?w4>z4VqrkbKUhua_7X7zb?B!>`P>2+J0ntfGYKa^yv6S1+C-=y;MY8 zjc{26KkPvHC&MazKw3FoU7mhbSpKQRa=Py`gL&ojAS}02KPrzw`6EHu!%@Sxgx&L^ z@a!x`sV=g;?Xz9m4k6bsd2HY?CS5}XNg#A+eE6y_@M}1Z6XNnO9haKaa+7C*G(1=b zdERN z(!|Ck<67!gG{qyEo8sY8UCa;-^`Xvj&2`UCnh;+$8D&~0H9ZrL$97_$yt~0)cpO{c zH+{{{=VlmoCagw$^&t*AT*|WYlnVk?C`VuoBtSyQ!$P2+ z%ww_$qPY6hoR~8%4-%-%_DjB!uU7b#FdWNJj(iD{Bp~HTImVSMZBhmHj}avRC^#IG z^2ZX=p^0;5|K)F2Te+MFp>vwLx?96Lb{vmGHA^G{A2@d2-q~WB+8c_+{2iF}*B1&l zFB*4X*3Z`=Z$~kn+~J;}6f8Q<c>eD4VIu z`EmZpnu2ucIFMu#m+8u7WqNhEI!*r3_rv8femdi2-~W^ONF;oz@jVsRLc&RG#o_bT zc1`5^I;NMPbeMgtpnU#EIbfuDA=B&P@{VP=qP5u(r_XW3r67WRN-hRUO^_F&e_8JB~XhggCKyacg?lYjav@v2826{q~+ zq50!~`OEhHa4Vh^*z)heVL(~7MG!3i_!v~F zQOZl@&vdpQvsGKN4%Mb~yo+N%T2M~<`w7cGKOUq@zdFw+k?OpCIMtQZ`V1@ZP7<%s zC7g7p3_IE6I6I%=WBziB8paSH*!3~7OWvvpC9IZxxCrTrXvh`g8eB1Bk}Kv-^~B|8 z7sZVi^$AO~iJd#!MIxDlV(08%5damxOXn+ne*V$(9j_Zeq99wgyL#D0a(nWw>tn@)^+wAH27; zX3h16rQKrN#d{Np#JGGuFSz#C($caFK5w7fus@B) z^m3kzQ0x_???(ot`924P&Idygk%NPOa*!}y?Sj0jxOA!_m0z8AV82W#(uebphar_- zafGk_;S>o3JV4TPk}xbepk2qzCwnG<4J4=4F!b=d8S|U;H?sIWHzQ^@`4ponp`K4zZ;@Djt4mrhgrPGyGuoPld01*U2jq;B|sPW_(4j ze5M78U+ouV+dv*G{KMNXMnh?t5bL3wDIYRYix}`vkbsqLG{vp1 zXxQDR?(iWA&`D)149yWUCuc=Nq)W8#+9A5Sd&LV|Tg0EB{F6C!XO;3-vwkZ5yl`x# z{MAL@Y9$Z3Qp-F89Z7=w_nb$b`sHoC{^9KxBPd9`%~I+~-i9SpMhk|fe_q$LS#qGs zL8t~gpU{*JqcRA4mL*DS*9v>tauE?)y18ZCQ}y-rFX9}=-rY814VqEXlDnGqVn>~P z|9utEv}MR(bwV|mj6D7YP~~N?oEmH%a3m5ug*kdBag=~MPD}0>CqIX&;`@cnr+`#BDfIHM0vYxbf^-&_zW<}P0KLmu zljG0w=~UUuEZk4$W`oiOmQmZti2};12}8xNl&SWs^Hll(Cy$8-yl(_nfQlF(fHqW& zc642{m`OI(mVw3|pv%q4lOpTFmea;Nb@=h8=D3>qXHvUGds~~>-RX$GJ~vIQ+|?*d zD1Khxm+Spf`76b*oU#CTZ9ppbz(F4k@TCh#fY%V@wS>Ps^{d+-@ek`|F-kMo(bJu1 zsw*&Z!hzza?_i`B(SSuXYJtwB@2W6-zT-hlC55wL<9OXGS*B$w($L4AV`%9e&HG#NQ;KjP7!O=EWSXB*ogXMs>B&5mz;u2D1|tJu{}})z2FjU# zN|}vA1YjN>SG@AP+t}tDe_`)zFT;Ol$@a!GyUUG zqbg9tm;w4&i$<;Nw8@c&M5*Xbp5Ths@fp$GwM%q#bcii&hWP!8$zp4dB|;)sp6N?P z?<@SWgC#(vuH-jR@C;Y^OsjJ0B61O`toSMakPe6uC4gY!KZ*tbmHo-`-78S;Hhey) zQOssVp->cggaemz=p5;DsOhF9I@)(dmOioMq~%X6J>|t0UzodY-P-AJGho(D>gwuj z?CR=hN~KbDg+jria~%1udr1T&hHBENb`=8A`F+%C0+GiY_(XMhOoPW?NQJfh?uM7W z#-T(|Z3xmH&^ zmd31qm+0#16{|Od#P3$Li2kg4GM(Rls8;w~>z7MD@VN{m1&tCNDEa{kA65YszD#4- zhVoZ;4`JK?_snnZc~TwCfiYs*gs+3{>gu_wudgo!9d`PicfMhZpEaV`aqV?KjF`sV z1_n~ehM9(gDV%k>D7Nnr?&ht+;X12t1?23ad|FROoQikZf>~ly$doVGV4MO>-vl`r zhBC<`ktkmwE8owGuosO+`B0dM#bRzOR+nZt91fQn8shRp8BqFs`)H20lLEGsT0^7b*q4B$^iU|Y26e7Qn$)VS4-Wmvt_;Eo)!`8b2u<{DZ!yDlWrxny9neMa0>GFh2t0 zXUaSuep&tobq3orKwrgGzHI9tjo~VPAPH0{e@KPOgT;wNnJ0bj>3kX7@=-hwbcxrTO!i-di+qMt&O~BLzHpXddCX>3YudjE`o;@9+KamyBtTDy%Ee#@S%7s0E@(|4G zxn@r#&ZQeQ9-vZW=PME6FSg^`MWM3-$3oOS0VF}V&R^|(@^#Pu`F(tF^sf77o)HN8Q+mztbvk;88g#Q3%2cM>Hr6%PqC7oMCPK?}e5Xg$mA# z%(Oej4iG?s1YmUuQ@t5mVqZJj`(P_+0`3JtA7btx?hT-^acUdV;`sEs!Mfl5ls4RpP7txuoM%#u; zEptv=5@~4egA9z)GVWo6r)6Zbsf%~++|@8v2q2MA1kl=gSOmcF>lBOmlhWzbtzBJR zOwemoMAV_gtkFpT-QB%c_4f9G00ao&^`k-n&zyJun`5T={m?a6i2R}pL)=3z=D}dYbI{_pP*3ah(u&sRMEXyl0VYosv*cak_ zJ0xE}TOkm`Fy;3K7>2?xfe>In@>LZfKC6(0pwNoY;_=2Td>h*aVV2YC>gtQ3kR@l5 zuvi?_!Edu_tdq-ARAv-5Y$x|I7%a;&L0ATWl5TpXjek1H+VkLi(~3da=V1ZdDRx#9 z04;!nkwA_YKS;>1Osk~&hagiJrXMeE5~kaO5$?B_pH4`>99K$KhGd$`U=q(QsE+}u z;H%TCeC8>|uhIr8f0f5H=F1cJ&wYPQz*4M;ey~ks<`wAZ2GKZe!L#)frtO3hd71UK zF$sP^CJGDSlD4+?5afA`5I`RYVAtUg0GDmOTrP8IDwTX)M@NSV9mg^Ka5mHT#`nB8 zX`XOt>_Nkfo}e3+Qs4pDr&5szv-y2BK_D;4#kg17)0auelh*@Qd2aEZ)qi^bODgr? zF=CBgO#p@=PQU&3(IJ4RzVHvHhG))vsyKIcWDh#32u0i+j*7-StR^U1&i<+CR5L5b zMj}f`XUte%KXc|wXmQz#e#gi*WEg^7UYCVJ4j6lQ*mg0Dj$}9r zL6aZC02x>aM_Ch}ly+T7Aha4d>*tfdI6U<_$<$|2KL-KJ z6}e=Eux5p$#nReepKkU%KGOmLNDE;49Uy>7k^trg1W<))VN@bN5HiirP->B_CB1Ta z#u)}Bi+Ev;(7g;sIL}w*6Eet?kukLmQ+-eRJnvu|8J2z}ohkH{!dK~%9Avyo;Rj_9 zkhc{x0g&hF_h0pSS3{QMw#q{t1reV5Dxud;7WJ*OUu>K_d(+WJ0Q`cOQ|A)^ze7Bx z2!KmKC!KU~1OPGT1Ste-=Gy-L{zZHC>=DUi3S%(;?UgH^{m^&5^L;sBXP)`)S)p+J z-wZSS8u_&_jo%BP5?9MQN)D=*bm%KNQT@-(heN(9qsmeFgHC@3<^QG^AO1*>Oguyc zdmMH={q(YjdU^r^&_Dq9j5q;o*syw0zEJ#DZ-3(S?yfEoGDD&nO%P3Iq;iDXl(T;} z3YU2T0$}57;YhCel#`d}p->+2{k9uA$nUud2BnoR?o#NfEVxH>rhK`SpH6FnUp$g! zFq@cw{F6xrw(WFb`z~Y|Q1JZ7H^@PdOk%J+_>qhfNrmYs%fUtkf$eONiF&_0%KDbY zK#K!=lgn2;JSwVuo-0cfI<@45I?LqQF&89ew|Yeh|jM@ChJ`5svot3&?zC`+>dF zkrL#|bQPEBK^nt@@&e^Aef$ho{xS|9NoAmA%O}6DHjt&&zuJ$R7uLjcgw--%M4DT- zw9Gnj^-*d8#6SR-9?b**nYS^v#&f;Dx3_lz*ZVP!VA$vGfBW0t^IgAK8Bb${w@*O4=SA`scgamVc*n{qF|Oqz(Zi z*65>20PXFY!-+)dLzztWM^U0@E%CQ3h(HgA!)v0^y0thkS_S3)!=N%qAglwz8=}<^ zgrV@mP~5OA91C^Ghlo&~cPhzmx1t_B2t%kz7W`V0B#mn8gO&$HCY!~q8ex(Fm$FDK zBm?@iKq%NYNam*5d&HDRRgM-wcKV%Smrnq5Kmf`Dz$xrc3aeV9T6MBZq^~>~U|7ce zFkb=PkcUO=@q^iZzF)xil+U1FVNi|%`UB!J?E7$1u?OkWRg}I8@2&iqMyP@ApLP%d zs+&O-Wo0TX_l0vY8c!0T)>B2eal+2=b52<~obotKz#+S_Y*rBfWTR`rf}@H6IN5P* zJHH^6O5Wbp)isXm{TK&ZpPf}_OC0l8HZ1-84!`7KW5gPLv(xmoL8f*~IQX>f9 zVkl|e1+*?2jjco4UNb)`c0X8#pGL>itF}Jq5S7b88VSTt*GL!$%7P#aSQflliAzfP zCEQ=wg+EOgZVm+SBJ$O&iRT+A;Gmog`f2p5(^NX!NTmm9vfX4E zRiBLeeh^m*zdEe)0#uK28LUnygO$mD6+=C|Q$&kS6On1>(*oErZqCW8Y6Nh^s3HK~ z1=RX-1R!tI&gU~1CX>nQIyyQ-nElK3e(0As-+%vy`8ME#0*GJ+#>LGbfGeSsPJx@m znNwQ^6DCMSV@MNqI077-4cCrVdk)I~kFHbrzb#LHz0I%Yh%sV~{`AvJAA<6~s;6h< z2w>g1=iigfX8)n9tEU+q#@9AyCQqL7+X)jUtk4bJ<+}TxEn61$@9DV6D-`&S6^ji5 zwOHa(Pc$Cy95;RX#&BI-W)uh7K@-4{z$c2m;HT4(EXrj#V5yY+Fu%7dEe`mm4NC(8 zt9j4;$C*}cQ`|H|iU4NbCE5Z4FglT@E&{)t5XwFHdoAxA@OLc{gv*K=H5y({)Pn%H z6vVV@Wv$ApA8zCSOUN=Zpz>sW1HuE-D)stdU)fXf%QQdEFcp1pu6vOHQBLFr<&g4a zU5?NrlSO3ug(BSCTC)I#0-EMxiqM?w>$!OQ_MKYmlqvr!37}Ar6Pf^!?ecnmBGJES z_wEisodV_m?X_#ye&}mo`{rSP<9zP9_m58%^?4`FXqbBAmGi#V+uJ`gn=7pR>0i74 z-q5_=nrCl@((m_+4=F~h(N{e4_;0&=lGpY0^rXhecO8G_wV!DBvxXd-HoY_hv;ME3 zgKq8Y?83nD#JF+e*G!o*^>_94^}DI0P)sT`-7p-xPz+^~i5Z!6`YoAECK?Jy+FB+| zc-{6!GTZsDD|ZD*S{BNBOJ{s|}i*==dbX^vxHM#E**V7>o#%=qW~ z>-||M{|Daows-Hf-ha3#=65u_@Y1dqckODQkP`PuU|uYW-{oA;J&zHEEsKs290R zTEw-iZvh-c^GXF+fee?^RDfX_mp`-svI67-GSHW@} z^{Bjo^+^Ii0@IY@hvQ`v0;j3YbpIrRc{Wz~{Gby`MJs!v{K|BL>>bvhCumWHLqNnjJrW{4OL9`+ftU zQ9M1H4K+Xg%-jio{L^X8Pdzmw-rb{5*|9@RetN~s(vqbMi(OqYZ&GWA+0>M_bJ^ok z>BPOAot@Km@7_b@-v)QO+UelI_a~F-FeZCjfAgE){>aZ7HVk>A z-|*p%xNe57f(E!6ox2FTnC6wV$n@r-h=)=lne{|_s!=#JygauLX6@RNYcFx#!v8+_ z&EuXz6;jj>6`MD&nE*!qd@7ZGD-}O4U^X|mY@0fD+OOmB_;w5s-XXKU^F_ATlu9KZ zfKb+9mc6a1srfmS9}BZ==#jB+0qhM;*gIA%a(4W+S-miSSAWhsd8b@1Nsx92bG<%{jQaxw9AYPtwp7E?SZ z_^O8ob*-VHpERta;<|+ZYX_(nf#*f3;`e#1vQ8x1}522I!BqiM!`zW)V+B=h)d zV$TwZ;_?@4Yf(l4i# z$bGzV~kl+{yF3DMzddQDYlBT(ZU5Wo7 zVLXHS_cTuZ@lVbR_x43fA*@7a0!PmePF*)V?z|z^Q=o{B8dak)b^n)n! z+3de>-n{vJU;N@%Gis}$#gk7yJ_i~S1kfJO&;>#o;-%+DG&3`v9E{E5U}5VB9h z%?sXHA*(J69ebA@Or5R-n(LX$=S4dIHKvshA^h2~v-dyzZ|A@4lWkkKu5HNW3ZKek z((lKt-$Yn6Ha2!mo;>9@!lcnh1h0p@smJp&SwNr+ltdl74V6$U1ll$`N{~S%jPCi@He* zgd1)aNiQNa-Aoo87sE(ewp9A?Jrl~6&vwWGNP-|SP#N2p;qn$;5(Bo84-Yb(M-x0q zNT^qrpan68UHNKZ^&Tu$mWM1~F#_I}%S^OB91wtfco5{_@ZmwUffkKMT*T$OuhfHt z^(k%-l3*AFWSmJ00CjZFojVtnzowxcz5+!~{8%@f--;D*Kf$pA11t{a`N6$``lxfJ z{p5enYgoH(f(S=sKhY7;L<8OoOajQl66jARMbGr9V)sSoKk$}!z3=O?@G#-2rI^gd{~!!+kqQ{wF3+p8Q9cveVF(N}}Lxyf}27&vGEnwR!ol zAhwss&gVTy2!X|cu_s?$!fi<)v_6=R`Vm%VMoWa?HUZcS7^mJS3 zec{Eatv~$Hxu(Utm-&@AeyS1ejz4x;U;)r60&qkU=a54?l)8Iwf6cm=e&$y|pEa?`r7#bymJJNaRUrz@B;HkRaI^ig3dOsq{6& zl=ZW6>FX8;;SHFMD=9iAoWb^-&OeU`C?BtCwn&46oFZ9M@ zv2Fp0I3=Q3>7f=?EXD7+@7rq4Gd@<)_fs4QS9~!U|M5X#6tdSn)!?V z7yJkYh`&TKaQHzC_1^P9epJapvPItyNRK~|$#7wpu=1NRpaTn_qO1po)g}zf&1Jlj z&VVXQ#~j~BKq-AcA3rJ%>BqSSw55EmYVhV%q;rW>j|d}F4+Y;Stk^hV)wRHH5oV-b zes@A@I=}7!3~?Wt8KMPHiZxGKVud4pD08TjoieD@{a^qYA4niP9~HvB$`^kzVC0kM zOlKJK8RB*%72o<`c^#GmEDml%GRf7mETA_v#mBd|?;+W6*@xF73|b5z0E7GEixNo) zBn#B-TNY|rie#d~!CQEF7$txCN*zCm1a+!&fwK^Pe{Wh$^Pjq9PfCUFDuvG;3M_zt z05VYi4D*CR47+l!d-}CYmmbzTcMb+CRxFzX%S>7Tg+dMl@P@HR0M|aS&5Xn*{7yHl ztAj_FCD3^MF4Gj&MNt0UtIC0{{i|w#2|h7UcBp-&bV07|OsN##yiWm>QuRe)o-+s2b62o!7}VCT>p>>KQ4hm#t0u4tn2IRzI4~FHVd=)?s@YTtO~3V$aw_H zJFqyy#UjZ>hl+5`e7@i>U%4QQVwBB=Ff18IS}!CD>_aYJkua16p@~c`Cql@({`nV0 zOBPMW?|kD(+(J>(Ju#x~A$6M9b zHiijc@h8)t(RJ%nT=RpZ`>B8&08&a3392p2ffQbh7{83K{HgfMBp}~&0lz{3u3M0n zK%tNoO;JbOv?wi_8;YW_aopyqQ>XqKB(M`54st)zL7)gAolZTFNYn^mNPq$C*@Z}c z*Nd0vB`0i<0O}@*Jw5?UB>~U^@I_?b9SCJTI3tjwKz1<58Hb?AS>L9%F0o?6F0rM( zQykx-i`&kMu@;N?$XgAeM+qqr=3U3j?!xHRCs70b#88=fZj=4RPZGK$h@}@mP&;y$Hp}_tlSUT~^6C zdH{v&K{0~>dM??yv+e)4_Z|Rp6nFM;O?OY+9F=nf2}vMwHW*{D$r%jB25f?jv+uKg z=XkcycmCsS+pVeU z>grVWtM{s^tGoR`*D=Q|S;ciSulFM$Zr;nLK7H(i1jGdre~3PlbNGs3ZpW7;qD9S@ z?Vw|ss+#@stIZWVU(mJgZjtMa3iJ47B6ijpqOWC-2><>;A>s+accf=P{1gC!6BxK- z2tY65mR+)P754(?8+_+ zzz87O^86{jY^;&}db6Tgw8vcms1ArQ1mIwxY{`Rbd+=zBiF8Ihv+X7E%=24Cw6{kz zHcb*2oN>H3dUBQAj+@f)vw5;t;VygY_%p~?LHtkvK;-UNyjsUD4VKkvSq~IZAuU%U zjw#(J?`Lpja~B516+vwQxB+@RKE)z;ZP-^^P#^(sFW}x-#h#Zhrb{b!U47uzvlj*`_(V0#MY1e|?~$ecmT) z12Sb_#l;FT#IbcYIIgfPTW;*?dtxBt_ThktUlUvQ_{FB3{=QJqpGFs6^_>sMl`$6Z zX_~DCD|*>-Wl5`&EMRCc(|Pesysvd0U_m&5itOm^6A!OmFV?JI5AyF7OO_lfuDS3{ z;;5JJyRx$-5p(`yQ5PWTel1I1hUOLawCM?Sl&%d-={Pl^|V(Yf;i1&%}&N)}S^~_Vml=?~} zWJ}JqB}kALmc;FnH^Yj|J@Qtc=qorkJr2rravSLi=R$D3hoa!1c%pb42`^%(bKwQ$ zeozO{12^?Run&s|)p$GNfeh3qG z0VsFRxi^IS;lAhYX~f{w@u4;|k~m-^Lfhk(y*P+x3dcX`yH19Yaf;ko3F&iLg$ra~ zv~I8X!a8|$$RKLY>XZq@N4pk!y^N3Q+!>Ze4@?%krPd7`-NZEq8&EIUe6h1o)NJp#ND-E1n@4BHVjw@a*)4O30Tpk)%jNkghIl$;d@Bz$w+J^TLm;35ui{E|9$!_WUAp_sD*z^Wq^0ayp$W7G78$ zPjVlA;QZ(X?CP?nc66s}^n)4(Dr1b`^HU>Xm_@iIA`yZBmQ8gD|hS5rWX;@x$Ffl8qHM1Lw* z@yhPRM4hjgETg26b8--X>ZmY6^1U#FfJH`0rxy3=d?@o+GA&j;^St=y>Zb)-a_*4N z1>#gWVxQ2UcW05Kk;K*w*1K5 z-zZ`9i}ip607gDv8M-2OhCQ+$>_FJX%aX{|+|Stc&M z;5@N#=43flPu^ePKDdeN@3p^t9Br4bK=%1{(%+l(WjeS#+yyUA9(_n0!EKAd#UW9J z$RR89QTUWCuMD{R8#TzuE~AXs6@UxgzpVz0JOvJhSVZiD{GxUF zmc9ymq+jq0f-uy1LclkJ(wEq$Irzjt+(Mt2_G6Yp5C zNEIFwR|tE;P#o`x05_sXNZ(s<@meAvw=V)0J?Eh~&Wo4Z(uea$s`>(_i^OaU#eJYJj^c+M& ziowBv*(AsNlgWWoT3cHyIo_YiWLqaqn)baZQ>H&-+xDP`M!``&Gn4k)79T2Ogb-FL z6_DXE#87(~NxZHxp(64%v%x&Udt^^S_Xxk3^j+dF#0_rMdw$-5?7gsBXG-T*LFJ^U z!YsYCl@q%0y*q3!3!{|DAPVU5W?#`quV-&$k7YJ3v0u{BLsvJ=|lAH$< zv}gDv|9;N$*|HQsj?Yb5^sFGt^k0@VS}h{lFVIVm(CRd5I%A`AFtk#qSkRLUV+%@liUkr zFW>LNWr~3N`Ug7hTOC{e!l`83iXA4~Xxe_mC+=o=3-p82b_^~XHlQPGsOLq;&* z7m=VLf?)MM0~Mk@StpWqB?R3N5vo9c8?nRLZ1${V zG8NEuy{)pcY75izoUse}$vhq(VRvehgy$39NqQ(=9=h&eK=rLLP4mojI-~jh{%w)S z_;?iriN8D5Q}I;SQ?oY3H%+S6t0OgbwJ)0L6CHMkXm)DEvcP;XEjUpax-PPoC3d{B zTWr|!yy)ue6ro6kIP=s~#WC||gE;v0rb2cXXS|1^)iP}^-H!n6tdQOdtF($>z2#!x zgI92#Ddr0jf&9AsPtkgSLRTD7yl#K+jxEn0&S&^V-PHLT!j-kVUZ8n!Lf*>jBNr%NIhQZsY%~PZS-!R-El!!2ZN2y`|8q@s zM#8j;?VfKDwHq>-bJoYxIE-Y1{FM;GQ|nL1>(5SBC+#@7V+GU?P8$6 zUrd`mOPqD;Nn+;YW_6=i<>esAn4;+76wLcvsvkb=O)eMGOFQ)9WTaY`PcsBwVYsGI zC>#Xx>+(NE>s<^Lg4YDkn;sqPDpWfm)-ZL>6IfI&qY{Ls<#Oq)&-nXObw@w8qjIJO zj^Ohc@^q1DiY*F|S12%a;6B!2w~20OTM5H;s`1JPiRzxdMFSXE{s-Xc80)1{E5DZZakmjghFc9(k1E@5)84!WGKp?>Pj!#Cq z8pJyw`#Oj~1=907rbwcMg4-*>P|J+>gu`=6K)jxZ5)qy+!#r(-d2fQxl`xtg=$68O zH4qYCmSL=;sASwQjL;{)a>UC+I1QiIjEwRjjYEStBa@*31h$65k*!!)JRHCb@=zQm zhAxjdK>p6=o-LDq(f+IBK=I5;w*+}bC_^~MLfMU}r?9SY|bDAn)EffQY(4?tTr_4R-sD)qb?(VM2WU~FCP6wH zh)qIP9AqHk5M~exB87yT@`pFZMf!fx^oSX_hWz5I@Bkl@Ggt;&rbD?_A`0;V0ZMRRpW*qOem4Kn^Vt=P%1#E){Y>#_$5UR$gc^C%8^>g&H}KLcq5I!(}pQq306j z2d)gpCe)|v_w4Bq{30Og>ccZ;%zg-U>`=Z7GDHBzZV2evXP=&(OqQSkoO|o4YtaMv zrEY{!z(pT@m+%Ub#m&O=3eqF~qJo4&z%67DfJ^?g3Q7LlFG;XS1O~(1&dIjhR(i>r z-(J(@t~hM8w!T!?($cyEt`X<)!mL?yzloNF21kS9$0_)o)d0v>hGBSBByY8eD)w%AXw7Uh^BaP4Tq>?80+H*qq4~H(Il4Ta25W&p)1C7MwKTT97o|!2+#LW zTu44$)13!id2s2YvBzdb^axVJD%@5-!}v$59AyX*u+amsv*-mt05VB_`ynSjwCH9r zt!ARIU^m{L=?yyQ&@%v3RAB`}P9`awto*F3T$avFBF}@qS{!r{UYvxx;F+SAuGlnX zaL9ThA1;o34S44U5)*;SdJ$=yjBS0-U8698V453Th0+#Ocrf{c6Tl1J$bjSyH25e0{5D7`of4J)xVY@(sHhAj5|veT_fMTV z{b}rr6Fd$urw5^g1YjKmAX$0>aMn!^PcwoQ8+0SwIQT{|oLA8MD#&~xdLqDCKP=!v zLDL32^7kC~EltKXDE@lKaOBNm^AD?kefec_k(+&afy)=FLtnXr( zmMTwzq)JC4Q(=B|T9T=ZXBwHB=LwKp)kiB-g+|vdjCDT7$Kx^1N<#G!zW)9`9#7{^ z(WRXLf(PI0IfzXIEy!=2n&YyyjcEefhI}^*q{NLRE?UVX_{bBS5WS+<&AAXjLLV#t4 zDFU|a0Vn~mGM4B!YQ`in87Xl+R2Ho4o*py;bsjT>rpO zNQi4yzWfE4Glif+QCu+fv9s98~?ce?Nxuu zW^=DO$NzPZ7XbDEa2aDR0PF#vE|-=7oPPbE&d~jl$8;lv`p@njMZjxWc?DWrh{{(j zf430fIDRjn?mV>oT;5&+z`c3~C6g)0{mT}?1~W5o+WH4?$v;&(99XsLvB|iuXIop_ z8gS5I5rAO=Z&(M>0{UoQUw;FL{Li(u4SzweDu6tbPfN z%}0_i7LI@s6uZ;A#V?}26=8o!m7#3?D*=!qAeSlRaOeS${ONNFA*@4u?oFQ*vm0l~ z*+R^I$AbOHvWH-R(4~On!`L7G?0gy=PJYcvNeY0WJNAQ5)-CzR)|%P8 z>8t84FVMS4=Cdj%@<9GV39l?<8L9O$jq7{80B;CLaz~y&_=P|v0PI!kJ}3L0i^JbJ zdQNE9;OU_Ug|Yw~C{DZX!QZ17aGB}_py0VV_rUVz_&f`ht-B%xA6kI)nOaRL1>s8( zAn~g)L>16;2C!crdmmWG4`u7=bSlJj zh}Y5Ms74q&aPzmxtc6T&I4Rzsh&l zll&!bcz!_y+Y`_RbRIJue+zK_rMm*q5W2Fr3S2-(f_EE-d8l>DFRULb00j;y2!Q;iGw%-|z05bm zPTqZ0#n+}y4D?v`A-yN5WCURI=Jm7V@kHqg0CD^!Ke}5tLf?=xf+TUv>Ld$96{K=` z_0AJND+CL@aLB>rulA+Fs*(~g3%M=!(+@-7(9d)3s$oSo+ox~bxMdBD;CP%s$qE2S zY)x%#{jYFpCHHtcU;B{ECIjDk=35`=6MYkPG!mGF9N+f{pF=+Mxy*9b43xEdKOyC0 z#OeA%=cvF`O_sadr%%2zg4V;NjN@)73d|%&@Hr0Nkc2J4`2zu02q+SM zDIhw8qcC*kISLF#17TM*q&`BG?%U^rVhY344?@Ka@YL!JAZ2@;54chRb<>f-6n#rC+r@Q47{oii<= zfA7J%(_f8-YXZz`x96UREH;w4ya--L-YK~8o-flC(iDq8q4DU6fD!dcgZzzARy=CvFCc-wE}W+Q-jyiA7S41cp zUhhNpJd|+o8dYNnrvabNs~Zp(2R|m(;?#K^oPsODwrz82V=@p3yyV4WJ8uRS!x{Jh zDw}%RTrnCT*8+d>mcFd0`*67QucL(b;;1 z6cYA6Y@-6b3MClA2=iOLQXG7^UL&AHgKzO~7hDU03_ zuB@N)6Ntc-@}@8hmd3#1QAXjl43q&Y&@U9AG6clRv~KI)O{)ySMhnNjd(%Vj|8E&N zf`~?2ApkFxqyWH$&;xP>zy|?nC|Lm@&|0=%YJF+K_a43X-Fs4dnnQX>_5w)!EMw0G z*iz1EIT5!L;-Xm>zHwpf=Q}Tv*Pvp2F_2OB7Yc`lm_1w43D@Fmb=KG8`H6;LBXJq;JP&sCDz-% zFoYtcwl^ZVhB?dI!j-jYRP>-_nPE=N#XkAI`d}a+$JRM>i|>ANMM6#4^`__!?S1TD zJlG3S^-AR}PgmaCDe5uEcU>`v%iQaMLC3)o!UvMx=doDN6_^Li zD~5~mi1pZWZjXn#ga~)N@(Yfwe$OfLr;i2@(t3L5LEQ)N^191p|ngvwS0^q_o_vDX%PB(&|gb3(9bfx(ssCJuo z3%g#N@S}hH_^M4ko90R)H*^ze+#tdp;MZtBA2yRjjn9p3O|>xVa+HPbW=UL&?R zDUnFjVqF{7{UH{M)sx_D+YTVUnfZzQ32@3T!`Q~ax>||{dKuV3t^(rVV~+#k!Ku5h zXn4*O4<#-PE8(DEC=sD>6!P4MUW-rs=8=$iWNW3U3gMWn=kmgN-Rf#po{-d)!-d0# zb4LZ}y!H4|%U&AkNc>D+Ab+n1K&ZU&paj6OEphurp>Ho)7=FPt5BlC%$q2xvO;62F zBvR`k0F~oK03_y|^wD!60{^b*{*%#Wfzhy{)20i(02;|43^EjD6l$5Jp8Ej^@AKOp z|BQSzZ#2-`+oJE>`N|p?!SSd7FHM~~7K zy!6cCj#&Ew)3z#2bmi;9^#k)K&hI>C*5W-=o2N!?$L3Scd*w9<$R3FHG_~(~aY;56 zukwb3SS@p1U*OSR4XAR-Ja=w9q{#r!Kr_EtEO^BCIo5$m^X5=X!H_xTh=|&tU1d-s==GEvm z1Ry3nKp0_zVuG|%JUEQyhiE+h+#K;tTeAoU3Uz#N4l+~RdQI^onGYp*IW$z1&Ra)c z3I)^A6_GzV0@udtQ^!-=;zytxC9Xch|I71Et6r1IItP0rxMT!i&6+39N0~kZ0T9`2 z_NzDE_^yw+t40j_G1-I1#QB#QRgDWSMK}9{5C8~)0fVCez&_nVV|fv(qOkB>!$4u9 zp!-qqe?`PMcdY#4YMF2p(cRsmzx0yo0a%v((#)B2zKdv{@v!C5A%f>HE(L(#%pi~o zUzu&kQ++o?d!wgxb$5%#hNhKO71bSx*Gc|v@_hm%kIg{Hdj7k3diGedqyv$6YmIcwIw0P_*y3-`ctAKYrKO zzU6G&Oq~Mv1-KQKncM^4GR>68nrUHLX|#q?0&#KpFT%ecY2I&{iRC+<{P*j|g8UU& zzX;iaa>HdDE;yu<$!7c}TB_k-NQ6US5efw5Y#DUjUzhv|l9b{1n^R`adv4miV^)Q$ z>pM8hM@|n#Cry$K(yf!j*vwl+?jC^`)_GyMEZ2k7-Km|D~0-7lHwAM%4I4v4Ep!hV4a{uZ3YXMxyhDKUh0gf8}Wq`KPx( z`GqsBZ0y)=PwZO$v6~lu`rqHbNZftRL~+I0)gRmTM8}eBI(jV9T)uPFm%shuny- zz8hwmKvkpAf(?pjJOcNeOXS6l``T;08|JeJUYvR^L7fBV^(T|v{AiI=JL}wz<(K`- zs^i{r&Bp5b2?-mmZC-=Cm=XfdNnWdaFM^Ie0SX8Fym-I`C>%K_i*$oq=gO(@P{Q}( znARhHwXH|+9@+Dm2rpcS#|E-2SeX37#xg9IU8kY()>NnWqI?cBf7$pO>mpLeho}uZ zH}4B0evlW}-UB83!Lz5GJomXt6RJg1Lr9!(?1W81eD~#-pZ;ddO;*Mcg+s4oIS>?U zX%g{TRCO!V%Z}=JJ8{cW%MXO9Ez-6o=_yA2Sw$?MZD1p(pG{Y3EWLv>^xF0 zCV4NdB75aqX!WAYF0$#ENF_TsWz?Lsh#UGte0r(FE*waruK^svPb0xUV=KNM(S z+M0;f!x=eBNIemNlW*@VxIycRoQ&tTF3HQceA0O$VDWVi%9moIS47_0f8@KfA%{*^TbK#U$e1c(zHHSXAs?})vwV2 z397N$?2xZx0AcS%>P5?g*3k`G@3!!ZM1Z6(eJKbTnHGuRNctR}_3jg}bPMtF1$qG| zURPtpP(+8B-(yGqgyesT(L0Yflz|=&Sm6m4LZKka7;X%A-cktQ`6{Yv`X2H$6EephhBCec#v>XaDvk75BB>3SO%O!S6?$R+14%={8 zCyTV{RIeobbf#a3Kz+7m+G*Vj&-u{vXI%f86=z;~=c+{~ox7u^zBwU>QRF!P*pkUe z09m8Wn(Vyd_ocIZPg0)5N$5j}zS!}DE_$NzaSV_Fhgx)>rrDJ>b-j~k%zJ+3 z(Z{Zwanx~7@P0uZ1R+x0&>F63>Th0nZYI>U%rZgn*=$CUJP3_9Z)MobhzwlLs4{)w z3F*p-%cGMPU(quEth=|Jc*SSep8bx0eeBFDZ(qIWq;q!EPngorhafq||3(?=*&m>Z z+LQ@eS3@Mz#t9wyarU8^!x8uEyKpN{KYhqM{f63gZo z>nAmZ`s_n{6?7gRJsoK@Rqg@gQ7HMcR=u%7Q2?R%B&kb6x8+lcXv&jVG~T?G4n!}> zzHAVAkZ7k&4ECHj*It!MC4}XEp-YiDTl9+b2aAH+OOR>c^5jp)mcNNExFUZPfOLD2 zez7>n`!<%S^jpcZPN-Sw9RA!*6!6%Wad9JWwAy=R0dS#i0yEb=SH=;9a)BQNz`xpg zr&T}R97(pPQ*rfRCyCs31#%xu^!YGnVY$l%VlN_kI5A|o5LP#l)7gwTYiaPYnbRt| z4&eoGgJ9cs9sN?$`O!L;Q5Jyxzb|F1;oSr-kF=Cg#%sfNa#~|uDE9W#Ll0&%{c=W? zWhSK%s2%|YL5YHNF5$~{d9mOJbG$cCt0|quWG|lqC?b9?dozeqnT(h>LA-Rq*%Kc> zT=FMy>`FIS0vKHt8j~JCDUxiN_(vWkJd#qnXI7%8md)D!la6hE{=8+{-{SG8Ff;54 zKnN%XSqK4%zR1btEJOmrz~LYgJ`IBR22~G$&t-t*b0m3}{9VHLo+;q$p$uC7jTLtE z`nOH|O-01W4$suj!YdfY8lzoD%0oad_`r}?*@|AtQO1#hviu(;SVL?(r(b*V+=ot{ zXRYmyc8P3y0OSuLkS?K~wV;#2kd$NpBzDPiF6qR#ahmF4i@s>i}AH8!JRRJjdR(6>O$|&PN!JFx<)$hFW==(1? zR)3-=+96VjD1@M2ih-GlNipCMka}{MN z?3PO={P?8h6St-_)-b+J;K;4__tX=n zJ~wi&k{R3lenUS8P=LJ1GTk0B%Dt2kK*?pQ8XI>PN*ojcoU3-#B_}-g$@d=jy~Wdw zmj+Tvksw$?CijfTi-tQ51;?wNnii)?PvUS!Q6?hnXKjK)Km8mBY;9a z7dKSL5~Fn*qbdN(p5+&O#zc5t3*Os6OBv&k55@~hGH1tYd2?!J8ohx~&~Kf7(!A&I zy5)@DeEy?nf8(?7TmJPA-7xP5AG&J#if`XOFZcdS>cx95Yu17Ce{HG08Ey z30!*M_g@vtBzpv+Bn$nG+w=3XyL2wl&FdUm;cwVn-xtQ{9zZFA4}z5@`Il!Vl~Km1 zfHiGQYujjx{+QeYC`CfE?b00rD&GSbag=j-%uvqypF}ep( zdKS$GQ;JZ+H-pA6ftE0lmF}4VWdRs@lwKZ>&IyfX51^#ySr$Kmh+xQZoDq8Gco`*# z5>{39AkBd>QlY61`l1Yd$@s!T4ujJ9o@ngq`+SM4}<@JO2yHX113 zV>wt1Ho@^P0_7@oAQ-JzVvL#qXquiy^&d3OQc5sQPXIiWnQG)va(O$hSZVzC22}Zv z|LYiDu)#Un`7xUJ07{xs`8|NLF^)7!HiO5d+K*>@E!VZZfkxKJe)Ljf4c zuMU+_!Wh@>QHsF$7J|~jSmvXz@LgI<*Y$okJfawN!Yfrnz3j3ZY!pGCyI95uq68ZO zCE6?@4*b-{lL(Ag!5T?G0E+WvRJgg^;w^%%Yp=M$62lv#w!paaY*wm0!SN0fNSRy@ z1zx6&rC}D1O)(e^lm%cg%Ct0sfKAIAEhR`$Z#c>9_iLf@KXDDd@V<=>|&;Z^%L~_Xfr*Mz7FDxd%}G==zb3(tQr(jf}Dgygmwf zbYx}LC<=g=v8Aj4>Vh0S=g02E#^U(pA)t~2DluC9)dM>&t<3Csmp_-Ct^0h2 zk;{rmE+PCrhrD1IPI|63ubYE) zgw8teIS-+9X3zj%Aq|V!4I`jy6oTwzeb=kEI@`8hoU<%L-Yh-@kN``Kl*a~RnF*d1 zjNUnnq5yaoSGrINJhsRQpHmrs@uHyJf0o9ZfH*0dgK{`Q1Y|`;JX?;rWY4*&_7~oa zreaBdajnELq9srA*DWgo@;^D4$xQZI`#k8-1M9Yp9spYZuVai-I{42X0enByA<_XJ9@;*TUG`I}~>G8#S6Z&@{7tDhK}6-X4_! z5x6usrU34Pj*SH^5nr0wy7fATBA}*{9uhqJIx;b~20GgLKB@vh<99Roh&8X*L@uWV z(p{%9dlbQw{6YGt%KT=%g9RJQHdfMSJlCTipqc$0+-_{Gcl-jmI3mdT9LK2W?_cJT z|4S0UYBX~*p2v|}G80N<#RGbt54e7Iqxn?p} zfu$@emxWE}I4nd*GT=pdZj7J~%}Q2c^?yXFJzDg+NGvwrZ<lqg1o4_*g8$7d$%bBEI9hof%)ku0L)x}xUwozS#f4C zXe8u0U?F;qOtI6!dd$Dd9& z1XHOw`)c{`3mpGPk0GDgtTV+KFMV&J2*qi5Bvxv>z5PNv7OOv;`vJumTz`iTBemZH zhbkMH*JFMz3Zr>0W6}dark6-o?+ar={s5;4TG=X#gb!Ayvn@;5zM!y;N=f?JA_2%+ z6a=*?dQ>7%-RRQ;!MA9d?>!*?TLJ;&&QQqz5xPhu_+hvYR#XH(R9P9iqoN{oGs5pi z+|_Vz4Tl34Al-=&jzxihPVtxq;b_3VC>|9>;(?CIe#L{rK>^ucufubth~Ymk2yG~y zn2%B&RL6=B(`@1}4-7$_VcibO&mf?z;E zLbtf$aWmrI9u5c34}}76g>W3lUI)bEXzXJG#G?uOYd~HGaCQ)noR0JKM8N~a#D=5K z_@RP9c>YLZSWDe){WltBw)yo8s&Iih;Y0aKl7}0d87S)^%QD@dxPtRU!0GQl(dzGS z8m?Xd;ho=bAruOAxxq0)PD2B#+Fd9C3}faV0LEwts5CKV576J^@kK2DiPft@DMJmewAKMK9a{!uFm$ZB}b*dk#)?baX;Ex{U7bo=|UZG>CoF z$6~RG5RZweRBBNslc9Jlvu*1<@b=r#ak)^}eeVSE-wN?~4}{}Ga36NXL-sl#8t+HX z1MB^#$rdioHy8{|hnoknndtW$6Lnojk3*|~aG=NG3n4>jo4U*yS0vyl7)4HH0g?>n zM+MJ65FAX}141B`p7L7eRD3{r()jV&whFsWU06ZrRa`*~r0>Rij^J_8CJ^W%R@z-% zM-68$zyptas2E(&4*zEL;Q)4|!L(}J#W9D<*U=-QQ9IK5^{bD`WU^0#aDt|3{_chw zZo0}{RWew=e$@{=KrluFaaWHx&<4y{y{}votgJoVG$JA%j|(%C z5n8HOXqf@o>L>BD0_pgnPnUBX;je0ZxNgRx2iTKPM#LW$uUzt>mE)1A5BVQr3(>Z1 ze>R(~L>RlULm>7#5OB`2>?$nRf{<-&hp7|I8F{> zxee22kw%gV!X!SK8%4ego(=L**X-8TJ&T@wcH@HQo_kJQb=9@&7A`!h1-&DgUxxsm zP@uBA`%J^hMPwPdD~5t6pzoVQE_K=wveSp543mh`PG^Urd-VaV>4pT|cUTjR5AIXza{E zqo-fj#ggl;d*?29^{8UQhBco+Z~aT{?Hw6Zm*cOy?xrp7su9D88O2b5TpsI}+t7}XI0EZEl->)r36~8Q*Of@DGNnvGDLQ9~BY^H?i_~a}gLPx2= zKu~!6LnYUYzS8G_5EPXMsH6rQhjnr&!wQ6h z+{QGsH6Ub=oi7JLX~y~h2_La+eJl1Rn9Q{BoA!n!}Tzr z9(*e-@7occ?}EaCla{>;3WO&BN#-l#L56XrbHfR&lU#~D#UTK*pLu5eY*zr*Ljd;R z`tq3oxM6wB;zJjo1jC?NpToC#MxxPEI4u*GF(?t??O6Kw;fZKy{a3^W~t7D&Chfds{>%l@uvZ7;gZp zAs=;vm^Xms>>&Jy8mIm)SWy){5b`Hb7G!!lj(_T|Bf0z0@`rOg7eE@`0Oo!*T{mpB z+EZ}p%F4>F+S=NdnwpwDjg3vup%OpU)YSa+lqpmH40r#eNt5rNJbCI5Cr_U8{pRM0 z-*0T3@YRZn%D*dWB4PM8!99*+u1Ak!0~!aLAbc;-B@)S(2L@tYuIq&$Cta6`M*E!p z{sB5c7mLNwQ%dM?2DUZQ>2w8pnpNn{RAIjpa1B#%4O1W}3m`;CqsMY|AmBeC7z~^Q zcXBu!JUbi?%|tqil&GqzJUSTkpBV}TPoP7ZWk^3CZh_yg&o&Hw0`|fC9E~`CiXLy0 zvnxa6k-Y}ZG^^+nl8A>mLi+eo%M5_lQLZF)=@6SIdb&aJ?((3pD;&k(897eaNhEl# zOg>^@^&S^hd><$lud%!absjtc&y74rQ2^Y)*pTc&VD!!pd18W?YW#lRjoEDad=Q)l z63c;T{}%Lzegxw0NunXj8^3bQACA771!N1$XLMCgnDu`(&C@nJXq~&e9YN%UgU%C< zBJs!zhjiG0gs6dhG!SVT#F?L$j&R)X_eVpa@a|A3v@0AAzg$sKu_YXiz=fZUL@HJz zygC>Rt%Q3Vy|Ks9V|YT-^cC3O6KKV+qHvg|wLYEBJfBLXUZi+H2wET--4u_0ps%kl z)z>!w*Kfh)Jn_JKPdp6tT2K#E?*i8mhIrJX_c9q`I2qSG1Mx?roQ?wdFY)`0lLG-d z|7oF6;B2_FAczzUx-+nR8hU6mc#WiFNsKZ)#NkJU@duOR5K!?AjeQfp8=qfRVbu%R zKb&NWH=}vD5l5k(zaB>AbY2%M^J2iR-O%V`EDk+t0#GUi{(2Z<4Ye{G&J*Zfyu1ok+>m$1;pcZ3$la!a^=;)9ds zE&XQggelKEwlyZ*=Ys(*7`En(L(BJ_GtA*7z1Sg*6b+e3)=v%xX%I@yglMD!fk2;O z1o}a`T@Z?05RcuFNMr{DV{<4(@rbO)@+z!bjl6tPiAUfui0Tv2AvoU(+jgF^EQ{i? zF_lX1K#w8n4M9N=2IBEJ#A6@7?+BYWmu8=-o(%09mcDn9+i$^PYAdzLEj5YV4Iv}>4@V!5^r~OdnFw% z@cIrMBXSA{je|Sqk%Kyq(IFvX40-^ij8QpB)?X0_Xm3Uf^F1JyCXxxbeS39v&39^Q zYBzxh(W18e;fl)sX>*Q#Xy(z!eG_7E&xDx^zcFS0l6z(zz5JU~W-WXq5DXFlZ4d{F$E)adY=*FOAiWfSh`{zp zq;e&M2nfaFS@b+!gn*#uk$e?p*i8q4h+3HpKkJxhSdZ!l z|9R__^N8FdbsC9sh=VI`=?_kaeQ^i-0#<0DkO*^YR0K(~VhfXBW-q$DuARjASSrx6RZ{8J-RQ7?e zY{nx`N3HzEDe}Sr=iSWSOFWR%TrUA~WT(UHb!3s(848E%dfDrsLoef{%F4>k6cF@2 zo5Bpd^8dXez>9{^nr?sAYAA@a5o|RPQ+gp4hP?iG;aw8{U^aK z3zCkuUx+6Rq}d ziRqzu%Tde|UD*1<%%Bdbp7S&{^cYa-IL&k9ru2;t zN~PlGv9J78rT~Zh_C#ZlaCi=BihCVw{ad`keX*VwKgfr1R2o`C6P!>PNh-{GMUVB(=?AoqvCAX z*TvZK?Yi!}9>jmU-*3DZ!r_SrpDO2b9Up{G@wgf8Mugu1_g0Dr(w)nP0^yGG`?Uqw zM+18v$QSQ*B*-(0Fi%pRFYSIvuS1H5(!7G>7VcIfiBUL25qAIl%9C4vdDWk^=Bu_e zbv*d41p|K*Cu*yWsoM6R#wTCA>FIA>@X_CY`pgj%f>9%q)vF&r3Wd6^wY4ph&1Oqt z8tA1zzqC;^w8hAUWicnd@I8}mKCdcOJ-Z{?X_|G;wy+WIt1_xus{Bgw{zSWm5zr0a%E-JZ_#4hQ{3u*0|PB;=ZyaTzAM|?J48)Q z-O4~9*p13Qro9-n*6p^oJx6cav~m8hJO|+O*?Kg(#Gg#g#=c*(f=NaL+vkD-;m4BR z-pMJd!of!DKgjj?p|2RwZAv4;V??Pst{V!j&uTuAZEjwXi$ppt(`>>yL2q&uu(^5= zGj|ihG32uyAZbqP4I)hrPUn3QUIT)z@8ivT8Aduc&I_xq6CKY+ddg3!vy;zc3?Pg{ z+YqAOMot5rI(6!{#>U2N=+aV5`J6`&((>63PT`F+jChEHNOO+}b3gK#4=%cnPzP4W zbi)#!PZWneV3*dN-QC@DdU|>|je5zAH@Z%Xun(xcVv#`uRlq&K@*f4-B zt}tWTBAznEKr;L1j^5ODSAKCt%-wX@u&)dm5p3DAu?DSwxTz*o(~inWA$U#lg3($i*eb&i7o)X*0w3(cwWG2aPt?@Z{?#!2DHz<5 zFosW*vH%Bc~rnBCU1xouySryDT;ISz-=e$j`e3U}F@ga3-{?2ApD+`+qY@Yp0?T9Y-UUZ z;ELZ|aVD<#vmpOB`S`t6UVTvo1)-xq>R=gvXdCfOoHi6$1JBp_YQ;2Tnwa39kf{&U zubo;w_0h^;Wz=%^IRgi_CM?77%|^Yt3N8N0X!&!BA%}eaP+eWS4r%0&f=Yfw;7I8K zI3km&(|dYO#|}`vWn|vQ2pR|fwAxxB>e1jxr9~>878M}>NVK0%wW*dqQ-Ku7QmVzT zdH{@*WQnla@8JD=km+^3FLKP1|EGsS32eBpyk^Uvhu|)}Fd@S{7Wd?V!huM6I0^>^ zgkjEtg2z1bA7mvNBQ;2>2(rw6utDxFQ=22IZ6FOvm*U z2zPlu4%drxoVCTUyneZipbQHVy*K3;t!BP(PKrl2mO0eKZ0v;NlkSdbrcdqY=$a6X z_6^LQJ>xC2X3f|=Fc5R9t1G$_iB#?{fBBX3{`Y^OkO_y0-9NixRxqHi@f&(GPnoZE zmo|YTPyqPWUq0g_1t6<>0e$g|NN4TOT>gb8KCkvNJdjy78rZgNQ&lwDj~>A23&3T+ zy1Y_1w6B2p?~t?7U1Oo|-9q6qgaF1a9~5fxHH&!wGeOM0p}C?t(A3); zneeBnwNp1BMZQvS5BxCCKJ8sfdVpA}O*U|8P&Qi1f$u#Lq^d3S$%gxyv+D z?n^rSc>9G#yCDzUM^5;vs@GR7S@Kh_^*>CIrvcEXFI{qZ2r-?5pemadCM z$z=S!EiHQl>cj()NYxIcFieS+ckv>v-xJY=fQv^FEhG_$5Z-Jd(O@Y@>!C z97QkwWt^*qbB#gB+Hn3dTw7ZigQ#eW7oU)S_Qk{QpI!E!p5{uOlV^Fkk1haE1eP*T|FYi9wO{hl0K5rFP`P2hJip_2z&dJPVU&Ekn zDeAqBgzu!{D@gb{623@~@l9AS?`43T(Hl|sHBdhxC_ed`5+FZhg_gdrw>K(cu^7&$ z9*s8xPM1FKdC)P;v~(00ulJz@h2`M0;5bIlBJV+bFV34b)Q)Z1aeapHX?uKjw!`=I zJs$~){;3=i<7LW^J%L-~2cF>Qa|lQYfQ25SDZ?C*K)Q=B`}`AsNgR42@gOd^Af!Buv43nP=skNZmjfF;7z3L+qPswyk3 zxL5=TNkDGcgS6K`psjl8(r;*fzX@X=nq;hCfkTfj2tfTWnwlo91ws1vs+0Qws}Vd| zRo$?M*B!EgFoY16%iy3I?196uzRJqb)ewM59UYxrufDqLH-5k02I2EHA9I4ifDd8b zBbgcq1bh&KFkBtdmzR#v_OBV_Aq)bhGSapVX9;qy6mG`bi}KZoy5_1D!1e?z?->rbRo zLLZ2U2wMJp`zP-@DdJb;?+JmL?y1aDl0GbmMnn-%e!&d|?`H)jPx(#doH;9VR!Kc6 zo)-WP0r6#U&CQbry$tR^gy0~-ETmu*hv`e_INA4ic6QB;Mx*O*xZ$10bNvu#%a<>& zi$o&1i4!MO)z{Ze2nK@?3B4BNKOJ_=ml00!`;EyE3G@WCDIonx5Dz|x*nn_DAmDd= zno(QfOGg%)5Bnkk^yhFs;0D#QH=0B?8HGrtAONT=;1~}7@E(ATFnb$H0Gw}J{)Hz# z=$_YMWB9XrKTrZ7X911409^LV%PNha@plNoDU$p>i;~Q)Di>qCigw7DVGIeE{L3g3fVQ@FQCs^4 z9s(lR8nvu|+1-6wq^`EgsIC%;WJ07<8KFm`BGem|PnPgYV6ZGj{;q8n_W-0>0<_h^ zB0#5vfbe`}zL1niWP9x~OMZYRD|Dn}TK~lB3IO3%*--$p%(}C)vvW=~+PAT~I&#uQ z7hSd2xcy-ue99>&M{q?>)wKCd7tgjAoHo7hXOW;@P1%Q$h}_I8MZ9{Rh`zK*g!Vj$ z9)VCFe#%-Te4&HGC;%MN!}2Gu`r?W&yZb(DjA`W?Dd++H-+a376sh=wEJ=ku?U#Ds zh8a*{*Y^_S;jLHBTNBg*Vn=L;crp5-XpeV_Y|eVLp}y%?Xc?r+ z3kZGQoYb^bWX8+~HNQWL#3h;IgR0g(!1b?}W;o#;i!JhDMr!ryH9f0Wua5rxH$UF- zzW?~y9}_9_`FJuZQl=)X`R@_~H48;cdynW&Ya*8RiC7{l1`-(&PjgsE-4oz2kp(e8 z&tQYQ-(kTR2*AkN;}yTT;smt%?^fjRM&`k);zE|VvNGAycjHvF@}*^iM z8=^hieiqNw3*RD;e{izc-O?)JJ#E6@wo1ekanT#60LWQC>iz&7$UkG*&!^JnN_RKI zf>9HI5m45BL-TzaIl(0@ugo#MBcX*%0-vG}@7A|W4VLVi4vrR7FmW~&sSQH@|6w2mj*@Oby5htufiG;yvp4jzMwKa7= zp1AO+Uug9WPb6V%DcDQ~iRrSi5DxixgieEbEvM2cvG4?g4=-A1gM=ncyuWVVyno_E z4)6X&i2~U*;!w|sjU2XbfA*w`<{9@7q;nN5yLXHJj#ou``ya(X*9)Q>)Slq54-}mb zfl&lF4K$fC`7FRsZu#2U|8sXUY#4O`7|F_yeP08Q^N9hHwe+OrcwzcjQgI}7kJP;| z#ULLpcJ6+>W$uD$8HX?ru4|fjY2YOh&&20Moan8Hj5v z(qQmY+-V!ILe^n#>^|zU4I>j{LB$uKp1uk<7=>{@I^z;X93n9d8%`(1O|mcncZ!DvoNMdk)KzxZSW9@5U9# zT@gE%IpxzMdKoX5-E;_hY4d>bm^n+{3{GT|{LbtfYKZL(j6>G0w-rO zX};``uM=pgtgQZCb#=`atS2E4+P4jouB@r)nz?l8&nGNc_-%i6^}3v<#ViUz&X&VD zobKlh@yKBwF3a^GeYl*V_XZmp9-p{)@z*9VTJ&Hb96nM$3^WL;9=i)AjBVRCeGY{G zzb!4T{;sYbkbnHMx88ckoe%u?*R3BvFWUa=zHJ}x=u4i67XJwlf>RS|^ORU3d+e8f zz4hWR|7Pp3J@vaUj2c0aY)4336>?%OsF`ool~y5%*|QLmwi{NSij(nha$dh1&O4Xv z^Yi{a@*fn&K88T|X`(ybEmGOk#Eg~U(}a5`Dr39@CX^HWP<$X@oRCZ=La`X%wI(*! z);8Q*QSsWZ_{KWujeGA|AfbsqT_5xo+GBBC!^Ul0VfUJ6bTG@*-VHY!q9s?xN~ zi0|+|B=ZJ?mlY$~5;!CrOc{|P47B=>)imExsN6oj>`&MAe>5~S{mk$8CqeE9)EZ~2 zoi9iWRaW*l&ziM<`jRF0%{*@Tz0;39?(0*RF8i-(OP76>ZpN|4ePiaa%l~KEqQwt3 zOr82%AQVb*TMj9?yBcqB^P6@sJhtFg`GWyp9pUIcoik^-H9Vhl*=5%Z%R^v0c5I#i zyZB9CU;jII?P?MI{R8dsc>EHO|IgecC4o0g%K;=kQ;{{pa#<1oe#qY&@{ul2`X0H{ zSK(rpPeOk3XyuT9U ze-cQ3nw;OC&ZYu^(9i4Z8~?1*9Q>2bY{7GAhz>i@48K1cs;C$UMwHVH1Gb~mNa7zi zH+lC7T&`8;hKC(qmE0HD7&A1(3TOe7uX4*;*-G@*XCObr5CT+Ar}`Z~z5dvpKqGxpV8vKgikx8k+2iCa`Byae&DZel9It2NdP7&LMM@fuw7!8WbG0C z5GTj^>59qSUH0NY+w!#!G7i2Xf0a&AxNC84n196V2{Xi`rlw=Vk-(xzBse=1^3U=6 z^#;FRt3u1M3Q3svyg`W5&6&zMu2aic-_ykf7f;22iEpIJ>^wtg%%XHU*%vyXLkbuTCRqt*ZP zz(DNm_r32!PrFG<3ZrIN&p!KfRWg}=rnR+wb~>FJ_ICkY_}>@)pAj@})(lN}@Bi^G zPTB&0U=`9hK;EcSc_Hvzem#FAEfm&yH-tT5Amoez_aO8bYdMx^2-b_wp8rWv9j->4 zeCZRrn@pC5PeMmhU!Ob^`IRCc--*MQKJg1kT?qRS<|px@AOJ5czi{NHVXuPcTGV|d z)X5ZGI(&8DqBx>BJX$dML70Jo{wv$s_(7POm1sGS*}H%g@e5zZ3{`Y=v@hKD+=?k% zp4%eccF7$(79Ta^6+=&_aB5NtaE`e^z?fqg`qAjtYwjmKw^diy{3a9%_dvAYFb`dg zJbbX->|4MAZzn;b=k$x_=81c~3urjN7JoqZTm5E#^)r2&PS}y&aYDD#Jt@NvjmeQ& zRtiqsFlB|%V2U~);%A+A8-#4^*_N}(G3{?=6WL!q@{vdOI~!-)woTW9{J+=P*;Nb2 zkL3Io*ZAIl`?t^W%Vi~rQ6qv)o7PsPQsy&lZEdqrfqn}CxY}JcEV$s_3qGL-^e<_K zE(M^NoE1Mx!UVCSU=c`L8DTrN2=T-FpaN9l0_n?IkAIi^l>oS{f2PAaZ~rc_t79SF z$=RY(s}$GFxKdPwD&?zS`AXQrD`V9%9bXYkzwiNz!naqsUpNW}_sn_Zd~OKgToU0V zT2bsh^kQ8aEm5S?I&r_frpd<37;fp z_wx8#wC<2lG6JBXl|PUS*R@CM<|Q)WX2-GgUER89?bAg#QY9wWWU@z1?rEDjF}5@4 zchdeq;ArH-Y`)?i&YVr4YX%AG#Vj3>JX8y2f8jm@xbTp!;-~zTOk3Dld$X0XKKG9g{^LG( z%RE5-ccs(mueG;#_lq3$fQXep$d1h@@CS#%pAO+yh z5P-M4tA+*VfBXFRKmfk4`;`EomJhl(mN5&Nq@S|W;L@&p9nSCiO# zz-c@D!cjl2kKak9QSyTo3=bgO4-sK&m)}+8O-#+Ub1|$>yJ`3xQhq)U9zU-~V5buD zI5?ggV91ShA$&*5mN$R?Fdb^5fiy=Zp&UT;%syQ@Wlz zBNpqwmI6>+U9%z(2y(c~Z9((cgG#J8JS-sjcMnugdbMl9@i`a?Pm0g#btdY>EB!UX zhfa}Wn{XMBeHs;gK%6V~oD=Vw{Ily5X??3V|Q~(+v0DHd<`cM(j z0@kkNt_gqWecv$ufP9Beo}H^V8F~|GVccYm()z3*TSe7aX|pur2wSU$>VRi z<&I$!fV01RcAY=s-)IDk8Bze)3jk!5Dd!?Q-JiRV41U5WuM`8elzUM*s*&$=`9YZG<_RyLH6266 zM=yg9x5~e@q63j*O-{TVZ5G{0 zzCV!UPZ7wVwTGTSCN8q+K5_B!snm%}0^bUSBCk2Wzl;MxK>+sbQ39~9?*SYNK=$_T ziY~Sy@H`Lj@q4O&$NPcild00zjPeU+4VF2pZRTH-bq_ z9PWEf+3-Yk4+pXli9647{6tdDIJ&zswYjs~6T=Ba}A3cuJ%K_S!RMF0on zPjL>q2ILycxvRgt`cZdHsenZ6JMFZS>zkUI^&iAe3?#2^}>1#x>apUT$ zY}U>TfN5rqzxn3(466W~^~JN^rU#5a=>avYkwuG+i{r${lNO;JDy^gejcKZhcAVw-l> z%^FBYYPcV6$>u@Y^ocDUje=ic;@6qf5D)~wObHVLV8PclM_hWcZ{_LB!@o2wPXPA1 zNnY%iv40G^0KgJx56-XKe|`N6)(gi6jDRc{7KSts{*c%i2J-QQn}B>T)3En1{lU%~ z0+Lh0-Ch9i2V~M&aES4ZM?d|~Vf#$uXkye1cEg6%RipF*j}JteD^?f*{ltVXDP~vB6xSYorI-|+C_36Z#XxL8?C5(ztV*sGnOqhvf4zwO zQP8dcNQXkFL3qS33C9gH*CY`Q%{`=7vj*%>to_fm|KY|S$zTrA@eQpND^@&mt7SR= z(bm?6tn&RHl|G96ZveqN2=n2fN`y(=jUed&1p(l56Mpt0kmq5<`8|+4?;7lv-D#eo~aK1&~flvnGauJ*yCL1aoDs<$)6@UZ;pmkUTAgBe+ zRlTd0|0Dj7o7n4AbyH=!%kQ#o%F-DM#!H_;ra^$D&#)%|tkV<$**NqBK+Xz+jo7dm zn;&+5%ZkseD77B{>tfW5Z{50;Rp>tF1pr;`<8Qq2)?pO@V)-YIzdCLte|_of3&q

FUPS)M~TOd@@dYW`_g%d#Ty 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 NT%G@Ls&Y7&{2%9{Xo>&; literal 0 HcmV?d00001 diff --git a/examples/sixels/main.go b/examples/sixels/main.go new file mode 100644 index 00000000..1a6b553c --- /dev/null +++ b/examples/sixels/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "github.com/charmbracelet/lipgloss/v2" + "image" + _ "image/jpeg" + _ "image/png" + "log" + "os" + "path" +) + +const failMessage = "usage: sixels " + +func main() { + // Pull a jpg or png from OS args + if len(os.Args) != 2 { + log.Fatalln(failMessage) + return + } + + fileInfo, err := os.Stat(os.Args[1]) + if err != nil { + log.Fatalln(failMessage) + return + } + + ext := path.Ext(fileInfo.Name()) + if ext != ".jpg" && ext != ".png" && ext != ".jpeg" { + log.Fatalln(failMessage) + return + } + + // Even on latest Windows Terminal preview, MINGW requires this + lipgloss.EnableLegacyWindowsANSI(os.Stdout) + lipgloss.EnableLegacyWindowsANSI(os.Stdin) + + bgColor, _ := lipgloss.BackgroundColor(os.Stdin, os.Stdout) + + imgFile, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalln(err) + } + defer func() { + _ = imgFile.Close() + }() + + img, _, err := image.Decode(imgFile) + if err != nil { + log.Fatalln(err) + } + + sixelImage := lipgloss.Sixel(img) + fmt.Println(lipgloss.NewStyle().Background(bgColor).RenderSixelImage(sixelImage)) +} diff --git a/examples/sixels/snake.jpg b/examples/sixels/snake.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5917fe15f8d467baf9b5e24042cb19a25ab059f7 GIT binary patch literal 39948 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 F|Jg6#G#3B> literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index d1367dd0..5aabc19a 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.18 require ( github.com/aymanbagabas/go-udiff v0.2.0 + github.com/bits-and-blooms/bitset v1.14.3 github.com/charmbracelet/colorprofile v0.1.2 - github.com/charmbracelet/x/ansi v0.4.2 + github.com/charmbracelet/x/ansi v0.4.5 github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a github.com/charmbracelet/x/term v0.2.0 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/muesli/cancelreader v0.2.2 github.com/rivo/uniseg v0.4.7 - golang.org/x/sys v0.24.0 + golang.org/x/sys v0.26.0 ) require ( diff --git a/go.sum b/go.sum index 53444c90..306f8252 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,18 @@ +github.com/CannibalVox/bitset v1.0.0 h1:fXgciTVIjZFPsV1Amr6mhkYcHBg0aQhYYMQZ3E2J27M= +github.com/CannibalVox/bitset v1.0.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/colorprofile v0.1.2 h1:nuB1bd/yAExT4fkcZvpqtQ2N5/8cJHSRIKb6CzT7lAM= github.com/charmbracelet/colorprofile v0.1.2/go.mod h1:1htIKZYeI4TQs+OykPvpuBTUbUJxBYeSYBDIZuejMj0= -github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= -github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -18,5 +23,5 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/sixel.go b/sixel.go new file mode 100644 index 00000000..c7904742 --- /dev/null +++ b/sixel.go @@ -0,0 +1,235 @@ +package lipgloss + +import ( + "github.com/bits-and-blooms/bitset" + "image" + "image/color" + "strconv" + "strings" +) + +const ( + sixelLineBreak = '-' + sixelCarriageReturn = '$' + sixelRepeat = '!' + sixelUseColor = '#' +) + +// SixelImage is a processed and ready-to-render image. A sixel escape string can be +// obtained with Style.RenderSixelImage. A SixelImage can be obtained with the Sixel +// method. +type SixelImage struct { + pixelWidth int + pixelHeight int + palette sixelPalette + pixels string +} + +// Width gets the width of the image in pixels +func (i SixelImage) Width() int { + return i.pixelWidth +} + +// Height gets the height of the image in pixels +func (i SixelImage) Height() int { + return i.pixelHeight +} + +// sixelBuilder is a temporary structure used to create a SixelImage. It handles +// breaking pixels out into bits, and then encoding them into a sixel data string. RLE +// handling is included. +// +// Making use of a sixelBuilder is done in two phases. First, SetColor is used to write all +// pixels to the internal BitSet data. Then, GeneratePixels is called to retrieve a string +// representing the pixel data encoded in the sixel format. +type sixelBuilder struct { + SixelPalette sixelPalette + + imageHeight int + imageWidth int + + pixelBands []bitset.BitSet + + imageData strings.Builder + repeatRune rune + repeatCount int +} + +// newSixelBuilder creates a sixelBuilder and prepares it for writing +func newSixelBuilder(width, height int, palette sixelPalette) sixelBuilder { + scratch := sixelBuilder{ + imageWidth: width, + imageHeight: height, + SixelPalette: palette, + } + + bandHeight := scratch.BandHeight() + scratch.pixelBands = make([]bitset.BitSet, bandHeight) + + return scratch +} + +// flushRepeats is used to actually write the current repeatRune to the imageData when +// it is about to change. This buffering is used to manage RLE in the sixelBuilder +func (s *sixelBuilder) flushRepeats() { + if s.repeatCount == 0 { + return + } + + // Only write using the RLE form if it's actually providing space savings + if s.repeatCount > 3 { + countStr := strconv.Itoa(s.repeatCount) + s.imageData.WriteRune(sixelRepeat) + s.imageData.WriteString(countStr) + s.imageData.WriteRune(s.repeatRune) + return + } + + for i := 0; i < s.repeatCount; i++ { + s.imageData.WriteRune(s.repeatRune) + } +} + +// writeImageRune will write a single line of six pixels to pixel data. The data +// doesn't get written to the imageData, it gets buffered for the purposes of RLE +func (s *sixelBuilder) writeImageRune(r rune) { + if r == s.repeatRune { + s.repeatCount++ + return + } + + s.flushRepeats() + s.repeatRune = r + s.repeatCount = 1 +} + +// writeControlRune will write a special rune such as a new line or carriage return +// rune. It will call flushRepeats first, if necessary. +func (s *sixelBuilder) writeControlRune(r rune) { + if s.repeatCount > 0 { + s.flushRepeats() + s.repeatCount = 0 + s.repeatRune = 0 + } + + s.imageData.WriteRune(r) +} + +// BandHeight returns the number of six-pixel bands this image consists of +func (s *sixelBuilder) BandHeight() int { + bandHeight := s.imageHeight / 6 + if s.imageHeight%6 != 0 { + bandHeight++ + } + + return bandHeight +} + +// SetColor will write a single pixel to sixelBuilder's internal bitset data to be used by +// GeneratePixels +func (s *sixelBuilder) SetColor(x int, y int, color color.Color) { + bandY := y / 6 + paletteIndex := s.SixelPalette.ColorIndex(sixelConvertColor(color)) + + bit := s.imageWidth*6*paletteIndex + (x * 6) + (y % 6) + s.pixelBands[bandY].Set(uint(bit)) +} + +// GeneratePixels is used to write the pixel data to the internal imageData string builder. +// All pixels in the image must be written to the sixelBuilder using SetColor before this method is +// called. This method returns a string that represents the pixel data. Sixel strings consist of five parts: +// ISC

ST +// The header contains some arbitrary options indicating how the sixel image is to be drwan. +// The palette maps palette indices to RGB colors +// The pixels indicates which pixels are to be drawn with which palette colors. +// +// GeneratePixels only produces the part of the string. The rest is written by +// Style.RenderSixelImage. +func (s *sixelBuilder) GeneratePixels() string { + s.imageData = strings.Builder{} + bandHeight := s.BandHeight() + + for bandY := 0; bandY < bandHeight; bandY++ { + if bandY > 0 { + s.writeControlRune(sixelLineBreak) + } + + hasWrittenAColor := false + + for paletteIndex := 0; paletteIndex < len(s.SixelPalette.PaletteColors); paletteIndex++ { + if s.SixelPalette.PaletteColors[paletteIndex].Alpha < 1 { + // Don't draw anything for purely transparent pixels + continue + } + + firstColorBit := uint(s.imageWidth * 6 * paletteIndex) + nextColorBit := firstColorBit + uint(s.imageWidth*6) + + firstSetBitInBand, anySet := s.pixelBands[bandY].NextSet(firstColorBit) + if !anySet || firstSetBitInBand >= nextColorBit { + // Color not appearing in this row + continue + } + + if hasWrittenAColor { + s.writeControlRune(sixelCarriageReturn) + } + hasWrittenAColor = true + + s.writeControlRune(sixelUseColor) + s.imageData.WriteString(strconv.Itoa(paletteIndex)) + for x := 0; x < s.imageWidth; x += 4 { + bit := firstColorBit + uint(x*6) + word := s.pixelBands[bandY].Word(bit) + + pixel1 := rune((word & 63) + '?') + pixel2 := rune(((word >> 6) & 63) + '?') + pixel3 := rune(((word >> 12) & 63) + '?') + pixel4 := rune(((word >> 18) & 63) + '?') + + s.writeImageRune(pixel1) + + if x+1 >= s.imageWidth { + continue + } + s.writeImageRune(pixel2) + + if x+2 >= s.imageWidth { + continue + } + s.writeImageRune(pixel3) + + if x+3 >= s.imageWidth { + continue + } + s.writeImageRune(pixel4) + } + } + } + + s.writeControlRune('-') + return s.imageData.String() +} + +// Sixel accepts a Go image and returns a SixelImage that can be rendered via +// Style.RenderSixelImage +func Sixel(image image.Image) SixelImage { + imageBounds := image.Bounds() + palette := newSixelPalette(image, sixelMaxColors) + scratch := newSixelBuilder(imageBounds.Dx(), imageBounds.Dy(), palette) + + for y := 0; y < imageBounds.Dy(); y++ { + for x := 0; x < imageBounds.Dx(); x++ { + scratch.SetColor(x, y, image.At(x, y)) + } + } + + pixels := scratch.GeneratePixels() + + return SixelImage{ + pixelWidth: imageBounds.Dx(), + pixelHeight: imageBounds.Dy(), + palette: palette, + pixels: pixels, + } +} diff --git a/sixel_palette.go b/sixel_palette.go new file mode 100644 index 00000000..e11207a5 --- /dev/null +++ b/sixel_palette.go @@ -0,0 +1,346 @@ +package lipgloss + +import ( + "cmp" + "container/heap" + "image" + "image/color" + "math" + "slices" +) + +// sixelPalette is a palette of up to 256 colors that lists the colors that will be used by +// a SixelImage. Most images, especially jpegs, have more than 256 colors, so creating a sixelPalette +// requires the use of color quantization. For this we use the Median Cut algorithm. +// +// Median cut requires all pixels in an image to be positioned in a 4D color cube, with one axis per channel. +// The cube is sliced in half along its longest axis such that half the pixels in the cube end up in one of +// the sub-cubes and half end up in the other. We continue slicing the cube with the longest axis in half +// along that axis until there are 256 sub-cubes. Then, the average of all pixels in each subcube is used +// as that cube's color. +// +// Colors are converted to palette colors based on which they are closest to (it's not +// always their cube's color). +// +// This implementation has a few minor (but seemingly very common) differences from the Official algorithm: +// - When determining the longest axis, the number of pixels in the cube are multiplied against axis length +// This improves color selection by quite a bit in cases where an image has a lot of space taken up by different +// shades of the same color. +// - If a single color sits on a cut line, all pixels of that color are assigned to one of the subcubes +// rather than try to split them up between the subcubes. This allows us to use a slice of unique colors +// and a map of pixel counts rather than try to represent each pixel individually. +type sixelPalette struct { + // Map used to convert colors from the image to palette colors + colorConvert map[sixelColor]sixelColor + // Backward lookup to get index from palette color + paletteIndexes map[sixelColor]int + PaletteColors []sixelColor +} + +// quantizationChannel is an enum type which indicates an axis in the color cube. Used to indicate which +// axis in a cube is the longest +type quantizationChannel int + +const ( + sixelMaxColors int = 256 + quantizationRed quantizationChannel = iota + quantizationGreen + quantizationBlue + quantizationAlpha +) + +// quantizationCube represents a single cube in the median cut algorithm. +type quantizationCube struct { + // startIndex is the index in the uniqueColors slice where this cube starts + startIndex int + // length is the number of elements in the uniqueColors slice this cube occupies + length int + // sliceChannel is the axis that will be cut if this cube is cut in half + sliceChannel quantizationChannel + // score is a heuristic value: higher means this cube is more likely to be cut + score uint64 + // pixelCount is how many pixels are contained in this cube + pixelCount uint64 +} + +// cubePriorityQueue is a heap used to sort quantizationCube objects in order to select the correct +// one to cut next. Pop will remove the queue with the highest score +type cubePriorityQueue []any + +func (p *cubePriorityQueue) Push(x any) { + *p = append(*p, x) +} + +func (p *cubePriorityQueue) Pop() any { + popped := (*p)[len(*p)-1] + *p = (*p)[:len(*p)-1] + return popped +} + +func (p *cubePriorityQueue) Len() int { + return len(*p) +} + +func (p *cubePriorityQueue) Less(i, j int) bool { + left := (*p)[i].(quantizationCube) + right := (*p)[j].(quantizationCube) + + // We want the largest channel variance + return left.score > right.score +} + +func (p *cubePriorityQueue) Swap(i, j int) { + (*p)[i], (*p)[j] = (*p)[j], (*p)[i] +} + +// createCube is used to initialize a new quantizationCube containing a region of the uniqueColors slice +func (p *sixelPalette) createCube(uniqueColors []sixelColor, pixelCounts map[sixelColor]uint64, startIndex, bucketLength int) quantizationCube { + minRed, minGreen, minBlue, minAlpha := uint32(0xffff), uint32(0xffff), uint32(0xffff), uint32(0xffff) + maxRed, maxGreen, maxBlue, maxAlpha := uint32(0), uint32(0), uint32(0), uint32(0) + totalWeight := uint64(0) + + // Figure out which channel has the greatest variance + for i := startIndex; i < startIndex+bucketLength; i++ { + r, g, b, a := uniqueColors[i].Red, uniqueColors[i].Green, uniqueColors[i].Blue, uniqueColors[i].Alpha + totalWeight += pixelCounts[uniqueColors[i]] + + if r < minRed { + minRed = r + } + if r > maxRed { + maxRed = r + } + if g < minGreen { + minGreen = g + } + if g > maxGreen { + maxGreen = g + } + if b < minBlue { + minBlue = b + } + if b > maxBlue { + maxBlue = b + } + if a < minAlpha { + minAlpha = a + } + if a > maxAlpha { + maxAlpha = a + } + } + + dRed := maxRed - minRed + dGreen := maxGreen - minGreen + dBlue := maxBlue - minBlue + dAlpha := maxAlpha - minAlpha + + cube := quantizationCube{ + startIndex: startIndex, + length: bucketLength, + pixelCount: totalWeight, + } + + if dRed >= dGreen && dRed >= dBlue && dRed >= dAlpha { + cube.sliceChannel = quantizationRed + cube.score = uint64(dRed) + } else if dGreen >= dBlue && dGreen >= dAlpha { + cube.sliceChannel = quantizationGreen + cube.score = uint64(dGreen) + } else if dBlue >= dAlpha { + cube.sliceChannel = quantizationBlue + cube.score = uint64(dBlue) + } else { + cube.sliceChannel = quantizationAlpha + cube.score = uint64(dAlpha) + } + + // Boost the score of cubes with more pixels in them + cube.score *= totalWeight + + return cube +} + +// quantize is a method that will initialize the palette's colors and lookups, provided a set +// of unique colors and a map containing pixel counts for those colors +func (p *sixelPalette) quantize(uniqueColors []sixelColor, pixelCounts map[sixelColor]uint64, maxColors int) { + p.colorConvert = make(map[sixelColor]sixelColor) + p.paletteIndexes = make(map[sixelColor]int) + + // We don't need to quantize if we don't even have more than the maximum colors, and in fact, this code will explode + // if we have fewer than maximum colors + if len(uniqueColors) <= maxColors { + p.PaletteColors = uniqueColors + return + } + + cubeHeap := make(cubePriorityQueue, 0, maxColors) + + // Start with a cube that contains all colors + heap.Init(&cubeHeap) + heap.Push(&cubeHeap, p.createCube(uniqueColors, pixelCounts, 0, len(uniqueColors))) + + // Slice the best cube into two cubes until we have max colors, then we have our palette + for cubeHeap.Len() < maxColors { + cubeToSplit := heap.Pop(&cubeHeap).(quantizationCube) + + // Sort the colors in the bucket's range along the cube's longest color axis + slices.SortFunc(uniqueColors[cubeToSplit.startIndex:cubeToSplit.startIndex+cubeToSplit.length], + func(left sixelColor, right sixelColor) int { + switch cubeToSplit.sliceChannel { + case quantizationRed: + return cmp.Compare(left.Red, right.Red) + case quantizationGreen: + return cmp.Compare(left.Green, right.Green) + case quantizationBlue: + return cmp.Compare(left.Blue, right.Blue) + default: + return cmp.Compare(left.Alpha, right.Alpha) + } + }) + + // We need to split up the colors in this cube so that the pixels are evenly split between the two, + // or at least as close as we can reasonably get. What we do is count up the pixels as we go through + // and place the cut point where around half of the pixels are on the left side + countSoFar := pixelCounts[uniqueColors[cubeToSplit.startIndex]] + targetCount := cubeToSplit.pixelCount / 2 + leftLength := 1 + + for i := cubeToSplit.startIndex + 1; i < cubeToSplit.startIndex+cubeToSplit.length; i++ { + c := uniqueColors[i] + weight := pixelCounts[c] + if countSoFar+weight > targetCount { + break + } + leftLength++ + countSoFar += weight + } + + rightLength := cubeToSplit.length - leftLength + rightIndex := cubeToSplit.startIndex + leftLength + heap.Push(&cubeHeap, p.createCube(uniqueColors, pixelCounts, cubeToSplit.startIndex, leftLength)) + heap.Push(&cubeHeap, p.createCube(uniqueColors, pixelCounts, rightIndex, rightLength)) + } + + // Once we've got max cubes in the heap, pull them all out and load them into the palette + for cubeHeap.Len() > 0 { + bucketToLoad := heap.Pop(&cubeHeap).(quantizationCube) + p.loadColor(uniqueColors, pixelCounts, bucketToLoad.startIndex, bucketToLoad.length) + } +} + +// ColorIndex accepts a palette color and provides the palette index of that color +func (p *sixelPalette) ColorIndex(c sixelColor) int { + return p.paletteIndexes[c] +} + +// loadColor accepts a range of colors representing a single median cut cube. It calculates the +// average color in the cube and adds it to the palette. +func (p *sixelPalette) loadColor(uniqueColors []sixelColor, pixelCounts map[sixelColor]uint64, startIndex, cubeLen int) { + totalRed, totalGreen, totalBlue, totalAlpha := uint64(0), uint64(0), uint64(0), uint64(0) + totalCount := uint64(0) + for i := startIndex; i < startIndex+cubeLen; i++ { + count := pixelCounts[uniqueColors[i]] + totalRed += uint64(uniqueColors[i].Red) * count + totalGreen += uint64(uniqueColors[i].Green) * count + totalBlue += uint64(uniqueColors[i].Blue) * count + totalAlpha += uint64(uniqueColors[i].Alpha) * count + totalCount += count + } + + averageColor := sixelColor{ + Red: uint32(totalRed / totalCount), + Green: uint32(totalGreen / totalCount), + Blue: uint32(totalBlue / totalCount), + Alpha: uint32(totalAlpha / totalCount), + } + + p.PaletteColors = append(p.PaletteColors, averageColor) +} + +// sixelColor is a flat struct that contains a single color: all channels are 0-100 +// instead of anything sensible +type sixelColor struct { + Red uint32 + Green uint32 + Blue uint32 + Alpha uint32 +} + +// sixelConvertColor accepts an ordinary Go color and converts it to a sixelColor, which +// has channels ranging from 0-100 +func sixelConvertColor(c color.Color) sixelColor { + r, g, b, a := c.RGBA() + return sixelColor{ + Red: sixelConvertChannel(r), + Green: sixelConvertChannel(g), + Blue: sixelConvertChannel(b), + Alpha: sixelConvertChannel(a), + } +} + +// sixelConvertChannel converts a single color channel from go's standard 0-0xffff range to +// sixel's 0-100 range +func sixelConvertChannel(channel uint32) uint32 { + // We add 327 because that is about 0.5 in the sixel 0-100 color range, we're trying to + // round to the nearest value + return (channel + 328) * 100 / 0xffff +} + +// newSixelPalette accepts an image and produces an N-color quantized color palette using the median cut +// algorithm. The produced sixelPalette can convert colors from the image to the quantized palette +// in O(1) time. +func newSixelPalette(image image.Image, maxColors int) sixelPalette { + pixelCounts := make(map[sixelColor]uint64) + + height := image.Bounds().Dy() + width := image.Bounds().Dx() + + // Record pixel counts for every color while also getting a set of all unique colors in the image + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + c := sixelConvertColor(image.At(x, y)) + count, _ := pixelCounts[c] + count++ + + pixelCounts[c] = count + } + } + + p := sixelPalette{} + uniqueColors := make([]sixelColor, 0, len(pixelCounts)) + for c := range pixelCounts { + uniqueColors = append(uniqueColors, c) + } + + // Build up p.PaletteColors using the median cut algorithm + p.quantize(uniqueColors, pixelCounts, maxColors) + + // The average color for a cube a color occupies is not always the closest palette color. As a result, + // we need to use this very upsetting double loop to find the lookup palette color for each + // unique color in the image. + for _, c := range uniqueColors { + var bestColor sixelColor + var bestColorIndex int + bestScore := uint32(math.MaxUint32) + + for paletteIndex, paletteColor := range p.PaletteColors { + redDiff := c.Red - paletteColor.Red + greenDiff := c.Green - paletteColor.Green + blueDiff := c.Blue - paletteColor.Blue + alphaDiff := c.Alpha - paletteColor.Alpha + + score := (redDiff * redDiff) + (greenDiff * greenDiff) + (blueDiff * blueDiff) + (alphaDiff * alphaDiff) + if score < bestScore { + bestColor = paletteColor + bestColorIndex = paletteIndex + bestScore = score + } + } + + p.paletteIndexes[c] = bestColorIndex + p.colorConvert[c] = bestColor + } + + return p +} diff --git a/style.go b/style.go index b4d4627e..34d7c02b 100644 --- a/style.go +++ b/style.go @@ -2,6 +2,7 @@ package lipgloss import ( "image/color" + "strconv" "strings" "unicode" @@ -499,6 +500,90 @@ func (s Style) applyMargins(str string, inline bool) string { return str } +// RenderSixelImage produces an ANSI-escaped string that, when written to a compatible +// terminal, will display the provided SixelImage. Incompatible terminals may display nothing, +// or may print the (very large) ANSI-escaped string as plain text. On most terminals, the size on +// screen will generally match SixelImage.Width and SixelImage.Height. However, compatible Windows +// terminals will always print 10 pixels per character horizontally and 20 pixels per character vertically, +// which will distort the image based on the current font. +// +// Fully-transparent pixels in the image will always display the terminal's background color under +// the image regardless of compatibility or settings. Semi-transparent pixels will attempt to mix +// the pixel color with the background color stored in this Style. If this Style has no background color, +// then the alpha channel for semi-transparent pixels will be ignored and the pixel color will be displayed +// as-is. +func (s Style) RenderSixelImage(image SixelImage) string { + b := strings.Builder{} + b.WriteRune(ansi.ESC) + // P;;q + // a = pixel aspect ratio (deprecated) + // b = how to color unfilled pixels, 1 = transparent + // c = horizontal grid size, I think everyone ignores this + b.WriteString("P0;1;0q") + // ";;; + // a = pixel width + // b = pixel height + // c = image width in pixels + // d = image height in pixels + b.WriteString("\"1;1;") + b.WriteString(strconv.Itoa(image.Width())) + b.WriteString(";") + b.WriteString(strconv.Itoa(image.Height())) + + bgColor := s.GetBackground() + hasBackground := bgColor != noColor + var bgRed, bgGreen, bgBlue uint32 + + if hasBackground { + styleRed, styleGreen, styleBlue, _ := bgColor.RGBA() + + // Sixel palette entries are 0-100 instead of any normal color system + bgRed = sixelConvertChannel(styleRed) + bgGreen = sixelConvertChannel(styleGreen) + bgBlue = sixelConvertChannel(styleBlue) + } + + for paletteIndex, c := range image.palette.PaletteColors { + // Initializing palette entries + // #;;;; + // a = palette index + // b = color type, 2 is RGB + // c = R + // d = G + // e = B + b.WriteRune(sixelUseColor) + b.WriteString(strconv.Itoa(paletteIndex)) + b.WriteString(";2;") + + var paletteRed, paletteGreen, paletteBlue uint32 + if hasBackground { + // Handle semi-transparency by mixing palette colors with the style's background + paletteRed = (c.Red*c.Alpha + bgRed*(100-c.Alpha)) / 100 + paletteGreen = (c.Green*c.Alpha + bgGreen*(100-c.Alpha)) / 100 + paletteBlue = (c.Blue*c.Alpha + bgBlue*(100-c.Alpha)) / 100 + } else { + paletteRed = c.Red + paletteGreen = c.Green + paletteBlue = c.Blue + } + + b.WriteString(strconv.Itoa(int(paletteRed))) + b.WriteRune(';') + b.WriteString(strconv.Itoa(int(paletteGreen))) + b.WriteRune(';') + b.WriteString(strconv.Itoa(int(paletteBlue))) + } + + // Encoded pixel data, this is all set up in sixelBuilder.GeneratePixels + b.WriteString(image.pixels) + + // ST ends the image + b.WriteRune(ansi.ESC) + b.WriteRune('\\') + + return b.String() +} + // Apply left padding. func padLeft(str string, n int, style *ansi.Style) string { return pad(str, -n, style) From 2c992946cea71f0a54524b9c85bffeee38f4f151 Mon Sep 17 00:00:00 2001 From: Stephen Baynham Date: Thu, 14 Nov 2024 15:30:45 -0800 Subject: [PATCH 2/4] tests and a few minor improvements --- examples/go.mod | 4 +- examples/go.sum | 14 +-- go.mod | 2 +- go.sum | 3 + sixel.go | 23 ++-- sixel_palette.go | 2 +- sixel_palette_test.go | 124 +++++++++++++++++++ sixel_test.go | 272 ++++++++++++++++++++++++++++++++++++++++++ style.go | 6 +- 9 files changed, 417 insertions(+), 33 deletions(-) create mode 100644 sixel_palette_test.go create mode 100644 sixel_test.go diff --git a/examples/go.mod b/examples/go.mod index 8b11d53f..abc6de2d 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -18,7 +18,7 @@ require ( require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/bits-and-blooms/bitset v1.14.3 // indirect + github.com/bits-and-blooms/bitset v1.15.0 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/keygen v0.5.0 // indirect github.com/charmbracelet/lipgloss v0.13.1-0.20240822211938-b89f1a3db2a4 // indirect @@ -26,11 +26,9 @@ require ( github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect - github.com/charmbracelet/x/input v0.2.0 // indirect github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/creack/pty v1.1.21 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/examples/go.sum b/examples/go.sum index 1b07d9f4..cd671bb8 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,11 +1,10 @@ -github.com/CannibalVox/bitset v1.0.0 h1:fXgciTVIjZFPsV1Amr6mhkYcHBg0aQhYYMQZ3E2J27M= -github.com/CannibalVox/bitset v1.0.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.15.0 h1:DiCRMscZsGyYePE9AR3sVhKqUXCt5IZvkX5AfAc5xLQ= +github.com/bits-and-blooms/bitset v1.15.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241031200731-4f70d4c680b8 h1:qm5hqKutTe3DcJHlEghLlZxBoKv+hqa8avqEUiSKIZY= @@ -22,8 +21,6 @@ github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 h1:NZKjJ7d/pzk/A github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U= github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc= github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk= -github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= -github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI= @@ -31,8 +28,6 @@ github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2 github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE= -github.com/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A= -github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= @@ -42,10 +37,6 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU= -github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -79,7 +70,6 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= diff --git a/go.mod b/go.mod index 5aabc19a..a40f1ac8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/aymanbagabas/go-udiff v0.2.0 - github.com/bits-and-blooms/bitset v1.14.3 + github.com/bits-and-blooms/bitset v1.15.0 github.com/charmbracelet/colorprofile v0.1.2 github.com/charmbracelet/x/ansi v0.4.5 github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a diff --git a/go.sum b/go.sum index 306f8252..3e7913cb 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,10 @@ github.com/CannibalVox/bitset v1.0.0 h1:fXgciTVIjZFPsV1Amr6mhkYcHBg0aQhYYMQZ3E2J github.com/CannibalVox/bitset v1.0.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.15.0 h1:DiCRMscZsGyYePE9AR3sVhKqUXCt5IZvkX5AfAc5xLQ= +github.com/bits-and-blooms/bitset v1.15.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/colorprofile v0.1.2 h1:nuB1bd/yAExT4fkcZvpqtQ2N5/8cJHSRIKb6CzT7lAM= github.com/charmbracelet/colorprofile v0.1.2/go.mod h1:1htIKZYeI4TQs+OykPvpuBTUbUJxBYeSYBDIZuejMj0= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= diff --git a/sixel.go b/sixel.go index c7904742..ce814dd2 100644 --- a/sixel.go +++ b/sixel.go @@ -25,13 +25,13 @@ type SixelImage struct { pixels string } -// Width gets the width of the image in pixels -func (i SixelImage) Width() int { +// PixelWidth gets the width of the image in pixels +func (i SixelImage) PixelWidth() int { return i.pixelWidth } -// Height gets the height of the image in pixels -func (i SixelImage) Height() int { +// PixelHeight gets the height of the image in pixels +func (i SixelImage) PixelHeight() int { return i.pixelHeight } @@ -48,7 +48,7 @@ type sixelBuilder struct { imageHeight int imageWidth int - pixelBands []bitset.BitSet + pixelBands bitset.BitSet imageData strings.Builder repeatRune rune @@ -63,9 +63,6 @@ func newSixelBuilder(width, height int, palette sixelPalette) sixelBuilder { SixelPalette: palette, } - bandHeight := scratch.BandHeight() - scratch.pixelBands = make([]bitset.BitSet, bandHeight) - return scratch } @@ -131,8 +128,8 @@ func (s *sixelBuilder) SetColor(x int, y int, color color.Color) { bandY := y / 6 paletteIndex := s.SixelPalette.ColorIndex(sixelConvertColor(color)) - bit := s.imageWidth*6*paletteIndex + (x * 6) + (y % 6) - s.pixelBands[bandY].Set(uint(bit)) + bit := s.BandHeight()*s.imageWidth*6*paletteIndex + bandY*s.imageWidth*6 + (x * 6) + (y % 6) + s.pixelBands.Set(uint(bit)) } // GeneratePixels is used to write the pixel data to the internal imageData string builder. @@ -162,10 +159,10 @@ func (s *sixelBuilder) GeneratePixels() string { continue } - firstColorBit := uint(s.imageWidth * 6 * paletteIndex) + firstColorBit := uint(s.BandHeight()*s.imageWidth*6*paletteIndex + bandY*s.imageWidth*6) nextColorBit := firstColorBit + uint(s.imageWidth*6) - firstSetBitInBand, anySet := s.pixelBands[bandY].NextSet(firstColorBit) + firstSetBitInBand, anySet := s.pixelBands.NextSet(firstColorBit) if !anySet || firstSetBitInBand >= nextColorBit { // Color not appearing in this row continue @@ -180,7 +177,7 @@ func (s *sixelBuilder) GeneratePixels() string { s.imageData.WriteString(strconv.Itoa(paletteIndex)) for x := 0; x < s.imageWidth; x += 4 { bit := firstColorBit + uint(x*6) - word := s.pixelBands[bandY].Word(bit) + word := s.pixelBands.GetWord64AtBit(bit) pixel1 := rune((word & 63) + '?') pixel2 := rune(((word >> 6) & 63) + '?') diff --git a/sixel_palette.go b/sixel_palette.go index e11207a5..ebc1b291 100644 --- a/sixel_palette.go +++ b/sixel_palette.go @@ -229,7 +229,7 @@ func (p *sixelPalette) quantize(uniqueColors []sixelColor, pixelCounts map[sixel } } -// ColorIndex accepts a palette color and provides the palette index of that color +// ColorIndex accepts a raw image color (NOT a palette color) and provides the palette index of that color func (p *sixelPalette) ColorIndex(c sixelColor) int { return p.paletteIndexes[c] } diff --git a/sixel_palette_test.go b/sixel_palette_test.go new file mode 100644 index 00000000..a95dc94f --- /dev/null +++ b/sixel_palette_test.go @@ -0,0 +1,124 @@ +package lipgloss + +import ( + "image" + "image/color" + "testing" +) + +type testCase struct { + maxColors int + expectedPalette []sixelColor +} + +func TestPaletteCreationRedGreen(t *testing.T) { + redGreen := image.NewRGBA(image.Rect(0, 0, 2, 2)) + redGreen.Set(0, 0, color.RGBA{255, 0, 0, 255}) + redGreen.Set(0, 1, color.RGBA{128, 0, 0, 255}) + redGreen.Set(1, 0, color.RGBA{0, 255, 0, 255}) + redGreen.Set(1, 1, color.RGBA{0, 128, 0, 255}) + + testCases := map[string]testCase{ + "way too many colors": { + maxColors: 16, + expectedPalette: []sixelColor{ + {100, 0, 0, 100}, + {50, 0, 0, 100}, + {0, 100, 0, 100}, + {0, 50, 0, 100}, + }, + }, + "just the right number of colors": { + maxColors: 4, + expectedPalette: []sixelColor{ + {100, 0, 0, 100}, + {50, 0, 0, 100}, + {0, 100, 0, 100}, + {0, 50, 0, 100}, + }, + }, + "color reduction": { + maxColors: 2, + expectedPalette: []sixelColor{ + {75, 0, 0, 100}, + {0, 75, 0, 100}, + }, + }, + } + + runTests(t, redGreen, testCases) +} + +func TestPaletteWithSemiTransparency(t *testing.T) { + blueAlpha := image.NewRGBA(image.Rect(0, 0, 2, 2)) + blueAlpha.Set(0, 0, color.RGBA{0, 0, 255, 255}) + blueAlpha.Set(0, 1, color.RGBA{0, 0, 128, 255}) + blueAlpha.Set(1, 0, color.RGBA{0, 0, 255, 128}) + blueAlpha.Set(1, 1, color.RGBA{0, 0, 255, 0}) + + testCases := map[string]testCase{ + "just the right number of colors": { + maxColors: 4, + expectedPalette: []sixelColor{ + {0, 0, 100, 100}, + {0, 0, 50, 100}, + {0, 0, 100, 50}, + {0, 0, 100, 0}, + }, + }, + "color reduction": { + maxColors: 2, + expectedPalette: []sixelColor{ + {0, 0, 75, 100}, + {0, 0, 100, 25}, + }, + }, + } + runTests(t, blueAlpha, testCases) +} + +func runTests(t *testing.T, img image.Image, testCases map[string]testCase) { + for testName, test := range testCases { + t.Run(testName, func(t *testing.T) { + palette := newSixelPalette(img, test.maxColors) + if len(palette.PaletteColors) != len(test.expectedPalette) { + t.Errorf("Expected colors %+v in palette, but got %+v", test.expectedPalette, palette.PaletteColors) + return + } + + for _, c := range test.expectedPalette { + var foundColor bool + for _, paletteColor := range palette.PaletteColors { + if paletteColor == c { + foundColor = true + break + } + } + + if !foundColor { + t.Errorf("Expected colors %+v in palette, but got %+v", test.expectedPalette, palette.PaletteColors) + return + } + } + + for lookupRawColor, lookupPaletteColor := range palette.colorConvert { + paletteIndex, inReverseLookup := palette.paletteIndexes[lookupRawColor] + if !inReverseLookup { + t.Errorf("Color %+v maps to color %+v in the colorConvert map, but %+v is does not have a corresponding palette index.", lookupRawColor, lookupPaletteColor, lookupPaletteColor) + return + } + + if paletteIndex >= len(palette.PaletteColors) { + t.Errorf("Image color %+v maps to palette index %d, but there are only %d palette colors.", lookupRawColor, paletteIndex, len(palette.PaletteColors)) + return + } + + colorFromPalette := palette.PaletteColors[paletteIndex] + if colorFromPalette != lookupPaletteColor { + t.Errorf("Image color %+v maps to palette color %+v and palette index %d, but palette index %d is actually palette color %+v", lookupRawColor, lookupPaletteColor, paletteIndex, paletteIndex, colorFromPalette) + return + } + } + }) + } +} diff --git a/sixel_test.go b/sixel_test.go new file mode 100644 index 00000000..5489c8d8 --- /dev/null +++ b/sixel_test.go @@ -0,0 +1,272 @@ +package lipgloss + +import ( + "image" + "image/color" + "regexp" + "strconv" + "strings" + "testing" +) + +func TestFullImage(t *testing.T) { + colorDrawRegex, err := regexp.Compile("^#(\\d+)(.+)$") + if err != nil { + t.Errorf("failed to compile regex: %+v", err) + return + } + + testCases := map[string]struct { + imageWidth int + imageHeight int + bandCount int + // When filling the image, we'll use a map of indices to colors and change colors every + // time the current index is in the map- this will prevent dozens of lines with the same color + // in a row and make this slightly more legible + colors map[int]color.RGBA + // Two bands, with a map of color to pixel strings, since we don't have any control over what + // order the colors will appear + pixels []map[sixelColor]string + }{ + "3x12 single color filled": { + 3, 12, 2, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + }, + []map[sixelColor]string{ + { + // ~ means all 6 pixels filled + {100, 0, 0, 100}: "~~~", + }, + { + {100, 0, 0, 100}: "~~~", + }, + }, + }, + "3x12 two color filled": { + 3, 12, 2, + map[int]color.RGBA{ + // 3-pixel high alternating bands + 0: {0, 0, 255, 255}, + 9: {0, 255, 0, 255}, + 18: {0, 0, 255, 255}, + 27: {0, 255, 0, 255}, + }, + []map[sixelColor]string{ + { + // F means top 3 pixels filled, w means bottom 3 pixels filled + {0, 0, 100, 100}: "FFF", + {0, 100, 0, 100}: "www", + }, + { + {0, 0, 100, 100}: "FFF", + {0, 100, 0, 100}: "www", + }, + }, + }, + "3x12 8 color with right gutter": { + 3, 12, 2, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + 2: {0, 255, 0, 255}, + 3: {255, 0, 0, 255}, + 5: {0, 255, 0, 255}, + 6: {255, 0, 0, 255}, + 8: {0, 255, 0, 255}, + 9: {0, 0, 255, 255}, + 11: {128, 128, 0, 255}, + 12: {0, 0, 255, 255}, + 14: {128, 128, 0, 255}, + 15: {0, 0, 255, 255}, + 17: {128, 128, 0, 255}, + 18: {0, 128, 128, 255}, + 20: {128, 0, 128, 255}, + 21: {0, 128, 128, 255}, + 23: {128, 0, 128, 255}, + 24: {0, 128, 128, 255}, + 26: {128, 0, 128, 255}, + 27: {64, 0, 0, 255}, + 29: {0, 64, 0, 255}, + 30: {64, 0, 0, 255}, + 32: {0, 64, 0, 255}, + 33: {64, 0, 0, 255}, + 35: {0, 64, 0, 255}, + }, + []map[sixelColor]string{ + { + // F means top 3 pixels filled, w means bottom 3 pixels filled + // ? means no pixels filled + {100, 0, 0, 100}: "FF?", + {0, 100, 0, 100}: "??F", + {0, 0, 100, 100}: "ww?", + {50, 50, 0, 100}: "??w", + }, + { + {0, 50, 50, 100}: "FF?", + {50, 0, 50, 100}: "??F", + {25, 0, 0, 100}: "ww?", + {0, 25, 0, 100}: "??w", + }, + }, + }, + "3x12 single color with transparent band in the middle": { + 3, 12, 2, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + 15: {0, 0, 0, 0}, + 21: {255, 0, 0, 255}, + }, + []map[sixelColor]string{ + { + // ^ means all pixels except the bottom one filled + {100, 0, 0, 100}: "^^^", + }, + { + // } means all pixels except the top one filled + {100, 0, 0, 100}: "}}}", + }, + }, + }, + "3x5 single color": { + 3, 5, 1, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + }, + []map[sixelColor]string{ + { + // ^ means all pixels except the bottom one filled + {100, 0, 0, 100}: "^^^", + }, + }, + }, + "12x4 single color use RLE": { + 12, 4, 1, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + }, + []map[sixelColor]string{ + { + // N means the top 4 pixels filled, + {100, 0, 0, 100}: "!12N", + }, + }, + }, + "12x1 two color use RLE": { + 12, 1, 1, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + 6: {0, 255, 0, 255}, + }, + []map[sixelColor]string{ + { + // @ means just the top pixel filled, ? means no pixels filled + {100, 0, 0, 100}: "!6@!6?", + {0, 100, 0, 100}: "!6?!6@", + }, + }, + }, + "12x12 single color use RLE": { + 12, 12, 2, + map[int]color.RGBA{ + 0: {255, 0, 0, 255}, + }, + []map[sixelColor]string{ + { + // ~ means all six pixels filled + {100, 0, 0, 100}: "!12~", + }, + { + {100, 0, 0, 100}: "!12~", + }, + }, + }, + } + + for testName, testCase := range testCases { + t.Run(testName, func(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, testCase.imageWidth, testCase.imageHeight)) + + currentColor := color.RGBA{0, 0, 0, 0} + for y := 0; y < testCase.imageHeight; y++ { + for x := 0; x < testCase.imageWidth; x++ { + index := y*testCase.imageWidth + x + newColor, changingColor := testCase.colors[index] + if changingColor { + currentColor = newColor + } + + img.Set(x, y, currentColor) + } + } + + sixelImage := Sixel(img) + if sixelImage.PixelHeight() != testCase.imageHeight { + t.Errorf("SixelImage had a height of %d, but a height of %d was expected", sixelImage.PixelHeight(), testCase.imageHeight) + return + } + if sixelImage.PixelWidth() != testCase.imageWidth { + t.Errorf("SixelImage had a width of %d, but a width of %d was expected", sixelImage.PixelWidth(), testCase.imageWidth) + return + } + + if !strings.HasSuffix(sixelImage.pixels, string(sixelLineBreak)) { + t.Errorf("SixelImage pixels were expected to end with a linebreak character '-' but instead ended with %q", sixelImage.pixels[len(sixelImage.pixels)-1]) + return + } + + pixelLines := strings.Split(sixelImage.pixels, string(sixelLineBreak)) + // We expect some bands of pixels, followed by a linebreak, so splitting on linebreak should produce + // one more entry than the number of bands + lineCount := len(pixelLines) - 1 + + if lineCount != testCase.bandCount { + t.Errorf("The SixelImage pixels are supposed to have %d bands of pixels but have %d instead", testCase.bandCount, lineCount) + return + } + + // Check each band of pixels in the image + for bandY := 0; bandY < lineCount; bandY++ { + expectedBandColors := testCase.pixels[bandY] + colorDraws := strings.Split(pixelLines[bandY], string(sixelCarriageReturn)) + + if len(colorDraws) != len(expectedBandColors) { + t.Errorf("The SixelImage had %d colors in band %d, but was expected to have %d.", len(colorDraws), bandY, len(expectedBandColors)) + return + } + + // Individually check each color in the band + for _, colorPixels := range colorDraws { + // # + matches := colorDrawRegex.FindStringSubmatch(colorPixels) + if matches == nil { + t.Errorf("Could not locate color palette change in color draw substring %q", colorPixels) + return + } + + paletteIndex, err := strconv.Atoi(matches[1]) + if err != nil { + t.Errorf("failed to convert %q to a palette index", matches[1]) + return + } + + if paletteIndex >= len(sixelImage.palette.PaletteColors) { + t.Errorf("Found palette index %d in the pixel buffer, but the palette only has %d colors.", paletteIndex, len(sixelImage.palette.PaletteColors)) + return + } + + foundColor := sixelImage.palette.PaletteColors[paletteIndex] + expectedPixelString, hasColor := expectedBandColors[foundColor] + if !hasColor { + t.Errorf("Found palette index %d (color %+v) with pixels %q in the pixel buffer, but it was not expected on band %d.", paletteIndex, foundColor, matches[2], bandY) + return + } + + if expectedPixelString != matches[2] { + t.Errorf("Palette index %d (color %+v) had pixel buffer %q, but expected %q", paletteIndex, foundColor, matches[2], expectedPixelString) + return + } + } + } + }) + } +} diff --git a/style.go b/style.go index 34d7c02b..96ab8d73 100644 --- a/style.go +++ b/style.go @@ -503,7 +503,7 @@ func (s Style) applyMargins(str string, inline bool) string { // RenderSixelImage produces an ANSI-escaped string that, when written to a compatible // terminal, will display the provided SixelImage. Incompatible terminals may display nothing, // or may print the (very large) ANSI-escaped string as plain text. On most terminals, the size on -// screen will generally match SixelImage.Width and SixelImage.Height. However, compatible Windows +// screen will generally match SixelImage.PixelWidth and SixelImage.PixelHeight. However, compatible Windows // terminals will always print 10 pixels per character horizontally and 20 pixels per character vertically, // which will distort the image based on the current font. // @@ -526,9 +526,9 @@ func (s Style) RenderSixelImage(image SixelImage) string { // c = image width in pixels // d = image height in pixels b.WriteString("\"1;1;") - b.WriteString(strconv.Itoa(image.Width())) + b.WriteString(strconv.Itoa(image.PixelWidth())) b.WriteString(";") - b.WriteString(strconv.Itoa(image.Height())) + b.WriteString(strconv.Itoa(image.PixelHeight())) bgColor := s.GetBackground() hasBackground := bgColor != noColor From c0fc732c9b8113bfa5de65c3148fb60fbaf2ce13 Mon Sep 17 00:00:00 2001 From: Stephen Baynham Date: Thu, 14 Nov 2024 15:40:40 -0800 Subject: [PATCH 3/4] try to reorder methods to make it easier to read --- sixel.go | 134 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/sixel.go b/sixel.go index ce814dd2..9e67ac47 100644 --- a/sixel.go +++ b/sixel.go @@ -8,6 +8,16 @@ import ( "strings" ) +// Sixels are a protocol for writing images to the terminal by writing a large blob of ANSI-escaped data. +// They function by encoding columns of 6 pixels into a single character (in much the same way base64 +// encodes data 6 bits at a time). Sixel images are paletted, with a palette established at the beginning +// of the image blob and pixels identifying palette entires by index while writing the pixel data. +// +// Sixels are written one 6-pixel-tall band at a time, one color at a time. For each band, a single +// color's pixels are written, then a carriage return is written to bring the "cursor" back to the +// beginning of a band where a new color is selected and pixels written. This continues until the entire +// band has been drawn, at which time a line break is written to begin the next band. + const ( sixelLineBreak = '-' sixelCarriageReturn = '$' @@ -35,6 +45,29 @@ func (i SixelImage) PixelHeight() int { return i.pixelHeight } +// Sixel accepts a Go image and returns a SixelImage that can be rendered via +// Style.RenderSixelImage +func Sixel(image image.Image) SixelImage { + imageBounds := image.Bounds() + palette := newSixelPalette(image, sixelMaxColors) + scratch := newSixelBuilder(imageBounds.Dx(), imageBounds.Dy(), palette) + + for y := 0; y < imageBounds.Dy(); y++ { + for x := 0; x < imageBounds.Dx(); x++ { + scratch.SetColor(x, y, image.At(x, y)) + } + } + + pixels := scratch.GeneratePixels() + + return SixelImage{ + pixelWidth: imageBounds.Dx(), + pixelHeight: imageBounds.Dy(), + palette: palette, + pixels: pixels, + } +} + // sixelBuilder is a temporary structure used to create a SixelImage. It handles // breaking pixels out into bits, and then encoding them into a sixel data string. RLE // handling is included. @@ -66,52 +99,6 @@ func newSixelBuilder(width, height int, palette sixelPalette) sixelBuilder { return scratch } -// flushRepeats is used to actually write the current repeatRune to the imageData when -// it is about to change. This buffering is used to manage RLE in the sixelBuilder -func (s *sixelBuilder) flushRepeats() { - if s.repeatCount == 0 { - return - } - - // Only write using the RLE form if it's actually providing space savings - if s.repeatCount > 3 { - countStr := strconv.Itoa(s.repeatCount) - s.imageData.WriteRune(sixelRepeat) - s.imageData.WriteString(countStr) - s.imageData.WriteRune(s.repeatRune) - return - } - - for i := 0; i < s.repeatCount; i++ { - s.imageData.WriteRune(s.repeatRune) - } -} - -// writeImageRune will write a single line of six pixels to pixel data. The data -// doesn't get written to the imageData, it gets buffered for the purposes of RLE -func (s *sixelBuilder) writeImageRune(r rune) { - if r == s.repeatRune { - s.repeatCount++ - return - } - - s.flushRepeats() - s.repeatRune = r - s.repeatCount = 1 -} - -// writeControlRune will write a special rune such as a new line or carriage return -// rune. It will call flushRepeats first, if necessary. -func (s *sixelBuilder) writeControlRune(r rune) { - if s.repeatCount > 0 { - s.flushRepeats() - s.repeatCount = 0 - s.repeatRune = 0 - } - - s.imageData.WriteRune(r) -} - // BandHeight returns the number of six-pixel bands this image consists of func (s *sixelBuilder) BandHeight() int { bandHeight := s.imageHeight / 6 @@ -208,25 +195,48 @@ func (s *sixelBuilder) GeneratePixels() string { return s.imageData.String() } -// Sixel accepts a Go image and returns a SixelImage that can be rendered via -// Style.RenderSixelImage -func Sixel(image image.Image) SixelImage { - imageBounds := image.Bounds() - palette := newSixelPalette(image, sixelMaxColors) - scratch := newSixelBuilder(imageBounds.Dx(), imageBounds.Dy(), palette) +// writeImageRune will write a single line of six pixels to pixel data. The data +// doesn't get written to the imageData, it gets buffered for the purposes of RLE +func (s *sixelBuilder) writeImageRune(r rune) { + if r == s.repeatRune { + s.repeatCount++ + return + } - for y := 0; y < imageBounds.Dy(); y++ { - for x := 0; x < imageBounds.Dx(); x++ { - scratch.SetColor(x, y, image.At(x, y)) - } + s.flushRepeats() + s.repeatRune = r + s.repeatCount = 1 +} + +// writeControlRune will write a special rune such as a new line or carriage return +// rune. It will call flushRepeats first, if necessary. +func (s *sixelBuilder) writeControlRune(r rune) { + if s.repeatCount > 0 { + s.flushRepeats() + s.repeatCount = 0 + s.repeatRune = 0 } - pixels := scratch.GeneratePixels() + s.imageData.WriteRune(r) +} - return SixelImage{ - pixelWidth: imageBounds.Dx(), - pixelHeight: imageBounds.Dy(), - palette: palette, - pixels: pixels, +// flushRepeats is used to actually write the current repeatRune to the imageData when +// it is about to change. This buffering is used to manage RLE in the sixelBuilder +func (s *sixelBuilder) flushRepeats() { + if s.repeatCount == 0 { + return + } + + // Only write using the RLE form if it's actually providing space savings + if s.repeatCount > 3 { + countStr := strconv.Itoa(s.repeatCount) + s.imageData.WriteRune(sixelRepeat) + s.imageData.WriteString(countStr) + s.imageData.WriteRune(s.repeatRune) + return + } + + for i := 0; i < s.repeatCount; i++ { + s.imageData.WriteRune(s.repeatRune) } } From dd50139317dd252811e1ebbab6a24739bd92058f Mon Sep 17 00:00:00 2001 From: Stephen Baynham Date: Thu, 14 Nov 2024 16:08:45 -0800 Subject: [PATCH 4/4] minor comment fail --- sixel_palette.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel_palette.go b/sixel_palette.go index ebc1b291..5c608b88 100644 --- a/sixel_palette.go +++ b/sixel_palette.go @@ -32,7 +32,7 @@ import ( type sixelPalette struct { // Map used to convert colors from the image to palette colors colorConvert map[sixelColor]sixelColor - // Backward lookup to get index from palette color + // Lookup to get palette index from image color paletteIndexes map[sixelColor]int PaletteColors []sixelColor }