From 4ad980ef244f080f793177b991b50fa6460c1eb8 Mon Sep 17 00:00:00 2001 From: qeesung <1245712564@qq.com> Date: Mon, 29 Oct 2018 00:19:44 +0800 Subject: [PATCH] Refactor the code to match the oop mode for easy mock and test --- ascii/ascii.go | 29 +++++++---- ascii/ascii_test.go | 23 +++++---- convert/convert.go | 37 +++++++++---- convert/convert_test.go | 18 ++++--- convert/resize.go | 89 +++++++++++++------------------- convert/resize_test.go | 106 +++++++++++++++++--------------------- image2ascii.go | 5 +- terminal/terminal.go | 51 ++++++++++++++++++ terminal/terminal_test.go | 30 +++++++++++ 9 files changed, 239 insertions(+), 149 deletions(-) create mode 100644 terminal/terminal.go create mode 100644 terminal/terminal_test.go diff --git a/ascii/ascii.go b/ascii/ascii.go index b2dbae7..077b4df 100644 --- a/ascii/ascii.go +++ b/ascii/ascii.go @@ -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 } diff --git a/ascii/ascii_test.go b/ascii/ascii_test.go index e0276b1..5a4fe6d 100644 --- a/ascii/ascii_test.go +++ b/ascii/ascii_test.go @@ -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 +} diff --git a/convert/convert.go b/convert/convert.go index 623ebd9..51f1943 100644 --- a/convert/convert.go +++ b/convert/convert.go @@ -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 diff --git a/convert/convert_test.go b/convert/convert_test.go index e25491e..58a7997 100644 --- a/convert/convert_test.go +++ b/convert/convert_test.go @@ -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: @@@ diff --git a/convert/resize.go b/convert/resize.go index e927049..3b79c76 100644 --- a/convert/resize.go +++ b/convert/resize.go @@ -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()) } diff --git a/convert/resize_test.go b/convert/resize_test.go index 93c0756..c1f9723 100644 --- a/convert/resize_test.go +++ b/convert/resize_test.go @@ -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) } } diff --git a/image2ascii.go b/image2ascii.go index 4c4f16a..497471e 100644 --- a/image2ascii.go +++ b/image2ascii.go @@ -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() } diff --git a/terminal/terminal.go b/terminal/terminal.go new file mode 100644 index 0000000..bd4df15 --- /dev/null +++ b/terminal/terminal.go @@ -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 +} diff --git a/terminal/terminal_test.go b/terminal/terminal_test.go new file mode 100644 index 0000000..038098e --- /dev/null +++ b/terminal/terminal_test.go @@ -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) +} -- 2.45.2