Merge pull request #44 from liamg/sixel-mk2

Sixel support
This commit is contained in:
Liam Galvin 2018-10-28 15:28:45 +00:00 committed by GitHub
commit 978d5067f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 430 additions and 28 deletions

View File

@ -10,9 +10,14 @@ The project is experimental at the moment, so you probably won't want to rely on
Ensure you have your latest graphics card drivers installed before use. Ensure you have your latest graphics card drivers installed before use.
Sixels are now supported.
![Example sixel](sixel.png)
## Aims ## Aims
- Full unicode support - Unicode support
- OpenGL rendering - OpenGL rendering
- Full customisation options - Full customisation options
- True colour support - True colour support
@ -23,6 +28,7 @@ Ensure you have your latest graphics card drivers installed before use.
- Resize logic that wraps/unwraps lines _correctly_ - Resize logic that wraps/unwraps lines _correctly_
- Bullshit graphical effects - Bullshit graphical effects
- Multi platform support - Multi platform support
- Sixel support
## What isn't supported? ## What isn't supported?
@ -32,13 +38,6 @@ Ensure you have your latest graphics card drivers installed before use.
<img alt="Overheating" src="https://imgs.xkcd.com/comics/workflow.png"/> <img alt="Overheating" src="https://imgs.xkcd.com/comics/workflow.png"/>
</p> </p>
## Build Dependencies
- Go 1.10.3+
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required headers and libraries.
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev xorg-dev`.
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel`.
## Platform Support ## Platform Support
| Platform | Supported | | Platform | Supported |
@ -47,25 +46,12 @@ Ensure you have your latest graphics card drivers installed before use.
| MacOSX | ⏳ | | MacOSX | ⏳ |
| Windows | ⏳ | | Windows | ⏳ |
## Planned Features ## Build Dependencies
| Feature | Done | Notes | - Go 1.10.3+
|-----------------------------|------|-------| - On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required headers and libraries.
| Pty allocation | ✔ | - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev xorg-dev`.
| OpenGL rendering | ✔ | - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel`.
| 8-bit (256) colour | ✔ |
| 24-bit (true) colour | ✔ |
| Resizing/content reordering | ✔ |
| ANSI escape codes | ✔ |
| UTF-8 input | ✔ |
| UTF-8 output | ✔ |
| Copy/paste | ✔ |
| Customisable colour schemes | ✔ |
| Config file | ✔ |
| Scrolling | ✔ |
| Mouse interaction | ✔ |
| Clickable URLs | ✔ |
| Sweet render effects | |
## Keyboard Shortcuts ## Keyboard Shortcuts

View File

@ -1,8 +1,15 @@
package buffer package buffer
import (
"image"
"github.com/go-gl/gl/all-core/gl"
)
type Cell struct { type Cell struct {
r rune r rune
attr CellAttributes attr CellAttributes
image *image.RGBA
} }
type CellAttributes struct { type CellAttributes struct {
@ -16,6 +23,64 @@ type CellAttributes struct {
Hidden bool 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.Enable(gl.TEXTURE_2D)
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)
gl.Disable(gl.TEXTURE_2D)
gl.Disable(gl.BLEND)
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 { func (cell *Cell) Attr() CellAttributes {
return cell.attr return cell.attr
} }

1
example.sixel Normal file

File diff suppressed because one or more lines are too long

View File

@ -81,6 +81,8 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.logger.Debugf("Setting viewport size...") gui.logger.Debugf("Setting viewport size...")
gl.Viewport(0, 0, int32(gui.width), int32(gui.height)) gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight)
gui.logger.Debugf("Resize complete!") gui.logger.Debugf("Resize complete!")
} }
@ -191,6 +193,8 @@ func (gui *GUI) Render() error {
} }
}() }()
gui.terminal.SetProgram(program)
for !gui.window.ShouldClose() { for !gui.window.ShouldClose() {
select { select {
@ -242,10 +246,20 @@ func (gui *GUI) Render() error {
if hasText { if hasText {
gui.renderer.DrawCellText(cell, uint(x), uint(y), nil) 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() gui.window.SwapBuffers()
} }
} }

View File

@ -214,6 +214,7 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
rect.setColour(bg) rect.setColour(bg)
rect.Draw() rect.Draw()
} }
} }
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) { func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) {

BIN
sixel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
sixel/img.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

0
sixel/img.jpg Normal file
View File

205
sixel/sixel.go Normal file
View File

@ -0,0 +1,205 @@
package sixel
import (
"fmt"
"image"
"image/color"
"strconv"
"strings"
)
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 += string(r)
}
continue
}
if r >= 0x30 && r <= 0x39 {
countStr = fmt.Sprintf("%s%c", countStr, r)
} else {
count, _ := strconv.Atoi(countStr)
output += strings.Repeat(string(r), count)
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<<uint(bit)) > 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) 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.Set(int(x), int(six.height)-int(y), color.RGBA{
R: colour[0],
G: colour[1],
B: colour[2],
A: 255,
})
}
}
return rgba
}

44
sixel/sixel_test.go Normal file
View File

@ -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())
}

View File

@ -12,6 +12,7 @@ var ansiSequenceMap = map[rune]escapeSequenceHandler{
'8': restoreCursorHandler, '8': restoreCursorHandler,
'D': indexHandler, 'D': indexHandler,
'M': reverseIndexHandler, 'M': reverseIndexHandler,
'P': sixelHandler,
'c': risHandler, //RIS 'c': risHandler, //RIS
'(': swallowHandler(1), // character set bullshit '(': swallowHandler(1), // character set bullshit
')': swallowHandler(1), // character set bullshit ')': swallowHandler(1), // character set bullshit

72
terminal/sixel.go Normal file
View File

@ -0,0 +1,72 @@
package terminal
import (
"fmt"
"image"
"image/draw"
"math"
"strings"
"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
}
if b >= 33 {
data = append(data, b)
}
}
six, err := sixel.ParseString(string(data))
if err != nil {
return fmt.Errorf("Failed to parse sixel data: %s", err)
}
originalImage := six.RGBA()
w := originalImage.Bounds().Size().X
h := originalImage.Bounds().Size().Y
x, y := terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine()
fromBottom := int(terminal.ActiveBuffer().ViewHeight() - y)
lines := int(math.Ceil(float64(h) / float64(terminal.charHeight)))
if fromBottom < lines+2 {
y -= (uint16(lines+2) - uint16(fromBottom))
}
for l := 0; l <= int(lines); l++ {
terminal.ActiveBuffer().Write([]rune(strings.Repeat(" ", int(terminal.ActiveBuffer().ViewWidth())))...)
terminal.ActiveBuffer().NewLine()
}
cols := int(math.Ceil(float64(w) / float64(terminal.charWidth)))
for offsetY := 0; offsetY < lines-1; offsetY++ {
for offsetX := 0; offsetX < cols-1; offsetX++ {
cell := terminal.ActiveBuffer().GetCell(x+uint16(offsetX), y+uint16((lines-2)-offsetY))
if cell == nil {
continue
}
img := originalImage.SubImage(image.Rect(
offsetX*int(terminal.charWidth),
offsetY*int(terminal.charHeight),
(offsetX*int(terminal.charWidth))+int(terminal.charWidth),
(offsetY*int(terminal.charHeight))+int(terminal.charHeight),
))
rgba := image.NewRGBA(image.Rect(0, 0, int(terminal.charWidth), int(terminal.charHeight)))
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
cell.SetImage(rgba)
}
}
return nil
}

View File

@ -32,6 +32,7 @@ const (
) )
type Terminal struct { type Terminal struct {
program uint32
buffers []*buffer.Buffer buffers []*buffer.Buffer
activeBufferIndex uint8 activeBufferIndex uint8
lock sync.Mutex lock sync.Mutex
@ -47,6 +48,8 @@ type Terminal struct {
mouseMode MouseMode mouseMode MouseMode
bracketedPasteMode bool bracketedPasteMode bool
isDirty bool isDirty bool
charWidth float32
charHeight float32
} }
type Modes struct { 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) { func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
terminal.bracketedPasteMode = enabled 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) { func (terminal *Terminal) ScrollUp(lines uint16) {
terminal.logger.Infof("Scrolling up %d", lines) terminal.logger.Infof("Scrolling up %d", lines)
terminal.ActiveBuffer().ScrollUp(lines) terminal.ActiveBuffer().ScrollUp(lines)