From 20c3c0cb1476a1b9f9029af9b47ecdbce38754f6 Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Sat, 27 Oct 2018 22:06:26 +0100 Subject: [PATCH] sixel working --- buffer/cell.go | 64 +++++++++++- example.sixel | 5 + gui/gui.go | 14 +++ gui/renderer.go | 1 + sixel/img.bmp | Bin 0 -> 614 bytes sixel/img.jpg | 0 sixel/sixel.go | 240 +++++++++++++++++++++++++++++++++++++++++++ sixel/sixel_test.go | 44 ++++++++ terminal/ansi.go | 1 + terminal/sixel.go | 46 +++++++++ terminal/terminal.go | 13 +++ 11 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 example.sixel create mode 100644 sixel/img.bmp create mode 100644 sixel/img.jpg create mode 100644 sixel/sixel.go create mode 100644 sixel/sixel_test.go create mode 100644 terminal/sixel.go diff --git a/buffer/cell.go b/buffer/cell.go index b07cbea..eaa73ee 100644 --- a/buffer/cell.go +++ b/buffer/cell.go @@ -1,8 +1,15 @@ package buffer +import ( + "image" + + "github.com/go-gl/gl/all-core/gl" +) + type Cell struct { - r rune - attr CellAttributes + r rune + attr CellAttributes + image *image.RGBA } type CellAttributes struct { @@ -16,6 +23,59 @@ type CellAttributes struct { Hidden bool } +func (cell *Cell) Image() *image.RGBA { + return cell.image +} + +func (cell *Cell) SetImage(img *image.RGBA) { + + cell.image = img + +} + +func (cell *Cell) DrawImage(x, y float32) { + + if cell.image == nil { + return + } + + var tex uint32 + gl.GenTextures(1, &tex) + gl.BindTexture(gl.TEXTURE_2D, tex) + gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + + gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + + gl.TexImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + int32(cell.image.Bounds().Size().X), + int32(cell.image.Bounds().Size().Y), + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + gl.Ptr(cell.image.Pix), + ) + gl.BindTexture(gl.TEXTURE_2D, 0) + + var w float32 = float32(cell.image.Bounds().Size().X) + var h float32 = float32(cell.image.Bounds().Size().Y) + + var readFboId uint32 + gl.GenFramebuffers(1, &readFboId) + gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFboId) + gl.FramebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, tex, 0) + gl.BlitFramebuffer(0, 0, int32(w), int32(h), + int32(x), int32(y), int32(x+w), int32(y+h), + gl.COLOR_BUFFER_BIT, gl.LINEAR) + gl.BindFramebuffer(gl.READ_FRAMEBUFFER, 0) + gl.DeleteFramebuffers(1, &readFboId) +} + func (cell *Cell) Attr() CellAttributes { return cell.attr } diff --git a/example.sixel b/example.sixel new file mode 100644 index 0000000..73e8e6f --- /dev/null +++ b/example.sixel @@ -0,0 +1,5 @@ +Pq +#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0 +#1~~@@vv@@~~@@~~$ +#2??}}GG}}??}}??- +#1!14@\ \ No newline at end of file diff --git a/gui/gui.go b/gui/gui.go index 487e5a1..a642d59 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -81,6 +81,8 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { gui.logger.Debugf("Setting viewport size...") gl.Viewport(0, 0, int32(gui.width), int32(gui.height)) + gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight) + gui.logger.Debugf("Resize complete!") } @@ -191,6 +193,8 @@ func (gui *GUI) Render() error { } }() + gui.terminal.SetProgram(program) + for !gui.window.ShouldClose() { select { @@ -242,10 +246,20 @@ func (gui *GUI) Render() error { if hasText { gui.renderer.DrawCellText(cell, uint(x), uint(y), nil) } + + if cell.Image() != nil { + ix := float32(x) * gui.renderer.cellWidth + iy := float32(gui.height) - (float32(y+1) * gui.renderer.cellHeight) + iy -= float32(cell.Image().Bounds().Size().Y) + gl.UseProgram(program) + cell.DrawImage(ix, iy) + } + } } gui.window.SwapBuffers() + } } diff --git a/gui/renderer.go b/gui/renderer.go index c37bd93..12f26a8 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -214,6 +214,7 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor rect.setColour(bg) rect.Draw() } + } func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) { diff --git a/sixel/img.bmp b/sixel/img.bmp new file mode 100644 index 0000000000000000000000000000000000000000..db1ba25054665c4c8271a2a12e829c64986f2871 GIT binary patch literal 614 zcmZ?rO=DsJ12Z700mQsO%m>7b3=%++fx!SO59Yv#loSSH5K)SN2Eo(;8F<-)LmkAG a*xZWhK4R5D!jiCg^jAkHmSO%Sqz(YDb9U?i literal 0 HcmV?d00001 diff --git a/sixel/img.jpg b/sixel/img.jpg new file mode 100644 index 0000000..e69de29 diff --git a/sixel/sixel.go b/sixel/sixel.go new file mode 100644 index 0000000..2a1d8b7 --- /dev/null +++ b/sixel/sixel.go @@ -0,0 +1,240 @@ +package sixel + +import ( + "fmt" + "image" + "image/color" + "strconv" + "strings" + + "github.com/go-gl/gl/v2.1/gl" +) + +type Sixel struct { + px map[uint]map[uint]colour + width uint + height uint +} + +type colour [3]uint8 + +func decompress(data string) string { + + output := "" + + inMarker := false + countStr := "" + + for _, r := range data { + + if !inMarker { + if r == '!' { + inMarker = true + countStr = "" + } else { + output = fmt.Sprintf("%s%c", output, r) + } + continue + } + + if r >= 0x30 && r <= 0x39 { + countStr = fmt.Sprintf("%s%c", countStr, r) + } else { + count, _ := strconv.Atoi(countStr) + for i := 0; i < count; i++ { + output = fmt.Sprintf("%s%c", output, r) + } + inMarker = false + } + } + + return output +} + +// pass in everything after ESC+P and before ST +func ParseString(data string) (*Sixel, error) { + + data = decompress(data) + + inHeader := true + inColour := false + + six := Sixel{} + var x, y uint + + colourStr := "" + + colourMap := map[string]colour{} + var selectedColour colour + + headerStr := "" + + remainMode := false + + var ratio uint + + // read p1 p2 p3 + for i, r := range data { + switch true { + case inHeader: + // todo read p1 p2 p3 + if r == 'q' { + headers := strings.Split(headerStr, ";") + switch headers[0] { + case "0", "1": + ratio = 5 + case "2": + ratio = 3 + case "", "3", "4", "5", "6": + ratio = 2 + case "7", "8", "9": + ratio = 1 + } + if len(headers) > 1 { + remainMode = headers[1] == "1" + } + inHeader = false + } else { + headerStr = fmt.Sprintf("%s%c", headerStr, r) + } + case inColour: + colourStr = fmt.Sprintf("%s%c", colourStr, r) + if i+1 >= len(data) || data[i+1] < 0x30 || data[i+1] > 0x3b { + // process colour string + inColour = false + parts := strings.Split(colourStr, ";") + + // select colour + if len(parts) == 1 { + c, ok := colourMap[parts[0]] + if ok { + selectedColour = c + } + } else if len(parts) == 5 { + switch parts[1] { + case "1": + // HSL + return nil, fmt.Errorf("HSL colours are not yet supported") + case "2": + // RGB + r, _ := strconv.Atoi(parts[2]) + g, _ := strconv.Atoi(parts[3]) + b, _ := strconv.Atoi(parts[4]) + colourMap[parts[0]] = colour([3]uint8{ + uint8(r & 0xff), + uint8(g & 0xff), + uint8(b & 0xff), + }) + default: + return nil, fmt.Errorf("Unknown colour definition type: %s", parts[1]) + } + } else { + return nil, fmt.Errorf("Invalid colour directive: #%s", colourStr) + } + + colourStr = "" + } + + default: + switch r { + case '-': + y += 6 + x = 0 + case '$': + x = 0 + case '#': + inColour = true + default: + if r < 63 || r > 126 { + continue + } + b := (r & 0xff) - 0x3f + var bit int + for bit = 5; bit >= 0; bit-- { + if b&(1< 0 { + six.setPixel(x, y+uint(bit), selectedColour, ratio) + } else if !remainMode { + // @todo use background colour here + //six.setPixel(x, y+uint(bit), selectedColour) + } + } + x++ + } + } + } + return &six, nil +} + +func (six *Sixel) Draw() error { + rgba := six.RGBA() + + var handle uint32 + gl.GenTextures(1, &handle) + + target := uint32(gl.TEXTURE_2D) + internalFmt := int32(gl.SRGB_ALPHA) + format := uint32(gl.RGBA) + width := int32(rgba.Rect.Size().X) + height := int32(rgba.Rect.Size().Y) + pixType := uint32(gl.UNSIGNED_BYTE) + dataPtr := gl.Ptr(rgba.Pix) + + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(target, handle) + + // set the texture wrapping/filtering options (applies to current bound texture obj) + // TODO-cs + //gl.TexParameteri(texture.target, gl.TEXTURE_WRAP_R, wrapR) + //gl.TexParameteri(texture.target, gl.TEXTURE_WRAP_S, wrapS) + gl.TexParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // minification filter + gl.TexParameteri(target, gl.TEXTURE_MAG_FILTER, gl.LINEAR) // magnification filter + + gl.TexImage2D(target, 0, internalFmt, width, height, 0, format, pixType, dataPtr) + + // unbind + gl.BindTexture(target, 0) + return nil +} + +func (six *Sixel) setPixel(x, y uint, c colour, vhRatio uint) { + + if six.px == nil { + six.px = map[uint]map[uint]colour{} + } + + if _, exists := six.px[x]; !exists { + six.px[x] = map[uint]colour{} + } + + if x+1 > six.width { + six.width = x + } + + ay := vhRatio * y + + var i uint + for i = 0; i < vhRatio; i++ { + if ay+i+1 > six.height { + six.height = ay + i + 1 + } + six.px[x][ay+i] = c + } + +} + +func (six *Sixel) RGBA() *image.RGBA { + rgba := image.NewRGBA(image.Rect(0, 0, int(six.width), int(six.height))) + + for x, r := range six.px { + for y, colour := range r { + rgba.SetRGBA(int(x), int(six.height)-int(y), color.RGBA{ + colour[0], + colour[1], + colour[2], + 255, + }) + } + } + + return rgba +} diff --git a/sixel/sixel_test.go b/sixel/sixel_test.go new file mode 100644 index 0000000..0ba5f79 --- /dev/null +++ b/sixel/sixel_test.go @@ -0,0 +1,44 @@ +package sixel + +import ( + "bytes" + "io" + "log" + "os" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/image/bmp" +) + +// from https://en.wikipedia.org/wiki/Sixel +func TestParsing(t *testing.T) { + + //raw := `q"1;1;16;16#0;2;0;0;0#1;2;94;75;22#2;2;97;78;31#3;2;97;82;35#4;2;97;82;44#5;2;94;78;25#6;2;91;78;41#7;2;69;60;38#8;2;56;50;35#9;2;63;56;35#10;2;41;38;31#0NB@@!8?@@BN$#1oCA?@!6?@?ACo$#3?O??A?!4@?A??O$#4?_w{{!6}{{w_$#5?G#2CA?@!4?@?AC#5G-#1{_#6K!4?__!4?K#1_{$#5B#4FRrrrz^^zrrrRF#5B$#3?G_#7CCGC??CGCC#3_G$#2?O#9?G!8?G#2?O$#8!4?GC!4?CG-#0NKGG!8?GGKN$#1?BFE!8KEFB$#3???@!8?@$#4!4?@@!4?@@$#5!4?A!6?A$#2!5?A!4?A$#7!6?A??A$#10!6?!4@$#6!7?AA` + raw := `q + #0;2;0;0;0#1;2;100;100;0#2;2;0;100;0 + #1~~@@vv@@~~@@~~$ + #2??}}GG}}??}}??- + #1!14@` + six, err := ParseString(raw) + require.Nil(t, err) + + img := six.RGBA() + require.NotNil(t, img) + + var imageBuf bytes.Buffer + err = bmp.Encode(io.Writer(&imageBuf), img) + + if err != nil { + log.Panic(err) + } + + // Write to file. + fo, err := os.Create("img.bmp") + if err != nil { + panic(err) + } + defer fo.Close() + fo.Write(imageBuf.Bytes()) + +} diff --git a/terminal/ansi.go b/terminal/ansi.go index 0fc7542..c05af3f 100644 --- a/terminal/ansi.go +++ b/terminal/ansi.go @@ -12,6 +12,7 @@ var ansiSequenceMap = map[rune]escapeSequenceHandler{ '8': restoreCursorHandler, 'D': indexHandler, 'M': reverseIndexHandler, + 'P': sixelHandler, 'c': risHandler, //RIS '(': swallowHandler(1), // character set bullshit ')': swallowHandler(1), // character set bullshit diff --git a/terminal/sixel.go b/terminal/sixel.go new file mode 100644 index 0000000..cfc780b --- /dev/null +++ b/terminal/sixel.go @@ -0,0 +1,46 @@ +package terminal + +import ( + "fmt" + "math" + + "github.com/go-gl/gl/all-core/gl" + "github.com/liamg/aminal/sixel" +) + +func sixelHandler(pty chan rune, terminal *Terminal) error { + + data := []rune{} + + for { + b := <-pty + if b == 0x1b { // terminated by ESC bell or ESC \ + _ = <-pty // swallow \ or bell + break + } + data = append(data, b) + } + + six, err := sixel.ParseString(string(data)) + if err != nil { + return fmt.Errorf("Failed to parse sixel data: %s", err) + } + + x, y := terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine() + terminal.ActiveBuffer().Write(' ') + cell := terminal.ActiveBuffer().GetCell(x, y) + if cell == nil { + return fmt.Errorf("Missing cell for sixel") + } + + gl.UseProgram(terminal.program) + cell.SetImage(six.RGBA()) + + imageHeight := float64(cell.Image().Bounds().Size().Y) + lines := int(math.Ceil(imageHeight / float64(terminal.charHeight))) + for l := 0; l <= int(lines+1); l++ { + terminal.ActiveBuffer().NewLine() + } + + return nil +} diff --git a/terminal/terminal.go b/terminal/terminal.go index f2b1646..563572a 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -32,6 +32,7 @@ const ( ) type Terminal struct { + program uint32 buffers []*buffer.Buffer activeBufferIndex uint8 lock sync.Mutex @@ -47,6 +48,8 @@ type Terminal struct { mouseMode MouseMode bracketedPasteMode bool isDirty bool + charWidth float32 + charHeight float32 } type Modes struct { @@ -87,6 +90,11 @@ func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Termin } } + +func (terminal *Terminal) SetProgram(program uint32) { + terminal.program = program +} + func (terminal *Terminal) SetBracketedPasteMode(enabled bool) { terminal.bracketedPasteMode = enabled } @@ -137,6 +145,11 @@ func (terminal *Terminal) ScrollDown(lines uint16) { } +func (terminal *Terminal) SetCharSize(w float32, h float32) { + terminal.charWidth = w + terminal.charHeight = h +} + func (terminal *Terminal) ScrollUp(lines uint16) { terminal.logger.Infof("Scrolling up %d", lines) terminal.ActiveBuffer().ScrollUp(lines)