summaryrefslogtreecommitdiffstats
path: root/sixel.go
diff options
context:
space:
mode:
Diffstat (limited to 'sixel.go')
-rw-r--r--sixel.go205
1 files changed, 205 insertions, 0 deletions
diff --git a/sixel.go b/sixel.go
new file mode 100644
index 0000000..4e44c79
--- /dev/null
+++ b/sixel.go
@@ -0,0 +1,205 @@
+// Package sixel can be used to render image.Image to the terminal using
+// various strategies (including sixel).
+package sixel
+
+import (
+ "bufio"
+ "fmt"
+ "image"
+ "image/color"
+ "image/color/palette"
+ "image/draw"
+ "io"
+)
+
+func scale100(c uint32) int {
+ return int(c) * 100 / 0xffff
+}
+
+var sixelPalette = append(color.Palette{color.Transparent}, palette.WebSafe...)
+
+// Print renders the given image as a sixel.
+func Print(w io.Writer, img image.Image) error {
+ palettized := image.NewPaletted(img.Bounds(), sixelPalette)
+ draw.FloydSteinberg.Draw(palettized, palettized.Bounds(), img, image.Point{})
+ bw := bufio.NewWriter(w)
+ if _, err := io.WriteString(bw, "\033P7;1q"); err != nil {
+ return err
+ }
+ for i, c := range sixelPalette[1:] {
+ r, g, b, _ := c.RGBA()
+ if _, err := fmt.Fprintf(bw, "#%d;2;%d;%d;%d", i, scale100(r), scale100(g), scale100(b)); err != nil {
+ return err
+ }
+ }
+ for row := 0; row < palettized.Bounds().Dy(); row++ {
+ y := palettized.Bounds().Min.Y + row
+ if row > 0 {
+ if row%6 == 0 {
+ if _, err := io.WriteString(bw, "-"); err != nil {
+ return err
+ }
+ } else {
+ if _, err := io.WriteString(bw, "$"); err != nil {
+ return err
+ }
+ }
+ }
+ for x := palettized.Bounds().Min.X; x < palettized.Bounds().Max.X; x++ {
+ c := palettized.ColorIndexAt(x, y)
+ if c == 0 {
+ if _, err := fmt.Fprint(bw, "?"); err != nil {
+ return err
+ }
+ } else {
+ char := '?' + 1<<(row%6)
+ if _, err := fmt.Fprintf(bw, "#%d%c", c-1, char); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ if _, err := io.WriteString(bw, "\033\\"); err != nil {
+ return err
+ }
+ if err := bw.Flush(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func isFullyTransparent(c color.Color) bool {
+ _, _, _, a := c.RGBA()
+ return a == 0
+}
+
+// PrintBlock renders the given image using block characters. Yeah I know it's
+// not a sixel but it could be used as a fallback if sixel isn't supported.
+func PrintBlock(w io.Writer, img image.Image) error {
+ bw := bufio.NewWriter(w)
+ for row := 0; row < img.Bounds().Dy(); row += 2 {
+ y := img.Bounds().Min.Y + row
+ if row > 0 {
+ if _, err := io.WriteString(bw, "\n"); err != nil {
+ return err
+ }
+ }
+ for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
+ hi := img.At(x, y)
+ if isFullyTransparent(hi) {
+ if y+1 >= img.Bounds().Max.Y || isFullyTransparent(img.At(x, y+1)) {
+ if _, err := io.WriteString(bw, "\033[49m "); err != nil {
+ return err
+ }
+ } else {
+ r, g, b, _ := img.At(x, y+1).RGBA()
+ if _, err := fmt.Fprintf(bw, "\033[38;2;%d;%d;%dm\033[49m▄", r>>8, g>>8, b>>8); err != nil {
+ return err
+ }
+ }
+ } else {
+ if y+1 < img.Bounds().Max.Y && !isFullyTransparent(img.At(x, y+1)) {
+ r, g, b, _ := img.At(x, y+1).RGBA()
+ if _, err := fmt.Fprintf(bw, "\033[48;2;%d;%d;%dm", r>>8, g>>8, b>>8); err != nil {
+ return err
+ }
+ } else {
+ if _, err := io.WriteString(bw, "\033[49m"); err != nil {
+ return err
+ }
+ }
+ r, g, b, _ := hi.RGBA()
+ if _, err := fmt.Fprintf(bw, "\033[38;2;%d;%d;%dm▀", r>>8, g>>8, b>>8); err != nil {
+ return err
+ }
+ }
+ }
+ if _, err := io.WriteString(bw, "\033[39m\033[49m"); err != nil {
+ return err
+ }
+ }
+ if err := bw.Flush(); err != nil {
+ return err
+ }
+ return nil
+}
+
+type xtermColor int8
+
+func (c xtermColor) RGBA() (r, g, b, a uint32) {
+ var col color.RGBA
+ switch c {
+ case 0:
+ col = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
+ case 1:
+ col = color.RGBA{R: 0xcd, G: 0x00, B: 0x00, A: 0xff}
+ case 2:
+ col = color.RGBA{R: 0x00, G: 0xcd, B: 0x00, A: 0xff}
+ case 3:
+ col = color.RGBA{R: 0xcd, G: 0xcd, B: 0x00, A: 0xff}
+ case 4:
+ col = color.RGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff}
+ case 5:
+ col = color.RGBA{R: 0xcd, G: 0x00, B: 0xcd, A: 0xff}
+ case 6:
+ col = color.RGBA{R: 0x00, G: 0xcd, B: 0xcd, A: 0xff}
+ case 7:
+ col = color.RGBA{R: 0xe5, G: 0xe5, B: 0xe5, A: 0xff}
+ case 60:
+ col = color.RGBA{R: 0x7f, G: 0x7f, B: 0x7f, A: 0xff}
+ case 61:
+ col = color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+ case 62:
+ col = color.RGBA{R: 0x00, G: 0xff, B: 0x00, A: 0xff}
+ case 63:
+ col = color.RGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}
+ case 64:
+ col = color.RGBA{R: 0x5c, G: 0x5c, B: 0xff, A: 0xff}
+ case 65:
+ col = color.RGBA{R: 0xff, G: 0x00, B: 0xff, A: 0xff}
+ case 66:
+ col = color.RGBA{R: 0x00, G: 0xff, B: 0xff, A: 0xff}
+ case 67:
+ col = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
+ default:
+ panic("not an xterm color")
+ }
+ return col.RGBA()
+}
+
+var xtermPalette = color.Palette{color.Transparent, xtermColor(0), xtermColor(1), xtermColor(2), xtermColor(3), xtermColor(4), xtermColor(5), xtermColor(6), xtermColor(7), xtermColor(60), xtermColor(61), xtermColor(62), xtermColor(63), xtermColor(64), xtermColor(65), xtermColor(66), xtermColor(67)}
+
+// PrintXterm16 renders the given image using the basic XTerm 16 colors. This
+// should have great compatibility and it looks pretty impressively bad.
+func PrintXTerm16(w io.Writer, img image.Image) error {
+ palettized := image.NewPaletted(img.Bounds(), xtermPalette)
+ draw.FloydSteinberg.Draw(palettized, palettized.Bounds(), img, image.Point{})
+ bw := bufio.NewWriter(w)
+ for row := 0; row < palettized.Bounds().Dy(); row++ {
+ y := palettized.Bounds().Min.Y + row
+ if row > 0 {
+ if _, err := io.WriteString(bw, "\n"); err != nil {
+ return err
+ }
+ }
+ for x := palettized.Bounds().Min.X; x < palettized.Bounds().Max.X; x++ {
+ c := palettized.At(x, y)
+ if c == color.Transparent {
+ if _, err := io.WriteString(bw, "\033[49m "); err != nil {
+ return err
+ }
+ } else {
+ if _, err := fmt.Fprintf(bw, "\033[%dm ", 40+c.(xtermColor)); err != nil {
+ return err
+ }
+ }
+ }
+ if _, err := io.WriteString(bw, "\033[49m"); err != nil {
+ return err
+ }
+ }
+ if err := bw.Flush(); err != nil {
+ return err
+ }
+ return nil
+}