1package chart23import (4 "image"5 "image/color"6 "slices"7 "strconv"8 "strings"9)1011// Sixel is the other way images reach a terminal, for terminals without12// kitty graphics (foot, xterm, mlterm, Windows Terminal and others). A13// sixel image is pixels drawn at the cursor, not text: the UI draws it14// after the frame, over the text chart the cells hold (see the ui15// package's sixel.go), so every pixel is set and the text never shows16// through. The image is sent whole each time, in at most as many colors17// as the terminal has color registers, run-length encoded.1819// Sixel returns the DCS sequence that draws img at the cursor, every20// pixel composited over bg, in at most registers colors (at least 2, at21// most 256).22func Sixel(img *image.RGBA, bg color.RGBA, registers int) string {23 registers = min(max(registers, 2), 256)24 b := img.Bounds()25 w, h := b.Dx(), b.Dy()26 if w <= 0 || h <= 0 {27 return ""28 }29 idx, pal := quantize(img, bg, registers)30 var out strings.Builder31 // P2 = 1: pixels not drawn keep what's under them, which only the32 // empty sixels past the image's last row are. The raster attributes33 // set square pixels and the image's size.34 out.WriteString("\x1bP0;1;0q\"1;1;")35 out.WriteString(strconv.Itoa(w) + ";" + strconv.Itoa(h))36 for i, c := range pal {37 out.WriteString("#" + strconv.Itoa(i) + ";2;" + percent(c.R) + ";" + percent(c.G) + ";" + percent(c.B))38 }39 bands := sixelBands(idx, w, h, len(pal))40 out.WriteString(bands)41 out.WriteString("\x1b\\")42 return out.String()43}4445// percent is a color channel as sixel's 0 to 100.46func percent(v uint8) string { return strconv.Itoa((int(v)*100 + 127) / 255) }4748// sixelBands encodes the image of register indexes, six49// rows at a time: for each color in the band, its row of sixels,50// run-length encoded, then "$" to go back for the next color, and "-"51// between bands.52func sixelBands(idx []int16, w, h, colors int) string {53 var out strings.Builder54 b := bands{w: w, rows: make([][]byte, colors), band: make([]int, colors)}55 for y0 := 0; y0 < h; y0 += 6 {56 if y0 > 0 {57 out.WriteByte('-')58 }59 b.gather(idx[y0*w:min(y0+6, h)*w], y0)60 for i, c := range b.used {61 if i > 0 {62 out.WriteByte('$')63 }64 out.WriteString("#" + strconv.Itoa(int(c)))65 writeRuns(&out, b.rows[c])66 }67 }68 return out.String()69}7071// bands holds the rows of sixels of one band of six pixel rows.72type bands struct {73 w int74 rows [][]byte // each color's row of sixels, made when first used75 band []int // the band each color was last used in, plus one76 used []int16 // the colors of the band, in order77}7879// gather makes the rows of the band at y0 from its pixels' registers.80func (b *bands) gather(px []int16, y0 int) {81 b.used = b.used[:0]82 for i, c := range px {83 if b.band[c] != y0+1 {84 b.band[c] = y0 + 185 b.used = append(b.used, c)86 if b.rows[c] == nil {87 b.rows[c] = make([]byte, b.w)88 }89 for x := range b.rows[c] {90 b.rows[c][x] = '?'91 }92 }93 b.rows[c][i%b.w] += 1 << (i / b.w) // each pixel has one color, so each bit is added once94 }95 slices.Sort(b.used)96}9798// writeRuns writes a row of sixels, runs of four or more as "!n", and99// leaves out the empty sixels at its end.100func writeRuns(out *strings.Builder, row []byte) {101 end := len(row)102 for end > 0 && row[end-1] == '?' {103 end--104 }105 for i := 0; i < end; {106 j := i + 1107 for j < end && row[j] == row[i] {108 j++109 }110 if n := j - i; n >= 4 {111 out.WriteString("!" + strconv.Itoa(n))112 out.WriteByte(row[i])113 } else {114 for range n {115 out.WriteByte(row[i])116 }117 }118 i = j119 }120}121122// quantize maps each pixel of img, composited over bg, to a register,123// and returns the registers' colors.124// Colors are kept exactly while there are at most registers of them;125// past that, their low bits are dropped until they fit, and each126// register is the average of the colors it stands for. Registers are127// numbered in the order their colors first appear, so the same image128// always encodes the same way.129func quantize(img *image.RGBA, bg color.RGBA, registers int) ([]int16, []color.RGBA) {130 b := img.Bounds()131 w, h := b.Dx(), b.Dy()132 px := make([]uint32, w*h) // 1<<24 | RGB133 for y := range h {134 for x := range w {135 c := over(img, b.Min.X+x, b.Min.Y+y, bg)136 px[y*w+x] = 1<<24 | uint32(c.R)<<16 | uint32(c.G)<<8 | uint32(c.B)137 }138 }139 shift := uint(0)140 for ; shift < 8; shift++ {141 if distinct(px, shift, registers) <= registers {142 break143 }144 }145 reg := map[uint32]int16{}146 var sums [][4]int // R, G, B sums and count per register147 idx := make([]int16, w*h)148 last, lastReg := uint32(0), int16(-1) // runs of one color are common: look each up once149 for i, p := range px {150 r := lastReg151 if p != last {152 k := bucket(p, shift)153 var ok bool154 if r, ok = reg[k]; !ok {155 r = int16(len(sums))156 reg[k] = r157 sums = append(sums, [4]int{})158 }159 last, lastReg = p, r160 }161 idx[i] = r162 s := &sums[r]163 s[0] += int(p >> 16 & 0xff)164 s[1] += int(p >> 8 & 0xff)165 s[2] += int(p & 0xff)166 s[3]++167 }168 pal := make([]color.RGBA, len(sums))169 for i, s := range sums {170 pal[i] = color.RGBA{uint8(s[0] / s[3]), uint8(s[1] / s[3]), uint8(s[2] / s[3]), 255}171 }172 return idx, pal173}174175// bucket is the key of pixel p with shift low bits of each channel176// dropped.177func bucket(p uint32, shift uint) uint32 {178 m := uint32(0xff>>shift<<shift) * 0x010101179 return p & (1<<24 | m)180}181182// distinct counts the buckets of px at shift, stopping past limit.183func distinct(px []uint32, shift uint, limit int) int {184 seen := map[uint32]struct{}{}185 last := uint32(0)186 for _, p := range px {187 if p == last {188 continue189 }190 last = p191 seen[bucket(p, shift)] = struct{}{}192 if len(seen) > limit {193 break194 }195 }196 return len(seen)197}198199// over is the pixel at x, y of the premultiplied img composited over200// bg, opaque.201func over(img *image.RGBA, x, y int, bg color.RGBA) color.RGBA {202 i := img.PixOffset(x, y)203 p := img.Pix[i : i+4 : i+4]204 a := int(p[3])205 return color.RGBA{mix(p[0], bg.R, a), mix(p[1], bg.G, a), mix(p[2], bg.B, a), 255}206}207208// mix is the premultiplied channel c of coverage a over under.209func mix(c, under uint8, a int) uint8 { return uint8(min(int(c)+int(under)*(255-a)/255, 255)) }