Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sixel support for images with pixel resolution #233

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,014 changes: 5,014 additions & 0 deletions _examples/image_pixel.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions block.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type Block struct {
Title string
TitleStyle Style

ANSIString string

sync.Mutex
}

Expand Down Expand Up @@ -103,3 +105,8 @@ func (self *Block) SetRect(x1, y1, x2, y2 int) {
func (self *Block) GetRect() image.Rectangle {
return self.Rectangle
}

// GetANSIString implements the Drawable interface.
func (self *Block) GetANSIString() string {
return self.ANSIString
}
35 changes: 35 additions & 0 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package termui

import (
"fmt"
"image"
"sync"

Expand All @@ -16,10 +17,44 @@ type Drawable interface {
SetRect(int, int, int, int)
Draw(*Buffer)
sync.Locker
GetANSIString() string
}

func Render(items ...Drawable) {
// draw background, etc for items with ANSI escape strings
for _, item := range items {
if len(item.GetANSIString()) > 0 {
continue
}
buf := NewBuffer(item.GetRect())
item.Lock()
item.Draw(buf)
item.Unlock()
for point, cell := range buf.CellMap {
if point.In(buf.Rectangle) {
tb.SetCell(
point.X, point.Y,
cell.Rune,
tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1),
)
}
}
}
tb.Flush()

// draw images, etc over the already filled cells with ANSI escape strings (sixel, ...)
for _, item := range items {
if ansiString := item.GetANSIString(); len(ansiString) > 0 {
fmt.Printf("%s", ansiString)
continue
}
}

// draw items without ANSI strings last in case the ANSI escape strings ended messed up
for _, item := range items {
if len(item.GetANSIString()) == 0 {
continue
}
buf := NewBuffer(item.GetRect())
item.Lock()
item.Draw(buf)
Expand Down
19 changes: 19 additions & 0 deletions widgets/exp/HACKING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
possible enhancements:

kitty https://sw.kovidgoyal.net/kitty/graphics-protocol.html

Terminology (from Enlightenment) https://www.enlightenment.org/docs/apps/terminology.md#tycat https://github.com/billiob/terminology

urxvt pixbuf / ...

Tektronix 4014

ReGis

reimplement 23imgdisplay:

check for X11 and not Alacritty (https://github.com/jwilm/alacritty/issues/1021)

---

if i remember correctly xterm has a size limit for sixel images - pixel width???
Empty file added widgets/exp/README.md
Empty file.
36 changes: 36 additions & 0 deletions widgets/exp/aaa_init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package exp

import (
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)

// the file name should appear at the top when alphabetically sorted (start with "aaa")
// because the init() functions are executed in alphabetic file order
func init() {
scanTerminal()
var drawFallback func(*widgets.Image, *Buffer) (error)
if drbl, ok := widgets.GetDrawers()["block"]; ok {
drawFallback = drbl.Draw
}
widgets.RegisterDrawer(
"block",
widgets.Drawer{
Remote: true,
IsEscapeString: false,
Available: func() bool {return true},
Draw: func(img *widgets.Image, buf *Buffer) (err error) {
// possible reattachments of the terminal multiplexer?
if isMuxed {
scanTerminal()
}

return drawFallback(img, buf)
},
},
)
}
60 changes: 60 additions & 0 deletions widgets/exp/drawer_iterm2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package exp

import (
"fmt"
"bytes"
"encoding/base64"
"errors"
"image/png"

. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)


func init() {
widgets.RegisterDrawer(
"iterm2",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return isIterm2 || isMacTerm},
Draw: drawITerm2,
},
)
}

func drawITerm2(wdgt *widgets.Image, buf *Buffer) (err error) {
wdgt.Block.Draw(buf)

img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}

imageDimensions := wdgt.GetVisibleArea()

// https://www.iterm2.com/documentation-images.html
if isIterm2 || isMacTerm {
buf := new(bytes.Buffer)
if err = png.Encode(buf, img); err != nil {
goto skipIterm2
}
imgBase64 := base64.StdEncoding.EncodeToString(buf.Bytes())
nameBase64 := base64.StdEncoding.EncodeToString([]byte(wdgt.Block.Title))
// 0 for stretching - 1 for no stretching
noStretch := 0
iterm2String := wrap(fmt.Sprintf("\033]1337;File=name=%s;inline=1;height=%d;width=%d;preserveAspectRatio=%d:%s\a", nameBase64, imageDimensions.Max.Y, nameBase64, imageDimensions.Max.X, noStretch, imgBase64))
// for width, height: "auto" || N: N character cells || Npx: N pixels || N%: N percent of terminal width/height
wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", imageDimensions.Min.Y, imageDimensions.Min.X, iterm2String)

return nil
}
skipIterm2:

return errors.New("no method applied for ANSI drawing")
}
102 changes: 102 additions & 0 deletions widgets/exp/drawer_kitty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package exp

import (
"fmt"
"errors"
"bytes"
"encoding/base64"
"image/png"

. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)

const (
kittyLimit = 4096
)

var (
// TODO: for numbering of ids
kittyImageCount int
)

func init() {
widgets.RegisterDrawer(
"kitty",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return isKitty},
Draw: drawKitty,
},
)
}

func drawKitty(wdgt *widgets.Image, buf *Buffer) (err error) {
if !isKitty {
return errors.New("method not supported for this terminal type")
}

wdgt.Block.Draw(buf)

// TODO: FIX THIS
termWidth, termHeight := getTermSizeInChars(true)
var _ = termWidth
/*
if termWidth == 0 || termHeight == 0 {
return errors.New("could not query terminal dimensions")
}
*/

img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}

var imgHeight int
imageDimensions := wdgt.GetVisibleArea()
if wdgt.Inner.Max.Y < termHeight {
imgHeight = wdgt.Inner.Dy()
} else {
imgHeight = termHeight-1
}
imgHeight = wdgt.Inner.Dy() // TODO: REMOVE THIS CRUTCH

// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#remote-client
// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#png-data
// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#controlling-displayed-image-layout
bytBuf := new(bytes.Buffer)
if err = png.Encode(bytBuf, img); err != nil {
return err
}
imgBase64 := base64.StdEncoding.EncodeToString(bytBuf.Bytes())
lenImgB64 := len([]byte(imgBase64))
// a=T action
// t=d payload is (base64 encoded) data itself not a file location
// f=100 format: 100 = PNG payload
// o=z data compression
// X=...,Y=,,, Upper left image corner in cell coordinates (starting with 1, 1)
// c=...,r=... image size in cell columns and rows
// w=...,h=... width & height (in pixels) of the image area to display // TODO: Use this to let Kitty handle cropping!
// z=0 z-index vertical stacking order of the image
// m=[01] 0 last escape code chunk - 1 for all except the last
var kittyString string
var zIndex = 2 // draw over text
settings := fmt.Sprintf("a=T,t=d,f=100,X=%d,Y=%d,c=%d,r=%d,z=%d,", imageDimensions.Min.X, imageDimensions.Min.Y, wdgt.Inner.Dx(), imgHeight, zIndex)
i := 0
for ; i < (lenImgB64-1)/kittyLimit; i++ {
kittyString += wrap(fmt.Sprintf("\033_G%sm=1;%s\033\\", settings, imgBase64[i*kittyLimit:(i+1)*kittyLimit]))
settings = ""
}
kittyString += wrap(fmt.Sprintf("\033_G%sm=0;%s\033\\", settings, imgBase64[i*kittyLimit:lenImgB64]))

wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", imageDimensions.Min.Y, imageDimensions.Min.X, kittyString)
return nil
}

// TODO:
// store images with ids in Kitty
63 changes: 63 additions & 0 deletions widgets/exp/drawer_sixel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package exp

import (
"fmt"
"bytes"
"errors"

"github.com/mattn/go-sixel"

. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)


func init() {
widgets.RegisterDrawer(
"sixel",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return sixelCapable},
Draw: drawSixel,
},
)
}

func drawSixel(wdgt *widgets.Image, buf *Buffer) (err error) {
wdgt.Block.Draw(buf)

img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}

// sixel
// https://vt100.net/docs/vt3xx-gp/chapter14.html
if sixelCapable {
byteBuf := new(bytes.Buffer)
enc := sixel.NewEncoder(byteBuf)
enc.Dither = true
if err := enc.Encode(img); err != nil {
return err
}
sixelString := wrap("\033[?8452h" + byteBuf.String())
// position where the image should appear (upper left corner) + sixel
// https://github.com/mintty/mintty/wiki/CtrlSeqs#sixel-graphics-end-position
// "\033[?8452h" sets the cursor next right to the bottom of the image instead of below
// this prevents vertical scrolling when the image fills the last line.
// horizontal scrolling because of this did not happen in my test cases.
// "\033[?80l" disables sixel scrolling if it isn't already.
wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", wdgt.Inner.Min.Y + 1, wdgt.Inner.Min.X + 1, sixelString)
// test string "HI"
// wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH\033[?8452h%s", wdgt.Inner.Min.Y+1, wdgt.Inner.Min.X+1, "\033Pq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\033\\")

return nil
}

return errors.New("no method applied for ANSI drawing")
}
Loading