1//       ___  _____  ____
  2//      / _ \/  _/ |/_/ /____ ______ _
  3//     / ___// /_>  </ __/ -_) __/  ' \
  4//    /_/  /___/_/|_|\__/\__/_/ /_/_/_/
  5//
  6//    Copyright 2017 Eliuk Blau
  7//
  8//    This Source Code Form is subject to the terms of the Mozilla Public
  9//    License, v. 2.0. If a copy of the MPL was not distributed with this
 10//    file, You can obtain one at http://mozilla.org/MPL/2.0/.
 11
 12package ansimage
 13
 14import (
 15	"errors"
 16	"fmt"
 17	"image"
 18	"image/color"
 19	"image/draw"
 20	_ "image/gif" // initialize decoder
 21
 22	_ "image/jpeg" // initialize decoder
 23	_ "image/png"  // initialize decoder
 24	"io"
 25	"net/http"
 26	"os"
 27	"strings"
 28
 29	"github.com/disintegration/imaging"
 30	"github.com/lucasb-eyer/go-colorful"
 31	_ "golang.org/x/image/bmp"  // initialize decoder
 32	_ "golang.org/x/image/tiff" // initialize decoder
 33	_ "golang.org/x/image/webp" // initialize decoder
 34)
 35
 36// Unicode Block Element character used to represent lower pixel in terminal row.
 37// INFO: http://en.wikipedia.org/wiki/Block_Elements
 38const lowerHalfBlock = "\u2584"
 39
 40// Unicode Block Element characters used to represent dithering in terminal row.
 41// INFO: http://en.wikipedia.org/wiki/Block_Elements
 42const fullBlock = "\u2588"
 43const darkShadeBlock = "\u2593"
 44const mediumShadeBlock = "\u2592"
 45const lightShadeBlock = "\u2591"
 46
 47// ANSImage scale modes:
 48// resize (full scaled to area),
 49// fill (resize and crop the image with a center anchor point to fill area),
 50// fit (resize the image to fit area, preserving the aspect ratio).
 51const (
 52	ScaleModeResize = ScaleMode(iota)
 53	ScaleModeFill
 54	ScaleModeFit
 55)
 56
 57// ANSImage dithering modes:
 58// no dithering (classic mode: half block based),
 59// chars (use characters to represent brightness),
 60// blocks (use character blocks to represent brightness).
 61const (
 62	NoDithering = DitheringMode(iota)
 63	DitheringWithBlocks
 64	DitheringWithChars
 65)
 66
 67// ANSImage block size in pixels (dithering mode)
 68const (
 69	BlockSizeY = 8
 70	BlockSizeX = 4
 71)
 72
 73var (
 74	// ErrImageDownloadFailed occurs in the attempt to download an image and the status code of the response is not "200 OK".
 75	ErrImageDownloadFailed = errors.New("ANSImage: image download failed")
 76
 77	// ErrHeightNonMoT occurs when ANSImage height is not a Multiple of Two value.
 78	ErrHeightNonMoT = errors.New("ANSImage: height must be a Multiple of Two value")
 79
 80	// ErrInvalidBoundsMoT occurs when ANSImage height or width are invalid values (Multiple of Two).
 81	ErrInvalidBoundsMoT = errors.New("ANSImage: height or width must be >=2")
 82
 83	// ErrOutOfBounds occurs when ANSI-pixel coordinates are out of ANSImage bounds.
 84	ErrOutOfBounds = errors.New("ANSImage: out of bounds")
 85
 86	// errUnknownScaleMode occurs when scale mode is invalid.
 87	errUnknownScaleMode = errors.New("ANSImage: unknown scale mode")
 88
 89	// errUnknownDitheringMode occurs when dithering mode is invalid.
 90	errUnknownDitheringMode = errors.New("ANSImage: unknown dithering mode")
 91)
 92
 93// ScaleMode type is used for image scale mode constants.
 94type ScaleMode uint8
 95
 96// DitheringMode type is used for image scale dithering mode constants.
 97type DitheringMode uint8
 98
 99// ANSIpixel represents a pixel of an ANSImage.
100type ANSIpixel struct {
101	Brightness uint8
102	R, G, B    uint8
103	upper      bool
104	source     *ANSImage
105}
106
107// ANSImage represents an image encoded in ANSI escape codes.
108type ANSImage struct {
109	h, w      int
110	maxprocs  int
111	bgR       uint8
112	bgG       uint8
113	bgB       uint8
114	dithering DitheringMode
115	pixmap    [][]*ANSIpixel
116}
117
118// Render returns the ANSI-compatible string form of ANSI-pixel.
119func (ap *ANSIpixel) Render() string {
120	// WITHOUT DITHERING
121	if ap.source.dithering == NoDithering {
122		if ap.upper {
123			return fmt.Sprintf(
124				"\033[48;2;%d;%d;%dm",
125				ap.R, ap.G, ap.B,
126			)
127		}
128		return fmt.Sprintf(
129			"\033[38;2;%d;%d;%dm%s",
130			ap.R, ap.G, ap.B,
131			lowerHalfBlock,
132		)
133	}
134
135	// WITH DITHERING
136	block := " "
137	if ap.source.dithering == DitheringWithBlocks {
138		switch bri := ap.Brightness; {
139		case bri > 204:
140			block = fullBlock
141		case bri > 152:
142			block = darkShadeBlock
143		case bri > 100:
144			block = mediumShadeBlock
145		case bri > 48:
146			block = lightShadeBlock
147		}
148	} else if ap.source.dithering == DitheringWithChars {
149		switch bri := ap.Brightness; {
150		case bri > 230:
151			block = "#"
152		case bri > 207:
153			block = "&"
154		case bri > 184:
155			block = "$"
156		case bri > 161:
157			block = "X"
158		case bri > 138:
159			block = "x"
160		case bri > 115:
161			block = "="
162		case bri > 92:
163			block = "+"
164		case bri > 69:
165			block = ";"
166		case bri > 46:
167			block = ":"
168		case bri > 23:
169			block = "."
170		}
171	} else {
172		panic(errUnknownDitheringMode)
173	}
174
175	return fmt.Sprintf(
176		"\033[48;2;%d;%d;%dm\033[38;2;%d;%d;%dm%s",
177		ap.source.bgR, ap.source.bgG, ap.source.bgB,
178		ap.R, ap.G, ap.B,
179		block,
180	)
181}
182
183// Height gets total rows of ANSImage.
184func (ai *ANSImage) Height() int {
185	return ai.h
186}
187
188// Width gets total columns of ANSImage.
189func (ai *ANSImage) Width() int {
190	return ai.w
191}
192
193// DitheringMode gets the dithering mode of ANSImage.
194func (ai *ANSImage) DitheringMode() DitheringMode {
195	return ai.dithering
196}
197
198// SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage
199// (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect).
200func (ai *ANSImage) SetMaxProcs(max int) {
201	ai.maxprocs = max
202}
203
204// GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage.
205func (ai *ANSImage) GetMaxProcs() int {
206	return ai.maxprocs
207}
208
209// SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x).
210func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error {
211	if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
212		ai.pixmap[y][x].R = r
213		ai.pixmap[y][x].G = g
214		ai.pixmap[y][x].B = b
215		ai.pixmap[y][x].Brightness = brightness
216		ai.pixmap[y][x].upper = ((ai.dithering == NoDithering) && (y%2 == 0))
217		return nil
218	}
219	return ErrOutOfBounds
220}
221
222// GetAt gets ANSI-pixel in coordinates (y,x).
223func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) {
224	if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
225		return &ANSIpixel{
226				R:          ai.pixmap[y][x].R,
227				G:          ai.pixmap[y][x].G,
228				B:          ai.pixmap[y][x].B,
229				Brightness: ai.pixmap[y][x].Brightness,
230				upper:      ai.pixmap[y][x].upper,
231				source:     ai.pixmap[y][x].source,
232			},
233			nil
234	}
235	return nil, ErrOutOfBounds
236}
237
238// Render returns the ANSI-compatible string form of ANSImage.
239// (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728)
240func (ai *ANSImage) Render() string {
241	type renderData struct {
242		row    int
243		render string
244	}
245
246	// WITHOUT DITHERING
247	if ai.dithering == NoDithering {
248		rows := make([]string, ai.h/2)
249		for y := 0; y < ai.h; y += ai.maxprocs {
250			ch := make(chan renderData, ai.maxprocs)
251			for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 {
252				go func(r, y int) {
253					var str string
254					for x := 0; x < ai.w; x++ {
255						str += ai.pixmap[y][x].Render()   // upper pixel
256						str += ai.pixmap[y+1][x].Render() // lower pixel
257					}
258					str += "\033[0m\n" // reset ansi style
259					ch <- renderData{row: r, render: str}
260				}(r, 2*r)
261				// DEBUG:
262				// fmt.Printf("y:%d | n:%d | r:%d | 2*r:%d\n", y, n, r, 2*r)
263				// time.Sleep(time.Millisecond * 100)
264			}
265			for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 {
266				data := <-ch
267				rows[data.row] = data.render
268				// DEBUG:
269				// fmt.Printf("data.row:%d\n", data.row)
270				// time.Sleep(time.Millisecond * 100)
271			}
272		}
273		return strings.Join(rows, "")
274	}
275
276	// WITH DITHERING
277	rows := make([]string, ai.h)
278	for y := 0; y < ai.h; y += ai.maxprocs {
279		ch := make(chan renderData, ai.maxprocs)
280		for n, r := 0, y; (n <= ai.maxprocs) && (r+1 < ai.h); n, r = n+1, y+n+1 {
281			go func(y int) {
282				var str string
283				for x := 0; x < ai.w; x++ {
284					str += ai.pixmap[y][x].Render()
285				}
286				str += "\033[0m\n" // reset ansi style
287				ch <- renderData{row: y, render: str}
288			}(r)
289		}
290		for n, r := 0, y; (n <= ai.maxprocs) && (r+1 < ai.h); n, r = n+1, y+n+1 {
291			data := <-ch
292			rows[data.row] = data.render
293		}
294	}
295	return strings.Join(rows, "")
296}
297
298// Draw writes the ANSImage to standard output (terminal).
299func (ai *ANSImage) Draw() {
300	fmt.Print(ai.Render())
301}
302
303// New creates a new empty ANSImage ready to draw on it.
304func New(h, w int, bg color.Color, dm DitheringMode) (*ANSImage, error) {
305	if (dm == NoDithering) && (h%2 != 0) {
306		return nil, ErrHeightNonMoT
307	}
308
309	if h < 2 || w < 2 {
310		return nil, ErrInvalidBoundsMoT
311	}
312
313	r, g, b, _ := bg.RGBA()
314	ansimage := &ANSImage{
315		h: h, w: w,
316		maxprocs:  1,
317		bgR:       uint8(r),
318		bgG:       uint8(g),
319		bgB:       uint8(b),
320		dithering: dm,
321		pixmap:    nil,
322	}
323
324	ansimage.pixmap = func() [][]*ANSIpixel {
325		v := make([][]*ANSIpixel, h)
326		for y := 0; y < h; y++ {
327			v[y] = make([]*ANSIpixel, w)
328			for x := 0; x < w; x++ {
329				v[y][x] = &ANSIpixel{
330					R:          0,
331					G:          0,
332					B:          0,
333					Brightness: 0,
334					source:     ansimage,
335					upper:      ((dm == NoDithering) && (y%2 == 0)),
336				}
337			}
338		}
339		return v
340	}()
341
342	return ansimage, nil
343}
344
345// NewFromReader creates a new ANSImage from an io.Reader.
346// Background color is used to fill when image has transparency or dithering mode is enabled.
347// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
348func NewFromReader(reader io.Reader, bg color.Color, dm DitheringMode) (*ANSImage, error) {
349	image, _, err := image.Decode(reader)
350	if err != nil {
351		return nil, err
352	}
353
354	return createANSImage(image, bg, dm)
355}
356
357// NewScaledFromReader creates a new scaled ANSImage from an io.Reader.
358// Background color is used to fill when image has transparency or dithering mode is enabled.
359// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
360func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) {
361	image, _, err := image.Decode(reader)
362	if err != nil {
363		return nil, err
364	}
365
366	switch sm {
367	case ScaleModeResize:
368		image = imaging.Resize(image, x, y, imaging.Lanczos)
369	case ScaleModeFill:
370		image = imaging.Fill(image, x, y, imaging.Center, imaging.Lanczos)
371	case ScaleModeFit:
372		image = imaging.Fit(image, x, y, imaging.Lanczos)
373	default:
374		panic(errUnknownScaleMode)
375	}
376
377	return createANSImage(image, bg, dm)
378}
379
380// NewFromFile creates a new ANSImage from a file.
381// Background color is used to fill when image has transparency or dithering mode is enabled.
382// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
383func NewFromFile(name string, bg color.Color, dm DitheringMode) (*ANSImage, error) {
384	reader, err := os.Open(name)
385	if err != nil {
386		return nil, err
387	}
388	defer reader.Close()
389	return NewFromReader(reader, bg, dm)
390}
391
392// NewFromURL creates a new ANSImage from an image URL.
393// Background color is used to fill when image has transparency or dithering mode is enabled.
394// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
395func NewFromURL(url string, bg color.Color, dm DitheringMode) (*ANSImage, error) {
396	res, err := http.Get(url)
397	if err != nil {
398		return nil, err
399	}
400	if res.StatusCode != http.StatusOK {
401		return nil, ErrImageDownloadFailed
402	}
403	defer res.Body.Close()
404	return NewFromReader(res.Body, bg, dm)
405}
406
407// NewScaledFromFile creates a new scaled ANSImage from a file.
408// Background color is used to fill when image has transparency or dithering mode is enabled.
409// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
410func NewScaledFromFile(name string, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) {
411	reader, err := os.Open(name)
412	if err != nil {
413		return nil, err
414	}
415	defer reader.Close()
416	return NewScaledFromReader(reader, y, x, bg, sm, dm)
417}
418
419// NewScaledFromURL creates a new scaled ANSImage from an image URL.
420// Background color is used to fill when image has transparency or dithering mode is enabled.
421// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
422func NewScaledFromURL(url string, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) {
423	res, err := http.Get(url)
424	if err != nil {
425		return nil, err
426	}
427	if res.StatusCode != http.StatusOK {
428		return nil, ErrImageDownloadFailed
429	}
430	defer res.Body.Close()
431	return NewScaledFromReader(res.Body, y, x, bg, sm, dm)
432}
433
434// ClearTerminal clears current terminal buffer using ANSI escape code.
435// (Nice info for ANSI escape codes - http://unix.stackexchange.com/questions/124762/how-does-clear-command-work)
436func ClearTerminal() {
437	fmt.Print("\033[H\033[2J")
438}
439
440// createANSImage loads data from an image and returns an ANSImage.
441// Background color is used to fill when image has transparency or dithering mode is enabled.
442// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
443func createANSImage(img image.Image, bg color.Color, dm DitheringMode) (*ANSImage, error) {
444	var rgbaOut *image.RGBA
445	bounds := img.Bounds()
446
447	// do compositing only if background color has no transparency (thank you @disq for the idea!)
448	// (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image)
449	if _, _, _, a := bg.RGBA(); a >= 0xffff {
450		rgbaOut = image.NewRGBA(bounds)
451		draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src)
452		draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over)
453	} else {
454		if v, ok := img.(*image.RGBA); ok {
455			rgbaOut = v
456		} else {
457			rgbaOut = image.NewRGBA(bounds)
458			draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src)
459		}
460	}
461
462	yMin, xMin := bounds.Min.Y, bounds.Min.X
463	yMax, xMax := bounds.Max.Y, bounds.Max.X
464
465	if dm == NoDithering {
466		// always sets an even number of ANSIPixel rows...
467		yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering
468	} else {
469		yMax = yMax / BlockSizeY // always sets 1 ANSIPixel block...
470		xMax = xMax / BlockSizeX // per 8x4 real pixels --> with dithering
471	}
472
473	ansimage, err := New(yMax, xMax, bg, dm)
474	if err != nil {
475		return nil, err
476	}
477
478	if dm == NoDithering {
479		for y := yMin; y < yMax; y++ {
480			for x := xMin; x < xMax; x++ {
481				v := rgbaOut.RGBAAt(x, y)
482				if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil {
483					return nil, err
484				}
485			}
486		}
487	} else {
488		pixelCount := BlockSizeY * BlockSizeX
489
490		for y := yMin; y < yMax; y++ {
491			for x := xMin; x < xMax; x++ {
492
493				var sumR, sumG, sumB, sumBri float64
494				for dy := 0; dy < BlockSizeY; dy++ {
495					py := BlockSizeY*y + dy
496
497					for dx := 0; dx < BlockSizeX; dx++ {
498						px := BlockSizeX*x + dx
499
500						pixel := rgbaOut.At(px, py)
501						_, _, v := colorful.MakeColor(pixel).Hsv()
502						color := colorful.MakeColor(pixel)
503						sumR += color.R
504						sumG += color.G
505						sumB += color.B
506						sumBri += v
507					}
508				}
509
510				r := uint8(sumR/float64(pixelCount)*255.0 + 0.5)
511				g := uint8(sumG/float64(pixelCount)*255.0 + 0.5)
512				b := uint8(sumB/float64(pixelCount)*255.0 + 0.5)
513				brightness := uint8(sumBri/float64(pixelCount)*255.0 + 0.5)
514
515				if err := ansimage.SetAt(y, x, r, g, b, brightness); err != nil {
516					return nil, err
517				}
518			}
519		}
520	}
521
522	return ansimage, nil
523}