Skip to content

Commit

Permalink
feat: add sixel to ansi package (#380)
Browse files Browse the repository at this point in the history
Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Co-authored-by: Stephen Baynham <sdbaynham@gmail.com>
Co-authored-by: Stephen Baynham <stephen.baynham@clipboardhealth.com>
  • Loading branch information
4 people authored Feb 21, 2025
1 parent 27c87a7 commit 052644d
Show file tree
Hide file tree
Showing 21 changed files with 2,937 additions and 0 deletions.
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
Binary file added ansi/fixtures/graphics/JigokudaniMonkeyPark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ansi/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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/rivo/uniseg v0.4.7
Expand Down
2 changes: 2 additions & 0 deletions ansi/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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=
Expand Down
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
1 change: 1 addition & 0 deletions ansi/sixel/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ansi/fixtures/graphics/*.png filter=lfs diff=lfs merge=lfs -text
136 changes: 136 additions & 0 deletions ansi/sixel/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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

0 comments on commit 052644d

Please sign in to comment.