erm/internal/app/darktile/sixel/decoder.go

433 lines
7.5 KiB
Go

package sixel
import (
"fmt"
"image"
"image/color"
"io"
"strconv"
"strings"
)
// See https://vt100.net/docs/vt3xx-gp/chapter14.html for more info.
type decoder struct {
r io.Reader
cursor image.Point
aspectRatio float64 // this is the ratio for vertical:horizontal pixels
bg color.Color
colourMap *ColourMap
currentColour color.Color
size image.Point // does not limit image size, just where bg is drawn!
scratchpad map[int]map[int]color.Color
}
func Decode(reader io.Reader, bg color.Color) (image.Image, error) {
return NewDecoder(reader, bg).Decode()
}
func NewDecoder(reader io.Reader, bg color.Color) *decoder {
return &decoder{
r: reader,
aspectRatio: 2,
bg: bg,
colourMap: NewColourMap(),
scratchpad: make(map[int]map[int]color.Color),
}
}
func (d *decoder) Decode() (image.Image, error) {
if err := d.processHeader(); err != nil {
return nil, fmt.Errorf("error reading sixel header: %s", err)
}
if err := d.processBody(); err != nil {
return nil, fmt.Errorf("error reading sixel header: %s", err)
}
return d.draw(), nil
}
func (d *decoder) readByte() (byte, error) {
buf := make([]byte, 1)
if _, err := d.r.Read(buf); err != nil {
return 0, err
}
return buf[0], nil
}
func (d *decoder) readHeader() ([]byte, error) {
var header []byte
for {
chr, err := d.readByte()
if err != nil {
return nil, err
}
if chr == 'q' {
break
}
header = append(header, chr)
}
return header, nil
}
func (d *decoder) processHeader() error {
data, err := d.readHeader()
if err != nil {
return err
}
header := string(data)
if len(header) == 0 {
return nil
}
params := strings.Split(header, ";")
switch params[1] {
case "0", "1", "5", "6", "":
d.aspectRatio = 2
case "2":
d.aspectRatio = 5
case "3", "4":
d.aspectRatio = 3
case "7", "8", "9":
d.aspectRatio = 1
default:
return fmt.Errorf("invalid P1 in sixel header")
}
if len(params) == 1 {
return nil
}
switch params[1] {
case "0", "2", "":
// use the configured terminal background colour
case "1":
d.bg = color.RGBA{A: 0} // transparent bg
}
// NOTE: we currently ignore P3 if it is specified
if len(params) > 3 {
return fmt.Errorf("unexpected extra parameters in sixel header")
}
return nil
}
func (d *decoder) processBody() error {
for {
byt, err := d.readByte()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if err := d.processChar(byt); err != nil {
return err
}
}
}
func (d *decoder) handleRepeat() error {
var countStr string
for {
byt, err := d.readByte()
if err != nil {
return err
}
switch true {
case byt >= '0' && byt <= '9':
countStr += string(byt)
default:
count, err := strconv.Atoi(countStr)
if err != nil {
return fmt.Errorf("invalid count in sixel repeat sequence: %s: %s", countStr, err)
}
for i := 0; i < count; i++ {
if err := d.processDataChar(byt); err != nil {
return err
}
}
return nil
}
}
}
func (d *decoder) handleRasterAttributes() error {
var arg string
var args []string
for {
b, err := d.readByte()
if err != nil {
return err
}
switch true {
case b >= '0' && b <= '9':
arg += string(b)
case b == ';':
args = append(args, arg)
arg = ""
default:
args = append(args, arg)
if err := d.setRaster(args); err != nil {
return err
}
return d.processChar(b)
}
}
}
func (d *decoder) setRaster(args []string) error {
if len(args) != 4 {
return fmt.Errorf("invalid raster command: %s", strings.Join(args, ";"))
}
pan, err := strconv.Atoi(args[0])
if err != nil {
return err
}
pad, err := strconv.Atoi(args[1])
if err != nil {
return err
}
d.aspectRatio = float64(pan) / float64(pad)
ph, err := strconv.Atoi(args[2])
if err != nil {
return err
}
pv, err := strconv.Atoi(args[3])
if err != nil {
return err
}
d.size = image.Point{X: ph, Y: pv}
return nil
}
func (d *decoder) handleColour() error {
var arg string
var args []string
for {
b, err := d.readByte()
if err != nil {
return err
}
switch true {
case b >= '0' && b <= '9':
arg += string(b)
case b == ';':
args = append(args, arg)
arg = ""
default:
args = append(args, arg)
if err := d.setColour(args); err != nil {
return err
}
return d.processChar(b)
}
}
}
func (d *decoder) setColour(args []string) error {
if len(args) == 0 {
return fmt.Errorf("invalid colour string - missing identifier")
}
colourID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid colour id: %s", args[0])
}
if len(args) == 1 {
d.currentColour = d.colourMap.GetColour(uint8(colourID))
return nil
}
if len(args) != 5 {
return fmt.Errorf("invalid colour introduction command - wrong number of args (%d): %s", len(args), strings.Join(args, ";"))
}
x, err := strconv.Atoi(args[2])
if err != nil {
return fmt.Errorf("invalid colour value")
}
y, err := strconv.Atoi(args[3])
if err != nil {
return fmt.Errorf("invalid colour value")
}
z, err := strconv.Atoi(args[4])
if err != nil {
return fmt.Errorf("invalid colour value")
}
var colour color.Color
switch args[1] {
case "1":
colour = colourFromHSL(x, z, y)
case "2":
colour = color.RGBA{
R: uint8((x * 255) / 100),
G: uint8((y * 255) / 100),
B: uint8((z * 255) / 100),
A: 0xff,
}
default:
return fmt.Errorf("invalid colour co-ordinate system '%s'", args[1])
}
d.colourMap.SetColour(uint8(colourID), colour)
d.currentColour = colour
return nil
}
func (d *decoder) processChar(b byte) error {
switch b {
case '!':
return d.handleRepeat()
case '"':
return d.handleRasterAttributes()
case '#':
return d.handleColour()
case '$':
// graphics carriage return
d.cursor.X = 0
return nil
case '-':
// graphics new line
d.cursor.Y += 6
d.cursor.X = 0
return nil
default:
return d.processDataChar(b)
}
}
func (d *decoder) processDataChar(b byte) error {
if b < 0x3f || b > 0x7e {
return fmt.Errorf("invalid sixel data value 0x%02x: outside acceptable range", b)
}
sixel := b - 0x3f
for i := 0; i < 6; i++ {
if sixel&(1<<i) > 0 {
d.set(d.cursor.X, d.cursor.Y+i)
}
}
d.cursor.X++
return nil
}
func hueToRGB(v1, v2, h float64) float64 {
if h < 0 {
h += 1
}
if h > 1 {
h -= 1
}
switch {
case 6*h < 1:
return (v1 + (v2-v1)*6*h)
case 2*h < 1:
return v2
case 3*h < 2:
return v1 + (v2-v1)*((2.0/3.0)-h)*6
}
return v1
}
func colourFromHSL(hi, si, li int) color.Color {
h := float64(hi) / 360
s := float64(si) / 100
l := float64(li) / 100
if s == 0 {
// it's gray
return color.RGBA{uint8(l * 0xff), uint8(l * 0xff), uint8(l * 0xff), 0xff}
}
var v1, v2 float64
if l < 0.5 {
v2 = l * (1 + s)
} else {
v2 = (l + s) - (s * l)
}
v1 = 2*l - v2
r := hueToRGB(v1, v2, h+(1.0/3.0))
g := hueToRGB(v1, v2, h)
b := hueToRGB(v1, v2, h-(1.0/3.0))
return color.RGBA{R: uint8(r * 0xff), G: uint8(g * 0xff), B: uint8(b * 0xff), A: 0xff}
}
func (d *decoder) set(x, y int) {
if x > d.size.X {
d.size.X = x
}
if y > d.size.Y {
d.size.Y = y
}
if _, ok := d.scratchpad[x]; !ok {
d.scratchpad[x] = make(map[int]color.Color)
}
d.scratchpad[x][y] = d.currentColour
}
func (d *decoder) draw() image.Image {
img := image.NewRGBA(image.Rect(0, 0, d.size.X, d.size.Y))
for x := 0; x < d.size.X; x++ {
for y := 0; y < d.size.Y; y++ {
c := d.bg
if col, ok := d.scratchpad[x]; ok {
if row, ok := col[y]; ok {
c = row
}
}
img.Set(x, y, c)
}
}
return img
}