From ff7ee333a1028850337aa332532cbc23c7bdde14 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 11 Apr 2018 17:57:15 +0300 Subject: Rename UIString to TString, move ansimage to lib/ and switch to tcell fork --- lib/ansimage/ansimage.go | 287 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 lib/ansimage/ansimage.go (limited to 'lib/ansimage/ansimage.go') diff --git a/lib/ansimage/ansimage.go b/lib/ansimage/ansimage.go new file mode 100644 index 0000000..a41c833 --- /dev/null +++ b/lib/ansimage/ansimage.go @@ -0,0 +1,287 @@ +// ___ _____ ____ +// / _ \/ _/ |/_/ /____ ______ _ +// / ___// /_> =2") + + // ErrOutOfBounds happens when ANSI-pixel coordinates are out of ANSImage bounds. + ErrOutOfBounds = errors.New("ANSImage: out of bounds") +) + +// ANSIpixel represents a pixel of an ANSImage. +type ANSIpixel struct { + Brightness uint8 + R, G, B uint8 + upper bool + source *ANSImage +} + +// ANSImage represents an image encoded in ANSI escape codes. +type ANSImage struct { + h, w int + maxprocs int + bgR uint8 + bgG uint8 + bgB uint8 + pixmap [][]*ANSIpixel +} + +func (ai *ANSImage) Pixmap() [][]*ANSIpixel { + return ai.pixmap +} + +// Height gets total rows of ANSImage. +func (ai *ANSImage) Height() int { + return ai.h +} + +// Width gets total columns of ANSImage. +func (ai *ANSImage) Width() int { + return ai.w +} + +// SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage +// (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect). +func (ai *ANSImage) SetMaxProcs(max int) { + ai.maxprocs = max +} + +// GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage. +func (ai *ANSImage) GetMaxProcs() int { + return ai.maxprocs +} + +// SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x). +func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error { + if y >= 0 && y < ai.h && x >= 0 && x < ai.w { + ai.pixmap[y][x].R = r + ai.pixmap[y][x].G = g + ai.pixmap[y][x].B = b + ai.pixmap[y][x].Brightness = brightness + ai.pixmap[y][x].upper = y%2 == 0 + return nil + } + return ErrOutOfBounds +} + +// GetAt gets ANSI-pixel in coordinates (y,x). +func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) { + if y >= 0 && y < ai.h && x >= 0 && x < ai.w { + return &ANSIpixel{ + R: ai.pixmap[y][x].R, + G: ai.pixmap[y][x].G, + B: ai.pixmap[y][x].B, + Brightness: ai.pixmap[y][x].Brightness, + upper: ai.pixmap[y][x].upper, + source: ai.pixmap[y][x].source, + }, + nil + } + return nil, ErrOutOfBounds +} + +// Render returns the ANSI-compatible string form of ANSImage. +// (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728) +func (ai *ANSImage) Render() []tstring.TString { + type renderData struct { + row int + render tstring.TString + } + + rows := make([]tstring.TString, ai.h/2) + for y := 0; y < ai.h; y += ai.maxprocs { + ch := make(chan renderData, ai.maxprocs) + for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { + go func(r, y int) { + str := make(tstring.TString, ai.w) + for x := 0; x < ai.w; x++ { + topPixel := ai.pixmap[y][x] + topColor := tcell.NewRGBColor(int32(topPixel.R), int32(topPixel.G), int32(topPixel.B)) + + bottomPixel := ai.pixmap[y+1][x] + bottomColor := tcell.NewRGBColor(int32(bottomPixel.R), int32(bottomPixel.G), int32(bottomPixel.B)) + + str[x] = tstring.Cell{ + Char: '▄', + Style: tcell.StyleDefault.Background(topColor).Foreground(bottomColor), + } + } + ch <- renderData{row: r, render: str} + }(r, 2*r) + } + for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { + data := <-ch + rows[data.row] = data.render + } + } + return rows +} + +// New creates a new empty ANSImage ready to draw on it. +func New(h, w int, bg color.Color) (*ANSImage, error) { + if h%2 != 0 { + return nil, ErrHeightNonMoT + } + + if h < 2 || w < 2 { + return nil, ErrInvalidBoundsMoT + } + + r, g, b, _ := bg.RGBA() + ansimage := &ANSImage{ + h: h, w: w, + maxprocs: 1, + bgR: uint8(r), + bgG: uint8(g), + bgB: uint8(b), + pixmap: nil, + } + + ansimage.pixmap = func() [][]*ANSIpixel { + v := make([][]*ANSIpixel, h) + for y := 0; y < h; y++ { + v[y] = make([]*ANSIpixel, w) + for x := 0; x < w; x++ { + v[y][x] = &ANSIpixel{ + R: 0, + G: 0, + B: 0, + Brightness: 0, + source: ansimage, + upper: y%2 == 0, + } + } + } + return v + }() + + return ansimage, nil +} + +// NewFromReader creates a new ANSImage from an io.Reader. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewFromReader(reader io.Reader, bg color.Color) (*ANSImage, error) { + img, _, err := image.Decode(reader) + if err != nil { + return nil, err + } + + return createANSImage(img, bg) +} + +// NewScaledFromReader creates a new scaled ANSImage from an io.Reader. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color) (*ANSImage, error) { + img, _, err := image.Decode(reader) + if err != nil { + return nil, err + } + + img = imaging.Resize(img, x, y, imaging.Lanczos) + + return createANSImage(img, bg) +} + +// NewFromFile creates a new ANSImage from a file. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewFromFile(name string, bg color.Color) (*ANSImage, error) { + reader, err := os.Open(name) + if err != nil { + return nil, err + } + defer reader.Close() + return NewFromReader(reader, bg) +} + +// NewScaledFromFile creates a new scaled ANSImage from a file. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewScaledFromFile(name string, y, x int, bg color.Color) (*ANSImage, error) { + reader, err := os.Open(name) + if err != nil { + return nil, err + } + defer reader.Close() + return NewScaledFromReader(reader, y, x, bg) +} + +// createANSImage loads data from an image and returns an ANSImage. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func createANSImage(img image.Image, bg color.Color) (*ANSImage, error) { + var rgbaOut *image.RGBA + bounds := img.Bounds() + + // do compositing only if background color has no transparency (thank you @disq for the idea!) + // (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image) + if _, _, _, a := bg.RGBA(); a >= 0xffff { + rgbaOut = image.NewRGBA(bounds) + draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src) + draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over) + } else { + if v, ok := img.(*image.RGBA); ok { + rgbaOut = v + } else { + rgbaOut = image.NewRGBA(bounds) + draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src) + } + } + + yMin, xMin := bounds.Min.Y, bounds.Min.X + yMax, xMax := bounds.Max.Y, bounds.Max.X + + // always sets an even number of ANSIPixel rows... + yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering + + ansimage, err := New(yMax, xMax, bg) + if err != nil { + return nil, err + } + + for y := yMin; y < yMax; y++ { + for x := xMin; x < xMax; x++ { + v := rgbaOut.RGBAAt(x, y) + if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil { + return nil, err + } + } + } + + return ansimage, nil +} -- cgit v1.2.3