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

Implement Sixel encoder/decoder support #381

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ed43f81
feat: Add sixels to ansi package
CannibalVox Jan 30, 2025
d44fac5
scanSize tests
CannibalVox Feb 1, 2025
16b7248
Update ansi/sixel/encoder.go
CannibalVox Feb 1, 2025
f672bee
Update ansi/graphics.go
CannibalVox Feb 1, 2025
9734d1f
pr comments
CannibalVox Feb 1, 2025
41a4558
raster/parseraster
CannibalVox Feb 2, 2025
8705df7
repeat/parserepeat
CannibalVox Feb 2, 2025
01e9042
remove sixel.Options and address pX conditional
raphamorim Feb 19, 2025
59b7744
remove WriteSixelGraphics func
raphamorim Feb 19, 2025
693be27
ignore p3
raphamorim Feb 19, 2025
0f4f102
move back to ;q
raphamorim Feb 19, 2025
2a70985
use byte instead of rune
raphamorim Feb 19, 2025
ce91bcb
remove WriteString
raphamorim Feb 19, 2025
af13a23
remove slices
raphamorim Feb 19, 2025
d68cbad
use Compare and isNan from 1.24
raphamorim Feb 19, 2025
e3b8780
fixup! use Compare and isNan from 1.24
raphamorim Feb 19, 2025
394b145
fixup! fixup! use Compare and isNan from 1.24
raphamorim Feb 19, 2025
39755ba
fixup! fixup! fixup! use Compare and isNan from 1.24
raphamorim Feb 19, 2025
d53c02c
remove code
raphamorim Feb 19, 2025
38f4f5a
chore: merge branch 'main' into feat-sixel-support
aymanbagabas Feb 19, 2025
226b4c2
add bench
raphamorim Feb 20, 2025
3653b43
rename file
raphamorim Feb 20, 2025
f1a551f
add palette_sort
raphamorim Feb 20, 2025
49ef044
remove stableCmpFunc
raphamorim Feb 20, 2025
eb42d4e
remove unnecessary stuff
raphamorim Feb 20, 2025
53d8da5
remove comments
raphamorim Feb 20, 2025
83cc248
feat(ansi): sixel: implement types and use io.Reader
aymanbagabas Feb 20, 2025
5735ce9
refactor(ansi): sixel: clean up and refactor, use quant
aymanbagabas Feb 20, 2025
92f58dc
feat: use aymanbagabas/quant instead of soniakeys/quant for sort
aymanbagabas Feb 20, 2025
eec3c2c
feat(ansi): sixel: support transparent color in encoder
aymanbagabas Feb 20, 2025
bd0387e
feat(ansi): sixel: add dithering support
aymanbagabas Feb 20, 2025
1271e57
chore(ansi): sixel: use unix line endings
aymanbagabas Feb 20, 2025
db2c579
fix(ansi): sixel: rename AddTransparent to NoTransparency
aymanbagabas Feb 20, 2025
d226daa
chore(ansi): sixel: remove unused and expose default color palette
aymanbagabas Feb 21, 2025
73138de
fix(ansi): sixel: lint errors
aymanbagabas Feb 21, 2025
e46a2b7
feat(ansi): sixel: add Palette field to Decoder
aymanbagabas Feb 21, 2025
fdb1448
feat(examples): add img2term example
aymanbagabas Feb 21, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@

testdata
*.png

# Allow graphics used for bench test
!ansi/fixtures/graphics/*.png
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added ansi/fixtures/graphics/JigokudaniMonkeyPark.png originally but i have a concern if it would come along when someone would install x/ansi, i assume not right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they wont as long as we use git-lfs

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions ansi/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ module github.com/charmbracelet/x/ansi
go 1.18

require (
github.com/bits-and-blooms/bitset v1.20.0
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.16
github.com/mattn/go-sixel v0.0.5 //go:build benchthis
github.com/rivo/uniseg v0.4.7
github.com/soniakeys/quant v1.0.0 // indirect; go:build benchthis
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not github.com/aymanbagabas/quant?

this is just for the bench?

)

require github.com/aymanbagabas/quant v0.0.0-20250220224823-9dea6ec382b5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to move to the first require no?

8 changes: 8 additions & 0 deletions ansi/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
github.com/aymanbagabas/quant v0.0.0-20250220224823-9dea6ec382b5 h1:BQwkJD8W8jgJpvJgA3klNG5W4pyrU4wJ7LynWik+uYc=
github.com/aymanbagabas/quant v0.0.0-20250220224823-9dea6ec382b5/go.mod h1:LZcbdMgqlAs77pcs8WZnQxNflVHNtzWWDDUJoBFC24A=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8=
github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
38 changes: 38 additions & 0 deletions ansi/graphics.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,49 @@ import (
"image"
"io"
"os"
"strconv"
"strings"

"github.com/charmbracelet/x/ansi/kitty"
)

// SixelGraphics returns a sequence that encodes the given sixel image payload to
// a DCS sixel sequence.
//
// DCS p1; p2; p3; q [sixel payload] ST
//
// p1 = pixel aspect ratio, deprecated and replaced by pixel metrics in the payload
//
// p2 = This is supposed to be 0 for transparency, but terminals don't seem to
// to use it properly. Value 0 leaves an unsightly black bar on all terminals
// I've tried and looks correct with value 1.
//
// p3 = Horizontal grid size parameter. Everyone ignores this and uses a fixed grid
// size, as far as I can tell.
//
// See https://shuford.invisible-island.net/all_about_sixels.txt
func SixelGraphics(p1, p2, p3 int, payload []byte) string {
var buf bytes.Buffer

buf.WriteString("\x1bP")
if p1 >= 0 {
buf.WriteString(strconv.Itoa(p1))
}
buf.WriteByte(';')
if p2 >= 0 {
buf.WriteString(strconv.Itoa(p2))
}
if p3 > 0 {
buf.WriteByte(';')
buf.WriteString(strconv.Itoa(p3))
}
buf.WriteByte('q')
buf.Write(payload)
buf.WriteString("\x1b\\")

return buf.String()
}

// KittyGraphics returns a sequence that encodes the given image in the Kitty
// graphics protocol.
//
Expand Down
135 changes: 135 additions & 0 deletions ansi/sixel/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package sixel

import (
"fmt"
"image/color"
"io"

"github.com/lucasb-eyer/go-colorful"
)

// ErrInvalidColor is returned when a Sixel color is invalid.
var ErrInvalidColor = fmt.Errorf("invalid color")

// WriteColor writes a Sixel color to a writer. If pu is 0, the rest of the
// parameters are ignored.
func WriteColor(w io.Writer, pc, pu, px, py, pz int) (int, error) {
if pu <= 0 || pu > 2 {
return fmt.Fprintf(w, "#%d", pc)
}

return fmt.Fprintf(w, "#%d;%d;%d;%d;%d", pc, pu, px, py, pz)
}

// ConvertChannel converts a color channel from color.Color 0xffff to 0-100
// Sixel RGB format.
func ConvertChannel(c uint32) uint32 {
// We add 328 because that is about 0.5 in the sixel 0-100 color range, we're trying to
// round to the nearest value
return (c + 328) * 100 / 0xffff
}

// FromColor returns a Sixel color from a color.Color. It converts the color
// channels to the 0-100 range.
func FromColor(c color.Color) Color {
if c == nil {
return Color{}
}
r, g, b, _ := c.RGBA()
return Color{
Pu: 2, // Always use RGB format "2"
Px: int(ConvertChannel(r)),
Py: int(ConvertChannel(g)),
Pz: int(ConvertChannel(b)),
}
}

// DecodeColor decodes a Sixel color from a byte slice. It returns the Color and
// the number of bytes read.
func DecodeColor(data []byte) (c Color, n int) {
if len(data) == 0 || data[0] != ColorIntroducer {
return
}

if len(data) < 2 { // The minimum length is 2: the introducer and a digit.
return
}

// Parse the color number and optional color system.
pc := &c.Pc
for n = 1; n < len(data); n++ {
if data[n] == ';' {
if pc == &c.Pc {
pc = &c.Pu
} else {
n++
break
}
} else if data[n] >= '0' && data[n] <= '9' {
*pc = (*pc)*10 + int(data[n]-'0')
} else {
break
}
}

// Parse the color components.
ptr := &c.Px
for ; n < len(data); n++ {
if data[n] == ';' {
if ptr == &c.Px {
ptr = &c.Py
} else if ptr == &c.Py {
ptr = &c.Pz
} else {
n++
break
}
} else if data[n] >= '0' && data[n] <= '9' {
*ptr = (*ptr)*10 + int(data[n]-'0')
} else {
break
}
}

return
}

// Color represents a Sixel color.
type Color struct {
// Pc is the color number (0-255).
Pc int
// Pu is an optional color system
// - 0: default color map
// - 1: HLS
// - 2: RGB
Pu int
// Color components range from 0-100 for RGB values. For HLS format, the Px
// (Hue) component ranges from 0-360 degrees while L (Lightness) and S
// (Saturation) are 0-100.
Px, Py, Pz int
}

// RGBA implements the color.Color interface.
func (c Color) RGBA() (r, g, b, a uint32) {
switch c.Pu {
case 1:
return sixelHLS(c.Px, c.Py, c.Pz).RGBA()
case 2:
return sixelRGB(c.Px, c.Py, c.Pz).RGBA()
default:
return colorPalette[c.Pc].RGBA()
}
}

// #define PALVAL(n,a,m) (((n) * (a) + ((m) / 2)) / (m))
func palval(n, a, m int) int {
return (n*a + m/2) / m
}

func sixelRGB(r, g, b int) color.Color {
return color.NRGBA{uint8(palval(r, 0xff, 100)), uint8(palval(g, 0xff, 100)), uint8(palval(b, 0xff, 100)), 0xFF} //nolint:gosec
}

func sixelHLS(h, l, s int) color.Color {
return colorful.Hsl(float64(h), float64(s)/100.0, float64(l)/100.0).Clamped()
}
Loading
Loading