From 9ecd7b5e79fe97d3da313332b8a4527278f1e3a7 Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Sun, 19 Jan 2020 17:56:49 -0700 Subject: [PATCH] Adding color library. Added a gradient to demo. Testing for color usage --- .pre-commit-config.yaml | 4 + color.go | 92 ++++++++++++++++++ color_test.go | 150 ++++++++++++++++++++++++++++++ demo.go | 41 ++++++-- go.mod | 1 + go.sum | 2 + main_test.go | 46 +++++++++ neopixel.go | 4 +- testdata/1024x1024-gradient-1.png | Bin 0 -> 5955 bytes testdata/1024x40-gradient-1.png | Bin 0 -> 1417 bytes testdata/2048x40-gradient-2.png | Bin 0 -> 1445 bytes 11 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 color.go create mode 100644 color_test.go create mode 100644 testdata/1024x1024-gradient-1.png create mode 100644 testdata/1024x40-gradient-1.png create mode 100644 testdata/2048x40-gradient-2.png diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46b8fc5..553dffa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,10 @@ repos: rev: 1.11.0 hooks: - id: forbid-binary + exclude: > + (?x)^( + testdata/.+\.png + )$ - id: shellcheck - id: git-check - repo: https://github.com/dnephin/pre-commit-golang.git diff --git a/color.go b/color.go new file mode 100644 index 0000000..aa1da4b --- /dev/null +++ b/color.go @@ -0,0 +1,92 @@ +package main + +import ( + "image" + "image/draw" + "image/png" + "os" + "strconv" + "strings" + + "github.com/lucasb-eyer/go-colorful" + "k8s.io/klog" +) + +// GradientTable contains the "keypoints" of the colorgradient you want to generate. +// The position of each keypoint has to live in the range [0,1] +type GradientTable []struct { + Col colorful.Color + Pos float64 +} + +// GetInterpolatedColor is the meat of the gradient computation. It returns +// an HCL-blend between the two colors around `t`. +// Note: It relies heavily on the fact that the gradient keypoints are sorted. +func (gt GradientTable) GetInterpolatedColor(t float64) colorful.Color { + for i := 0; i < len(gt)-1; i++ { + c1 := gt[i] + c2 := gt[i+1] + if c1.Pos <= t && t <= c2.Pos { + // We are in between c1 and c2. Go blend them! + t := (t - c1.Pos) / (c2.Pos - c1.Pos) + return c1.Col.BlendHcl(c2.Col, t).Clamped() + } + } + + // Nothing found? Means we're at (or past) the last gradient keypoint. + return gt[len(gt)-1].Col +} + +// HexToColor converts a hex string to a Color +func HexToColor(s string) colorful.Color { + c, err := colorful.Hex(s) + if err != nil { + klog.Errorf("error converting hex string to color: %v", err) + return colorful.Color{} + } + return c +} + +// ColorToUint32 converts a color object to a uint32 +// for use by the neopixel +func ColorToUint32(color colorful.Color) uint32 { + hex := color.Hex() + hex = strings.Replace(hex, "#", "", -1) + klog.V(10).Infof("hex value: %s", hex) + value, _ := strconv.ParseUint(hex, 16, 32) + + return uint32(value) +} + +// GradientPNG generates a gradient PNG as an example +func GradientPNG(gradient GradientTable, h int, w int) { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + + colorList := GradientColorList(gradient, h) + for vert, color := range colorList { + draw.Draw(img, image.Rect(0, vert, w, vert+1), &image.Uniform{color}, image.Point{}, draw.Src) + } + + outpng, err := os.Create("gradient.png") + if err != nil { + klog.Error("Error storing png: " + err.Error()) + } + defer outpng.Close() + + err = png.Encode(outpng, img) + if err != nil { + klog.Error(err) + } +} + +//GradientColorList generates a list of colors for a GradientTable +// length: the number of colors you want +func GradientColorList(gradient GradientTable, length int) []colorful.Color { + var list []colorful.Color + for j := 0; j < length; j++ { + c := gradient.GetInterpolatedColor(float64(j) / float64(length)) + klog.V(10).Infof("color: %v", c) + list = append(list, c) + } + return list +} diff --git a/color_test.go b/color_test.go new file mode 100644 index 0000000..c79c3eb --- /dev/null +++ b/color_test.go @@ -0,0 +1,150 @@ +package main + +import ( + "fmt" + "os" + "testing" + + "github.com/lucasb-eyer/go-colorful" + "github.com/stretchr/testify/assert" +) + +var testGradient1 = GradientTable{ + {HexToColor("#9e0142"), 0.0}, + {HexToColor("#d53e4f"), 0.1}, + {HexToColor("#f46d43"), 0.2}, + {HexToColor("#fdae61"), 0.3}, + {HexToColor("#fee090"), 0.4}, + {HexToColor("#ffffbf"), 0.5}, + {HexToColor("#e6f598"), 0.6}, + {HexToColor("#abdda4"), 0.7}, + {HexToColor("#66c2a5"), 0.8}, + {HexToColor("#3288bd"), 0.9}, + {HexToColor("#5e4fa2"), 1.0}, +} + +var testGradient2 = GradientTable{ + {HexToColor("#4e3cec"), 0.0}, + {HexToColor("#5b3ee8"), 0.1}, + {HexToColor("#6941e5"), 0.2}, + {HexToColor("#7643e1"), 0.3}, + {HexToColor("#8346dd"), 0.4}, + {HexToColor("#9148da"), 0.5}, + {HexToColor("#9e4bd6"), 0.6}, + {HexToColor("#ab4dd2"), 0.7}, + {HexToColor("#b94fcf"), 0.8}, + {HexToColor("#c652cb"), 0.9}, + {HexToColor("#e157c4"), 1.0}, +} + +func TestParseHex(t *testing.T) { + tests := []struct { + name string + hex string + want colorful.Color + }{ + {"blue", "#0000ff", colorful.Color{R: 0, G: 0, B: 1}}, + {"yellow", "#ffff00", colorful.Color{R: 1, G: 1, B: 0}}, + {"red", "#ff0000", colorful.Color{R: 1, G: 0, B: 0}}, + {"black", "#000000", colorful.Color{R: 0, G: 0, B: 0}}, + {"green", "#00ff00", colorful.Color{R: 0, G: 1, B: 0}}, + {"white", "#ffffff", colorful.Color{R: 1, G: 1, B: 1}}, + {"notacolor", "ff", colorful.Color{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := HexToColor(tt.hex) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestColorToUint32(t *testing.T) { + tests := []struct { + name string + color colorful.Color + want uint32 + }{ + {"white", colorful.Color{R: 1, G: 1, B: 1}, uint32(16777215)}, + {"black", colorful.Color{R: 0, G: 0, B: 0}, uint32(0)}, + {"green", colorful.Color{R: 0, G: 1, B: 0}, uint32(65280)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ColorToUint32(tt.color) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGradientTable_GetInterpolatedColor(t *testing.T) { + tests := []struct { + name string + value float64 + want colorful.Color + }{ + {"one", 1.0, colorful.Color{R: 0.3686274518393889, G: 0.30980394385954535, B: 0.635294122225692}}, + {"two", 1.1, colorful.Color{R: 0.3686274509803922, G: 0.30980392156862746, B: 0.6352941176470588}}, + {"three", 1.2, colorful.Color{R: 0.3686274509803922, G: 0.30980392156862746, B: 0.6352941176470588}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := testGradient1.GetInterpolatedColor(tt.value) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGradientPNG(t *testing.T) { + tests := []struct { + name string + gradient GradientTable + h int + w int + testFile string + }{ + {"1024x40 1", testGradient1, 1024, 40, "1024x40-gradient-1.png"}, + {"1024x1024 1", testGradient1, 1024, 1024, "1024x1024-gradient-1.png"}, + {"2048x40 2", testGradient2, 2048, 40, "2048x40-gradient-2.png"}, + } + for _, tt := range tests { + os.Remove("gradient.png") + t.Run(tt.name, func(t *testing.T) { + GradientPNG(tt.gradient, tt.h, tt.w) + assert.FileExistsf(t, "gradient.png", "gradient.png should exist") + match := deepCompare("gradient.png", "testdata/"+tt.testFile) + assert.Truef(t, match, "the files must match") + }) + os.Remove("gradient.png") + } +} + +func TestGradientColorList(t *testing.T) { + tests := []struct { + name string + gradient GradientTable + length int + want []colorful.Color + }{ + {"one", testGradient1, 1, []colorful.Color{{R: 0.6196077933795217, G: 0.003922138953572327, B: 0.2588235191354816}}}, + { + "two", + testGradient2, + 5, + []colorful.Color{ + {R: 0.3058824116295, G: 0.23529411042695136, B: 0.9254902064503917}, + {R: 0.4117647268595018, G: 0.2549019781745598, B: 0.8980392249706234}, + {R: 0.5137254867393809, G: 0.2745098482413161, B: 0.866666674535384}, + {R: 0.6196078211495166, G: 0.29411772176478884, B: 0.8392156924085546}, + {R: 0.72549015896929, G: 0.3098040307126474, B: 0.8117647099046337}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GradientColorList(tt.gradient, tt.length) + fmt.Println(got) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/demo.go b/demo.go index 307e38c..20f4818 100644 --- a/demo.go +++ b/demo.go @@ -3,12 +3,14 @@ package main import ( "github.com/spf13/cobra" "k8s.io/klog" + "time" ) var ( - demoBrightness int - demoDelay int - demoCount int + demoBrightness int + demoDelay int + demoCount int + demoGradientLength int ) func init() { @@ -17,6 +19,21 @@ func init() { demoCmd.Flags().IntVar(&demoDelay, "delay", 100, "The delay in ms of the demo program.") demoCmd.Flags().IntVar(&demoCount, "count", 1, "The number of loops to run the demo.") demoCmd.Flags().IntVar(&demoBrightness, "brightness", 150, "The brightness to run the demo at. Must be between min and max.") + demoCmd.Flags().IntVar(&demoGradientLength, "gradient-count", 2048, "The number of steps in the gradient.") +} + +var demoGradient = GradientTable{ + {HexToColor("#9e0142"), 0.0}, + {HexToColor("#d53e4f"), 0.1}, + {HexToColor("#f46d43"), 0.2}, + {HexToColor("#fdae61"), 0.3}, + {HexToColor("#fee090"), 0.4}, + {HexToColor("#ffffbf"), 0.5}, + {HexToColor("#e6f598"), 0.6}, + {HexToColor("#abdda4"), 0.7}, + {HexToColor("#66c2a5"), 0.8}, + {HexToColor("#3288bd"), 0.9}, + {HexToColor("#5e4fa2"), 1.0}, } var demoCmd = &cobra.Command{ @@ -25,19 +42,31 @@ var demoCmd = &cobra.Command{ Long: `Runs a demo.`, Run: func(cmd *cobra.Command, args []string) { + // Initialize the LEDs led, err := newLEDArray() if err != nil { klog.Fatal(err) } defer led.ws.Fini() - for i := 1; i < (demoCount + 1); i++ { + // Loops through our list of pre-defined colors and display them in order. + for i := 0; i < (demoCount); i++ { for colorName, color := range colors { klog.Infof("displaying: %s", colorName) - _ = led.display(color, demoDelay, 150) + _ = led.display(color, demoDelay, demoBrightness) + } + _ = led.fade(led.color, minBrightness) + time.Sleep(500 * time.Millisecond) + + // Second part of demo - go through a color gradient really fast. + klog.V(3).Infof("starting color gradient") + colorList := GradientColorList(demoGradient, demoGradientLength) + for _, gradColor := range colorList { + _ = led.display(ColorToUint32(gradColor), 0, demoBrightness) + time.Sleep(time.Duration(demoDelay) * time.Nanosecond) } } - _ = led.display(off, 0, 0) + _ = led.fade(led.color, minBrightness) }, } diff --git a/go.mod b/go.mod index 9928a7a..6ad1ee8 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/brutella/hc v1.2.0 + github.com/lucasb-eyer/go-colorful v1.0.3 github.com/rpi-ws281x/rpi-ws281x-go v1.0.5 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index fcd8203..0fd14f6 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/gosexy/to v0.0.0-20141221203644-c20e083e3123/go.mod h1:oQuuq9ZkoRpy+2 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= diff --git a/main_test.go b/main_test.go index a154364..80dd42e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,13 @@ package main import ( + "bytes" + "io" + "log" + "os" "testing" + + "k8s.io/klog" ) // workaround for some weird go 1.13 testing thing with flags @@ -16,3 +22,43 @@ func init() { minBrightness = 30 maxBrightness = 200 } + +const chunkSize = 64000 + +func deepCompare(file1, file2 string) bool { + // Check file size ... + + f1, err := os.Open(file1) + if err != nil { + klog.Fatal(err) + } + defer f1.Close() + + f2, err := os.Open(file2) + if err != nil { + klog.Fatal(err) + } + defer f2.Close() + + for { + b1 := make([]byte, chunkSize) + _, err1 := f1.Read(b1) + + b2 := make([]byte, chunkSize) + _, err2 := f2.Read(b2) + + if err1 != nil || err2 != nil { + if err1 == io.EOF && err2 == io.EOF { + return true + } else if err1 == io.EOF || err2 == io.EOF { + return false + } else { + log.Fatal(err1, err2) + } + } + + if !bytes.Equal(b1, b2) { + return false + } + } +} diff --git a/neopixel.go b/neopixel.go index 878f5a8..b72e2f6 100644 --- a/neopixel.go +++ b/neopixel.go @@ -16,10 +16,9 @@ var colors = map[string]uint32{ "teal": uint32(0x33ffd1), "pink": uint32(0xff08c7), "white": uint32(0xffffff), + "black": uint32(0x000000), // This basically equates to off. } -const off = uint32(0x000000) - type wsEngine interface { Init() error Render() error @@ -74,6 +73,7 @@ func (led *LEDArray) display(color uint32, delay int, brightness int) error { } for i := 0; i < len(led.ws.Leds(0)); i++ { led.ws.Leds(0)[i] = color + led.color = color klog.V(10).Infof("setting led %d", i) if err := led.ws.Render(); err != nil { klog.Error(err) diff --git a/testdata/1024x1024-gradient-1.png b/testdata/1024x1024-gradient-1.png new file mode 100644 index 0000000000000000000000000000000000000000..38aa3cbb4ae0e0e1db70407b8c18ee3126164842 GIT binary patch literal 5955 zcmb_gTWnNS6g@L7rIgozAdgU-Nkt55z#1Z<5H1525hZ~AFhQU+6a64DG+?a)_F@WD z19ZN81+>72CPqppQNdQ*gqn^Cq8}!uhL^<%34^5)K&WNhXRX`L?o5e~_|fUix%=$1 z_u6Ye&V6&m@+BklCg&MrMlOAF@hW4o~<#U)l5Y_8F_Ld=v}rIsBIUK>2Gqr>eTDMj7+Nk)G=lWMh3tx$Jj$uNf*o;w$7M zxY#EP(fsLlVdLnH@Tsj5Ri3h-2DbEJz zZL+vcf|aM>B)lG6_AAIX*DsJQmET)hT!Qt=&a{ou{THm*_;o7K)#%u-vWeDoL?3_LvTgT3_+^EbQA=*fb zX{CY@z-WEMk-&A{8|+^Y7)*G#jWr2a)NU!c;^&ZP$?XovIn{&;e4tE)>9Tk7nRBxm3uHQI2a0FsP zZg#;ltWqxB-*OR71Il=`AP>bcMV@hP|HjnL5^Ss3;pIiLBuOF`l#W@S2jO zt6+zSVVRYfltZi<6%CLLWG@sUcmovK2vr{)VuJu3suZ)3C?^pxRg+YOWxZhfq}6J# zlmQ#1Jza{*Q|YUerwk)!I{!v$(5;ar;;FlvogE+!D)xUYM$ffN&oE6dUbG4AC7n$k zP7UA}`S4E`W1B2D>&ot=sFK2t4M@!)=?FicC4tv-NeWEmD%( zFh5{a#&ImJsw5P<{Yit669|^~W7Y!Q3`i?%(W#E3sCie*VCpbm=^ZG*8BI~0U5;4~ zp;1L#^>;$jiLwBYIq|5}Varz_Dl^kdE^7c}Rq&LC#7{m;*IUV<#vR2>1V)3~sW!4c z&%~JN7dS&KsG`k;86BL;+K~C?{i6|Ydtb1=PLx* z0C6INj{=M&q1yziz>((CiBmB-L~r%lz^Dkb<63oL_DoLh&|HaSHVt?LoC2!{@0O9}(qY{^7|9Njk#>rf6hm&q&yEXr#4Xh3rN&y)9s!I`weEU@uYB_^MX@$J zh(uJAENo879qWLIcyn7OEdYB*_qbWIljtyNsX)Xop0029$w>cgQ&x_|tk|`EPZs)a zM%Mzed9ylJw_i1zQZeRfxKX3#Q8givWQ;><`UucaURW3}_Ea10khL~Iz=0&gifoO4 z&}Jj%6@R9#{B|>fj+Beer}TQ&{!>jZQhSeV)dWuE7a8OxK zxL~ag#lIifGpC3Dib?$Bv2|6l`RZ5|fz_@h9&$8;?*xj?fXz+STh;C0CBavji!6vw z#r^)qzMkuk3+gjFKD6AM)YEf&xG8%Uu>mw&^}4=zihJpD;#`|WyCl(dd8P=iJtQA;>`lHy$R*OJB29(g8K4jj5m9+AEN>Y`9Tt z12u`X-{!47D@(!SpxB9Jr30H8E$Ep)R%A*P8-N*K_+d4=MzN#V?dL3$7NEp`YhNc? znqF8UpMAp~0yWQiH28HvQMY2BNO`XTL5SEVvfz~+V6EIH&oSm!qRo(AsFdjyB6_C2 zSu|CCcd|fY=_CQ>Ux;tyCVF%pb6h19+q241+OFyzhkUk67_Dy|sWFm@GN@RD(5hjV za!Nl5qp#%xhrB4W7dnq*q!A;JH-wEW67T0L4i$aw$_Adq8DqdpUyL=sh)$DC)_!{W VnN!o}JSOLurB5thd^lYC#$RvO@ACiv literal 0 HcmV?d00001 diff --git a/testdata/1024x40-gradient-1.png b/testdata/1024x40-gradient-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf69f4e70507c3ad5284c9078e83b53ad110c80 GIT binary patch literal 1417 zcmV;41$O$0P)f&5V(jjurK1y)s%`DQxS9E z9yGd|h-6?Z7PQz4xHsDpnKFd^g^+vwtI*-jjeEZnf)w768HKy~tM$VOi zP*QgF2)yg+P9PLG1EG|GaNyNJVA$0wa0WujXCTWO+YCgvBuX_0o<-bE0s|YD#0IQ% zF4-LSyc&~loP0IzAi#lrU0sbi@Mw;Gmf1LQV54RBQ9YyPm;>w9Vl~F(o8!)0n1;@^ z1KZv|_SIOoB$g)-+t1JX)}qd;8t=*wyW03~WoaZ!HQ_?fX;S=9p4*j7fRss<{{`e>l75 zVw7`{;0d=g7pKOF1W&laPhot`)w4_xTvtOXVoc33rn(xFGU653Hpjk*n`2Bxj47`M z!9|RLZ4u{On2H!v5o1zDyaL;{QxSvUBF4Z)jDd?70~av{wng0W7E=*pDq>7UjL8>q z2LTXli`X|#MT{xS1i?j&fo%~F4a8RCQVoKA<77L5lxj>F2&TTAfT?i;!M+-|BrtFX zdJLR_K*~Tcb>_mez$$TqDmkE(u$6HOH8I&9!qbU32-mx+QTxcs9qrGuN`lBd{*jzBv}I z#o?K21lG;5@66>(wQ(QYp3pbPn3`h_>`V1-jz>i{rh0-krn+nD{opJUlg~1J&Xr}} z51!qRcXRAJa}86ydc#Ziyjs}Rwi+)DWHiUa$HpUY2103=YTa#dWgs;dCS$5s;Nf;^ zj&BEd^r5u8J2l6*gLQMPY_1-#F)}t+Hwg+n`zf?)d_Op^rj&tT64p2aq2zl|UX3v| z#{zKsn3B;zsxhRV&|_*XVyea*Sa<@NV2w2vzOK-hekt|26DH&R*fz&T1F_ZEXdu-X zlCQ?bRIkA8V@mB~Or;u=(W_gF*MnP&l=5m!MgytFkbE^R)tHQ_UV%IMQ0nM|$!H*X zH3lx#*MnP&lv<0JN;L=eeJ``Mct5zc$blQD>%ol^1}@c8So#&tCYp6%oJ=vD8XSI;6I)_4VONnqe2rogR54(z*0w2v|H1S^er1r|EjDB_=4 z#JW_^BL3NM|23D*GG`J0nP8U&GE4ProK~MZE&poT_LGLQ&lXEHCEqwHQ@sJ#T$pMu zOv|;n0_*^9Xi@TkU{yQ1=4m@aLUiuiu;@&Vh(t98vaI|%r?x`;9Dj<-1%rJRcc53ew? zaq`_3GZ0MYH5#WZlLHSU&Z{A?G2))yLSWzB$#@W0fm;$3cxWKDQ)c6Yfg2|dJi&H_ zA;Gg6&$_zi!oW2b2JT|cfom=dEKGF~Q()cuPQvEWZKvHn?y)fk_B~*eIoIC+00960 Xg74f-lq+!m00000NkvXXu0mjfUdpFF literal 0 HcmV?d00001 diff --git a/testdata/2048x40-gradient-2.png b/testdata/2048x40-gradient-2.png new file mode 100644 index 0000000000000000000000000000000000000000..239479eab7d4a1b52c4a4128ccafe5668aee6133 GIT binary patch literal 1445 zcmZuxeM}o=7=Nj7tF!f-OXg)k8b4yyLDp&3QJ}r4R3VyXQ{$Lv@mSpwx2c7Nj*rSw z&a5uDVO%CNl&OFCv9Lg$i9=J66BFWUT?`^sK6X>r8rs>5UAZDqeBbv<0rj7|_n!Cp z_&vYh^W5=WmM7HfH?Ic(s4FYVpN8-I0Z;;^0-h$`3=;t49j+{YKBtAT1ZTgX#Ll9>q|uNw zNzR@Gl>cx7TakAM!mx}VBc118`3|DF{^5Z4{SkCYjJ&u3wUK0S30?7h?TmO~?(2IY z6*4c&9-Up%zKb&0x?ZBX>)&l>d3ev$lY`-XR&!adK_0to|D`8}>v8skX@gjwhdjo) zND}qICu`PJf4x98t+51t^=oEwQQxnHMzI7Lvf=mG(4kXiTqIFrBsj@4-~~iHD}it6 z5I}ha-nZ_^YB!q|8Yj9WPix;6q)@6(W=r_nSK@}>{RQVO2qqo|)imj>ozlo;k8@+^ z=-7!mc$sd>@Gi$t`KQbdI#jBF>1Jg%g)^8abc3pobEDP!J2S_~z&&fJU*+te8>aUw zDOL67jeBq0@&Q7!JRy|Juosd6f%dr5S+3E{4H~yL#yqyFkhUU~XVn#hRb& zI?C1jX9q7VUTtKRsV^0X_b8ueLNqbrwN%8c!}gvE*uBlYF}R)#){ z=)dYmhF>zCl^!?9N(+r-cyK#F;xvhVJVgDb)ub;kfy7OnkvwBJjkQstC zA{w&0fTp`&6T1^O>}t+X@YLG9-4Q=ghhxNRVcS?ho}_Kqg2x>xgth>Zeo@Cb=&DUG zO!%cQ(55XUZ(!82L}>1IvTL@AHNI+Y)ns?!|C&b(OPuYuHa?(_lQ~y51EeBcSvTJi ztW|KG`0?^|?O(fdQ=bM)Ef7Mg%-Z1Q+wm?MEPcVzeQXH2N}g`JuHol=EqZ*M7Uv9Q zMfw#R8#16(h;-ZcxP%dXyx2WIoM<7x!B!l(@H;Kt_I3r5UszXx-Xb@ rLwRUD8SX2(p5N?LNE~|;082|xUMhX}Vb5F){(eEF*;3y7DAV>2Ikjvy literal 0 HcmV?d00001