~dricottone/image2ascii

4ad980ef244f080f793177b991b50fa6460c1eb8 — qeesung 6 years ago 9e6742d
Refactor the code to match the oop mode for easy mock and test
M ascii/ascii.go => ascii/ascii.go +20 -9
@@ 10,35 10,46 @@ import (
	"reflect"
)

func NewPixelConverter() PixelConverter {
	return PixelASCIIConverter{}
}

type PixelConverter interface {
	ConvertPixelToASCII(pixel color.Color, options *Options) string
}

type PixelASCIIConverter struct {
}

// ConvertPixelToASCII converts a pixel to a ASCII char string
func ConvertPixelToASCII(pixel color.Color, options *Options) string {
func (converter PixelASCIIConverter) ConvertPixelToASCII(pixel color.Color, options *Options) string {
	convertOptions := NewOptions()
	convertOptions.mergeOptions(options)

	if convertOptions.Reversed {
		convertOptions.Pixels = reverse(convertOptions.Pixels)
		convertOptions.Pixels = converter.reverse(convertOptions.Pixels)
	}

	r := reflect.ValueOf(pixel).FieldByName("R").Uint()
	g := reflect.ValueOf(pixel).FieldByName("G").Uint()
	b := reflect.ValueOf(pixel).FieldByName("B").Uint()
	a := reflect.ValueOf(pixel).FieldByName("A").Uint()
	value := intensity(r, g, b, a)
	value := converter.intensity(r, g, b, a)

	// Choose the char
	precision := float64(255 * 3 / (len(convertOptions.Pixels) - 1))
	rawChar := convertOptions.Pixels[roundValue(float64(value)/precision)]
	rawChar := convertOptions.Pixels[converter.roundValue(float64(value)/precision)]
	if convertOptions.Colored {
		return decorateWithColor(r, g, b, rawChar)
		return converter.decorateWithColor(r, g, b, rawChar)
	}
	return string([]byte{rawChar})
}

func roundValue(value float64) int {
func (converter PixelASCIIConverter) roundValue(value float64) int {
	return int(math.Floor(value + 0.5))
}

func reverse(numbers []byte) []byte {
func (converter PixelASCIIConverter) reverse(numbers []byte) []byte {
	for i := 0; i < len(numbers)/2; i++ {
		j := len(numbers) - i - 1
		numbers[i], numbers[j] = numbers[j], numbers[i]


@@ 46,12 57,12 @@ func reverse(numbers []byte) []byte {
	return numbers
}

func intensity(r, g, b, a uint64) uint64 {
func (converter PixelASCIIConverter) intensity(r, g, b, a uint64) uint64 {
	return (r + g + b) * a / 255
}

// decorateWithColor decorate the raw char with the color base on r,g,b value
func decorateWithColor(r, g, b uint64, rawChar byte) string {
func (converter PixelASCIIConverter) decorateWithColor(r, g, b uint64, rawChar byte) string {
	coloredChar := rgbterm.FgString(string([]byte{rawChar}), uint8(r), uint8(g), uint8(b))
	return coloredChar
}

M ascii/ascii_test.go => ascii/ascii_test.go +14 -9
@@ 30,6 30,7 @@ func TestMergeOptions(t *testing.T) {

// TestConvertPixelToASCIIWhiteColor convert a white image pixel to ascii string
func TestConvertPixelToASCIIWhiteColor(t *testing.T) {
	converter := NewPixelConverter()
	assertions := assert.New(t)
	r, g, b, a := uint8(255), uint8(255), uint8(255), uint8(255)
	pixel := color.RGBA{


@@ 41,14 42,14 @@ func TestConvertPixelToASCIIWhiteColor(t *testing.T) {

	defaultOptions := NewOptions()
	defaultOptions.Colored = false
	convertedChar := ConvertPixelToASCII(pixel, &defaultOptions)
	convertedChar := converter.ConvertPixelToASCII(pixel, &defaultOptions)
	lastPixelChar := defaultOptions.Pixels[len(defaultOptions.Pixels)-1]
	assertions.Equal(convertedChar, string([]byte{lastPixelChar}),
		fmt.Sprintf("White color chould be converted to %s", string([]byte{lastPixelChar})))

	defaultOptions.Colored = false
	defaultOptions.Reversed = true
	convertedChar = ConvertPixelToASCII(pixel, &defaultOptions)
	convertedChar = converter.ConvertPixelToASCII(pixel, &defaultOptions)
	firstPixelChar := defaultOptions.Pixels[0]
	assertions.Equal(convertedChar, string([]byte{firstPixelChar}),
		fmt.Sprintf("Reversed white color chould be converted to %s", string([]byte{firstPixelChar})))


@@ 56,6 57,7 @@ func TestConvertPixelToASCIIWhiteColor(t *testing.T) {

// TestConvertPixelToASCIIBlackColor convert a white image pixel to ascii string
func TestConvertPixelToASCIIBlackColor(t *testing.T) {
	converter := NewPixelConverter()
	assertions := assert.New(t)
	r, g, b, a := uint8(0), uint8(0), uint8(0), uint8(0)
	pixel := color.RGBA{


@@ 67,20 69,21 @@ func TestConvertPixelToASCIIBlackColor(t *testing.T) {

	defaultOptions := NewOptions()
	defaultOptions.Colored = false
	convertedChar := ConvertPixelToASCII(pixel, &defaultOptions)
	convertedChar := converter.ConvertPixelToASCII(pixel, &defaultOptions)
	firstPixelChar := defaultOptions.Pixels[0]
	assertions.Equal(convertedChar, string([]byte{firstPixelChar}),
		fmt.Sprintf("Black color chould be converted to %s", string([]byte{firstPixelChar})))

	defaultOptions.Colored = false
	defaultOptions.Reversed = true
	convertedChar = ConvertPixelToASCII(pixel, &defaultOptions)
	convertedChar = converter.ConvertPixelToASCII(pixel, &defaultOptions)
	lastPixelChar := defaultOptions.Pixels[len(defaultOptions.Pixels)-1]
	assertions.Equal(convertedChar, string([]byte{lastPixelChar}),
		fmt.Sprintf("Reversed Black color chould be converted to %s", string([]byte{lastPixelChar})))
}

func TestColoredASCIIChar(t *testing.T) {
	converter := NewPixelConverter()
	assertions := assert.New(t)
	r, g, b, a := uint8(123), uint8(123), uint8(123), uint8(255)
	pixel := color.RGBA{


@@ 91,20 94,21 @@ func TestColoredASCIIChar(t *testing.T) {
	}
	defaultOptions := NewOptions()
	defaultOptions.Colored = true
	coloredChar := ConvertPixelToASCII(pixel, &defaultOptions)
	coloredChar := converter.ConvertPixelToASCII(pixel, &defaultOptions)
	assertions.True(len(coloredChar) > 1)
}

// TestReverseSlice test reverse a slice
func TestReverseSlice(t *testing.T) {
	converter := PixelASCIIConverter{}
	s := []byte{1, 2, 3, 4, 5}
	reversedSlice := reverse(s)
	reversedSlice := converter.reverse(s)
	expectedReversedSlice := []byte{5, 4, 3, 2, 1}
	assert.True(t, reflect.DeepEqual(reversedSlice, expectedReversedSlice),
		fmt.Sprintf("%+v reversed should equal to %+v", s, expectedReversedSlice))

	s = []byte{1, 2, 3, 4}
	reversedSlice = reverse(s)
	reversedSlice = converter.reverse(s)
	expectedReversedSlice = []byte{4, 3, 2, 1}
	assert.True(t, reflect.DeepEqual(reversedSlice, expectedReversedSlice),
		fmt.Sprintf("%+v reversed should equal to %+v", s, expectedReversedSlice))


@@ 113,6 117,7 @@ func TestReverseSlice(t *testing.T) {

// ExampleConvertPixelToASCII is a example convert pixel to ascii char
func ExampleConvertPixelToASCII() {
	converter := NewPixelConverter()
	// Create the pixel
	r, g, b, a := uint8(255), uint8(255), uint8(255), uint8(255)
	pixel := color.RGBA{


@@ 125,7 130,7 @@ func ExampleConvertPixelToASCII() {
	// Create the convert options
	defaultOptions := NewOptions()
	defaultOptions.Colored = false
	convertedChar := ConvertPixelToASCII(pixel, &defaultOptions)
	convertedChar := converter.ConvertPixelToASCII(pixel, &defaultOptions)
	fmt.Println(convertedChar)
	// Output: @
}
\ No newline at end of file
}

M convert/convert.go => convert/convert.go +28 -9
@@ 36,10 36,29 @@ var DefaultOptions = Options{
	StretchedScreen: false,
}

func NewImageConverter() *ImageConverter {
	return &ImageConverter{
		resizeHandler:  NewResizeHandler(),
		pixelConverter: ascii.NewPixelConverter(),
	}
}

type Converter interface {
	Image2ASCIIMatrix(image image.Image, imageConvertOptions *Options) []string
	Image2ASCIIString(image image.Image, options *Options) string
	ImageFile2ASCIIMatrix(imageFilename string, option *Options) []string
	ImageFile2ASCIIString(imageFilename string, option *Options) string
}

type ImageConverter struct {
	resizeHandler  ResizeHandler
	pixelConverter ascii.PixelConverter
}

// Image2ASCIIMatrix converts a image to ASCII matrix
func Image2ASCIIMatrix(image image.Image, imageConvertOptions *Options) []string {
func (converter *ImageConverter) Image2ASCIIMatrix(image image.Image, imageConvertOptions *Options) []string {
	// Resize the convert first
	newImage := ScaleImage(image, imageConvertOptions)
	newImage := converter.resizeHandler.ScaleImage(image, imageConvertOptions)
	sz := newImage.Bounds()
	newWidth := sz.Max.X
	newHeight := sz.Max.Y


@@ 51,7 70,7 @@ func Image2ASCIIMatrix(image image.Image, imageConvertOptions *Options) []string
			pixelConvertOptions := ascii.NewOptions()
			pixelConvertOptions.Colored = imageConvertOptions.Colored
			pixelConvertOptions.Reversed = imageConvertOptions.Reversed
			rawChar := ascii.ConvertPixelToASCII(pixel, &pixelConvertOptions)
			rawChar := converter.pixelConverter.ConvertPixelToASCII(pixel, &pixelConvertOptions)
			rawCharValues = append(rawCharValues, rawChar)
		}
		rawCharValues = append(rawCharValues, "\n")


@@ 60,8 79,8 @@ func Image2ASCIIMatrix(image image.Image, imageConvertOptions *Options) []string
}

// Image2ASCIIString converts a image to ascii matrix, and the join the matrix to a string
func Image2ASCIIString(image image.Image, options *Options) string {
	convertedPixelASCII := Image2ASCIIMatrix(image, options)
func (converter *ImageConverter) Image2ASCIIString(image image.Image, options *Options) string {
	convertedPixelASCII := converter.Image2ASCIIMatrix(image, options)
	var buffer bytes.Buffer

	for i := 0; i < len(convertedPixelASCII); i++ {


@@ 71,21 90,21 @@ func Image2ASCIIString(image image.Image, options *Options) string {
}

// ImageFile2ASCIIMatrix converts a image file to ascii matrix
func ImageFile2ASCIIMatrix(imageFilename string, option *Options) []string {
func (converter *ImageConverter) ImageFile2ASCIIMatrix(imageFilename string, option *Options) []string {
	img, err := OpenImageFile(imageFilename)
	if err != nil {
		log.Fatal("open image failed : " + err.Error())
	}
	return Image2ASCIIMatrix(img, option)
	return converter.Image2ASCIIMatrix(img, option)
}

// ImageFile2ASCIIString converts a image file to ascii string
func ImageFile2ASCIIString(imageFilename string, option *Options) string {
func (converter *ImageConverter) ImageFile2ASCIIString(imageFilename string, option *Options) string {
	img, err := OpenImageFile(imageFilename)
	if err != nil {
		log.Fatal("open image failed : " + err.Error())
	}
	return Image2ASCIIString(img, option)
	return converter.Image2ASCIIString(img, option)
}

// OpenImageFile open a image and return a image object

M convert/convert_test.go => convert/convert_test.go +12 -6
@@ 35,6 35,7 @@ func TestOpenNotExistsFile(t *testing.T) {

// TestImage2ASCIIMatrix test convert a image to ascii matrix
func TestImage2ASCIIMatrix(t *testing.T) {
	converter := NewImageConverter()
	imageTests := []struct {
		imageFilename string
		asciiMatrix   []string


@@ 60,7 61,7 @@ func TestImage2ASCIIMatrix(t *testing.T) {
			convertOptions.FitScreen = false
			convertOptions.Colored = false

			matrix := ImageFile2ASCIIMatrix(tt.imageFilename, &convertOptions)
			matrix := converter.ImageFile2ASCIIMatrix(tt.imageFilename, &convertOptions)
			if !reflect.DeepEqual(matrix, tt.asciiMatrix) {
				t.Errorf("image %s convert expected to %+v, but get %+v",
					tt.imageFilename, tt.asciiMatrix, matrix)


@@ 70,6 71,7 @@ func TestImage2ASCIIMatrix(t *testing.T) {
}

func TestImageFile2ASCIIString(t *testing.T) {
	converter := NewImageConverter()
	imageTests := []struct {
		imageFilename string
		asciiString   string


@@ 85,7 87,7 @@ func TestImageFile2ASCIIString(t *testing.T) {
			convertOptions.FitScreen = false
			convertOptions.Colored = false

			charString := ImageFile2ASCIIString(tt.imageFilename, &convertOptions)
			charString := converter.ImageFile2ASCIIString(tt.imageFilename, &convertOptions)
			if charString != tt.asciiString {
				t.Errorf("image %s convert expected to %+v, but get %+v",
					tt.imageFilename, tt.asciiString, charString)


@@ 95,6 97,7 @@ func TestImageFile2ASCIIString(t *testing.T) {
}

func TestImage2ReversedASCIIString(t *testing.T) {
	converter := NewImageConverter()
	imageTests := []struct {
		imageFilename string
		asciiString   string


@@ 110,7 113,7 @@ func TestImage2ReversedASCIIString(t *testing.T) {
			convertOptions.Colored = false
			convertOptions.Reversed = true

			charString := ImageFile2ASCIIString(tt.imageFilename, &convertOptions)
			charString := converter.ImageFile2ASCIIString(tt.imageFilename, &convertOptions)
			if charString != tt.asciiString {
				t.Errorf("image %s convert expected to %+v, but get %+v",
					tt.imageFilename, tt.asciiString, charString)


@@ 121,6 124,7 @@ func TestImage2ReversedASCIIString(t *testing.T) {

// BenchmarkBigImage2ASCIIMatrix benchmark convert big image to ascii
func BenchmarkBigImage2ASCIIMatrix(b *testing.B) {
	converter := NewImageConverter()
	convertOptions := DefaultOptions
	convertOptions.FitScreen = false
	convertOptions.Colored = false


@@ 128,12 132,13 @@ func BenchmarkBigImage2ASCIIMatrix(b *testing.B) {
	convertOptions.FixedHeight = 200

	for i := 0; i < b.N; i++ {
		_ = ImageFile2ASCIIMatrix("testdata/cat_2000x1500.jpg", &convertOptions)
		_ = converter.ImageFile2ASCIIMatrix("testdata/cat_2000x1500.jpg", &convertOptions)
	}
}

// BenchmarkSmallImage2ASCIIMatrix benchmark convert small image to ascii
func BenchmarkSmallImage2ASCIIMatrix(b *testing.B) {
	converter := NewImageConverter()
	convertOptions := DefaultOptions
	convertOptions.FitScreen = false
	convertOptions.Colored = false


@@ 141,17 146,18 @@ func BenchmarkSmallImage2ASCIIMatrix(b *testing.B) {
	convertOptions.FixedHeight = 200

	for i := 0; i < b.N; i++ {
		_ = ImageFile2ASCIIMatrix("testdata/husky_200x200.jpg", &convertOptions)
		_ = converter.ImageFile2ASCIIMatrix("testdata/husky_200x200.jpg", &convertOptions)
	}
}

// ExampleImage2ASCIIMatrix is example
func ExampleImage2ASCISString() {
	converter := NewImageConverter()
	imageFilename := "testdata/3x3_white.png"
	convertOptions := DefaultOptions
	convertOptions.FitScreen = false
	convertOptions.Colored = false
	asciiString := ImageFile2ASCIIString(imageFilename, &convertOptions)
	asciiString := converter.ImageFile2ASCIIString(imageFilename, &convertOptions)
	fmt.Println(asciiString)
	/* Output:
@@@

M convert/resize.go => convert/resize.go +35 -54
@@ 1,18 1,28 @@
package convert

import (
	"errors"
	"github.com/mattn/go-isatty"
	"github.com/nfnt/resize"
	terminal "github.com/wayneashleyberry/terminal-dimensions"
	"github.com/qeesung/image2ascii/terminal"
	"image"
	"log"
	"os"
	"runtime"
)

func NewResizeHandler() ResizeHandler {
	return &ImageResizeHandler{
		terminal: terminal.NewTerminalAccessor(),
	}
}

type ResizeHandler interface {
	ScaleImage(image image.Image, options *Options) (newImage image.Image)
}

type ImageResizeHandler struct {
	terminal terminal.Terminal
}

// ScaleImage resize the convert to expected size base on the convert options
func ScaleImage(image image.Image, options *Options) (newImage image.Image) {
func (handler *ImageResizeHandler) ScaleImage(image image.Image, options *Options) (newImage image.Image) {
	sz := image.Bounds()
	ratio := options.Ratio
	newHeight := sz.Max.Y


@@ 28,8 38,8 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {

	// use the ratio the scale the image
	if options.FixedHeight == -1 && options.FixedWidth == -1 && ratio != 1 {
		newWidth = ScaleWidthByRatio(float64(sz.Max.X), ratio)
		newHeight = ScaleHeightByRatio(float64(sz.Max.Y), ratio)
		newWidth = handler.ScaleWidthByRatio(float64(sz.Max.X), ratio)
		newHeight = handler.ScaleHeightByRatio(float64(sz.Max.Y), ratio)
	}

	// fit the screen


@@ 37,7 47,7 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {
		options.FixedWidth == -1 &&
		options.FixedHeight == -1 &&
		options.FitScreen {
		fitWidth, fitHeight, err := CalcProportionalFittingScreenSize(image)
		fitWidth, fitHeight, err := handler.CalcProportionalFittingScreenSize(image)
		if err != nil {
			log.Fatal(err)
		}


@@ 51,7 61,7 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {
		options.FixedHeight == -1 &&
		!options.FitScreen &&
		options.StretchedScreen {
		screenWidth, screenHeight, err := getTerminalScreenSize()
		screenWidth, screenHeight, err := handler.terminal.ScreenSize()
		if err != nil {
			log.Fatal(err)
		}


@@ 65,16 75,13 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {

// CalcProportionalFittingScreenSize proportional scale the image
// so that the terminal can just show the picture.
func CalcProportionalFittingScreenSize(image image.Image) (newWidth, newHeight int, err error) {
	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
		return 0, 0,
			errors.New("can not detect the terminal, please disable the '-s fitScreen' option")
func (handler *ImageResizeHandler) CalcProportionalFittingScreenSize(image image.Image) (newWidth, newHeight int, err error) {
	screenWidth, screenHeight, err := handler.terminal.ScreenSize()
	if err != nil {
		log.Fatal(nil)
	}

	screenWidth, _ := terminal.Width()
	screenHeight, _ := terminal.Height()
	sz := image.Bounds()
	newWidth, newHeight = CalcFitSize(
	newWidth, newHeight = handler.CalcFitSize(
		float64(screenWidth),
		float64(screenHeight),
		float64(sz.Max.X),


@@ 88,13 95,13 @@ func CalcProportionalFittingScreenSize(image image.Image) (newWidth, newHeight i
// Either match the width first, then scale the length equally,
// or match the length first, then scale the height equally.
// More detail please check the example
func CalcFitSizeRatio(width, height, imageWidth, imageHeight float64) (ratio float64) {
func (handler *ImageResizeHandler) CalcFitSizeRatio(width, height, imageWidth, imageHeight float64) (ratio float64) {
	ratio = 1.0
	// try to fit the height
	ratio = height / imageHeight
	scaledWidth := imageWidth * ratio / charWidth()
	scaledWidth := imageWidth * ratio / handler.terminal.CharWidth()
	if scaledWidth < width {
		return ratio / charWidth()
		return ratio / handler.terminal.CharWidth()
	}

	// try to fit the width


@@ 105,43 112,17 @@ func CalcFitSizeRatio(width, height, imageWidth, imageHeight float64) (ratio flo
// CalcFitSize through the given length and width ,
// Calculation is able to match the length and width of
// the specified size, and is proportional scaling.
func CalcFitSize(width, height, toBeFitWidth, toBeFitHeight float64) (fitWidth, fitHeight int) {
	ratio := CalcFitSizeRatio(width, height, toBeFitWidth, toBeFitHeight)
	fitWidth = ScaleWidthByRatio(toBeFitWidth, ratio)
	fitHeight = ScaleHeightByRatio(toBeFitHeight, ratio)
func (handler *ImageResizeHandler) CalcFitSize(width, height, toBeFitWidth, toBeFitHeight float64) (fitWidth, fitHeight int) {
	ratio := handler.CalcFitSizeRatio(width, height, toBeFitWidth, toBeFitHeight)
	fitWidth = handler.ScaleWidthByRatio(toBeFitWidth, ratio)
	fitHeight = handler.ScaleHeightByRatio(toBeFitHeight, ratio)
	return
}

func ScaleWidthByRatio(width float64, ratio float64) int {
func (handler *ImageResizeHandler) ScaleWidthByRatio(width float64, ratio float64) int {
	return int(width * ratio)
}

func ScaleHeightByRatio(height float64, ratio float64) int {
	return int(height * ratio * charWidth())
}

// charWidth get the terminal char width on different system
func charWidth() float64 {
	if isWindows() {
		return 0.714
	}
	return 0.5
}

// isWindows check if current system is windows
func isWindows() bool {
	return runtime.GOOS == "windows"
}

// getTerminalScreenSize get the current terminal screen size
func getTerminalScreenSize() (newWidth, newHeight uint, err error) {
	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
		return 0, 0,
			errors.New("can not detect the terminal, please disable the '-s fitScreen' option")
	}

	x, _ := terminal.Width()
	y, _ := terminal.Height()

	return x, y, nil
func (handler *ImageResizeHandler) ScaleHeightByRatio(height float64, ratio float64) int {
	return int(height * ratio * handler.terminal.CharWidth())
}

M convert/resize_test.go => convert/resize_test.go +46 -60
@@ 2,29 2,15 @@ package convert

import (
	"fmt"
	"github.com/mattn/go-isatty"
	terminal2 "github.com/qeesung/image2ascii/terminal"
	"github.com/stretchr/testify/assert"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"strings"
	"testing"
)

// TestGetTerminalScreenSize test fetch the terminal screen size
func TestGetTerminalScreenSize(t *testing.T) {
	assertions := assert.New(t)
	_, _, err := getTerminalScreenSize()
	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
		assertions.True(err != nil)
	} else {
		assertions.True(err == nil)
	}
}

// TestScaleImageWithFixedHeight test scale the image by fixed height
func TestScaleImageWithFixedHeight(t *testing.T) {
	handler := NewResizeHandler()
	assertions := assert.New(t)
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)


@@ 35,7 21,7 @@ func TestScaleImageWithFixedHeight(t *testing.T) {
	options.Colored = false
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	scaledImage := handler.ScaleImage(img, &options)
	sz := scaledImage.Bounds()
	oldSz := img.Bounds()
	assertions.Equal(100, sz.Max.Y, "scaled image height should be 100")


@@ 44,6 30,7 @@ func TestScaleImageWithFixedHeight(t *testing.T) {

// TestScaleImageWithFixedWidth test scale the image by fixed width
func TestScaleImageWithFixedWidth(t *testing.T) {
	handler := NewResizeHandler()
	assertions := assert.New(t)
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)


@@ 54,7 41,7 @@ func TestScaleImageWithFixedWidth(t *testing.T) {
	options.Colored = false
	options.FixedWidth = 200

	scaledImage := ScaleImage(img, &options)
	scaledImage := handler.ScaleImage(img, &options)
	sz := scaledImage.Bounds()
	oldSz := img.Bounds()
	assertions.Equal(oldSz.Max.Y, sz.Max.Y, "scaled image height should be changed")


@@ 63,6 50,7 @@ func TestScaleImageWithFixedWidth(t *testing.T) {

// TestScaleImageWithFixedWidthHeight test scale the image by fixed width
func TestScaleImageWithFixedWidthHeight(t *testing.T) {
	handler := NewResizeHandler()
	assertions := assert.New(t)
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)


@@ 74,7 62,7 @@ func TestScaleImageWithFixedWidthHeight(t *testing.T) {
	options.FixedWidth = 200
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	scaledImage := handler.ScaleImage(img, &options)
	sz := scaledImage.Bounds()
	assertions.Equal(100, sz.Max.Y, "scaled image height should be 100")
	assertions.Equal(200, sz.Max.X, "scaled image width should be 200")


@@ 82,6 70,8 @@ func TestScaleImageWithFixedWidthHeight(t *testing.T) {

// TestScaleImageByRatio test scale image by ratio
func TestScaleImageByRatio(t *testing.T) {
	handler := NewResizeHandler()
	terminal := terminal2.NewTerminalAccessor()
	assertions := assert.New(t)
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)


@@ 92,58 82,52 @@ func TestScaleImageByRatio(t *testing.T) {
	options.Colored = false
	options.Ratio = 0.5

	scaledImage := ScaleImage(img, &options)
	scaledImage := handler.ScaleImage(img, &options)
	sz := scaledImage.Bounds()
	oldSz := img.Bounds()
	expectedHeight := int(float64(oldSz.Max.Y) * 0.5 * charWidth())
	expectedHeight := int(float64(oldSz.Max.Y) * 0.5 * terminal.CharWidth())
	expectedWidth := int(float64(oldSz.Max.X) * 0.5)
	assertions.Equal(expectedHeight, sz.Max.Y, fmt.Sprintf("scaled image height should be %d", expectedHeight))
	assertions.Equal(expectedWidth, sz.Max.X, fmt.Sprintf("scaled image width should be %d", expectedHeight))
}

// TestScaleToFitTerminalSize test scale image to fit the terminal
func TestScaleToFitTerminalSize(t *testing.T) {
	assertions := assert.New(t)
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)
	assertions.True(img != nil)
	assertions.True(err == nil)

	options := DefaultOptions
	options.Colored = false
	options.FitScreen = true

	// not terminal
	if !isatty.IsTerminal(os.Stdout.Fd()) &&
		!isatty.IsCygwinTerminal(os.Stdout.Fd()) &&
		os.Getenv("BE_CRASHER") == "1" {
		ScaleImage(img, &options)
// TestCalcFitSize test calc the fit size
func TestCalcFitSize(t *testing.T) {
	handler := ImageResizeHandler{
		terminal: terminal2.NewTerminalAccessor(),
	}

	cmd := exec.Command(os.Args[0], "-test.run=TestScaleToFitTerminalSize")
	cmd.Env = append(os.Environ(), "BE_CRASHER=1")
	stdout, _ := cmd.StderrPipe()
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}

	// Check that the log fatal message is what we expected
	gotBytes, _ := ioutil.ReadAll(stdout)
	got := string(gotBytes)
	expected := "can not detect the terminal, please disable the '-s fitScreen' option"
	if !strings.HasSuffix(got[:len(got)-1], expected) {
		t.Fatalf("Unexpected log message. Got %s but should contain %s", got[:len(got)-1], expected)
	fitSizeTests := []struct {
		width         int
		height        int
		toBeFitWidth  int
		toBeFitHeight int
		fitWidth      int
		fitHeight     int
	}{
		{width: 100, height: 80, toBeFitWidth: 50, toBeFitHeight: 120, fitWidth: 66, fitHeight: 80},
		{width: 100, height: 80, toBeFitWidth: 120, toBeFitHeight: 50, fitWidth: 100, fitHeight: 20},
	}

	// Check that the program exited
	err = cmd.Wait()
	if e, ok := err.(*exec.ExitError); !ok || e.Success() {
		t.Fatalf("Process ran with err %v, want exit status 1", err)
	for _, tt := range fitSizeTests {
		t.Run(fmt.Sprintf("%d, %d -> %d, %d",
			tt.width, tt.height, tt.toBeFitWidth, tt.toBeFitHeight), func(t *testing.T) {
			fitWidth, fitHeight := handler.CalcFitSize(
				float64(tt.width),
				float64(tt.height),
				float64(tt.toBeFitWidth),
				float64(tt.toBeFitHeight))
			if fitWidth != tt.fitWidth || fitHeight != tt.fitHeight {
				t.Errorf("%d, %d -> %d, %d should be %d, %d, but get %d, %d",
					tt.width, tt.height, tt.toBeFitWidth,
					tt.toBeFitHeight, tt.fitWidth, tt.fitHeight,
					fitWidth, fitHeight)
			}
		})
	}
}

// ExampleScaleImage is scale image example
func ExampleScaleImage() {
	handler := NewResizeHandler()
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)
	if err != nil {


@@ 155,7 139,7 @@ func ExampleScaleImage() {
	options.FixedWidth = 200
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	scaledImage := handler.ScaleImage(img, &options)
	sz := scaledImage.Bounds()
	fmt.Print(sz.Max.X, sz.Max.Y)
	// output: 200 100


@@ 163,6 147,7 @@ func ExampleScaleImage() {

// BenchmarkScaleImage benchmark scale big image
func BenchmarkScaleBigImage(b *testing.B) {
	handler := NewResizeHandler()
	imageFilePath := "testdata/cat_2000x1500.jpg"
	img, err := OpenImageFile(imageFilePath)
	if err != nil {


@@ 176,12 161,13 @@ func BenchmarkScaleBigImage(b *testing.B) {
	options.FixedWidth = 100

	for i := 0; i < b.N; i++ {
		_ = ScaleImage(img, &options)
		_ = handler.ScaleImage(img, &options)
	}
}

// BenchmarkScaleSmallImage benchmark scale small image
func BenchmarkScaleSmallImage(b *testing.B) {
	handler := NewResizeHandler()
	imageFilePath := "testdata/husky_200x200.jpg"
	img, err := OpenImageFile(imageFilePath)
	if err != nil {


@@ 195,6 181,6 @@ func BenchmarkScaleSmallImage(b *testing.B) {
	options.FixedWidth = 100

	for i := 0; i < b.N; i++ {
		_ = ScaleImage(img, &options)
		_ = handler.ScaleImage(img, &options)
	}
}

M image2ascii.go => image2ascii.go +3 -2
@@ 24,7 24,7 @@ var convertDefaultOptions = convert.DefaultOptions
func init() {
	flag.StringVar(&imageFilename,
		"f",
		"",
		"docs/images/lufei.jpg",
		"Image filename to be convert")
	flag.Float64Var(&ratio,
		"r",


@@ 60,7 60,8 @@ func init() {
func main() {
	flag.Parse()
	if convertOptions, err := parseOptions(); err == nil {
		fmt.Print(convert.ImageFile2ASCIIString(imageFilename, convertOptions))
		converter := convert.NewImageConverter()
		fmt.Print(converter.ImageFile2ASCIIString(imageFilename, convertOptions))
	} else {
		usage()
	}

A terminal/terminal.go => terminal/terminal.go +51 -0
@@ 0,0 1,51 @@
package terminal

import (
	"errors"
	"github.com/mattn/go-isatty"
	terminal "github.com/wayneashleyberry/terminal-dimensions"
	"os"
	"runtime"
)

const (
	charWidthWindows = 0.714
	charWidthOther   = 0.5
)

func NewTerminalAccessor() Terminal {
	return Accessor{}
}

// Terminal get the terminal basic information
type Terminal interface {
	CharWidth() float64
	ScreenSize() (width, height int, err error)
	IsWindows() bool
}

type Accessor struct {
}

func (accessor Accessor) CharWidth() float64 {
	if accessor.IsWindows() {
		return charWidthWindows
	}
	return charWidthOther
}

func (accessor Accessor) IsWindows() bool {
	return runtime.GOOS == "windows"
}

func (accessor Accessor) ScreenSize() (newWidth, newHeight int, err error) {
	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
		return 0, 0,
			errors.New("can not detect the terminal")
	}

	x, _ := terminal.Width()
	y, _ := terminal.Height()

	return int(x), int(y), nil
}

A terminal/terminal_test.go => terminal/terminal_test.go +30 -0
@@ 0,0 1,30 @@
package terminal

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestNewTerminalAccessor(t *testing.T) {
	assertions := assert.New(t)
	accessor := NewTerminalAccessor()
	assertions.True(accessor != nil)
}

func TestAccessor_CharWidth(t *testing.T) {
	assertions := assert.New(t)
	accessor := NewTerminalAccessor()
	charWidth := accessor.CharWidth()
	if accessor.IsWindows() {
		assertions.Equal(charWidthWindows, charWidth)
	} else {
		assertions.Equal(charWidthOther, charWidth)
	}
}

func TestAccessor_ScreenSize(t *testing.T) {
	assertions := assert.New(t)
	accessor := NewTerminalAccessor()
	_, _, err := accessor.ScreenSize()
	assertions.True(err != nil)
}