mirror of https://github.com/liamg/aminal.git
sixel working
This commit is contained in:
parent
0cc73f1b71
commit
20c3c0cb14
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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@\
|
14
gui/gui.go
14
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 614 B |
|
@ -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<<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) 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
|
||||
}
|
|
@ -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())
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue