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 Graphics Implementation #352

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

CannibalVox
Copy link

This PR adds support for sixels to x/ansi. Sixels are a protocol for writing images to the terminal by writing a large blob of ANSI-escaped data. They function by encoding columns of 6 pixels into a single character (in much the same way base64 encodes data 6 bits at a time). Sixel images are paletted, with a palette established at the beginning of the image blob and pixels identifying palette entries by index while writing the pixel data.

Sixels are written one 6-pixel-tall band at a time, one color at a time. For each band, a single color's pixels are written, then a carriage return is written to bring the "cursor" back to the beginning of a band where a new color is selected and pixels written. This continues until the entire band has been drawn, at which time a line break is written to begin the next band.

Sixel writing and reading take the form of the sixel.Encoder and sixel.Decoder types, in order to match the existing kitty graphics implementation as closely as we reasonably can. The only issue is that because there is no way to know the size of a sixel blob in advance (unlike the kitty graphics protocol, we can't determine the size of the payload from the width and height), we can only determine the boundaries of the payload by detecting the ST sequence (or BEL or whatever we're using to close the payload). As a result, the sixel decoder accepts a byte slice that contains only the payload and relies on the caller to determine the boundaries.

I included an options type in the parameters for WriteSixelGraphics, but there are not yet any options. I'd like to include a dithering implementation in the future, but I wanted to get the actual sixel implementation checked in first.

Copy link
Member

@aymanbagabas aymanbagabas left a comment

Choose a reason for hiding this comment

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

Impressive work here!

Initial review here with few comments to abstract and generalize the implementation. I wonder if we need a sixel.Options here, instead, we can place encoder/decoder customizations and options directly in the structure definitions.

ansi/graphics.go Outdated Show resolved Hide resolved
ansi/sixel/encoder.go Outdated Show resolved Hide resolved
"strconv"
"strings"

"github.com/bits-and-blooms/bitset"
Copy link
Member

Choose a reason for hiding this comment

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

Is there anyway we can get rid of this dependency?

Copy link
Author

Choose a reason for hiding this comment

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

The only way I see it working is writing our own bitset implementation- is that something you want to pursue?

ansi/sixel/encoder.go Outdated Show resolved Hide resolved
Comment on lines +261 to +268
// sixelColor is a flat struct that contains a single color: all channels are 0-100
// instead of anything sensible
type sixelColor struct {
Red uint32
Green uint32
Blue uint32
Alpha uint32
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// sixelColor is a flat struct that contains a single color: all channels are 0-100
// instead of anything sensible
type sixelColor struct {
Red uint32
Green uint32
Blue uint32
Alpha uint32
}
// sixelColor is a flat struct that contains a single color: all channels are 0-100
// instead of anything sensible
type RGBAColor color.RGBA

Copy link
Author

Choose a reason for hiding this comment

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

Are we good with the fact that RGBA colors sourced from outside this package will not work properly?

Copy link
Member

Choose a reason for hiding this comment

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

RGBA colors that are not sixel.RGBAColor can be converted using sixelConvertColor

// - If a single color sits on a cut line, all pixels of that color are assigned to one of the subcubes
// rather than try to split them up between the subcubes. This allows us to use a slice of unique colors
// and a map of pixel counts rather than try to represent each pixel individually.
type sixelPalette struct {
Copy link
Member

Choose a reason for hiding this comment

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

Should we expose this and be able to pass a custom Palette in *Options?

Comment on lines 72 to 89
func (e *Encoder) encodePaletteColor(w io.Writer, paletteIndex int, c sixelColor) {
// Initializing palette entries
// #<a>;<b>;<c>;<d>;<e>
// a = palette index
// b = color type, 2 is RGB
// c = R
// d = G
// e = B

w.Write([]byte{sixelUseColor}) //nolint:errcheck
io.WriteString(w, strconv.Itoa(paletteIndex)) //nolint:errcheck
io.WriteString(w, ";2;")
io.WriteString(w, strconv.Itoa(int(c.Red))) //nolint:errcheck
w.Write([]byte{';'}) //nolint:errcheck
io.WriteString(w, strconv.Itoa(int(c.Green))) //nolint:errcheck
w.Write([]byte{';'})
io.WriteString(w, strconv.Itoa(int(c.Blue))) //nolint:errcheck
}
Copy link
Member

Choose a reason for hiding this comment

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

Let's define sixel.BasicColor, sixel.RGBAColor, sixel.HLSColor types that implement color.Color and fmt.Stringer. We can also have a sixel.ParseColor([]byte) (color.Color, error) to decode a color from bytes.

Copy link
Author

Choose a reason for hiding this comment

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

What is a basiccolor in this case, is it just getting a palette index with no color information?

Copy link
Member

Choose a reason for hiding this comment

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

Correct

Comment on lines +283 to +284
type Decoder struct {
}
Copy link
Member

Choose a reason for hiding this comment

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

Could we make the palette customizable?

Suggested change
type Decoder struct {
}
type Decoder struct {
Palette *Palette
}

Copy link
Author

Choose a reason for hiding this comment

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

Does this just cover default colors, or should we also ignore color definitions when a palette is provided?

Copy link
Member

Choose a reason for hiding this comment

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

We use default colors if Palette is nil

Copy link
Author

Choose a reason for hiding this comment

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

Right, but do the passed in palette colors get overwritten by defined colors, or do they only replace the default colors?

ansi/sixel/decoder.go Outdated Show resolved Hide resolved
ansi/sixel/encoder.go Outdated Show resolved Hide resolved
@aymanbagabas
Copy link
Member

Tests are failing because the ansi package targets go1.18 which doesn't support the cmp and slices packages

@CannibalVox
Copy link
Author

I think I should probably mention: the encoder previously used a single stringbuilder in order to minimize allocations. These Raster/Repeat/Color/etc. methods will each add an allocation every time they're called, which will impact performance.

@aymanbagabas
Copy link
Member

I think I should probably mention: the encoder previously used a single stringbuilder in order to minimize allocations. These Raster/Repeat/Color/etc. methods will each add an allocation every time they're called, which will impact performance.

Fair point. Let's change these methods to be writers WriteRaster(io.Writer, ...) (int, error) etc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants