cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Charts: test that sixel images decode to the composited plot

Dami committed b86ff79
commit
b86ff790297260f9bd8444da911d10e39b56d8f2
parent
2f52c8217d9679a45966d945de6539ec453c9eb7
author
Dami <dami.etoile@gmail.com>
kind
test risk score 0.0 of 3, Jev's estimate
1 file changed, +96 -0
Minternal/chart/sixel_test.go96++++++++++++++++++++++++++++++
M internal/chart/sixel_test.go +96 -0
@@ -79,3 +79,99 @@ func TestSixelChart(t *testing.T) {
7979 t.Error("an empty image drew something")8080 }8181 }82+83+// A chart's sixel image decodes to the image composited over the84+// background, each channel within sixel's percent steps, and pixels85+// without coverage unset.86+func TestSixelDecodes(t *testing.T) {87+ bg := color.RGBA{250, 250, 245, 255}88+ for _, ct := range []sheet.ChartType{sheet.ChartLine, sheet.ChartPie, sheet.ChartArea} {89+ img := Image(ct, budget, 40, 12, Options{CellW: 7, CellH: 15}, testPalette)90+ got := decodeSixel(t, Sixel(img, bg, 256))91+ b := img.Bounds()92+ for y := range b.Dy() {93+ for x := range b.Dx() {94+ want := over(img, x, y, bg)95+ c, set := got[image.Pt(x, y)]96+ if set != (want.A != 0) || set && (diff(c.R, want.R) > 3 || diff(c.G, want.G) > 3 || diff(c.B, want.B) > 3) {97+ t.Fatalf("%v at %d,%d: %v %v, want %v", ct, x, y, c, set, want)98+ }99+ }100+ }101+ }102+}103+104+func diff(a, b uint8) int { return max(int(a)-int(b), int(b)-int(a)) }105+106+// decodeSixel reads what Sixel writes: color definitions, "#n"107+// selections, sixels and their runs, "$" and "-".108+func decodeSixel(t *testing.T, s string) map[image.Point]color.RGBA {109+ t.Helper()110+ body, ok := strings.CutPrefix(s, "\x1bP0;1;0q")111+ body, ok2 := strings.CutSuffix(body, "\x1b\\")112+ if !ok || !ok2 {113+ t.Fatalf("not a sixel sequence: %.30q", s)114+ }115+ d := sixelDecoder{body: body, pal: map[int]color.RGBA{}, out: map[image.Point]color.RGBA{}}116+ for d.i < len(body) {117+ d.step()118+ }119+ return d.out120+}121+122+type sixelDecoder struct {123+ body string124+ i int125+ x, y, cur int126+ pal map[int]color.RGBA127+ out map[image.Point]color.RGBA128+}129+130+func (d *sixelDecoder) num() int {131+ n := 0132+ for ; d.i < len(d.body) && d.body[d.i] >= '0' && d.body[d.i] <= '9'; d.i++ {133+ n = n*10 + int(d.body[d.i]-'0')134+ }135+ return n136+}137+138+func (d *sixelDecoder) step() {139+ ch := d.body[d.i]140+ d.i++141+ switch ch {142+ case '"':143+ for d.i < len(d.body) && (d.body[d.i] >= '0' && d.body[d.i] <= '9' || d.body[d.i] == ';') {144+ d.i++145+ }146+ case '#':147+ d.cur = d.num()148+ if d.i < len(d.body) && d.body[d.i] == ';' {149+ var c [4]int150+ for k := range c {151+ d.i++152+ c[k] = d.num()153+ }154+ pc := func(v int) uint8 { return uint8((v*255 + 50) / 100) }155+ d.pal[d.cur] = color.RGBA{pc(c[1]), pc(c[2]), pc(c[3]), 255}156+ }157+ case '$':158+ d.x = 0159+ case '-':160+ d.x, d.y = 0, d.y+6161+ default:162+ run := 1163+ if ch == '!' {164+ run = d.num()165+ ch = d.body[d.i]166+ d.i++167+ }168+ for range run {169+ for dy := range 6 {170+ if (ch-'?')&(1<<dy) != 0 {171+ d.out[image.Pt(d.x, d.y+dy)] = d.pal[d.cur]172+ }173+ }174+ d.x++175+ }176+ }177+}