~dricottone/image2ascii

9e6742da409008fc764fa344fc324438ce7928eb — qeesung 6 years ago 0048ecc
add the fit options to scale the image to fit the screen
M convert/convert.go => convert/convert.go +14 -12
@@ 16,22 16,24 @@ import (

// Options to convert the image to ASCII
type Options struct {
	Ratio          float64
	ExpectedWidth  int
	ExpectedHeight int
	FitScreen      bool
	Colored        bool
	Reversed       bool
	Ratio           float64
	FixedWidth      int
	FixedHeight     int
	FitScreen       bool // only work on terminal
	StretchedScreen bool // only work on terminal
	Colored         bool // only work on terminal
	Reversed        bool
}

// DefaultOptions for convert image
var DefaultOptions = Options{
	Ratio:          1,
	ExpectedWidth:  -1,
	ExpectedHeight: -1,
	FitScreen:      true,
	Colored:        true,
	Reversed:       false,
	Ratio:           1,
	FixedWidth:      -1,
	FixedHeight:     -1,
	FitScreen:       true,
	Colored:         true,
	Reversed:        false,
	StretchedScreen: false,
}

// Image2ASCIIMatrix converts a image to ASCII matrix

M convert/convert_test.go => convert/convert_test.go +4 -4
@@ 124,8 124,8 @@ func BenchmarkBigImage2ASCIIMatrix(b *testing.B) {
	convertOptions := DefaultOptions
	convertOptions.FitScreen = false
	convertOptions.Colored = false
	convertOptions.ExpectedWidth = 200
	convertOptions.ExpectedHeight = 200
	convertOptions.FixedWidth = 200
	convertOptions.FixedHeight = 200

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


@@ 137,8 137,8 @@ func BenchmarkSmallImage2ASCIIMatrix(b *testing.B) {
	convertOptions := DefaultOptions
	convertOptions.FitScreen = false
	convertOptions.Colored = false
	convertOptions.ExpectedWidth = 200
	convertOptions.ExpectedHeight = 200
	convertOptions.FixedWidth = 200
	convertOptions.FixedHeight = 200

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

M convert/resize.go => convert/resize.go +80 -10
@@ 18,26 18,39 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {
	newHeight := sz.Max.Y
	newWidth := sz.Max.X

	if options.ExpectedWidth != -1 {
		newWidth = options.ExpectedWidth
	if options.FixedWidth != -1 {
		newWidth = options.FixedWidth
	}

	if options.ExpectedHeight != -1 {
		newHeight = options.ExpectedHeight
	if options.FixedHeight != -1 {
		newHeight = options.FixedHeight
	}

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

	// fit the screen
	// get the fit the screen size
	if ratio == 1 &&
		options.ExpectedWidth == -1 &&
		options.ExpectedHeight == -1 &&
		options.FixedWidth == -1 &&
		options.FixedHeight == -1 &&
		options.FitScreen {
		fitWidth, fitHeight, err := CalcProportionalFittingScreenSize(image)
		if err != nil {
			log.Fatal(err)
		}
		newWidth = int(fitWidth)
		newHeight = int(fitHeight)
	}

	//Stretch the picture to overspread the terminal
	if ratio == 1 &&
		options.FixedWidth == -1 &&
		options.FixedHeight == -1 &&
		!options.FitScreen &&
		options.StretchedScreen {
		screenWidth, screenHeight, err := getTerminalScreenSize()
		if err != nil {
			log.Fatal(err)


@@ 50,6 63,63 @@ func ScaleImage(image image.Image, options *Options) (newImage image.Image) {
	return
}

// 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")
	}

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

// CalcFitSizeRatio through the given length and width,
// the computation can match the optimal scaling ratio of the length and width.
// In other words, it is able to give a given size rectangle to contain pictures
// 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) {
	ratio = 1.0
	// try to fit the height
	ratio = height / imageHeight
	scaledWidth := imageWidth * ratio / charWidth()
	if scaledWidth < width {
		return ratio / charWidth()
	}

	// try to fit the width
	ratio = width / imageWidth
	return ratio
}

// 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)
	return
}

func 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() {

M convert/resize_test.go => convert/resize_test.go +10 -10
@@ 33,7 33,7 @@ func TestScaleImageWithFixedHeight(t *testing.T) {

	options := DefaultOptions
	options.Colored = false
	options.ExpectedHeight = 100
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	sz := scaledImage.Bounds()


@@ 52,7 52,7 @@ func TestScaleImageWithFixedWidth(t *testing.T) {

	options := DefaultOptions
	options.Colored = false
	options.ExpectedWidth = 200
	options.FixedWidth = 200

	scaledImage := ScaleImage(img, &options)
	sz := scaledImage.Bounds()


@@ 71,8 71,8 @@ func TestScaleImageWithFixedWidthHeight(t *testing.T) {

	options := DefaultOptions
	options.Colored = false
	options.ExpectedWidth = 200
	options.ExpectedHeight = 100
	options.FixedWidth = 200
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	sz := scaledImage.Bounds()


@@ 152,8 152,8 @@ func ExampleScaleImage() {

	options := DefaultOptions
	options.Colored = false
	options.ExpectedWidth = 200
	options.ExpectedHeight = 100
	options.FixedWidth = 200
	options.FixedHeight = 100

	scaledImage := ScaleImage(img, &options)
	sz := scaledImage.Bounds()


@@ 172,8 172,8 @@ func BenchmarkScaleBigImage(b *testing.B) {
	options := DefaultOptions
	options.Colored = false
	options.FitScreen = false
	options.ExpectedHeight = 100
	options.ExpectedWidth = 100
	options.FixedHeight = 100
	options.FixedWidth = 100

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


@@ 191,8 191,8 @@ func BenchmarkScaleSmallImage(b *testing.B) {
	options := DefaultOptions
	options.Colored = false
	options.FitScreen = false
	options.ExpectedHeight = 100
	options.ExpectedWidth = 100
	options.FixedHeight = 100
	options.FixedWidth = 100

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

M image2ascii.go => image2ascii.go +44 -15
@@ 12,20 12,48 @@ import (

var imageFilename string
var ratio float64
var expectedWidth int
var expectedHeight int
var fixedWidth int
var fixedHeight int
var fitScreen bool
var stretchedScreen bool
var colored bool
var reversed bool

var convertDefaultOptions = convert.DefaultOptions

func init() {
	flag.StringVar(&imageFilename, "f", "", "Image filename to be convert")
	flag.Float64Var(&ratio, "r", 1, "Ratio to scale the image, ignored when use -w or -g")
	flag.IntVar(&expectedWidth, "w", -1, "Expected image width, -1 for image default width")
	flag.IntVar(&expectedHeight, "g", -1, "Expected image height, -1 for image default height")
	flag.BoolVar(&fitScreen, "s", true, "Fit the terminal screen, ignored when use -w, -g, -r")
	flag.BoolVar(&colored, "c", true, "Colored the ascii when output to the terminal")
	flag.BoolVar(&reversed, "i", false, "Reversed the ascii when output to the terminal")
	flag.StringVar(&imageFilename,
		"f",
		"",
		"Image filename to be convert")
	flag.Float64Var(&ratio,
		"r",
		convertDefaultOptions.Ratio,
		"Ratio to scale the image, ignored when use -w or -g")
	flag.IntVar(&fixedWidth,
		"w",
		convertDefaultOptions.FixedWidth,
		"Expected image width, -1 for image default width")
	flag.IntVar(&fixedHeight,
		"g",
		convertDefaultOptions.FixedHeight,
		"Expected image height, -1 for image default height")
	flag.BoolVar(&fitScreen,
		"s",
		convertDefaultOptions.FitScreen,
		"Fit the terminal screen, ignored when use -w, -g, -r")
	flag.BoolVar(&colored,
		"c",
		convertDefaultOptions.Colored,
		"Colored the ascii when output to the terminal")
	flag.BoolVar(&reversed,
		"i",
		convertDefaultOptions.Reversed,
		"Reversed the ascii when output to the terminal")
	flag.BoolVar(&stretchedScreen,
		"t",
		convertDefaultOptions.StretchedScreen,
		"Stretch the picture to overspread the screen")
	flag.Usage = usage
}



@@ 44,12 72,13 @@ func parseOptions() (*convert.Options, error) {
	}
	// config  the options
	convertOptions := &convert.Options{
		Ratio:          ratio,
		ExpectedHeight: expectedHeight,
		ExpectedWidth:  expectedWidth,
		FitScreen:      fitScreen,
		Colored:        colored,
		Reversed:       reversed,
		Ratio:           ratio,
		FixedWidth:      fixedWidth,
		FixedHeight:     fixedHeight,
		FitScreen:       fitScreen,
		StretchedScreen: stretchedScreen,
		Colored:         colored,
		Reversed:        reversed,
	}
	return convertOptions, nil
}

M image2ascii_test.go => image2ascii_test.go +4 -4
@@ 18,15 18,15 @@ func TestParseOptions(t *testing.T) {
	ratio = 0.5
	fitScreen = false
	colored = false
	expectedHeight = 100
	expectedWidth = 100
	fixedHeight = 100
	fixedWidth = 100
	opt, err :=parseOptions()
	assertions.True(err == nil)
	assertions.Equal(ratio, opt.Ratio)
	assertions.False(fitScreen)
	assertions.False(colored)
	assertions.Equal(expectedWidth, opt.ExpectedWidth)
	assertions.Equal(expectedHeight, opt.ExpectedHeight)
	assertions.Equal(fixedWidth, opt.FixedWidth)
	assertions.Equal(fixedHeight, opt.FixedHeight)
}

func TestParseUsage(t *testing.T) {