From 4cece4582617ae1508a877c55292a50bc5d6addf Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Fri, 21 Feb 2025 12:32:03 -0300 Subject: [PATCH] Update decoder and encoder Co-authored-by: Ayman Bagabas --- ansi/sixel/color.go | 50 ++++++++++++++++++++++++++++++---------- ansi/sixel/color_test.go | 22 +++++++++--------- ansi/sixel/decoder.go | 14 +++++++---- ansi/sixel/encoder.go | 2 +- 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/ansi/sixel/color.go b/ansi/sixel/color.go index 761d6c30..a5ecec23 100644 --- a/ansi/sixel/color.go +++ b/ansi/sixel/color.go @@ -13,7 +13,7 @@ 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 uint) (int, error) { +func WriteColor(w io.Writer, pc, pu, px, py, pz int) (int, error) { if pu <= 0 || pu > 2 { return fmt.Fprintf(w, "#%d", pc) } @@ -21,6 +21,29 @@ func WriteColor(w io.Writer, pc, pu, px, py, pz uint) (int, error) { 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 { + r, g, b, a := c.RGBA() + if a == 0 { + return Color{Pu: 3} // Transparent color + } + 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) { @@ -43,7 +66,7 @@ func DecodeColor(data []byte) (c Color, n int) { break } } else if data[n] >= '0' && data[n] <= '9' { - *pc = (*pc)*10 + data[n] - '0' + *pc = (*pc)*10 + int(data[n]-'0') } else { break } @@ -62,7 +85,7 @@ func DecodeColor(data []byte) (c Color, n int) { break } } else if data[n] >= '0' && data[n] <= '9' { - *ptr = (*ptr)*10 + uint(data[n]-'0') + *ptr = (*ptr)*10 + int(data[n]-'0') } else { break } @@ -74,13 +97,16 @@ func DecodeColor(data []byte) (c Color, n int) { // Color represents a Sixel color. type Color struct { // Pc is the color number (0-255). - Pc uint8 - // Pu is an optional color system (1: HLS, 2: RGB, 0: default color map). - Pu uint8 + 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 uint + Px, Py, Pz int } // RGBA implements the color.Color interface. @@ -95,7 +121,7 @@ func (c Color) RGBA() (r, g, b, a uint32) { } } -var colors = map[uint8]color.Color{ +var colors = map[int]color.Color{ // 16 predefined color registers of VT340 0: sixelRGB(0, 0, 0), 1: sixelRGB(20, 20, 80), @@ -116,14 +142,14 @@ var colors = map[uint8]color.Color{ } // #define PALVAL(n,a,m) (((n) * (a) + ((m) / 2)) / (m)) -func palval(n, a, m uint) uint { +func palval(n, a, m int) int { return (n*a + m/2) / m } -func sixelRGB(r, g, b uint) color.Color { - return color.RGBA{uint8(palval(r, 0xff, 100)), uint8(palval(g, 0xff, 100)), uint8(palval(b, 0xff, 100)), 0xFF} //nolint:gosec +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 uint) color.Color { +func sixelHLS(h, l, s int) color.Color { return colorful.Hsl(float64(h), float64(s)/100.0, float64(l)/100.0).Clamped() } diff --git a/ansi/sixel/color_test.go b/ansi/sixel/color_test.go index 282c3796..5844fdc8 100644 --- a/ansi/sixel/color_test.go +++ b/ansi/sixel/color_test.go @@ -9,11 +9,11 @@ import ( func TestWriteColor(t *testing.T) { tests := []struct { name string - pc uint - pu uint - px uint - py uint - pz uint + pc int + pu int + px int + py int + pz int expected string }{ { @@ -181,9 +181,9 @@ func TestColor_RGBA(t *testing.T) { func TestSixelRGB(t *testing.T) { tests := []struct { name string - r uint - g uint - b uint + r int + g int + b int want color.Color }{ { @@ -231,9 +231,9 @@ func TestSixelRGB(t *testing.T) { func TestSixelHLS(t *testing.T) { tests := []struct { name string - h uint - l uint - s uint + h int + l int + s int want color.Color }{ { diff --git a/ansi/sixel/decoder.go b/ansi/sixel/decoder.go index 9b505ee8..60754030 100644 --- a/ansi/sixel/decoder.go +++ b/ansi/sixel/decoder.go @@ -349,7 +349,8 @@ func (d *Decoder) Decode(r io.Reader) (image.Image, error) { var currentX, currentBandY, currentPaletteIndex int // data buffer used to decode Sixel commands - data := make([]byte, 0, 16) // arbitrary number of bytes to read + data := make([]byte, 0, 6) // arbitrary number of bytes to read + // i := 0 // keeps track of the data buffer index for { b, err := rd.ReadByte() if err != nil { @@ -364,7 +365,7 @@ func (d *Decoder) Decode(r io.Reader) (image.Image, error) { case b == CarriageReturn: // CR currentX = 0 case b == ColorIntroducer: // # - data = data[0:] + data = data[:0] data = append(data, b) for { b, err = rd.ReadByte() @@ -387,8 +388,11 @@ func (d *Decoder) Decode(r io.Reader) (image.Image, error) { return img, ErrInvalidColor } - currentPaletteIndex = int(c.Pc) - palette[currentPaletteIndex] = c + currentPaletteIndex = c.Pc + if c.Pu > 0 { + // Non-zero Pu means we have a color definition to set. + palette[currentPaletteIndex] = c + } // palette[currentPaletteIndex] = color.RGBA64{ // R: uint16(imageConvertChannel(uint32(c.Px))), // G: uint16(imageConvertChannel(uint32(c.Py))), @@ -396,7 +400,7 @@ func (d *Decoder) Decode(r io.Reader) (image.Image, error) { // A: 65525, // } case b == RepeatIntroducer: // ! - data = data[0:] + data = data[:0] data = append(data, b) for { b, err = rd.ReadByte() diff --git a/ansi/sixel/encoder.go b/ansi/sixel/encoder.go index 1e6fdc4f..ea1b0188 100644 --- a/ansi/sixel/encoder.go +++ b/ansi/sixel/encoder.go @@ -138,7 +138,7 @@ func (s *sixelBuilder) SetColor(x int, y int, color color.Color) { paletteIndex := s.SixelPalette.ColorIndex(sixelConvertColor(color)) bit := s.BandHeight()*s.imageWidth*6*paletteIndex + bandY*s.imageWidth*6 + (x * 6) + (y % 6) - s.pixelBands.Set(uint(bit)) + s.pixelBands.Set(uint(bit)) //nolint:gosec } // GeneratePixels is used to write the pixel data to the internal imageData string builder.