From f7a74244b9a0963d93fc38197d89a71c5d3eeae5 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:19:03 -0500 Subject: [PATCH] Examples: graphics, framebuffer, font, PBM and BMP examples --- examples/README.md | 23 +++++ examples/assets/font_8x14.bin | Bin 0 -> 3584 bytes examples/assets/font_8x16.bin | Bin 0 -> 4096 bytes examples/assets/font_8x8.bin | Bin 0 -> 2048 bytes examples/assets/longstreet.bmp | Bin 0 -> 1546306 bytes examples/assets/micropython.pbm | Bin 0 -> 2105 bytes examples/assets/runner.bmp | Bin 0 -> 345738 bytes examples/assets/warrior.bmp | Bin 0 -> 73866 bytes examples/bmp565_scroll.py | 67 +++++++++++++++ examples/bmp565_scroll_sprite.py | 142 +++++++++++++++++++++++++++++++ examples/bmp565_simpletest.py | 54 ++++++++++++ examples/bmp565_sprite.py | 124 +++++++++++++++++++++++++++ examples/font_list.py | 79 +++++++++++++++++ examples/framebuf_simpletest.py | 94 ++++++++++++++++++++ examples/graphics_simpletest.py | 87 +++++++++++++++++++ examples/pbm_create_new.py | 90 ++++++++++++++++++++ examples/pbm_simpletest.py | 55 ++++++++++++ examples/pygraphics_landscape.py | 101 ++++++++++++++++++++++ examples/rotations.py | 92 ++++++++++++++++++++ 19 files changed, 1008 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/assets/font_8x14.bin create mode 100644 examples/assets/font_8x16.bin create mode 100644 examples/assets/font_8x8.bin create mode 100644 examples/assets/longstreet.bmp create mode 100644 examples/assets/micropython.pbm create mode 100644 examples/assets/runner.bmp create mode 100644 examples/assets/warrior.bmp create mode 100644 examples/bmp565_scroll.py create mode 100644 examples/bmp565_scroll_sprite.py create mode 100644 examples/bmp565_simpletest.py create mode 100644 examples/bmp565_sprite.py create mode 100644 examples/font_list.py create mode 100644 examples/framebuf_simpletest.py create mode 100644 examples/graphics_simpletest.py create mode 100644 examples/pbm_create_new.py create mode 100644 examples/pbm_simpletest.py create mode 100644 examples/pygraphics_landscape.py create mode 100644 examples/rotations.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7d4d139 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,23 @@ +# Examples + +Small programs that show pygraphics drawing on a display. Each one says in +its docstring what it needs and where it has been run. Run them from the +repository's root, so the ones that load a picture or a font find it in +[assets/](assets/). + +| Example | What it shows | +|---|---| +| [graphics_simpletest.py](graphics_simpletest.py) | Shapes and text drawn straight on the display | +| [framebuf_simpletest.py](framebuf_simpletest.py) | The same shapes drawn into a FrameBuffer, then copied on | +| [pygraphics_landscape.py](pygraphics_landscape.py) | A landscape drawn in bands, scaled to any display | +| [rotations.py](rotations.py) | The display turned through its four rotations | +| [font_list.py](font_list.py) | Every bitmap font in a folder | +| [pbm_simpletest.py](pbm_simpletest.py) | A one-bit PBM picture shown in colour | +| [pbm_create_new.py](pbm_create_new.py) | A one-bit logo drawn, then shown in colour | +| [bmp565_simpletest.py](bmp565_simpletest.py) | A whole BMP picture loaded and shown | +| [bmp565_sprite.py](bmp565_sprite.py) | A sprite walking, read from a sprite sheet | +| [bmp565_scroll.py](bmp565_scroll.py) | A tall picture scrolled a line at a time | +| [bmp565_scroll_sprite.py](bmp565_scroll_sprite.py) | A runner on a scrolling street, played by touch | + +Full apps that use pygraphics are in +[pydevices-examples](https://github.com/PyDevices/pydevices-examples). diff --git a/examples/assets/font_8x14.bin b/examples/assets/font_8x14.bin new file mode 100644 index 0000000000000000000000000000000000000000..2c6d8bb1270b3fb1cb96ce8f27749378cfb3c5fb GIT binary patch literal 3584 zcmZWrKa1m55PxhymGEskku`fwiYr9|J`sdB9Qg`qQiI2HoCE{v4l{CSehjb-W0oBuO!Ml$9Q%FDklFW;w~W0$e|>&_{>%2RA~cFt={8*{ z=bP$w*X(YqH$*Z=!D!k{N5K(e@H2{{QhkE~fWbxZdbL{pegYt*a9LuVvq|U*!WobZ zes)?oO@cwK(6Hz&z_+jog+k;wYqFU6_*PYwXe8{JZR|5oJ5G|KP;8*Y%7rMAxq*uy3K_+d zAo5YXZpyU5Z`$7=dmm8!f)0rX{BFr@xw^Kj=h?@=eg8S4o3sNOI z?;e`XjmIbifK8ajF+fs$p?tcdplOg3ygdx$WCamnUTZ`uCyIINe zu+Yviw^=r)GKtrq7p|%_hB1^|OgHW^5-nWgX^H(gqkK!wK%0TF2ze4hr=3Bk zp)^ShL10D!05L-doH+`poXpm7g8Z>rEEZj0YpB%8$G&vA21c}^^G1BpzB21s-ccxq zi?POc9LIYkcru0p281w(maOUA1(gdZ^&<=w>{CwVErU?bxoK)lDLLgMM00^ZA0?VB z)Kl73ybt7pP0!Z+=x)+wf^t`(sluSZMqT6bexhRsRe_Kh)E1mQh6W#C366eGgVr?9 z$N&|EvnBdW>E~2?a!Ac!Lfx;_PY8NkDw&$v7Q<3=tB;%xUth$TVk&a5gEI z%U?;SncBK;5bX^i;#pUBh;y@2R%n0?uy{Vq645ASdXgch6`NCC$rV2fl%!pZz;JY^ zJIR7S@^^c}5Ieo=_3g2%>)k)~rmp$4j!uvhdT&QdRF?wmb4?ZN^Fp*f2Eqn@;!JdB z9&4u~jEtw&PP^F+&yZ2h0@2X%xH3P9Xoub{^b}~&f4r(HS1;pi{WgrI#t4?*{5fSZ zpH+HStJK6U?L^ncU~QL-efh)i_v7Eg-@m^Nf0U5VU^m#=MT$)X9^|1$n*nyn`4A|l zaQHjRH3zhZKpN91k7DctbavsOgL9GF4q z4FJ{z^!*8twZq^*Bk3W*$I=oY^P2jfUpW=C!T`s`NQtq*(}6Labo#Bq!v+REG3^vz zT~p!}C9tqsmAd{S0PSvwK3Jf2F6e>14 zp*eL3U3_T&(tiL#E#?m{2A(#u^ml!wb*L_$@F&Vgo3hu-KC_@-VDNkxmc3x`D*EUD DG*ErA literal 0 HcmV?d00001 diff --git a/examples/assets/font_8x16.bin b/examples/assets/font_8x16.bin new file mode 100644 index 0000000000000000000000000000000000000000..0f3cbdf6e199ce54e28ba49dafd83ba0ba5c54d9 GIT binary patch literal 4096 zcmZu!y=xpt6dxSeYB?+25(~L;;c{`{yo5yplYb#isznYP7ei2WImoiM8vhSYpCyKmn2$Gn*{W*^$~zs}F!zdJu~h0^Y> zcDu{XZl}00wgWW9ftNfeni$Q>a(G`X%7k&CN^}vh1qdlvm3C zW_EqM?Y6hqvo}nBkc^IwyW^u#@<7>3sLopNupx5*Jca)H>gwu`M+A&Ft}3i^wn$wg z4E#+g6BOwXeVR|fv84Xf8=yCcbfsOYRMwGKU6n`5&1^PP?Y#QbcJ^scKPZZ(@#e9N z;x$EIzMhfaP@59=jrxO7pA0!HwDUhLXIbMeW=)d3b&ZeU!lhYWxwj%#SX)e1sMmyX zl%^7beuI}GOJyO?QiiOLur7C{W}A7C9d9eBg3G;tiL;?By~Klr{L^|Hs@7sb6%)p+ zm_ipo>(e-4cd8S3QYJ~_%bOmScHY6|Ba!b=jQfaQRKOiT+o6NJ#4P*`ks><1w~$Y# z>bO**ci-baTy_sxOFX}U=juEyRYaqRg%15GqVx4N1`F%;dNou34k#fVIVAk|=zxYg z{$B91zCeVt3*De*Yl&Fbb}RI*K7BQPRh`zFFU70sHj-aESN|Y5Sz>c)kD8C66#5Z& zL%-i|U}`#r(Ho)PRt&+O`_y$PCqVVz+;!^@Jo9M@8u45qc9^!29R7^lDP~{lW6+055-#=l2xe_z)BU4ll|FSqt+8 zEYbj4f5L51#C2kR41v^dsX5J4-A^NgdPWK2?xYFqL5vfPc(OCbCo5TwzIlgxb*A6X zA~826_8+}Bs3(NYpg+JLEws@58ESsgMLBTVx08FxT(#cKQf#Q1%<2i zC;k|BhQp-^L#E*l;bFevT$KS8<_DNQj>qkC`5W;Yb8P4HjxzFrQSp~5r2WJG_PF@l zM3Z;uXK-hFx?6!w>jN8rsp`-gi8U54n6!T5GQ z-~KaS!z9OVMvp)t>QefH&V{%>RPQ){L>G_5AHoOzqrsy61>QK-3N^3 zjyUohf7Q&#$3k?5&iGBfI@Uqe6Jg<_jW01l!PqYCIAF#A zvrECh{c-sD>F?p^-`|HHB<^dl9V~9MfR6?4^y$6m2KYYzT@v^`pMSE~-5&ie(b-^( z@&%mzg8nFadJX=m-;X_Aak+>4MPqil?jMi+i?RfMFYoF6e4GV-_3yclho8^$lin2G zPVyoBvkCf{Z)ZO5$p?O=2Y%-J-1DC=3g_z{{#rZm8k;zK3%ZO<)!2uXrg8S>IzmF5{$lNEP zAK}Jw%o2j*yTwm1l?L`r0{p*LjhVx|s$jax50&4~SLfxiS>#U_^yh`wsMqH(!!RKc zaq98WBbumRK%9UKyfTf&0){011b7_kd;iek9Ts}&hkU*JXl*I+SnjOAh~qrXo_W`0 LxfSSnqPPD6Uqait literal 0 HcmV?d00001 diff --git a/examples/assets/font_8x8.bin b/examples/assets/font_8x8.bin new file mode 100644 index 0000000000000000000000000000000000000000..824b61dede6a4f6f2907386bdd0cc01f64d4d065 GIT binary patch literal 2048 zcmX|Cy=o*!5H1vqlVPQ>VTAN(H^Kiha<&L|EpN0+jzf{On5Db`}_N5#yEoaUbfcO)^2f}s2k36Q{fz{jf9hTvv(&U z@$xZ#zqMZo_QZXt9>~Z6%79~+wXJ5AUCU!xmM6%^e?;wUZ^02SsE+$(B3ZZ+P8`=#1ga2KugJ7*>wL2+tEol2N{Sd~ zCRvstmQ~3DtfQ83iF^q2`5b%+d^QL(W;;W!2^jGXAWsv%YwBz(T5q);?ZXaX#5k}G z`J*9Y{@^SCWp=<8hy0~_?PQ?TsZs;9K_sC7l}wi~C9x2mebyk&eAf2(xk`D~m#H3J z64$EdI&t`E8kZOKWwPgqvOc=sZ~J=Gmjwba&{JW=!z~&H{RVq*r9-oc7t|?t=VXj_ zZ`)fTF6)yzCE-$@{lY+}=;KrmbX3fnxU5T*VTbF7_>?NDZy!taGmZofl-jiIfBtX( zelS!<-JQaJ$_Fze#>f2WNA>uq7Y~eeW9N#gsxj|TQNbCGkCR7F#;ho=D4B#T6Pc_l zcVydL@E=Ft|C`Y+4ooQR5v=fQTanLyOV3w-rss!$(u1OXqD@?F6!{4Bgf}{h{6+ti zQU5{=<0%SHS%5K@EyZpPCZbgel&WHdU~TBO`C{yXg$)l0lDc`@#; zC)#uQ*wb!L)bShc-SV&HRAy~^*ZLOo19LAPB&I*+fAFO)aZk|icX#L)40ur8U>i#! zP8m)f7uy;w5!{qN6?D2`H`d literal 0 HcmV?d00001 diff --git a/examples/assets/longstreet.bmp b/examples/assets/longstreet.bmp new file mode 100644 index 0000000000000000000000000000000000000000..92dea1ea1713c85c6498fc7eca6f730dad7bdcd9 GIT binary patch literal 1546306 zcmeFaJ+3vmlCHNe$`IrUYC|Ze&rPK=po3EasBoV?f^7{8bS9Ij@=Vz$S)kA zfNLC?`r?Zni~nSh3}!NuZ!PSdnPl=O8P9si%(eDc|Mma;um8{g_C^2w*B?Ls$B&`yPn?^}95&x}3F{`ULnB#mD`{Acn086W?XOTvE^>`@&jQ#5z&@Sg>P zb1^uzH2i159{e}+{(=827@Uc~xnmH|INIE;6Don zXCiQJ+2{E0k6-tGoy*Ydl|RFOfBv%fYc>jJmVb`_WRCpf*FS%eecHyyX-a%X{kWc6 zuKi4YEUx+){uAa}e3S{%)zGTHly4D<^Xh>AVqQypwdg&M9jhy5|Iv4_@Zb0dSViNk zdYu3EzPsGNcV}^9dA01nKYyW_2S1}@U>T8f>T>?u`{~C1y*q~)>nmmd{q{RO*TB!{ z7+6Q;jQX7a%aZyxSe~rA~Nr<)A?`w$pcE#2)v(_RaXfAS$K6- zblzRB^PfcwPOjtKoUFV$_|M{fvob>OtsDNcV2|o}FC(k34F0oVaCMa4SwH+|!5;iK z^X`EEEEt@Lz`14NKMVHYznOOs`)~FePF8Uot!K9Xv*l+ZaBf-WzdwE*``nXCQCa%l zQu#GXmA>2`zt%p7p+2t7KbN7|D;NJUL+SV5+6)MPr8BAK)vp>~)W6mFvr#y+eCWUU z+o!?|drgkgt>Z7}Q2tI=TFN%1#22O4__NAClOKz#&iOB7gW^|nSjZorO~#=Ym8+Ev zTN*zK`D3bxygXm_`4*8lua3O`dMQZxV+QZz&#m?H{MqN9$Bxw%$NuZ3&)t8J>G+8F zlOOocDpK#OC-q-1eRO{4KeqYm#8&D*o_p``pRh%alYQH)&iHc^M;)co zOU@cGYfl;Cil6t1*LcgR!dI9_M^^b)N9`T$h5qY_w{S&Urp*7Bqr`z%^G7tL|8Z~9 zbV;$ue+LhDT!r{Ab%Q>IDGZI zuU`K-uE@YNM~3{Z$bVAA_v!1r|9Iy0=U;K(J@+5N78y|Xef}iPI)9&-KCLId|3+qC zo^AJg{=(}|<}LJ}%+7p9>c^4%r$v2_{;u{PndjR1SNs-k9e-iEIC4IJk9a<@7o+|Y zvSR66C;qE_T&H7wuHV}`_Mc3^eSP=+7cxTMf$?9hWB=Sc_G{}ui|_Zf z>c20BPj6ZLM~)l+xq~^s%^DxFzRTTvP?|;l(jDi}zZ`J_~p3m-wUf z8edlV_r&iFtvde+HykfHA{J={Uq5!a_w0rIF;#p=oa4_v{|zKue+_y632)AM%wO&J z9_n1HA%AYBWzzWhi%WHTR{7V*?kz1k|B2)1d5f?{5!M_#67Jax`D3c!zug&i{u65C zS>Ncfl)HKx^2dI26z2VBnZJ9a_wAqf&wpx0Yd- $(E@t397T#}!>ey8c^{zi)hZ z?wRwS%)Jul*0G$o%U1Xkw#b08@8ADQnsxrpQQo_6;=k~ji>&`j+g-Smeh7a&^DC;;nuDH!gO)ueEzNx-oIbYf5MVH zf2kcy-Y#3_Pn_nqY(nA7rOTdG{{C^^v4?s834hLk6hGgISA3(dk8I|TXV&<)zy6E! zLjH2OrTN*NOZD#<-DmX4`A_EBCp_EwA;tQK$Ce{rfip3i?qtat0B^Pj}C_xYcc zyL(FxD!+)daxT#~o4;FRpVJ@qU(Anl1MS7n|H53&QCz?Ev8E3z&v{O~cj{^CKjA{= zVt4G;JLw+JUun9etY0tI)_?f^-%iZMeJT0Rzu%yJVJY3x+||k#j#q74?XS*{M-|7K z5vco5?sfdaBYow818iBI^+?Hc1{JC`5lYFUX)poXjv*k4-jdS7= z$osF%-%o`kzw-01J1tkAHHYFS*Q(po^l!F&JhC{}j)3!@+=I|FmzWQVk2o#evj5U_ zNlELA>z_TI?cZ#9?P%jXe*~QWB>wWePmX0v4k^EgvvMxcDfO({w%T8v?~f|>dq#l$ z*WyAH4AHpED6Sx9pt%T$J+u@bgRa zrd2w%760eAzdHY1hGwt4-+wNy^zWKZz~5M)v*r zOVZ-}5;s=q!dI#N)%j=gV{z5}{^NO0J~>GJm&>=}tW=Ni#cebF+(TXeX3JYd;=DS> z{U@H`ty}+v>Eg)iogw@d{ri37xbh3%|2v1N-YeDm&)HyPuQYF3rH}N< zvP|!&<9r%{9{=T5R+1+7A95UTubF@2R^4&Cf3@|Y=KGD{X>)?XS)U zNh2cw|M5Gi?eCCZw*ry!BcqQpGy)^*KOS}XkM5w^|6plk1m^tbD9roA&tEm~O1;!p z{GZ?c>im(>M;RJ{e*d|+y7#{y-(0?MvTECEe|0`s8X19p{|Q^<8rk=szerk~zluAl zSNJNmzdC9QyJtGp-mG9S{a+e-VZ z^TE={2=x1p;u-(vum9paI;MQ#WYt#uRBC^9{>bQ~42^)^f6glL9{Vp&b;sB6=h}<+ z+WjLw<5KRpT^=lrj6je7%HQsz=#b;eFMR)RBsyo5(fZHXU}SIAyesvN^vSZ!8LU`e zWsm=ID=SIk`ALq_?KSgn+ODLu`&U~Z;`@Kr-1+rs{U@>pU&Wo&+iHJxemJH$(v1N8$M2N3 zze9fA8br$L#v5m)BLM%U>sO|#$M5O5BBeb7bN+Kt%KO95mw9s4mfDK{^V?sY-yUn6 zkspD6|GBu*6SQ>wm&;$poz&~|m1}=>etv9`S|5Ra|4D4gHL~x|Uy>H*3n#0#!dI#N z)%o>tMs9fo`u)fAoP2ta`Y)HiiaV)S_~N#ie(s^Jf3xMwmZ&K)?Sep7DSF`Y+C-W6EE}ozyFSDz(2lKR>=mZI6K7f6glL9{Vp&b;sB6=h}<+ z+WjLw<5KRpUA{feI3pc_9{-iU-AB^Zw&? zd)Y6NziQrRrDz(2l z|7?zCuD;)YJkQCe2dV#Z`NGMnt?kypE%ELIi?anmo9sfzsh@3FY_U-x~;UoI)63_XO{2x zAH_5N&tLz=d2~$q!pW+w_^H(X>ijeLvAAlz|D09gJ@#Lm>W;7B&$SotwfjeW#--eG zySzmt&a0!xf8}rYQFO?0!bb zB#q}MIZC(J%)e>7lG5&9ZGDLE|M|>V?`!=hu}rtP|G@YE#;Bb4AFtcXevy2cCs%Dt z{*v}r=Z}p)O7;Bv_s)DARlZ~7=PO}+)BIK3NxiN1SLcJIkr9CZ_?^=BcgS!2PQ?5> zLLZkIS^sg=;6J*9X8(_m|H&oi{O2gl`@_$dd2-d3+KT`4+h3i3GDUOO-tRvbSNHz+ zipBlvAbTq|D09gJ@#Lm>W;7B&$SotwfjeW#--eGyS!aIPO7QLf8}rY zQFO?0LZgn@6C>f4P;Fr1AVDN9p#O`8RD> zQri8itq<}2zm@M7t^XvJ=@$1N`2N8dq4WOZb$i(_lD}%+m3mA5lJ-~UkBvV{;roC5 zd?@6TspUIjT|P(}83Fju%8w(Xk1{j@BkR9@Kgc|}YCG$%+45j%WCZ5?=P1mt|NQ(_ z+)2G5KYWkmx4$}nWb{#nMxft+F0S+hEnOew@`aOCTc597`>XT8(#Qz(`%hv^hLL^$ z`HQ5*`K!2-dWEl2`>XRuMjvHp1p56avm1YYkoqr|FPyB}3SZnd)8Es-+45j%WCX_j zC!XQ0TmOaW;>hbh|0uOj=}e{c$=|m7-zq;c`Y1yq(Br@K6H|_<#IKyc%6n2T^HFYp zbv{@c8G(NPu|>k){`xP>;UmfyPF8KjPo?%(=Z}m&%Fqbt{g>KBQrLfTly0x#FKt&+ z+WjLw0RL(H-STHTu1IN*z?}cs4Y~Q3{o&`!Jh^Hc@{zW`I=?;EI3qs- z{r+=Y(UY{4zg+$*?xbFyuUz}9^Ydei)cOeY`%hv^hLL@L{*tsfUpQH{6~0RCugDc5qFOM;D>LbwOzx0z+j;X}2oG+eQwPila?XS+Sk1=xdBhc?Zwn+Hf zU;l+Ud_?)HxRZLtPo?%(=jX>4sqGQa`!BVNq_F?wDBWJeU)rvuwEIVV%Gv3*UA{fe zI3pc_9{-iU-ABY8&#Ew!b=GKi)XgjR5?|q{iIf9!_b{LB9E^H zvTEz|m1}=>{^;nRU9R7M5?eBi?ECYVq{aEGxRZK?uTuM~^UvmJ=IZG+6rIXHq+nJzuEFL5jeN(xc|g6ymj}FVY)c-y3apK?Nd5aDSh&{?f$pQpUcqf zmG}5B{p6HmD)B4lukxPM%Y2mEU!6Z2g)__d`;RRW{`S{@VGbWrzHqW?D}E}qzdHX+ zek`t9@4wV8lEVIzqjY->e`&jt((WJeDQBnKc6p0PoL5JW|H|L)qv(+1$}fEX?>x4& zSFH74YK5MZRr9XY+tZg7=W0hA=lLVhO44|KlB0Bc&HS6TD=F>%)z*jj{-4i` z^}g1B63cXp`wx8oZ;Z-$|M9xL>=((Gd2-dZE`W^F^ z=C9&T>J9ly+h3gzl14@V{$o<(_fPyzYv+%QKFZJtjI96q{UV-PwO#GER{daUWCZ5? z=P1nk!OvgCozxrh!T$2wU!6ZP`Y1yq(Cim(>M;RJ{e*ekr#-Agk{>$YHC#$x?7q`vy_w;YJ zJXjhTfpPzdXL#$@e_^^f^19DIO6^lRQz?D&x9$G7%8!gb%Fqb(_%HqBlw&IKE9bBB zp47{Hl-pmO50*wopx=LNk?^;_{tI*Xi1LM#Ra@~>sr}XYBcqQpGy;16rFM}N_MaT3 z+iUns+m)1d|AFp?nU8Y&tMlt)jNJSP z^!tx368`qre_;+EQT{6Kq+an;sr}XY`SC?+dj$0UOYI^l>_0h5x7YBOwks*^{t=&Y zcDiksZ;vz1NJpT@f8}rYQFO?0c^4%6_I&=oqhhJIrz_C==Z+-iQmGlTmOaW;>h{@ z_p`F<3U&SyTgXtNsdMT;GmrIvDE;XOODmw43SMR^8I2vt-N#mI_{_U^- z;=GW*TyAN8cIQ(4@8)FX)v^D?2P?OuY-!5zyjto#`>m8Ke9q*rjL>`QcK!>WudF%J za5Q=jF;spLS>;@!b2k6IjI6#g-TH5o3n@qOmHidlvb(OySskT!*6;jBceHDNR`ci9 zW9gpUU!*ylGN1oWKCZYbS^vG4KXUKt`;#jo^}aUV??1jTDf4HQ|2{UZx}rDwkE!D4 zfw|`=_W4&u>s{@@fAHVkM8kjS`mo=RcSrG=?Z4Z9j-vef&ocj+@!z4>@AIGIik|(Y z>%SHGJ4Et%eR!Y$ge`imC;R^VDrwgF&x`#oJ$|46_?fbN?veU$MgA_)d{#f+>p$@v zZ{7MYOczJa=RYg%JN5QW{uAf9Ezh&UmrIvDtNfiJ`kcPJ&wo6(hQIywUzo#37WvPK z`EEUZkN?DJ>6ZPMrc26v{%%oyMt|PqzqQ}*BS)5BfBgE4c<i1dDae4qb>En30JzCXW8nsxquv3*L9-seBQdyvmT zQva>Ue@e9X>gRj?C!XW2TmOaW;>h{@z2f?$-n_|w;ykzIc~9S{)|D=fT)7SU; zkLT9#x4-@ibNI+2f1j8>ttapCpExbuvj5U_Ntw@oTGaRG@0?BR@Kjb*x&gb7B%cu6>E&hv{a>o|;7xdhw^dHs# z)ac*a@AvyJe*PCTWR-t!9G~3!8~x{=GUxfS&wnxj_w^nA%k$|hsm;GHhEH!9{!7<~ z{eFBp1^4v--TreFI@Vs-`~4R`|BD&2%3m9&H`o3~|GB5kdA{uP-%QB5YlHvt zd^$^N^Vh}b?KQ)H>H4tWk8h`B&Go(8e~zO3{jFvGn)sYh>-+rYxT0r)>Gxk&Eg)w zeA~F3RNI^UC(d(Qo@a$Gmo9r&`6m%$cdhU9AJ47fZ-4z4=J1h4zFkaCtLZ)d6Q`wH z_FtMVDf9WKQDbwx&VPUYvi3!eS=j>BUw#p1um8kzymjlpFkKuupTBR6ckkJo{3p(HTb^fyFPAQR zR{6U}dC&g6&wo7khQIywUzo#37WsR|_X)jvkN?DJ>6ZPMrc26v{u3g-UmxG(zuLES zh_ki%`^EMtJ$i@#R(`iZQ}?0u21R>{Aa=ClOn!PU*SIs_Nb11 zV*0e6z<(ADJ}v5d^cViKU=RMAp&9-;C5L}5EN0_QR`d*!nJ`{UO?f5Fe_7|cfD%<|5ER_mZs>p7Dji>qe;(Z9}upYbuU zh{SnyIRDkY9fG{}2%N``)fLP7@6?&o=DMw-aaKLffBGj4I5Rf_XK`bBwX**6=NP^Z zw)_|SzP`va^gUy@r6d0N`tn&Z*1!FPw#hk}!4V2>hKxRo|87)_>ry*TH>b zj0>_#%kxD&C$%&!+$ldw0ScTIJYePr{$}4 z-nk6TUOD{NVoQ@h8-+8=!+#pKH0PYjkHuBPe;U48=U7DIygJ~&7FXK*^VqSvV)#$X zSnWKkXq;6K{8wYDC+{q7EUy;+>*1_)u4P2dsmu8<{;Mzgt`xq1RiZ6TKZhCXD`o$M zBZ}|;h2zmavX06b^*R5A-|fTquiC_gXPm*yimSx>FC=N~Xhme+Unl%GMq66y{j99I zLijJGsXt{^blzPr{MS!fzQnsZS$TEvU!JJG)Rhr>Z{4&0tEufB*Jgisr7Y z$A9vani-=b+T-7O?j#lb%|+>*rF;DslBTEc&tJgbI|-VUvoG#{-@}acm9YQ9r~Skk{o8!bcfzG( zj`m!BYY+ajj@ujRbN&nYVX{L0!ZZB4kT@rI4F2B0%Zh8zt^e4FOr3boxiCjumJ_W4g36ta3|VDR^JR#x?2??1;K&CF~;aiQduw%~77{GMFznE&|Bgl!1^_|EOg z6x`c)z5j$Y8gcTR9G9Hur^A2uMtRTH_5Kr<sNr_Z%+d5@0Hene7EcFdG+iO ztKjecNblDJz5n95wvRsWw_ogc>5u`qAe<;!fBrla|i` ze>=r}r@rX@CuF(jFOt&Ug1?<&zDr-m{O9=7uL8l}E-~M&AA0}E+{@4Zm_12r84&#K z7WW67F~Y`+uD3_=ErCdFT$&d}bfme=?4|`Rk!htcm&8 zHg0dKt;c_2KH*CL1P1rNZ(_#kTKfDa)bX~@gW%69W^b!U@4t}0z9aDWHf}7gXU2cd zg7K`JBNO~t#OsZ9jQUT%_9Gs_-y7MPy(Z^Brpf&-eg7}#HccA%n~l|5%d`L7lPucf zCoTT>uQZI+&H;aKIFa`X* znW52Z>+zp&L&p}U=JVWHxc?Z9(EH2j{g=+XbeBSufxq{2vf>K+{70)ozBiHIe3wk~ zvhTXwKIY#Q@q0s^J^st_#|-9}^V7iJ8(6WvhF<^ivl;iB*dxl|KkLYxQ(uq&;uWB{ z5B#0Oi{+K}`j2N+Uj4E4h&-(S@b@3%RQC8U&mZPr@HfU^um8w&ZuRHfhxvDG{88$! zf6@ETzn_(T$DpL-x48cWYv~B|`Y*o!kiP_m|I)EVif#n-{*&1^x6ND6rNV!@Xq-CeEYtF;II<^Hwd}vxY4q2=?OC-&oy!M*`iPu!rt_a8h4#|#|AqPeN8s-q zs`{=}xBlxVE-V55`l52i+3;Vy?if8jgO?Ro3IB~U=a*X%nfKQT|M?8f?7yFtRaXfA z&2T4WS4HRD^}>I`&+PWyoUFV$_TSg?|LA_$eRE5`%TFo4{r>ee8~?k@uZ+-p>vsN& zKmV{k#`Bl=QZ#pEvi>6@t@{Cgb5VL{Y3IN2X|eqg_Mc{Q^BTOFZy z*MDWi-qV)z-@EBpe|7I+;_54b|19XdI%@A|5B{@YkLq{_4|iMz{Aa=79g%x}oA93n zd+^`PI|2T)U~nb^=az;4EZBqpX5K;Yp9O<65jeLj{Aa-){5SIsg8wWSoQc4>W#K;y z_TayncM$w%!Qe~;&Mgc7S+EEH&Afx)KMMwDB5-b5_|Jkp_;2PN1piquI1_<$%ff#a z?7@FC?;!Zkg29;xoLd(DvtSSYn|TMpe-;eRMBv=A@Sg>H@ZZck2>!ERa3%uhmWBT; z*n|IO-a+u61%opYIJYePXTcu)H}ejH|121siNLvK;Xe!Z;J=x75d3Gs;7kP0Eeroy zum}Imyo2CB3kGK*aBf-n&w@SpZ{{5Y|5-3N6M=Kf!haU*!GAOFAo$OM!I=o0TNeJa zU=RMAc?ZFN77Wfr;M}tCp9OpH-^@D*{3kO_|JmDnFyR)7XGtf5B{5Z2f=?949-O0+_LbW z1$*${%sUAFvtV#00_T>6|18*p|7PAn@Sg>PGZ8qqEc|D|9{e}+4ubzI7@Uc~xnmH|INIE;6DonXCiQJS@_R_J@{|t9R&YbFgO!|bIZbi z7VN=)Gw&ey&w|032%K9M{U~nb^=az;4 zEZBqpX5K;Yp9O<65jeLj{Aa-){5SIsg8wWSoQc4>W#K;y_TayncM$w%!Qe~;&Mgc7 zS+EEH&Afx)KMMwDB5-b5_|Jkp_;2PN1piquI1_<$%ff#a?7@FC?;!Zkg29;xoLd(D zvtSSYn|TMpe-;eRMBv=A@Sg>H@ZZck2>!ERa3%uhmWBT;*n|IO-a+u61%opYIJYeP zXTcu)H}ejH|121siNLvK;Xe!Z;J=x75d3Gs;7kP0Eeroyum}Imyo2CB3kGK*aBf-n z&w@SpZ{{5Y|5-3N6M=Kf!haU*!GAOFAo$OM!I=o0TNeJaU=RMAc?ZFN77Wfr;M}tC zp9OpH-^@D*{3kO_|JmDnFyR)7XGtf5B{5Z2f=?949-O0+_LbW1$*${%sUAFvtV#00_T>6 z|18*p|7PAn@Sg>PGZ8qqEc|D|9{e}+4ubzI7@Uc~xnmH|INIE;6DonXCiQJS@_R_J@{|t9mM|oI{d#`4rd~8ZdvEQfBDzFUSD7T@~?mU zJN^9AKj?>!SG}lq@ORg{fc?j@mYCna&GGlQzpr_LzdPCc>tATcO_@FwzU4^=n+fwA8(l~ zI^sQl%PI6+k@xQ9kNUwq=ilJ3#>7>5z5bIJ@|L-3)3?L?3;$ikRBt# zY0}(v@I59+euw9Oxc{9a#lCcp|HkRtVg7~x?CG(oQSU!~>c{?*-)_r${Jc-reQdxT z_`~|oh8Ejedi*y|ACr0S^B4HfmK?j#JO!)6@^w?ZapZ`Ya+hP92`p66xf z!rTab_dkDu|90Wc??<2iMrK}i-~Iey{pZJhr+uye#_8K({)PW`Vov(f=f9De*OkHN zA2RCL`XiwApCd``YH3cReg2zGcSL8hZ@3Kn=Z`n`-3avhkLKVJ`mp|UV~)LW1oZwJ zr4RlKM;u2o0zLj4r*8-T$lLh-y~KT|t=@klGp}1U-h2PS>rqD_uU2{Gihuv*E*^XQ zH$xxRe|JTA|918M<9m;udtS01v3Eaz@ZXLk_V{m{z8(0(`fo@6s83q|kwHe>NA$se zyGH-se)alK7?dqLk$)3o?ruY7-d*Nj`0rlQ%B}1DC)CMSPaphO9{-bbdi*EU$yQGv z)_*56*1C4B|A;rY{_CR;{%b|y%=|w8jnj8OfAoHFPoJqj{BEM(c>a7R{aXKx)3?L? zOREO(cPD4Lw)_0Y&gh?ccX<8+|K;MpLz>oqGxXg*|H6Meuom~B&wqU9OHcMVn)T;> zY(1It@bACGV~=AV0j>YW>BIUDtmQ|b$A4q%KAL&4{>zU(QfUNw{MSG8%3bH(^Dowa zJJR0gzj6BRzyF}V%UYCujOTxQ5UKUw41M?W2mkFsTiA;p|MlMU%FN3vz`N&P_%Dq7 zK1b|7e{R}ipM6W8j`1gCbS$sFza`;P?)`xIcNhAc|KwTTE?crDKk>2Q5B}SQxws#) z{*!p`v9;z8>%TbeI~+Uz$;`IP)|x-~ZwKCTePI9bEGMh2`?uEo!GF2f-<{_C_fP-W z>t)R!{C78NrS@h0x691S{jlZ_{wu}*&UEL$J)XJvy^#AL-2dK5$ojU~f3weB#Gd(j zBv-$M|JFzT{uZ47=<|uumwdKC?D3P~xF&L0-^-dmtpDz(E#Cw2ACV(-p1*B#!_*0T zbnhI$?&lBdzkKxXO%46$Q|A-Ib84BxC-6tt6R$4iitzVucT$-1pG1bY#23%2VY(bq ze$9TAE9Yvr*7Gm?cPC?|w)6heG9u?afj{`K#K7I@vHyfB?UvmK|J@zOC$^vZPfMMD z9{l&)?@x^W9(}g*AFZ(H<&(+#SpV%2>HT^!?>~NO!-Vb3AJ%{S(Z@aV{g=9{og(W$ z_;1$;@7piw=0u9{>$;l4DQY!p8xI0 zU$6h@4unXP-^3o@^X|?c{I?^2)TbW*#VbH|pIC9{=g;upj{NodPwsHZChR`)ANvrW zf9x3H{rlA8zdV2NAHIJ-#(A&*cU>vuL<&d+Q1O z!GAmQ7y2)zPsl6X*SdW8Z^!69qfa^i30b`5xj9VpkKjLi{xQaS-hVz@n*As6hxOkW zA?r)U{?icWpTl!6G03U8I~Iz5-wXG@>mz@E3#tG5DB-i=Kg_@5EL-`H?l9?<63m~@ zrRlX4_-{O-&nY?YKYnV%gzd~9)_=QF>HDu{q*9)&|KPt}BfM|F;6Hj-;otwelh9HB z(ag&~eD6y$Tz&?|`tQ#8?%a0hztRkwOLz1Ub-W7PnLn)mcI8m~$L<>YP5d!^^4n(? z{@XS7clRswU+&4VHcfcT^DDXft@ZO4`0sAkO6{}%{C^vCk9}TG${Le-6n@#aa-`eh z-#;x8a9_Ibzr4BbN`?RKqvG0@*?+Wd+v&%eKe{&G>vXm9!tb52-sfI*o;82)-;VrIpJe^F%goFDu;vf`+mS!# zKYwk$$G+ST@oIp1l=n#g^B4GUNB(sFqtDkU{=%i~@s@t`kNkF9-uv6%>5M*Rm^Y%v zhCldkNB*4u#4Ncj^EXZ(Q+O|b@ZXO7_4tqZBbMkt-ZEKq#C!ghQ^-5KcQ1eN-;VtC z`j1HJr*DV(7yjFkzdrws(g*+T8vDEZ)#JZ$`ryC2;1=1{dhK&-)_r${Jc-reLVl}z#pFf?Z{t`|HkQKGVi_rfd6*nkNTwbA5)~i z=N0#HO|BcYM!~Bc&-;MXY7oqx8XlyT<@&wp~KOWPj$;J-U#ynEYP{|R#=^xgmb1^(NS!#@9w%)IQr`}xEAZ%6*9 zPg?(t)3?L?3;*rNU!VU*W?okYpMUH~!2NyF`p=OhceOO9(LR4q+8xoE>>Dlv|J@(u zJ$umWKbnI_=)?MN&-m}qE4}|l>4X1vi1Utp=<(k;eLL_+-sZnu=HI+p;FSaZ{mDDo z)cbE_=5?#ad+$GZJ?aPqe|It`ZTI+ZhCZzSWW=$JM?mjCzW3<4=Oz0Qd-wAP|HX0N z;kd_tjnl{XAo%>F9)}Zi`~1f>&(vv-oX8m~|TTj;f`1fCSjQZbM*N&L^cL?|kNx1Jw)_>xgJ+^iJ9Elctxc|6| zKIcD+I9k>L{_bMp>UPRz+7x|;s@mFjM_ zzdHY}=smk#=fBnQ0e{aXWnJ&#Ka2O?y7;`gX86y7J*wl)l&rlr_|JmDwJ~~Y?eL!k zd+^`P`vd;7U~nb^=az;4EZBqpX5K;Yp9O<65jeN(9sc9z&=$|1<%u?)KR=n^YyJ1< zFP8jCUHJUtnFL<#KcbIcv;LEMh(P%7ne=h5uk@dz2K+sl8LPfu?Y}>MB?9TY1GD^5 z9j-3Szg9$CU(c2PV`}X9qna@PUeAnGi`V;)iLvpYs|)_KBI5dbuJ)feYi_GpXy zkL#(iY4HyKS+4(tW%$pAhdXMy)_?AvVcvhPF7S5;IrD8^?LQ)hUvu=)j4Ln9zw;d2 zS;Lk76K4MW1^nF^^Ih7$+JF8tyEy`B- z!5;jlqb+|H{Aa;ner%Ci9|8E!f<5@JPFXz{{+8JUf9`K4=lOGWfxq>vtZLzE|B17}->Qf{r`|jKhxzw8d|c7fYyF4ke^3kO_|JmDnFyR) z7XGtf5B{5Z2f=?949-O0+_LbW1$*${%sUAFvtV#00_T>6|18*p|7PAn@Sg>PGZ8qq zEc|D|9{e}+4ubzI7@Uc~xnmH|INIE;6DonXCiQJ z**pA)@86%xTkguQ^&kHJHy49b)2{X({{He*(#qF;rT@TRISwc0T#;a3_j>=~?|)BYtaZIt`wxHr+lt6J`FHpa&;QQhOLL{y`VY_lG%-2loU8qZ z=P##_rN7QA{RjT^aXI14tNn-PeH@ZZck2>!ERa3%uhmWBT;*n|IO-a+u6 z1%opYIJYePXTcu)H}ei+|IPj1U{HD{0_T=>{*%uoupJwLa~Ybwa`qp6D-=KDV=xwxo<{cZ)60{0Q~vtO&$e$}{v2kkuT<+lTRy;_bsV2qAN(ips#zaCG5UM- z8U7;%!QUM;!hi7J9o+eC!hi6e&%>4b@E`nlC2zU5M*SB*ag#{$HqMRLSMB@XxQ4#^ z=SMOteF;6I@{aRcvYNh5z6`Ei2aN!GG|dHB-IyY5f;JF_Xyg zHqK4gQ{fru{co7xD!*HP@2KN^8UgrE-bE?Tk|Xc0`Z0JK9RaQX>f_%Vztn%W_4oE? z_WaS&Kf4_KC)9(#yWR=#A2A62?%)spga7W}&TkX`ga3RUuH1+J;J+(*%eAHTU;KQo z_gEs$+xGo$Z~d+F!}eQe_vYtD7HO>!=<(n9zwDCx`{6c!F)z05r}rCk^|wYE`K1xi z`tP^jwf{l|@#ddv#UI!C$FF|fvR_NhwV&f}rvB3CBE2>Oef}e7QJm#B#VZ$g;ZO2% z@gMQu3?C_Psb6R6uZ=SDY9r9&KMj3E7Wt3q;}Z1m$As}7;ZL|v*Je~(`f{mh`*DBx zJtEf^*X3&Rb6o0gzt=_>d8HBP^&gKo{VutW(`EjZ^V0ZJ9sJZ+{{5kl&m12!eoE_$ z+Y9^5ZMolkCGObo3$a@oVWhW4pwEA72pau-L^y|IDd%GTZN=c!{9gYFQ8oG~Pv&2m zt@t?h52eC?r*h|CcaQ&g)@l7_{@hvYKY!)KREGS8`7y6j2XULeTB(|UzmU&q)#SJB z&c*!e6Q$qp^PfNV_5C>BSpVr`a@v`C|BXJOWp{<^eR4 z{?n+jx!ykirBTnPP`v$|T>QbGO$47`i{5|4jL%iL&w2El`I8x%k7M7A@JBULx%Aaa zP3Ql(Kl~mUumAZR+;IhV{&Vy(Z$bny3jf^^+o!bIxOcu$dc zpW}_WHHVJ`~7Iv_!HuIy~oC*^PT%>FZBygHo!TtsPI zPfWNM&!4o0)4t+!vHm-cE$tP{`j6i6&R32;B9r)u?@;u|T;8vdTB7#o2maGW>V4-q z|B=~<2U)-Qv%6}d2sxMvFG_$xU118{^XimeK9X7e}0SZ-5{xP`Q46BqQ>@G*?;7h z_WVn9wdkXK=Re^w<^}xOM)8@oIsZB15Q+KwU$Ggnlbd;&PM<&F75Bf-!mHIIMK97!TdhFJJWHWx{ z_>;VmE&O*)ET7hj^I!ZlS?)V&OFo-@!GBL<JEd)_{&(xkP*5674}jd z{IWc!hW>MFt=wK%nvUZ3<1_m5b8Y%*aqV&YcSY~n?b`Z}?|*p>7{@0*PUl~&|DMg# zXz#WDi+vIw^E!?=lOO+#Hh-?1l#jQy`r`U~kMaC(l%JV$w*DhN#w%b{aL`NR6}JpOF`$G#H=efu$6eoBr%KR@1&`L#aA`tL6K*?;Ntt=<&6 z|CLv+tmN@FYga{1JjE__P(w4)X zw7(-Q&Z7RvK5cdU&9#4L1fSb>=)W-Dj-Q;*TrS0p9plo*_;cmcJ!XpPC7#4pY%HHO z;f=SPqvbDc)upuBe=Y-e_dMr6H}14YWACr-!dxrf+Sa>oH}Mt!=UCyh_`MNcN!lOo zjr1zr*4y79lF#cy>c22bx+9LSKSPi4S8I)#($GiDF)OskB{cJIt#w{w2+hPh)$9NF zyx8y3V>|!325HxZT!csb=St`HTJxVjAI9fu`OCE{X>px?dzgP` zh#&PI-?#F&t=NGQD>6y!m-slHf8&UT#~lB7)$gCfc|42Cy(;yCTGGtFeqHH)tNob> zoLe^ZpW~;!=jh{mLyob(1`F$G^T(vfdMn;?{8KI@#nH#S(Ql@YkK^{^TR~<@m?+ zF>h3wb4vV${R>-5k6P_pMB=4z zm$oUTY1?Z5Ja(+ESo}vG5-0iFkfYp@@aJZ2idT4Md`vM7{g-cD&dSF>rO)x_49NX! z^=Gbqt7x26Pv}1vYmS9PjIAa|=~no2Ib-~}^66fVzZ`u&f1FRGYy6kC>8G^XKZ_g7 ztIhe(A9-St%oe8kM;u@Oj1m6QHJ6`~i@%@m@5kqI|NI_@?=Q;n%jfpvI^%OJBXUk% zdH;oxY9DcY$ua&bEZ3fY*HDF*{;RcW9hEccOZ^u{Np~dtxwFYl?uho{4HsU$2bR3_n zE4vk))`Tm6}9e^qqeU9YYGT!hE?bLG=L@lTsRIz#vqTj}^qTXiX6opgQFj-&tGoUFXM zQUCG0dF}WAH1s+Cm=Qj1`%h-;{8soA?n3(beY`SC@2orYpL@5r_Z)pRZ^;}iMxbq2 zM|@223+G=)3+2T>68}6MVQPFX_pjZbxJ~~!t}|>A{Efdq#D6|Jz5C3cTLF&n*IIw| z_Q^kAH~-2zBER+POZUge|KyUP{~SO4d*laa7>bWOqP_T-+!FdPJU>3_?@?GHZa+S5 z{ri7mZRyd;6wO^b`!9~Sf1IDcVh0F+ak?D)HI97$FDx&$y6s$kJfFtLF|V#&-kXcT zsimF&!svxZdH&LK$Y*pe&30+L!t+!ArE9)${S}sP)!&=%x0Ty()psgE^VdDv3 zTkF48{U_36bIo@CBW8#``rV3G%3r+V3Xfa$&E|)#rAMv$ZDMg+Ew=vi>1)L+ z)9B;tweuglkDs;m^T%r{Dp&gP=j2xXz4=)G-9f*t|786)Hvjr`rTe`+^`4LQ-yQr> zn_B6*(=2}{#a+7&f0%M z%836$KI5ZVzFOt6{+r=wrmUU+`Lt<*kohOZmN4dovL@x2&!I zWbG%bz&`%4{yURHJO7a*{GUGl!m@rnvR|u@&hcNp$5{W}LBE~8so^-!e z-)uhCe|PXlZL||7;HK>^=LB?)vBsfVc8pW{$D&$GPp_biRKU_`5T{JGbrp z$LleshIsMq;`pOkkIs~PRmW`18y)*R$Nu8>D3#jc?<|CE`bWIF^i$&c?mpf*%6s?C zt^X*>Ojen{HhpwP{BISce1X5csko+h&VSA@#9)a(4Sm9&Ga&oZw-?rb*TnY;t;qV1 zW-y*b%lwHS9a~%?{LTI&4&?{_djbj9_tE)}$kNWgb^64AOrOl>@%oI;V&`K0cYWM< zYLWe?oqt2q<&VUFlFRci5$;|oe=d*r!udMRjd{L1f2W8(r!UTb%mWisnt#(6mr~;A z)tG;u!^G7+W&aULt@&5~#5p{Z{TKQy^*ykjVT{^5-0_I&z${=}%@?|tOhT@m{)ygLYwn4j=F zEoJ&-{uTQ8IKH2y)cW&hXJeba_?m2Lm0IlLaZ*jre_?dOqZ)sy`-DF~$71~#8(M#y z^KblqpmRoFRQ4olY_E03e?DV8^YU+TrR3Z2$Ij(CoqyY@TK?KKfjkZQs3FuKN4~x%~56sa)1CVO>9H8X19p|F!62vf#gwXq;K5$A237$VHAn;W73N?my0? zExhV}{}DGl+wpIj(>P6j(^mKsUUB~sjysOr2=w@m858DsJtf>RYqahY{)9}c`;V|) zZr%04KaNvV{$n3=AGyc1n*2PvQhwq2pW~@#Z^VE8okf0M_>){K{v9%)y~r0u8t$+;t2_|GRwzpwY-_|s=D z8OOVyzqua7?Wf0c_3OtRXNDu7^CsvmqtL=f0#pOMxZphNUx26 z^WV(aqb$CEU!$y^2me{Tf9hk5-24c@e-`Xf9r^J^YI_9WKMMxiZ@y3~M1mHgl_TWE9RR13QXTf0qxT6IB+q?Ap>&qerC)WX*Mn?etvv}W( zj{e!@u>P}PkLoy^qnWFR|121siNLvK;Xe!Z;J=x75d3Gs;7kP0Eeroyum}Imyo2CB z3kGK*aBf-n&w@SpZ{{5Y|5-3N6M=Kf!haU*!GAOFAo$OM!I=o0TNeJaU=RMAc?ZFN z77Wfr;M}tCp9OpH-^@D*{K zykWV-9qZLo^p2o3$JglN|Rz2_^{C79;AOFF9o%g&y*!|2IcSrG= z?ZbcY-`(VX{P%e;f5!yJ9qZw{qxj7B;XnBAZgM~V!}`x#&-Km$cSrG=?ZbcY-`(VX z{D<{k$9nGvx50Sb9mQw15C6e`ca!_^Uu*rx@A&cGch|qP{kx<1%=UZyH~#IofBtX( z{eQGQv%3D)cYEgAe`fr5==H~c!FncM-w;Fo|8Kr$|C>K${r~*ma&^~Ib@hkqwet1V zT8ewd=TiGSMDlrk=<{DZ`u?#*;_HheOIykR=7Z+H%8&5v%1L?J)>3u#*V?AMx%QtI z`(1kc@n2(oB=Pa7ko_8c#Lj2_tvK-KIX{^FHdC<9Y^7@I|Hvi{mkD{abF+Gclt&+mM`|IQM z#un!NCj`h0Nn4>qw%+^;|G|IJ2z>mPaf0s(@qs!{DHlO-Ezc=dtX;Q`Y zjkRwXk#p)A_n)4=4}TT+`S&+t@&A7BG}`$2{wka!@2|1;&tb;;NL*vZ{w4|5e=dSr^hd126US zxq-Ub&ELIhIQ(-RrSpc{UlpBq*W2g6@o$KHo{v=hx3>P;+K>G^)Bd|TS$TCI|COA8 zbhcu399Bl?y><8aujP-k^WV$J>MQ&BkKX0g_c-#)OPpCZEHJkFMUn%Sl&W&BuRO|6%Ti9&WwdqrZa4n@wU{jZ@yCCS^`HOuZ|%Q1^3V9@Ls|dW-#`ACM;rCG z^5y!*+SiXa&U7Qt=RcpRc%P$||BFnWpXTB}zmA)0-;F8u`bOa6zsCAV;uDic$2Izb zzdx@y3V;9G7q>T@ZR5W>eF1;Oc;QF*=GtP)YpFWFmfDr{x%S^c!#&sV@n6MF6eEdG zo($sS=L#$HZy{j)cTZ&ZZ*|^(E%)*4to!?4Cc9?368oC^#dD>tyuZfU-#^y7_VDAs zf%Tt;zF_{XI2P-_T?tv&uQ~te=nMEOto{7?eat`Qb!EwSaXS3BjMw;m+pDRl6>Ch{-EM0tp8e(dSCvy|9rk$`?}|I!Z-Z)KF(@a^zmQCO+V)< z${&2O+kTX8ICgpc%i6DvF!D+x(C5GLZ-{*6-^v;lo@>7}!boq8z{h_r zHz1$wpB;zRC?h{N0zLkVeSzce^~6u7=SCN4ni2T;kKWzc_cqeo2YW%le~={hkr{_z&y9iS=*Kh~r!t0r<~% z*v!6+D7L*L@bMqke^~$F-#;k+eaPO=8++gNIA2EK<3Ftbu>OPp;J=F*5D(q8{rC^- zKdk>QW*_&zGxxlH*}LrV{B}S7!}<^FKll&pzl#~T+n^u+wbp;UIvW3dcl}G-zdMT0 zY`Px=h~l(!KtNf{8y(h;ExzD{0QG% zn`4{zYN5vY;JHew^ z>pwdpuCM9izlJ-64i@YqtvFt;Z>;_6`f}U3 z_Mc3_y?y`qui~bk@uK{}7rX77ky<;e>u)%I{QB;V;Zs}h^WXS4M1JzuaMMifn)-9j z^V8epV)&Q|M{*c?ElA)^vvsro&MwN z>yKY+Ki~WjLrZ>YZByQCyHAY%9)14!ueL6$y5$qQAC#s4{quh?e}De+$FJp|A9c*O zUoHRjRc@R5HQW9kv3y!Dto`@nSxaCqx~Fvf{rOAz^+8pV!*2O@ zz>hl*`mDnlkY`^v|6=`jT~wdZlCA&JdF=;_KVEYGQCore+^*QC`;Xddw#{e6d&gdW z`;YG%GEYyKq2RZ3TQLvzB{*> z@n67CiN+EC)mC5g&)-Ttm2|BCcI7bi-w(2Wda$znVZsl8;ri^y{G0wK_aAAFpYqK~ zSYPVc;e5ZX-2QC)yGD2Ke)<06?|(Gu`{~CIf1xc4cQ3^9>d#-rbEWO-_V!SAsy?$=}wsQO5 z=MMK^_bBh#Ki_{X{%Z4UJ&$T`ufOVb)N-l*Z_bFd?9b}<_l)lodZqOrtq`-(t<1XL z^Y%~vTI)Yzl>Qt3`=gKRzIz!Qx7UAj?LQ&X`}MKUe+hj;9?x2Gly1NIt33f^M^5pV ze|LQQu)Y2RfA{b?lWe)9KIZpB~UbLi(^(fYx<_xkz08t<6%Tv}m;W@gZFCOSe?8GS>)b2-S6ltj_y1Pk|E_KS zEN(2Xc9s8V=KcQpuvK1#&91*>ombkHTjreE_AMiFPF%W#`O8MXOZ_$0kpl1D9s=t*^?SrS$5zzXN?kK)(5^mdfB|q;!@cHj(gx*`O&wmMh zLY{ozNsi;~H-EJi7(0^Y!u(uaOV#6rVR^}~$c z^Kbp%f0_RLt>`{-VEg&sT>G<8IJ5kK|2|yRKK+RDzWJ-Iz?e~*8EY%Oc;@7Dzvs^I zu86;f|4Gw4f8^T7+CP&Yi>toMe=$4Z@$C9b)_J9E*jjot+rC94&Z}d+|M;m5KL0(B zE$tO|`EOwM#rjVhiSy38(towp-~98p?|JvL-iwd1{@a!ARsN%y_xlIW|M30$>WW>TJKYijBrhN1BIp6v? zY%M*SZEufQKCKs?|9(WCo|NcbKhG?><~x=_e*61(g>QE`)`R+TZPe0MxBs-r@6+FJ z|NV$F-80l;_?^dRGO2Vvy_#F)`m0%ww!Qeg)$Q*S&nNXH<3A07L~5s(+Uq|R`>NjM z`nJ0LCq;d)zJ~rI2R44he_gIhS6pGrH!~`i#%|LbhpnYYv+eH{)2H;t_aEQgHR=0l zpb`JIzW=w{e{0)+O2qf;C;N|`S9`)Bd32xhlfN!ktzLh%9Jspu{o?wB9(n%byN~iS z7cYlc{hqJX+tur@R)1Ev|Ac7o*+;kjYdK^lKV5N!Dc}5bIkl%hVQcBpZ2No0cK2R+ z{_DJ-{f<|M|JJU*TK&QEzg=1U_-~neh>g|juODqx`@g#VU8B2qzpnIO>-&HGYs!}M z{Cuqcb|vxSKblLc4w=bMS6pGrH!F{Af7j^l-LEVChxOmyEZo(*kN=jrhupS${nc{w z>h|x7-m}}i(tlY0J)4zPz5n>Hwf^JP;rQ>n>tEXbs_4AC-X8zS=WqSn?{5}T{3}2G z^-2j{{k66!PipJmQrUNNvhwOa{%frNUhyU&+X}tB1|0-@EcZeT+ z|7qw8_^UV&>%Z|BKDFeW|8(>P{8bz%e8YcFrD?3+AOBU{%TERR+bv~pPs%Ce--!nd=K;2sGr)X{ml7yWt84s_s4$}pNc~yKIFe1`U3u{^;7F~ zcKT|i;`!e*86EeZ%)*+j_x!oyAmJPSdnQYxz5e*G;-;VTR>n)Isq!g&Z~G&!;n;lX zcB;DmW$llS{@LaF{MY}@5Le=xy{iA*+5LO%`fF=H%Ij~_FL5?UGgtrdU&{^1=NTY7 z4l@xrw``C9TK+gY|6GP>Z-4)n zf5p%E7~B)Pr?=|-_qV^}H8TQFC*i*Sv;Y3-AAkEhe#Xb(zW6=4W#_+t`UhS!Bk*Jj z?(Ms*|7K<%l)X2GPi@`#ul!^IIhql8DgpQR8~(Gf_x*8vVh^1EEMjnS9Z!t@9({)Y zEZ#SJ#PVsqfd4GmqdJ}z`F;8e|5-4&PduO06Zp@9J@{{idHBzQ!I=o0TNeJaU=RMA zc?VtNzppR)bUF9>mwzqi<<8$TZ6*Temc7n@|MIV8{>piKvX<-BxeU!-xz2yB&!BQK z@zctVga2kD_}ub(|9yQCC-R%VsrvQBd@-3mYq@>!_gqTW_Po!3jy$IEZ-3YESDs_( zTkoa0IY;uIX=`Kj*4jt?$L#&nKZqS2fAHU18JfK=t^b5H+Oo5Vzkm7%_#-3FM&Zoz zJ^o|P=r449`++}R51h%5#Z~wEkLaUSpnO}MxooY<%4EY5g-Ycp zga59N`d%%L`H%SH8IZ`5XVK+3F;5?JT+S=kw_DC$F?~vJdi=+nm43H)C^JR|%$c!O z<6iksiTHl~)cJ2V+GWPT-+oM7)g!(Cz+Zg?u8P?E+S%WKe4ky9e=FCbj#?Y<Zo1gipadbPQCw#pZG;F<=5KhFVr@D z;xh9p^IxM%pZ9)NR$Zaie~vcZ^M9}9k6O}`kU!I+j(p9Wx+*&FuD8d3%o~5_ihU&e z^c|R5Y$&hnpZ41cLVzS5BP6I^qyVksQ<_&!XNEx{8w@u)_>1t zXtwwL{v+ZXf3&}rKdk>|W4vqm9{(|Qa>v2;V-JEq{Qc25;r;%L?YEjgqP({D*W_8$ zQEO*>{7){~{U?m^s*C)`p8*qNlIM7f_o!4jbMqP6@BeYD zGrx1K$Gsdseh&{dg^AZI#PvbLws6+U-fZGHUN?tH*y5 zL;4p`*?yb?|GkTetFETkf6fSwKuXi_C+xa9cu)8Fuhw$q^GfZ<{TVyHDq`hT|$h1?)BF5Q91G5EV8TJLN_>p!tvZXN@F?_^{3RrUI>yt-nAQO0siiZ@cymBf8jf2>}dZu@7P25 zZ;ZCI)Q-`8MxS*4E8lr=P8yLg1@rGSXt=hQdjHj*u+(#jMDTZQWS`TT)_=}tWIR5S zJvnyg#CzcHIYeC7(^3C1Z^V@B#k|GG;P1MqKC7i3|M6#T^u(BERQ~QB|BiW!kI9s| zPfoaWF2~itoKp=m|JB=Yt&QzHE9SfOw#R?+c`wC>Kl$ZvAux-rsMX1-vjP5g;blcX zwEp97JQFEA=hEEE|Mr#4!ZikozZ_-o--?(%rOqDzvG>G${qt{*KCUxQrlT4DdkRk@ z{nYx;@xTo7GZox_@VJbOJj(R?kH?zUeX&U`_B+b{Zi z^ho>{($nH8Jexj|1AlwO^GUt%{YMPZb3J~xSLTU1qu=a8{)^!9`S9P9h`7G5&VRhR zV**P&<>`a}u8-RrTI}^-i9Yc61{&_UhSYyFW9IxOZE!@zSOb{Ouj%U3;hTpN2m0w`+v=@0agC;z)iRcl?Ce zf7+a`&~x|8{#p3^ZGRH3?_uaapO+R-{<*Z@Iv@O9AKhoPDE^DT+k)@E#P6}%@iSt* zTQ7b8mA=`cONakjVJe9hW&`||IvGKr>H)wFV27X{$Ff${qeJ6zDsX={g-ziMX5Dk ztCf_VmSAgQac5I1} z+*$CST_m4ZQ?LKn4Lk?4`-myBV#^wN`lu8>|9BoZmObwAU!Fgig=-9gKg*cCrLJE8 z$=$9zjTZO8e{Uh;`s>j9FXuPRzdS0}$L$R*_WCcr{~!<2_``p1py8ft(ECqj-`qCm zMn4Vfzk8y)cdL5;`7Dj>gTK8=xUYBYzue54rqxpFb1?Y3FT(q_?EEKpv@*BG$C6&! z%1W^0M)0>U1^4z${1=kb;wd~Ee}8mu44>M%?>}OQzFR;)HJ;oZFT0TMep|f4e@|s( zb-$hecy-4FmUzn3SL2mU3I0~c@rm{K`maPE_~NW*_` zU}*FjwEk<+r{OP0A2EpU|BXiP9pzI0F%^!NHhtjl9V}dZ6&n9(=mURONA5lC`Tir0 z*uP7F|00H!}Vx1OMf0)}IEFMn<6QKk`}63;r7!f0S|l3*YlC zp2D;7`~@V9jDYVyVo098)Od2wW!R7S{vW*;M@AoIXawZ`m-!O{Oa9B#SL2mUiRXWy zEggYg|CQ(if9cpFMK=Pe|HxQ5|CQ;>pYN{`>044<-cO}+r5tx2zJH~QgC7;f6wD-q{pfM@clo>zIJb9pfKYdrk@$C}tarB?ADzW*1$r+SW`679YE>HDwe+b;QYYw?2r_KN&IypUo8q}=Joh5 z{=_XjZrx4h?>?v$)_-TRCs(cWUyH9+KKPUI#@3C1-hVB=TKVA5jXL)H5rF?%?;TCP zKcd*zjsX0pVM}w4cEoXBJOVoZwLar&#R&6nJnlH=e|wF7e|@$2;ZMr=H{Z2#878w` z9?xGuTy6yV{O8Cc?&!H6_@nn;Ze)?x8Ug+Kk62^km^V76&pkL*<4^dn6^%3Vwf+;* zXv?3v@prGZ{>!fbxz-Y!GQ;EhSMOxA$A8S3%(!AdA)~$i6L03W;P0J$X|Af*f5Z>} zdmH@OFY~0~FGm^tr-{*9&*}Fc)5h~JIS}_B{LXtTA?vSe#D7E{ul~Rv_#0=m$A5DF z!Sk=oz~B%3jq^t($NWe9@yKh}f4S8k)j(XA@}vd1%i*@he%IUJmd1b2#o*M^J^rgl zy%o7KW8m*p`uMta{%i5o$_IaEQUSUb{sCAT83DcjX#Ezy_$f4{g1?c`M;RIct^XWt?$Zzc33SOJ?cMR`?+rcV*U3_Of75B%*&!2Nyd_aEJf_^Utghv$D|9QXLIexF&pH>AA$ z-3R4h{vC_mTT1Et*W#;{5B}c5i{*9b{nz5Fl@Is=?qmJ)XYkh-)n}Zo_aE`cGg0^)kB=m;JXe+d)4uY03K5^kbmCDVcn(zNv%$*i>gjWFR+0_9bY(XFdKCPTaq~EAz3VCBFy%%}45eHT3$=vE>M) zG|l`g?7BL5uYLau|GkeqcSSw^le-O{KH_REe->*ui#pou$$1)B4X@ zhK$EYvM0yxoOqAU%-sQNzW)-}wd3*V=$~C~)PKwy5hZ&qCWycCe>pN+HSU#vHb*m8 z-{U|2%#EByGb+1_f468L_HvZLe>1Urdf6WT$>+U98h`T3-$Gy(Tl|%u|ME9kTjh7p ze>w^G^wGZ$BzQ2F#hUR@;7g_r>qYE%*43SrPN~&%Y(c zz~7Szn(w>Tf6h7X|9mOtVKPg=gjdziTT zO5nddQGKabN9`T$!GC>}mCt?$4|iMz{8y$*pL0j#p5G??rzfm_=JQ#&>k8O^^-tH3 z+ZutpqWA1}o&V_DrFe~xz_U5Hv-h(8qwiGVXLJnijNo(IcK)lcZjjp=f#))CchA{> z^zC^3jE}+HQG90m&VTfmM0kylz%%2&L$781*P3CFUmJlPBKf>NIRCYtFCf1@0?&*6 zECsAX2t-k;M{olB5Y{$mIHZt$8&H3-Ie<9}ZGd>3I zr)A9*ivQp|9^cV%SQDLh*9!lQ`!1d<-c8E7s}uiG3#0FSlv@{{H`fgR!GCx2E&hYU zI1;1daCaP^*gpIR|J}{4_>cNA`rb#myQBEb_TfMH?{0F%e{dK_Vssqtj^h*ChyUQe zySWwrQ9nlC`zUvJ6rb5X{0INtO|JN_<*?a)-d+Fd_GicE#PU7<8~=9PU;px-|7d$= zb^XL&?Ps?A6Zx^YX7L}@Rdd9D{`mhLjs3^`Y3u*%UvjO~Qg!uT(Y{4IPO78Ne>pBB zjrpM8Jbp|y=W6{YXG@*_w#{G7ZJIoJf7M#@dqn$k)#P`)#zufgvF<*qS>hANz>huNiNgla2t>!s!$TdgZnfA0a}vE&8Z_c1ylqX;12}ktS`*R@&Co zFYT1J7VT5rcU=^OMw{mgoM3?S`$({9EgPD{r=a z{ix$ie*~DAx|`^HiI2~LhCZra_@H`nZsq#vw{T4R?$t`w)L&{>(%GS{mRz6wzH(VV z5Br)C;L&EkX#L>$)6qw52p_dJgm13ZwZeO~R9*eGwkdD6ea(2|oOA?;4|&&99O#wX zN_>P2Eq%gYsa{E!`fH>~+p?8Dh<|eZmik*;i}tAvy!0c$+)*@p*MAauA(ys7MZDGd zX*T}E-ptQz`}$GGnf?ecIeF*NdEyj_57R2YxuzO@lqd7AU!T+~TXqaVp;d2)TVmg4K=bGiN2SR+3_ z0>q&Bh2lW3(l%d1JC)s1u2SBBpjT;IukqjijbHh*{{7$LnJYdnw_EB@UwYhLd_3QN z{>qVB8-X7G31R8>KmXy_SDN)SVJL z@+bLZP{t% zt>cepRk{D==YKxCk}q3+{!&~0(evL{`*ZnN|Lsb;_|JD8^))`ut>Kt3-Ta$AbMsTu z_Tu*Ej{UZB`}6JZ8sDA!)#E?0S-REnhxOmiB;40G@gMb|=9u_1UH_QMxw0{#GXLW9 z-}|DxXUjJJBi=Op)mDI9Uh1*mV*R%#f&2`HIShM9*6MlDEKD!oi?W|vJBF*Uo|f|G zI{$KhcpSEt9_8yq>e>G-Ry0I~i%mBiG4yoO@lWc634Pv$%t zLn-05v^vYJx%{;G^%wPl+s|$NwsQOP?Q^U6-7^AO|F!7T@Q3wZPh`$HH}zk<{%g~x z;ScM-bC}Xzsm6aA`gHtZ{ilt~N$2_g6OL%>xZ@|x{?lf+wv#>oxtpZP7VE#0c5FXJ zpZ~U7DW4(jm(M^>ZUlV)~+mub$|) z%%?GN?zs$;hVTEef30J4N`20Me8o&)iN8F3KCdPJ`KzsTUt-Abhhk7LHX$=yet zK8Zc`LQ1$TtW#hbirqYW%06Psd+v1%~yXHfC=-&-b5jL|exlzW>MX(>U@j72kh}$3}M? z`p?}lIR5;!+P?qXH$IQ=Kd}DO#pJZJ#DDR1_r9lzS$f6vkv%n^|Hke297|taAEYm` zZGZmT(_Vb8v>&JAxIO~D|8mcb^=WPY)mDG_{$D*#Z_X9}mEFNt-^%Cp;`sOfTJ_K6 zhfn*`Bme!QyWY4zZ)RlGwfX*|@tpZ-@z+}a&9vPv+y4BwT}ORkP;^@{)KIw=OD z+ZKPV=YON^>dNWXe>2z5BZuETBXFhvTI;`A|J2J9f3;WLZS}g{`?hE7aW0R5_;2>T z-|}C&=JHcq3}^e1FYmXN+n;YAJoS&jmHxx}uRlH~lo0>XwdUQSOK%S9d>bu4( z{a0Ik(epoIf87t(e|=Xg4>y)qBL1T~9Yfqg&#^l<&mY!*mQj00U03=K>%Vs}V|^9kKj$xQ zq3@W^b|pdsV*O_wv$xb|=f5_8SpU6+8r$m-|52^ZVBCiOn2hIY$oUWJKijyyp*BnZ zF?0D*`S*WY|NKo1oZqf*KY!&&ZH<8VujRT{J+1t^>tEe|>-v$OjzEw9^xuV!&*Z=D z&U=aHe9DVU>5p~wuWmmbU8HD6K>SDN)*K=KK1TlYgIX%{p{+mvZza@Hb@gA-zGl2} zjvIkK|J50zT%v?)=Y1}r^ONvhBQ5R$pR23C?LX;l+zK7ndgb_cBa1!l2naPb_fcKK zM>#rq`lx<-0@$vfTA{PkS1VOh|LXSBUL&P70vuOstBTqyDJIndBY^$Q<KIPoMIK`zpzH9Zj^5ptP+i#6E^7A7=e27UXM)b;U zB|gG|mOkOHRIj8<{WI~;^;_z1Z7tfTHt^Dq0CUGC(fWb;)6*yXY3m>5uT_65AN=XL zu{@LbpdX44y-Hh&k3a7*zg%9tm~P$tSg)UQrAF?H{BEUU{U;;!tS!ftiPHKZ<*SwA z_^Y)ce8YdwVraIv#Gv@4u-% zla}d8Dc4(SFHY}iEj{;Cg66MV{73Pi7q(;LFdvEYYUuG_>a;-}5MU zcLbl?KKuv&-A%9f@9ut}{qsh^`7ixO8&Y~k;5>G$u9*Gz+wXYIjDS@%&Z@`xZ)WUK z_P5{9;>Pl7;XjM_k7Y#8sSEzIV2|oJhZ*ZDh5sxVw2sOd^}&A@?7@FC?-uyag29;x zoLd(DvtSSYn|TMpe-;eRMBv=A@Sg>H@ZZck2>!ERa3%uhmWBT;*n|IO-a+u61%opY zIJYePXTcu)H}ejH|121siNLvK;Xe!Z;J=x75d3Gs;7kP0Eeroyum}Imyo2CB3kGK* zaBf-n&w@SpZ{{5Y|5-3N6M=Kf!haU*!GAOFAo$OM!I=o0TNeJaU=RMAc?ZFN77Wfr z;M}tCp9OpH-^@D*{p_e|>?! zyWR!tKaRD;{Qhms-=Dv%dx5_@+578XXzE=T-Laa%-(5^x-7fo&XE~Ye?%#sHt2wx% zJ-7awUH@2@2Y+|)a7UZ6{u`SoSCs;Pcd&4Eo6djxJ${jUAXypsq|e)yYL^%8Kd$D^ zZ?EmYKYpbM{qd_XDto-8-~1!L-In+M{DsaSj`Xj(|N83UR{bY9T)XrJ{P{dwx$pcZ zvB+(izj6AQ!aioK`6J)S70?#`yOOh7TU!5d^oS+;kGD(~9r2#ODyuch5xQ%s<$1z|3>L!PT{}a$h`mj9{-Kg2migFr12}% z`>%i9$DbWS_#TrZzr*uC-2cvz zVqdz)f8+G+F#p1T_Vn1)sP`W~^<)3ZZ@1+=e%>eRJ~rSE{9*lPLyK)KJ^mY~kIB6E z`3wAKOO9P_TK_Rced|8@G!N@PyI8%grV;4TB`wy)DY{{{!P4B-^`rtpiXuYeZ9{-Kgw*!Ch->&3p{U>)D zvSq4jcO4XSx;xon{>A!lNB;W!CwIEE?V%6;+cEaL^hxVKVQz%J`=7tSf4lJJ_oL5$ zBQr0%?|%NU{`2F$)4tY!&oEs4;giA{Snam&ygf|wKS*E zKL5?8JEAk$H(Un(^T!+eZUlP$M|1E9eOUjwF~?pw0($?A(g**ABaR~(fgb;j)3*bE z%Ws3YhAn6f5e+x|Mk%a z|FxoUW`3Xl#_7ACKYG8or_aDyucrBws?yOXnA+kO6HXY|j! zJ3N1Z|8nu)Ax-PQ8T#&@f8oC!Sd076=Rdylr6+qF&HD2`ww}y+`1fDpvBxowfYyKG z^kMx6*776J)2h6{_(C7Ro&+>NJk~R5>j}3qD-!9C>{gCya#CwmeHGf$D#c|)^*!izC zYu%M@%^&=?18=!Lu>WM1+huFbAN-e#{oQHKf4}{{*UOqe`0sAkO6|+~@2;6P->x-( z@LwtZccwf4?eWaT?}gk4$%??|l+yZM@ZX)B)!JtN%|3S#8uIl>u6_&u)nfLRJm)|9 zd}8z^pKTC(Y^Jy-vQOX3nm_n&S7yb3M2^gP{`xB$TN1v_yM=LCPrF>%j zw@0M+>&3kP_^AyOwljZN|LsQ~_sI8O>aKQ*tpDJ@T_e12zu-T5f8qJxorI41k7i!} z;rkEx@6ISbv+dA-?YY*S;pijou>N~y{CDWJ_>b=%#(wY5AH7TP`P&Y>`F#le7k_J@ z=a}v)0MX|Im##&hkgV`L9J?dtRm$A-!_e%srK^w`l$S13drp ztpCOcSzk);Kek`^TNtGHV*R&1TJLJ1*MIT-hurn@^T3_?ga39VSMNWW zeREs*Z`b(l+%LWV!dW*x+MPf6Z&wc4f9cHHo5D|``DsjSNQ!>n%li9Qc>cE|f6jk$ zM;nu)Ii|9*65N$P_-{x4s88ZQ@)ysd;kNCqC-4XV?Z{u~znDHDuXJDQ^5MT7qx+0L z<@_gP@s{W2FwH-L|M2<880&fe`D|(SpTHm1e`AEKFBSVwL!5sO&%MMTr{?ZhDEfUb z-2bkR{QWJY{_CTJ&xZdn|Bkb4#_@*HYlW@rXXB|L;yhNB#Hzw|6c%lG{oYEufd+ktbeDFU8C7 z+*9wvOYykZVLZTq2cEu+j12KBeuESx-cD4dek3Kv(P2ob%BGQ*fB4#$M!5J0jQsD) z_^#Zx>%X%RHn!Z*N7Qi^xH5mp|5oKt{KxKU`%U~Yee&C87XDi`_E+zh>%Z8YJXiZKWS={^;0juhY@W3-@2<5@%X7{*zd&@~+Jv@;?*xwWipA_WhIGe=G7w`y~0_DkCrNhc=!h1c>2 z|7migSyw*)v-u;I=pVl`Sv1E}_B$`3yo;x<?!rt_^+Kl`0pvaj5v;R|Fw*~H14gyAM(Eu?DW=I`Ho_3 z+=u*c)yQAJUp4+~r;o|J_Wm>ccRg+KJt*}bQ&gAtA^(f_IhJw{wD`}AxXhw_z7`*O zc^sxK;XhCp>w!A|wa~Z1_)BNU`=i6pFFr5G&-nh07!`At)%vfEK3xCKiRL|9DfeG1 zeemBs$r!)aHU4X-Zw3Bv{X3qfwiZhLCs!Nt&QztYIwj^ym#%T{uAa}=!5_6&B&;|uk&Ba$jk1#oZ7nL%nsD`>$o>HCf}e*B_jZ8Ui_6 z#q%${^z@al@m~*p$p8A{^yJdz{^M&8xym)~_B9ESW}HV;wB&mJ;J+0auJK4X1PjQuM6RO&zC&E$V|^ud3tM0>6MsPkVt zeb@6x=L@cX*P@U2rqqA!^sO-dQq}?)$kGw0~e}Vs2V^8*{)PFtnT|fT9 ze-iJN-s}9wSHAhX%{<<+^<>P$_rI(Z_m%dg)PL>tA^%$`-Ye}(jsM#6J{ozE|E(1B z)%K;vf6O2;#V?uScV)$IGJ8Qefe;mR3kyl3LYsX*s?~H+~m$U!uv1yHI_AQN${A0#TnwRsS z?VZjEQy<3Ps~9x?Ym4K!QcMU@663G`3;};vG3U1{`Jeb^jrTNv#$6gy@So4bta;-< zjW}x70sdw&aCJNEKN;m#c?W-2b1|cR&jyCQ;-zV#D)PO(f|DE4w`;Y(X0^{$_%=yPY(|<&sSJLf4Kh_kHVfyXZsKLU-slI zb<{Kc2mVr#*l*oz|Ka}MevIXgc&7j8+85U!xrpque!l#PxuqOihmWBT`*n|Ii&LH?tgTbB%>|6E<|Ka)jeR+!=`CR|u z?|)-4*mc=#|Kaa1cO~uoxM%tg{GG>P$2GJ4hrj>bk+SqL&-5Su{x=&X)xFmfql!ue;Vw;e?4aq z`>*%^gW0`&PXzWYYy9`??|Aj}z`hLi9y$B(umAn}JAT^xpf?J8mN)+U>wkFl^uV6{ zXdE^B@2CI%`X7GU`#>WS`_*Cm_tSrP_4L4g>}Vaaf^Bw|(vM`!J(@q^17TVUrzhQpl-*J;j^1Hv*FQ32u^=qiB-!JXAuf0Cj zvsjkv0pq^}eLucO`SW?Go6qxCS6|52-151e#`?1!X!T!Te8JyYJa$?R|H)a@>(8C2 z(LGl9j~E1hx>S|b2LHi-Wu%PT3jTxt#?h6o8UBO+(u9m(Tk1dmj+sP`-~F{=KILu^ zUjMr5bLCfCKR!-RYytk0vnb?Q*2wvlo_``8bB?Lhf9d|OjbG?L-TG_yr+51~v3go7 z@Sji*{;pyh{v!s#-xd79fAHTG+}UlyfAF8p!^~;;5B{6UTdb{8|M~alYUd@={GPx5 zt*t+|z1x0n>)Q3Pp2f0U57hXt{o7Gue?Oe(&*w$A{qTCDul`(5WBpkVl=|@5dv;YdDs&Eymwm40c^#>pvkXMIWt`@s~y`p7;Gj zOX0s=xwDVE#(zBOV*Is< zDxa?NpWXNAX+Pe`|H@;s+m_}2YrR9u?sD^ea);|bcYVx%Y*cb*_%F3I*1mTAyHTTa zymkHyqaI&E@wV5*;t&3GB6$B=%Kb;o*j%~OypDb|e=%Tt-R~&(!{|tT1n-D>a!hcu9_AYJK_|G+-nKSgULHPa)PiyfPTCaRbiT}!L z3%A7eZyW78{Hp*1z8AsU7^a@kgaT{`>8_`?9|e@csR3?lnc+ zeTFyYj<;$Xe|^jNysWjpz5jPEY5e!czwXQVKEMQjLLBFNY&^OC5C+@$^Lbl)erJmM zj-CBStbG5CeMR3lS;k*TA1z`2gg&14$3I$HcmEIk^~HI`(#C&8-}fJN{Ke?A`Q!EA zZ^g*3vrkt3_xh$?a$vdtm{up|K-Qu);kpN-*wSlyJgFNboT}0@9}@S`qxXj`L*M`^4@*&zk=~s zvN?L^>kRo#V=XV^b?&`F_Mbf-^VaV5ue&~s_uA`Mj_|tsX8c!r{Y&Je=wq^C{JHl} zkpHbq#oRsn_^)XEEuoLdGW-dTzR$ql+$gWvn(<%B^)Hz(=RV#-_!C}n{kvxTSJ*4| zU(NU{*&b(POr-p_`D6Fd8t}J5q}SUA<3Hl2{_{5~75=lwUs?d^=HjI{I^cLSK1TfKlkaEkiXV@&r%N8A1lRurF~)lxpzn0x!CyW+fPdP%Xyyf z^vO2#PNTAY|1TWH-6cN1VLNv1RI~p|uXkGu=W^|@6w&+ai}9Zu9d|CypSumsQN-!n zT>ko>SWn(*jk|rw-`_ufeVzaK<+bNiZPU73d-sX?YJ1x1KbI$>kJ$_9qwLR*U&!A- z-~Rsj{Kqe^J^%G}-Zt%5uD#WwdXN3l^j|-J|9m@t<>KGFp7=|>a`oRM-Yf2R&Ok>+h4E|N8km&;3gBuN)t_^~nFOqIDJj_2pkVzH;l4 z|6Re~O8!e_U-|q$$5(DW^1mziqc+)pUtjXfKrH*oFQfSK9XJ2Fn68I;jr}{k{!O=G zmyNa4yFT4I?XCZEkMpYN-Md}mKg!?y91qzPd|U@3{0ChzrJ+q zoGqmIzbbn7ZrAmnd|KP`-p?QMzk9RPy7$I^_W4~q-OnHLzt#xex190c`1>y|(c$%P zD*MCzzxy(l+jH@sf48Ofe$4sPFFK$9`ukTq@AB`L&gXxA33oo%%UB=l0pmZq|6;%V zd3#-Kn(yCGjN@y=e4j^bxzfph{u-;dY#HsF-@ZTJ+kUKfv8<#A#DBgY#AojEv$=l$ za`|8R=QFe~w$IA=>ubNHr?HLS1GfM8^9#Q3Oux_g2Yi;Jy7Gn zbN6ZSSCW5;_0HU9_+wA zbKCblo&WofbW5$hzjwJ3|K;6Bk=5WYm4W5PKYKj%w=Lzbr~SP->N$Gbf5H*H8}8VE zeEy3KU7JX`9{RpO7fE90-P{e2ngJ#z7%_(I_L>s^^|ikrJ6cC9{&Vv-!;`!GY|d=o z%3uEeUuY(qF(%LOM_bGK68`$y*NVno_1OO7>jxea>9_dICytl%s7b8x9T~HIJAbJR zjOzFK6S|E1cm$Shc|H5kzuxfY^MC)FZhtRsG>_K!k8>d=(D3B)7pKqWwUs}gE_Ys- za+k*E{B3F8=gpsw*JPIZb(%Fir5TZZ>Z;2-Z}zK%zrObOVMhB% zL;sQS=zMq0C!c)qh-*e(-pXv>>c4#c6|1>qS(*R(+SiWC9`)7u@7#SZuQ4LXMJ~;h zmGMZ=Jh$Xu{un6p{#aScUv2&=?a})lyo@-?(0@#Z>%TO8OcIZHE@w37kA}2e$e$g} zaM~~xzv|!`xt!V7%-f@JY(K*m-6f{Zc>5zF^ZYtX{guIcKv4)VvoP} z^0R5CsfWL^{3}j}yHr?_r8MvJ=FgXW|BKWYdw(t?qmNAdC!>kH$F5psUF@A~Mz4_2 zGXLfBzgU~4%S!m`Ykzc{-`zu0}d@@4)0OXpwZdlKK$KQH|G-2KORt!C*n`IvE3 z?7#l+pX5xDa=Gvoda*wLbG;gxuWx;9OtqK!y!!3Wh}84iaQ)}^Y2AD;f4Th6=Sz1! zpZ}%XdmbCJj!5!9;)h4kmUnZ#653BE|4YX|^i$pT`TKwA`esGzS?##~b9=aMPJ_Qx z2H4A2u6!>4%awl?7c-B>_>b?z@pCS<)BXI}*;Y86i+}I>utj%CuKt-3drn)f|6(I& z>9T(QkpDf0tC}M*{;L~t-L3oi%iVwT+tZy#{#V0{*0#of*WZ6hWqpMpnZfBXvXzaam6GHK_>#{T>MoA>(h-MoJL{qe8+vgrHq{mozLC&A%%vHOR<|3dzJ-qxN!?_;b<_W=8kuKB+I##esC zTa3Thbzy87@n$avf9VM9yVm%R===U7#~+P)WG?y3Mw+%s<7wef)`7o$3Ckb3mH)j) zT5{mI|4Qg1Cdq$HAD5sjV0Zka^?c8UpEUH>-)%nM&)6dD0pq{dc>D6-`}Jb*eEiM1 zj|?dOGYo>iuz@@rdH;T0=s$l8H(&JE`aZpS{aNo~d8!9Y{#SDS8^-u- ziO(DSmx{vP>puP~7=Ob%%VkU0f3D9$|GDc!zpcG~Z-!cr-uSQd`d7x()O&pVb@>5* ztucFAIrd-4_)E;3bI0rWs)O(U@#svYF#bM`8lB@Y{v)n>K7TXxF>mynUQ{pm(}~tI zYq9b_$^qZ6^ZU=lQ}|CfBi3yG%)Fn^(HeaI@l2x1kIMMZefp)2Kbt|IE{V;ne5(tI&J2#HWvWGizG;JG-~jy4=0~X|3g~++NIR9j)=-=d<6CJzA3B z&!^AzAJeIm|CQbAiER~Y-}f@WBhS_shaE-X_~hMf30UtpZ}!yDGoHvV-vkID-y|A{Anxj*BZAcmFw}J?*pC} z{$#Bde>UZA9wzLcEe-EK#{Ab)|C5NAcPw@ObEBJ@8}a!s#@~5d%xtOLe?9rR?;DK2 zGh@7JTXp`E@ig~7YW$VaJ=VVDn(jKuP|L>Op88jf@cR2z?ms&|a=l?PFPf4qWUMsv z{%9Tg&)>qoU&mv*Y0ut1x75!EsH|@N>&JP;J*@Yi7=@s&y6>2iR<4L znV7v#HU6_1W760{Hhna0_!Ay|-+;f_abB^#djBzLL>*1V=(G6~Uc>wg{H@5t75h}< zzY_Wkf6OS)YxvKlUg$4b8uIV&58tD-&#JomuZZ0J+idY4UsI%h+v6|igkk*Qzx&gp za|EUSvwcuIt-)VyoA&l$`=S5r?Q5s;{6QOw<^F5E_blc7c(?dZ>S}rS+YjekYBgor z`X8nEZ_r=Q-p8N3;-&BBuiX15p&{zl=lSznsH@*E9rEUH(N{l6YUzP;|Aq1M<5f4G z=dZ3l?ft8*uch}& zi~Bs)&bRbF%1{r$fAHT`X9D~O|6RqNv|H*w-TP(X^@nc#;rKbWe(6oT*Yp7V2mjUZ zGGg0O|4F3yJ@iZ6`p|!M<$K%LE_u{o929 zw0wVm7Opyi!+&%(!)cb&YWm}<=-sHiPTJI*dqH(geL z?zx`2HtnsceZOZhU)lqQ|1$aCAOEJlb3C=~!$)EL-+znMm0DU>|LY7$=67;@sh0db z<88rTDQo_Ab^h~v-=3F9yuQizJwnsFtpD&q<6q*ZFaGJeQ|-#SzV`i|#e8WG9RADX zM-m^Q%6?DLM}ludV&NMSYF^ZTDX}eFuLD_u1Ds zZSjBcm#IIu9{gRyByUCn}!#=?ZZf_orbz;e~RnRIKJiBxc#08>|3_ZfA%vUwbLK=lK!i$)80N` ze?I@S>#LpKm!aMxKm3<-L*07b`rs`;Mjwu?YbW_k%iCw~`bM|k8-+c~*Z42z4}bk< zO#Be|_4W79sh@s-`R%*&skUidZ~J@lqjA)S|5ABbgOlF;edzoCE{r%X{I^x5B4UT(T>!jR^K3?;7;OP7- z=fu(NYei$PddB+i`wx=?j?TaG{u`Tr?!}Gf(H{P*%BSAuOXh2D|GnKuY>dvoa{Dv7 zea(pMQ`cz!eGK@baCH8a_g^ae{O4P)d~W?d%xE9!;XlsSl1_VDrf|E@)dz3=@?ZMj zC%(!KHv5}yoj#-fWUGzu+ELk~zOnve0yzgn{Y{0~o^Mtfka z|6U{7K_K$KdNb@%;_zQ#URRnYecboC2QxbV%H?mP+uwr~?V}v)KhFHvfyn=~WA~K$ z4*w0w|2_=9@4w*l-={F6bsS^;NBJMwj~GP$rxmlO)#Lb&9H{hqQTOKW{rkmV`kz9p z$^13l`uqN0*Kt3MiaEz)`>*ziU$g!3{5ugKKjxYEcU<%NpQ*2Qs^srWMz=pFUQcXA z{P*^i;YT|6T>n}2VvdA3 ziK)!{@4}zGoR*pSg8Z*9{=vR`XHq`@>}!8~te)CJ)PIH?ntHp;qRCzRUjL52y*%JA zoq@?19?!&bOX&*d+T>g;%?M7Dqc>Ddwqjr4!wX=BSfB9(axrO7uO81fB&i#k{ zZ%?Aik9x5G49&>@kpG?ZK&$^~-#{IvObAA`T zN{&#L<4ld#`- zZogu#ya&X8@9{DgLDy@sxs&T(!xKLLEsw`eTe|)eR~+xcn&k8{@4uh$=Z?R*?_cSA z{_fc4*ctR%R+9hm7@d03#vjFbTk`Kt)aV{-)PF8vul{weCfv2J@$G&6=`N*ZX36XN zBmM7F$7{P6Nx>_(^wYkaCz1Db>!0U;UgdHv+52X;1%GW!p7_sjQ#N(RpT$v0{>6;6 z`6utc-1q_e|z``Ni97P@n4zo+^x?1XSOQ3|JM`$y#L1LpDn$Q zGSmY>{%5Eunxfnb`5#zn>4AR#y{|IP_(T4O&wsN$;P|h|c-D38Yx{Hm;r?HX6Lu;y z*nfs*(G&VKDQQOBS2n|G7_(c00^-+lJ@{jAGpABg>9esue_y^igv2VDQz z#MDlI;(z$=J)Rni7Uh4nZQ9#^SR>w|KP8|4+V#~=gQwOWIQ)0G;=@wsJdM!r*PF?7 z_V@lIwX;3ry4e1YZojqn_bzAqFYb*qpX8Un`FpWPCZX{+_xY=K`)+-s+uxg`o})kf z=j2NWSwv-kE|Vg5DDfuq~+iNL;P$NKLx0>7_>^ZA$StGr+9wjbU8z6|vq z`Qblh_gM73Y95_`<=i~F{oW|-S$?el$WU(zUnBPD{43|n(e3ZakH%3S{wvJuUXix$ ztG(x<^PKEcYygJ7EPex$!;OP7-@4wvhe|7n!-`?}sm~}*l|Ayp$WI>A6 z==|%$!>OA`w?8Xd&uV9^|6WF6rr-X<{l90iqjfZg|4!YJb4z?ZjlOUD5rgl)e@y@X zAFr*Vd8E#A-2c)c%ZUJ+qC&f0?{bz8@ubB^j>&@KIQQ{8^dQ(z5#B?oK#R zjxW`ce{8%h_-j2g>ip;TzCACIknbd;cUk}8gJPff>5G55?o_+7uCM*p2%y~Izl0me z9TFd*%6?DLMd%S>VI=KtbK6Q(gQmFOVj7@2mj%F5WF5Z z{FiV8x#P@#Ir?O95{GzS>wc>iGy*d1s zafif5GCt1rQhA>-!-m>)FoHei#v%=hs=|znnkzUjKYnMjhedzf@ipJ?B4T9?HEwcODg; zXV(k=_2-T3{bzGB^5_o#!EuTFF6a7@5qfUj@E`nlHQk5*kjHgAKX`Q%@7X^52mf77 z?%_Y=f5`v5XQM{53ID-=qZ!N9fA|miAM!u=j~Sp`;8s7mvmEQsdf@OM@;~H%@ZZh6 zC!Rh#yRkgi1Bd_k?8on>9?11F*2j9lT>sWS?}qK&9*FfWmX-7X`|p=O$G-UcNB{m= zUM0`%r?!!L=$+~wyZ8CpOL`jH_&s3!m+#@eEx@1O_n7A%fdBf=EG`Y2T6zHf(_oM4 zXz6{Fp&o$$G#CU+Ej-#_2@o^k5#a(*4v!8PIjb4jGv-Da-;=zn3=<4@|s=O63xmD;n}{xkmvaNcm~ zElf@DUn;`uubb&VLrt2l{902J_*E+Jf^xsSD%pS%lYLKGT2Z zsv*Tz$vRUL{I@=1XL~r`f5Z*FN*GIRBlU1y@ZVX4*Iz!{f8cL@CT8v775(VF z?LQ``kG`)j@Hd`}@h!lA8qc2M`R5)|;Xe%qdm^xJS@=(bJ@~KZ41)hO80?9_zGdM* z4ff!_o-+vk(_pYC0{fPQ|1{Wx|9Z|K_)mkuo(Sw)7XH&<5B}>pgWx|6274l~Z&~9 z|Ad);zJb3~OmiSzx(#Nh9L@5_w#p3nB5 zIIDmBH7w)$SDT7C^@WPM@$4y{e-@rv-r+wD23vX_W#B&z_TaysGYS6F zV6Z0w`<8|OG}wdxdd?vDPlLgp2<%%H{?lL&{_8n|;6Dupdm^xJS@=(bJ@~KZ41)hO z80?9_zGdM*4ff!_o-+vk(_pYC0{fPQ|1{Wx|9Z|K_)mkuo(Sw)7XH&<5B}>pgWx|6 z274l~Z`mvShv)D2@%QOe)g9kl|CxJseFo%AQdq|LTNm?Hww~=j+<#dm^6Tu!O#gwub>hCl zp3L?i?*FY2^|kh4rvEr=qu+hmpM3wbTuUSWTZ^gEy_xSnCPs%pQy2Ivjre-o&Gw%- ztIvSK9=+rH|JI|bY>(hSjb~5s{Bs#?@vY!L4F=2uvfd4euga6W$rPsoL8VsiU z7;ED_0RL&Q2mi(CDq9NwX)svU*VxMK0r*dYJ^0V1s&0<`*LOGW`qEk)wboPD=h&Ki z!1&L;pS$8T-D9RtIrtM~ey893^YzurjaE4j)!GB>KYEgGN4UyP-eWoyOtPJ09YweaSbxl_c~*;n|_=csjlotWNjPvE~+=InBJi~1V-Yy9Wm zeKX%w;9ePDHHZ5zYY?Nq7wkXxsRO$ILJ_};wy<6Bryu$2Yc&3IBaZuj_Sjl7eSIwN zx(CSrO8ClcbJyr!yWj9%j;qr3*T(VAt;2t%jHR}_GXdA_JN%bos%G7FF}!=r@LvsQ zXWQPLf@}64{yXETcKJ2&dqS)5UoC5~?Vmux{9}OsVl1^Rn;*NUvAewa<-> z)01n5|JtcDC7(>lxMPF=3_*QU<6`vmn&H1b^0@TVDH(Ho@Sg^IRL7Y3>`*KGr@`P3 zr05?b{HMVl{MSP!{HMWSPXzWY3;$`b2mke)LGYgjgFO-0w=Ddp!5;kAa|XeG8VvSC zVBfOFe|Y|{kHUQ!>OFGyAD;gMf4wBtmpA^y^MBy4o*#`8$p8Atd({K~X|M%P1okZp|7oxX|Mi?f@Sg^QJrUTqtnnY7 z|Ldc0Uxs>*oc)LA|G-}_N%iH8|M2`D_^anfqXhE5KJvJSzWA~JK z_TMjm(x|rX5BPfuZ}xE*|F!1bv&w-#J8n;!7XQs6sjVIG_ax@zSd9PHyWdi`M{QK= GmHI#1p`y$H literal 0 HcmV?d00001 diff --git a/examples/assets/micropython.pbm b/examples/assets/micropython.pbm new file mode 100644 index 0000000000000000000000000000000000000000..2bb093c6d64fd845c7a7d2aced66ef46e37042c4 GIT binary patch literal 2105 zcmcK3&ubKC7zgn0yYD)$4U-*dL8h`~2Rz5_s+5IMQqEjE7o{g?8T)BGT@}=nLkyi7_v1T-T=5(}l@zZmY7cPyp zj9<<-`cjx^Bjydi(YS-=c zsouGszj52HPk?{*Jg{un&ogvvA7{AlnmPLyGTyXvAD&}@dSvHZbrRdmXvZ*6c1~}* z>pqiC{(Qun%!%2?4NO}z{KR$EADXGkiJM|`XwmWJZcgldg4J5^-Rd&$mIXfdTdSIz z();nW^@b8c78B>O+-ji4RBmc)KbSzhyamt4^jme>aGmGAhjYBmG?Lt*z{blaeW67j za;YlxK~hCFLTpPf?t&MdMykA6LKJ=%4edoht?)5;?P>zlOG_`mGj6VZey{xx8`3^F~fv(QU z-*suxHNSkwj4Uis54KX%Mez(JeM{AMi~^V02^2YdopRGQ6yBHRLZg=FDBj2mV#(?J1KcK7ELeDqGXquzhv=fJZgN}A%`e$Uk8{ev@bd=ll zY1wQPE;P*lKL^95vjfFl5AN3*v|&G6q^-mg+dlx0E%yet8g|IvSPkpn0|)mSdh6c; zmA!_Yb$T8+_^4cQl4A11!oA#;hs7CoCI>O_=1+OSVw4e;XPJA8Ie}+c!_LLhhXw_` o#iVrBuvbnEw~hH~SPmRgraL|?r)yRXJzjPXewC2E?SDT03l_2?`2YX_ literal 0 HcmV?d00001 diff --git a/examples/assets/runner.bmp b/examples/assets/runner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..59e9ad72e4501da1cb79c4426712c07162743950 GIT binary patch literal 345738 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 J3_R)#{QsyAF(m*1 literal 0 HcmV?d00001 diff --git a/examples/assets/warrior.bmp b/examples/assets/warrior.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d3329de306e1e2945fb492e8ba13642e33a28d96 GIT binary patch literal 73866 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

Kd_H)iwEz~m`|U%CBp=qYlF zt#651qh?n9y*Wd4$n9B~IMR>VJVLlu#@})^e_L%nx4)P3xgU-f_Yu;lZD5a}yw?^E{CM9@ zuQ}(}v&hdS`A3EJ3O1)Ypf8)BOY>)q= display_drv.height: + st["phase"] = "scroll" + return + # scroll phase + i = st["i"] + display_drv.vscsad(i % display_drv.height) + draw_bg(0, i % display_drv.height, 0, i % image.height) + st["i"] = i + 1 + + app.every(_tick, period=1) + + +main() diff --git a/examples/bmp565_scroll_sprite.py b/examples/bmp565_scroll_sprite.py new file mode 100644 index 0000000..4678713 --- /dev/null +++ b/examples/bmp565_scroll_sprite.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""A runner on a scrolling street. + +Scrolls a street picture across the display a column at a time while a +runner sprite runs on it. Touch or click the top half to jump and shoot, +the bottom left to jump, and the bottom right to shoot. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics. + +Run it from the repository's root: + python examples/bmp565_scroll_sprite.py + +On a board, copy longstreet.bmp and runner.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from collections import namedtuple + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import BMP565 + +point = namedtuple("point", "x y") + +display_drv.rotation = 90 + +display_drv.fill(0) + +image = BMP565("examples/assets/longstreet.bmp", streamed=True, mirrored=True) +print(f"\n{image.width=}, {image.height=}, {image.bpp=}") + + +def draw_bg(dest_x, source_y, count=1, source=image): + # 1D row slice is ``count * source.width`` pixels; blit as a vertical + # strip (width=count, height=source.width). Do not use display_drv.height — + # tall panels (e.g. Android after rotation) are larger than the bitmap. + display_drv.blit_rect( + source[source_y : source_y + count], dest_x, 0, count, source.width + ) + + +char_sprites = BMP565("examples/assets/runner.bmp", streamed=True) +print(f"\n{char_sprites.width=}, {char_sprites.height=}, {char_sprites.bpp=}") +char_height = char_sprites.height // 3 +char_width = char_sprites.width // 6 +bg = char_sprites[0] +print(f"{char_width=}, {char_height=} {bg=:#0x}\n") + +run_sprites = [point(x * char_width, 0) for x in range(6)] +shoot_sprites = [point(x * char_width, char_height) for x in range(6)] +jump_sprites = [point(x * char_width, char_height * 2) for x in range(2)] +jump_shoot_sprites = [point((x + 2) * char_width, char_height * 2) for x in range(2)] +shot_sprite = point(4 * char_width, char_height * 2) + + +def draw_sprite( + dest_x, dest_y, source_x, source_y, source=char_sprites, width=char_width, height=char_height +): + display_drv.blit_rect( + source[source_x : source_x + width, source_y : source_y + height], + dest_x, + dest_y, + width, + height, + ) + + +def main(): + st = { + "i": 0, + "scroll": 0, + "char_y": display_drv.height - char_height, + "char_x": 200, + "shot_location": 0, + "sprites": run_sprites, + } + + def _on_motion(event): + if not event.buttons[0]: + st["sprites"] = run_sprites + return + touched_point = event.pos + if touched_point[1] < display_drv.height // 2: + st["sprites"] = jump_shoot_sprites + if not st["shot_location"]: + st["shot_location"] = 1 + elif touched_point[0] < display_drv.width // 2: + st["sprites"] = jump_sprites + elif touched_point[0] > display_drv.width // 2: + st["sprites"] = shoot_sprites + if not st["shot_location"]: + st["shot_location"] = 1 + else: + st["sprites"] = run_sprites + + def _tick(_=None): + # Auto-service handles QUIT; never poll from on_tick. + if app.quit_requested: + return + i = st["i"] + if i > display_drv.width: + st["scroll"] = i % display_drv.width + display_drv.vscsad(st["scroll"]) + draw_bg(i % display_drv.width, i % image.height, 1) + st["i"] = i + 1 + if i < display_drv.width: + return + + draw_x = st["scroll"] + st["char_x"] + sprite = st["sprites"][st["i"] % len(st["sprites"])] + draw_sprite(draw_x, st["char_y"], sprite.x, sprite.y) + if st["shot_location"]: + draw_sprite( + draw_x + char_width + st["shot_location"], + st["char_y"], + shot_sprite.x, + shot_sprite.y, + ) + st["shot_location"] += 8 + if st["shot_location"] > (display_drv.width - char_width) // 2: + display_drv.fill_rect( + draw_x + char_width + st["shot_location"], + st["char_y"], + char_width, + char_height, + bg, + ) + st["shot_location"] = 0 + + app.on(app.events.MOUSEMOTION, _on_motion) + # ~20 fps once scrolling; first columns also tick so quit is always serviced. + app.every(_tick, period=50) + + +main() diff --git a/examples/bmp565_simpletest.py b/examples/bmp565_simpletest.py new file mode 100644 index 0000000..d89b8f4 --- /dev/null +++ b/examples/bmp565_simpletest.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Load a BMP picture and show it. + +Loads the whole of a 16-bit BMP into memory and copies it to the display +two ways: as a slice, bmp[x0:x1, y0:y1], and as bmp[:]. Loading it all +needs RAM: a small board such as an RP2040 raises MemoryError. See +bmp565_scroll_sprite.py for reading a picture a line at a time instead. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/bmp565_simpletest.py + +On a board, copy warrior.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import BMP565, hline + +try: + from time import sleep_ms +except ImportError: + from time import sleep + + def sleep_ms(milliseconds): + sleep(milliseconds / 1000) + + +try: + bmp = BMP565("examples/assets/warrior.bmp") +except MemoryError: + raise MemoryError("this board doesn't have enough RAM to load the full image") + +print(f"{bmp.width=}, {bmp.height=}, {bmp.bpp=}") + +display_drv.fill(0x0) +display_drv.show() +display_drv.blit_rect(bmp[0 : bmp.width, 0 : bmp.height], 0, 0, bmp.width, bmp.height) +display_drv.show() +sleep_ms(1000) + +display_drv.fill(0x0) +display_drv.show() +sleep_ms(250) + +hline(bmp, 0, bmp.height // 2, bmp.width, 0xFFFF) +display_drv.blit_rect(bmp[:], 0, 0, bmp.width, bmp.height) +display_drv.show() +sleep_ms(1000) diff --git a/examples/bmp565_sprite.py b/examples/bmp565_sprite.py new file mode 100644 index 0000000..4952e93 --- /dev/null +++ b/examples/bmp565_sprite.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Walk a sprite around the screen. + +Reads a sprite sheet from a BMP a piece at a time, and draws one frame of +a walking figure every tenth of a second, turning now and then and +stopping at the edges. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics. + +Run it from the repository's root: + python examples/bmp565_sprite.py + +On a board, copy warrior.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from collections import namedtuple + +try: + from random import choice +except ImportError: + + def choice(seq): + return seq[0] + + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import BMP565 + +image = BMP565("examples/assets/warrior.bmp", streamed=True) +print(f"\n{image.width=}, {image.height=}, {image.bpp=}") +sprite_height = image.height // 4 +sprite_width = image.width // 3 +bg = image[0] +print(f"{sprite_width=}, {sprite_height=} {bg=:#0x}\n") + +back, right, fwd, left = [x * sprite_height for x in range(4)] +directions = [fwd, left, right, back] +a, b, c = [x * sprite_width for x in range(3)] +positions = [a, b, c, b] +print("Sprite coordinates:") +for col in [fwd, left, right, back]: + print(f"{(a, col)} {(b, col)} {(c, col)} {(b, col)}") + + +def draw_sprite( + dest_x, + dest_y, + source_x, + source_y, + source_image=image, + width=sprite_width, + height=sprite_height, +): + display_drv.blit_rect( + source_image[source_x : source_x + width, source_y : source_y + height], + dest_x, + dest_y, + width, + height, + ) + + +display_drv.fill(bg) + +point = namedtuple("point", "x y") +step = 3 +st = { + "location": point(0, 0), + "dir": choice(directions), + "pos_i": 0, +} + +draw_sprite(*st["location"], a, fwd) + + +def _tick(_=None): + if app.quit_requested if app else False: + return + location = st["location"] + direction = st["dir"] + if st["pos_i"] == 0 and choice((True, False, False, False, False)): + direction = choice(directions) + st["dir"] = direction + if direction == fwd and location.y + sprite_height > display_drv.height - step * 4: + return + if direction == back and location.y < step * 4: + return + if direction == left and location.x < step * 4: + return + if direction == right and location.x + sprite_width > display_drv.width - step * 4: + return + + pos = positions[st["pos_i"]] + if direction == fwd: + display_drv.fill_rect(location.x, location.y, sprite_width, step, bg) + location = point(location.x, location.y + step) + elif direction == back: + display_drv.fill_rect( + location.x, location.y + sprite_height - step, sprite_width, step, bg + ) + location = point(location.x, location.y - step) + elif direction == left: + display_drv.fill_rect( + location.x + sprite_width - step, location.y, step, sprite_height, bg + ) + location = point(location.x - step, location.y) + elif direction == right: + display_drv.fill_rect(location.x, location.y, step, sprite_height, bg) + location = point(location.x + step, location.y) + st["location"] = location + draw_sprite(*location, pos, direction) + st["pos_i"] = (st["pos_i"] + 1) % len(positions) + + +app.every(_tick, period=100) diff --git a/examples/font_list.py b/examples/font_list.py new file mode 100644 index 0000000..99737d7 --- /dev/null +++ b/examples/font_list.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Show every bitmap font in a folder. + +Finds each .bin font in examples/assets/, draws the alphabet and the +font's name in it, and scrolls the display when the list runs past the +bottom. Font files are named fontname_WxH.bin, W wide and H high. Set +EXPORT to True to also save each font as a .py module. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/font_list.py + +On a board, copy the font_*.bin files to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import Font, FrameBuffer, RGB565 +import os + + +# Change this to match your setup: True also saves each font as a .py file. +EXPORT = False + +display_drv.fill(0) +y_pos = 0 + + +def main(): + global y_pos + + # Specify the directory containing the .bin files + directory = "examples/assets/" + + def gather_font_files(directory): + font_files = [] + for file in os.listdir(directory): + if file.endswith(".bin"): + file_path = "/".join([directory, file]) + font_files.append(file_path) + return font_files + + font_files = gather_font_files(directory) + + bg_color = 0 + fg_color = 0x07E0 + scale = 1 + + for font_file in font_files: + try: + font = Font(font_file) + except RuntimeError as e: + print(e) + continue + + if EXPORT: + font.export(font.font_data.replace(".bin", ".py")) + + height = font.height + font_name = font.font_name + + buffer = bytearray(display_drv.width * height * 2) + fb = FrameBuffer(buffer, display_drv.width, height, RGB565) + fb.fill(bg_color) + string = "".join([chr(i) for i in range(65, 91)]) + ": " + font_name.split("/")[-1] + font.text(fb, string, 0, 0, fg_color, scale) + display_drv.blit_rect(buffer, 0, y_pos % display_drv.height, display_drv.width, height) + y_pos += height + if y_pos > display_drv.height: + display_drv.vscsad((y_pos - display_drv.height) % display_drv.height) + display_drv.show() + + +main() diff --git a/examples/framebuf_simpletest.py b/examples/framebuf_simpletest.py new file mode 100644 index 0000000..746bdcc --- /dev/null +++ b/examples/framebuf_simpletest.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw into a FrameBuffer, then copy it to the display. + +Draws the same shapes as graphics_simpletest.py, but into a pygraphics +FrameBuffer the size of the screen, and copies the whole buffer on with +blit_rect. This is how you draw without flicker on a display that has no +memory of its own. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/framebuf_simpletest.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import FrameBuffer, RGB565 +from array import array # for defining a polygon + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +WIDTH = display_drv.width +HEIGHT = display_drv.height +FONT_WIDTH = 8 + +# Create a frame buffer +BPP = display_drv.color_depth // 8 # Bytes per pixel +ba = bytearray(WIDTH * HEIGHT * BPP) +fb = FrameBuffer(ba, WIDTH, HEIGHT, RGB565) + + +# Define color palette +class pal: + BLACK = 0x0000 + WHITE = 0xFFFF + RED = 0xF800 if not needs_swap else 0x00F8 + GREEN = 0x07E0 if not needs_swap else 0xE007 + BLUE = 0x001F if not needs_swap else 0xF800 + CYAN = 0x07FF if not needs_swap else 0xFF07 + MAGENTA = 0xF81F if not needs_swap else 0x1FF8 + YELLOW = 0xFFE0 if not needs_swap else 0xE0FF + ORANGE = 0xFD20 if not needs_swap else 0x20FD + PURPLE = 0x8010 if not needs_swap else 0x1080 + GREY = 0x8410 if not needs_swap else 0x1084 + + +# Define objects +triangle = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + + +# Main loop +def main(scroll=False, animate=False, text1="framebuf", text2="simpletest", poly=triangle): + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + fb.fill(pal.BLACK) + fb.poly(0, y, poly, pal.YELLOW, True) + fb.fill_rect(WIDTH // 6, HEIGHT // 3, WIDTH * 2 // 3, HEIGHT // 3, pal.GREY) + fb.line(0, 0, WIDTH - 1, HEIGHT - 1, pal.GREEN) + fb.rect(0, 0, 15, 15, pal.RED, True) + fb.rect(WIDTH - 15, HEIGHT - 15, 15, 15, pal.BLUE, True) + fb.hline(WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, pal.MAGENTA) + fb.vline(WIDTH // 2, HEIGHT // 4, HEIGHT // 2, pal.CYAN) + fb.pixel(WIDTH // 2, HEIGHT * 1 // 8, pal.WHITE) + fb.ellipse(WIDTH // 2, HEIGHT // 2, WIDTH // 4, HEIGHT // 8, pal.BLACK, True, 0b1111) + fb.text(text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, pal.WHITE) + fb.text(text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, pal.WHITE) + display_drv.blit_rect(ba, 0, 0, WIDTH, HEIGHT) + display_drv.show() + + fb.hline(0, 0, WIDTH, pal.BLACK) + fb.vline(0, 0, HEIGHT, pal.BLACK) + + scroll_range = range(min(WIDTH, HEIGHT)) if scroll else [0] + for _ in scroll_range: + fb.scroll(1, 1) + display_drv.blit_rect(ba, 0, 0, WIDTH, HEIGHT) + display_drv.show() + + +launch = lambda: main(animate=False) # noqa: E731 + +wipe = lambda: main(scroll=False) # noqa: E731 + +main() +display_drv.show() diff --git a/examples/graphics_simpletest.py b/examples/graphics_simpletest.py new file mode 100644 index 0000000..240adc9 --- /dev/null +++ b/examples/graphics_simpletest.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw shapes and text straight onto a display with pygraphics. + +Fills the screen, then draws a triangle, rectangles, lines, a pixel, an +ellipse and two lines of text, each with one pygraphics function call on +the display itself, with colours from palettes. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics, and palettes. + +Run it from the repository's root: + python examples/graphics_simpletest.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from array import array # for defining a polygon +from palettes import get_palette +import pygraphics + + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +WIDTH = display_drv.width +HEIGHT = display_drv.height +FONT_WIDTH = 8 + +# Define color palette +pal = get_palette(swapped=needs_swap) + +# Define objects +triangle = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + + +# Main loop +def main(animate=False, text1="Shapes", text2="simpletest", poly=triangle): + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + pygraphics.fill(display_drv, pal.BLACK) + pygraphics.poly(display_drv, 0, y, poly, pal.YELLOW, True) + pygraphics.fill_rect( + display_drv, + WIDTH // 6, + HEIGHT // 3, + WIDTH * 2 // 3, + HEIGHT // 3, + pal.GREY, + ) + pygraphics.line(display_drv, 0, 0, WIDTH - 1, HEIGHT - 1, pal.GREEN) + pygraphics.rect(display_drv, 0, 0, 15, 15, pal.RED, True) + pygraphics.rect(display_drv, WIDTH - 15, HEIGHT - 15, 15, 15, pal.BLUE, True) + pygraphics.hline(display_drv, WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, pal.MAGENTA) + pygraphics.vline(display_drv, WIDTH // 2, HEIGHT // 4, HEIGHT // 2, pal.CYAN) + pygraphics.pixel(display_drv, WIDTH // 2, HEIGHT * 1 // 8, pal.WHITE) + pygraphics.ellipse( + display_drv, + WIDTH // 2, + HEIGHT // 2, + WIDTH // 4, + HEIGHT // 8, + pal.BLACK, + True, + 0b1111, + ) + pygraphics.text( + display_drv, text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, pal.WHITE + ) + pygraphics.text( + display_drv, text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, pal.WHITE + ) + + pygraphics.hline(display_drv, 0, 0, WIDTH, pal.BLACK) + pygraphics.vline(display_drv, 0, 0, HEIGHT, pal.BLACK) + + +launch = lambda: main(animate=False) # noqa: E731 + +main() +display_drv.show() diff --git a/examples/pbm_create_new.py b/examples/pbm_create_new.py new file mode 100644 index 0000000..2437b57 --- /dev/null +++ b/examples/pbm_create_new.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw a one-bit logo, then show it in colour. + +Draws a logo into a one-bit MONO_HLSB FrameBuffer, the format a PBM file +holds, colours it through a two-entry palette, and copies it to the +display as it is and with its background left transparent. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/pbm_create_new.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import Draw, FrameBuffer, MONO_HLSB, RGB565 + + +def draw_logo(logo): + w = logo.width + h = logo.height + black = background = 0 + blue = amber = 1 + center_x = w // 2 + center_y = h // 2 + unit = min(w, h) // 2 + draw = Draw(logo) + + logo.fill(background) + + draw.circle(center_x, center_y, unit, blue, True) + draw.circle(center_x, center_y, int(unit * 0.9), black, True) + draw.circle(center_x, center_y, int(unit * 0.8), amber, True) + + left = int(center_x - (unit * 1.2) // 2) + top = int(center_y - (unit * 1.0) // 2) + draw.round_rect(left, top, int(unit * 1.2), int(unit * 1.0), unit // 7, black, True) + + left = int(center_x - (unit * 1.1) // 2) + top = int(center_y - (unit * 0.9) // 2) + draw.round_rect(left, top, int(unit * 1.1), int(unit * 0.9), unit // 9, amber, True) + + left = center_x - (unit * 3 // 8) + top = center_y - (unit * 3 // 8) + size = unit // 4 + + draw.fill_rect(left, top, size, size, black) + draw.fill_rect(w - (left + size), h - (top + size), size, size, black) + + size = size // 2 + draw.circle(w - (left + size), top + size, size, black, True) + draw.circle(left + size, h - (top + size), size, black, True) + + +display_drv.fill(0xF800) + +w = h = 64 +logo = FrameBuffer(bytearray((w + 7) // 8 * h), w, h, MONO_HLSB) +draw_logo(logo) + +buf = bytearray(w * h * 2) +fb = FrameBuffer(buf, w, h, RGB565) +palette = FrameBuffer(memoryview(bytearray(2 * 2)), 2, 1, RGB565) + +# White on black — left and middle columns (opaque top, chromakey bottom). +palette.pixel(0, 0, 0x0000) +palette.pixel(1, 0, 0xFFFF) +fb.blit(logo, 0, 0, -1, palette) +display_drv.blit_rect(buf, 0, 0, w, h) +display_drv.blit_transparent(buf, 0, display_drv.height // 2, w, h, 0x0) +display_drv.blit_rect(buf, display_drv.width // 3, 0, w, h) +display_drv.blit_transparent( + buf, display_drv.width // 3, display_drv.height // 2, w, h, 0x0 +) + +# Green / white on blue staging — right column. +palette.pixel(0, 0, 0x0FF0) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x000F) +fb.blit(logo, 0, 0, -1, palette) +display_drv.blit_rect(buf, display_drv.width * 2 // 3, 0, w, h) +display_drv.blit_transparent( + buf, display_drv.width * 2 // 3, display_drv.height // 2, w, h, 0x000F +) + +display_drv.show() diff --git a/examples/pbm_simpletest.py b/examples/pbm_simpletest.py new file mode 100644 index 0000000..d55c0bd --- /dev/null +++ b/examples/pbm_simpletest.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Load a PBM picture and show it in colour. + +Reads a one-bit PBM file into a FrameBuffer, colours it through a +two-entry palette, and copies it to the display twice: once as it is, and +once with its background left transparent. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/pbm_simpletest.py + +On a board, copy micropython.pbm to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import FrameBuffer, RGB565, pbm_to_framebuffer + + +display_drv.fill(0xF800) +logo = pbm_to_framebuffer("examples/assets/micropython.pbm") + +# Colorize mono → RGB565 via a 2-entry palette, then blit to the display. +buf = bytearray(logo.width * logo.height * 2) +fb = FrameBuffer(buf, logo.width, logo.height, RGB565) +palette = FrameBuffer(memoryview(bytearray(2 * 2)), 2, 1, RGB565) + +# Set bits → white; unset bits stay 0 (used as chromakey below). +palette.pixel(0, 0, 0x0000) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x0000) +fb.blit(logo, 0, 0, -1, palette) + +display_drv.blit_rect(buf, display_drv.width // 3, 0, logo.width, logo.height) +display_drv.blit_transparent( + buf, display_drv.width // 3, display_drv.height // 2, logo.width, logo.height, 0x0 +) + +# Green / white on blue; transparent blit keys on the staging fill color 0x000F. +palette.pixel(0, 0, 0x0FF0) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x000F) +fb.blit(logo, 0, 0, -1, palette) + +display_drv.blit_rect(buf, display_drv.width * 2 // 3, 0, logo.width, logo.height) +display_drv.blit_transparent( + buf, display_drv.width * 2 // 3, display_drv.height // 2, logo.width, logo.height, 0x000F +) + +display_drv.show() diff --git a/examples/pygraphics_landscape.py b/examples/pygraphics_landscape.py new file mode 100644 index 0000000..d69bb41 --- /dev/null +++ b/examples/pygraphics_landscape.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw a landscape with pygraphics: sky, sun, mountains and a house. + +The picture scales to whatever display the board_config provides, from a +small OLED to a desktop window. It is drawn in horizontal bands so it fits +the memory of a small board: each band is drawn into a FrameBuffer and +copied to the display before the next one. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics, and multimer. + +Run it from the repository's root: + python examples/pygraphics_landscape.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev +import sys + +import pygraphics +from multimer import ticks_diff, ticks_ms +from pygraphics import RGB565, FrameBuffer + +app = appdev.App(board_config) + +# The picture is designed at 320x240 and scaled to the display. +W, H = display_drv.width, display_drv.height +SX, SY = W / 320, H / 240 +S = min(SX, SY) +BAND = max(1, min(H, 12800 // W)) # rows per band: about 25 KB of RGB565 + +# Colours go to the panel in its own byte order, so nothing is swapped later. +SWAP = getattr(display_drv, "requires_byteswap", False) +if SWAP: + display_drv.disable_auto_byteswap(True) + + +def rgb(r, g, b): + c = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + return ((c & 0xFF) << 8) | (c >> 8) if SWAP else c + + +SKY = [rgb(10, 20, 90), rgb(20, 50, 140), rgb(40, 90, 190), + rgb(70, 130, 220), rgb(110, 170, 240), rgb(160, 205, 250)] +SUN, SUN_RING = rgb(255, 220, 0), rgb(255, 150, 0) +ROCK, SNOW = rgb(110, 110, 130), rgb(250, 250, 250) +GRASS, GRASS_DARK = rgb(40, 160, 60), rgb(20, 110, 40) +WALL, ROOF, DOOR = rgb(220, 60, 50), rgb(120, 40, 30), rgb(80, 50, 20) +WHITE, BLACK = rgb(255, 255, 255), rgb(0, 0, 0) + + +def x(v): + return int(v * SX) + + +def y(v, y0): + return int(v * SY) - y0 + + +def scene(fb, y0): + """The whole picture, drawn y0 rows up; the band clips what falls outside.""" + for i, c in enumerate(SKY): + fb.fill_rect(0, y(i * 27, y0), W, int(27 * SY) + 1, c) + fb.circle(x(255), y(58, y0), int(34 * S), SUN_RING, True) + fb.circle(x(255), y(58, y0), int(28 * S), SUN, True) + fb.triangle(x(-20), y(170, y0), x(90), y(60, y0), x(200), y(170, y0), ROCK, True) + fb.triangle(x(70), y(80, y0), x(90), y(60, y0), x(110), y(80, y0), SNOW, True) + fb.triangle(x(110), y(170, y0), x(210), y(85, y0), x(330), y(170, y0), ROCK, True) + fb.triangle(x(192), y(100, y0), x(210), y(85, y0), x(228), y(100, y0), SNOW, True) + fb.fill_rect(0, y(162, y0), W, H - int(162 * SY), GRASS) + fb.fill_rect(0, y(162, y0), W, max(1, int(4 * SY)), GRASS_DARK) + fb.fill_rect(x(220), y(160, y0), x(70), int(50 * SY), WALL) + fb.triangle(x(212), y(160, y0), x(255), y(126, y0), x(298), y(160, y0), ROOF, True) + fb.fill_rect(x(248), y(182, y0), max(1, x(14)), int(28 * SY), DOOR) + if W >= 160: # captions need room; a tiny display gets the picture alone + fb.round_rect(8, 8 - y0, 136, 26, 6, BLACK, True) + fb.text16("pygraphics", 16, 13 - y0, WHITE) + fb.text(sys.implementation.name, 10, H - 24 - y0, WHITE) + fb.text("pygraphics " + pygraphics.implementation(), 10, H - 12 - y0, WHITE) + fb.rect(0, -y0, W, H, WHITE) + + +def draw(): + buf = bytearray(W * BAND * 2) + fb = FrameBuffer(buf, W, BAND, RGB565) + t0 = ticks_ms() + for y0 in range(0, H, BAND): + rows = min(BAND, H - y0) + scene(fb, y0) + display_drv.blit_rect(memoryview(buf)[: W * rows * 2], 0, y0, W, rows) + display_drv.show() + print("landscape %dx%d drawn in %d ms" % (W, H, ticks_diff(ticks_ms(), t0))) + + +draw() +app.run() diff --git a/examples/rotations.py b/examples/rotations.py new file mode 100644 index 0000000..3310e17 --- /dev/null +++ b/examples/rotations.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Turn the display through all four rotations. + +Every two seconds the display turns another 90 degrees and changes colour, +and says which rotation and colour it shows. Use it to check that a +board_config's rotation settings put the picture the right way up. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics, and palettes. + +Run it from the repository's root: + python examples/rotations.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import Draw, text16 +from palettes import get_palette + +pal = get_palette() +draw = Draw(display_drv) + +FONT_W = 8 +FONT_H = 16 + + +def center_text(text, y, fg, bg): + x = (display_drv.width - len(text) * FONT_W) // 2 + # Opaque label: fill bg strip then draw glyphs. + draw.fill_rect(x, y, len(text) * FONT_W, FONT_H, bg) + text16(display_drv, text, x, y, fg) + + +def clear_screen(color): + """Expand rectangles from the center toward the edges.""" + width = display_drv.width + height = display_drv.height + x_center = width // 2 + y_center = height // 2 + + for i in range(min(x_center, y_center)): + x = x_center - i + y = y_center - i + draw.rect(x, y, 2 * i + 1, 2 * i + 1, color) + + +def main(): + colors = ( + ("Red", pal.RED, pal.WHITE), + ("Green", pal.GREEN, pal.BLACK), + ("Blue", pal.BLUE, pal.WHITE), + ("Black", pal.BLACK, pal.WHITE), + ("White", pal.WHITE, pal.BLACK), + ("Yellow", pal.YELLOW, pal.BLACK), + ("Cyan", pal.CYAN, pal.BLACK), + ("Magenta", pal.MAGENTA, pal.BLACK), + ) + + st = {"rotation": 0, "color_idx": 0} + + def poll(): + rotation = st["rotation"] + color_idx = st["color_idx"] + display_drv.rotation = rotation * 90 + height = display_drv.height + fg = colors[color_idx][2] + bg = colors[color_idx][1] + + draw.fill(bg) + draw.rect(0, 0, display_drv.width, height, pal.WHITE) + clear_screen(bg) + center_text("Rotation", height // 3 - FONT_H // 2, fg, bg) + center_text(str(rotation * 90), height // 2 - FONT_H // 2, fg, bg) + center_text(colors[color_idx][0], height // 3 * 2 - FONT_H // 2, fg, bg) + st["color_idx"] = (color_idx + 1) % len(colors) + st["rotation"] = (rotation + 1) % 4 + return False + + def _tick(_=None): + poll() + + app.every(_tick, period=2000) + + +main()