Added dithering and improved performance

This commit is contained in:
Zoraiz 2021-09-06 17:34:49 +05:00
parent 8be1d4c9b5
commit 2db0093e58
13 changed files with 220 additions and 152 deletions

View File

@ -1,10 +1,10 @@
# ascii-image-converter
[![release version](https://img.shields.io/github/v/release/TheZoraiz/ascii-image-converter?label=Latest%20Version)](https://github.com/TheZoraiz/ascii-image-converter/releases/latest)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/TheZoraiz/ascii-image-converter/blob/master/LICENSE.txt)
[![ascii-image-converter-lang](https://img.shields.io/badge/Language-Go-blue)](https://golang.org/)
![Github All Releases](https://img.shields.io/github/downloads/TheZoraiz/ascii-image-converter/total?color=brightgreen&label=Release%20Downloads)
[![ascii-image-converter](https://snapcraft.io/ascii-image-converter/badge.svg)](https://snapcraft.io/ascii-image-converter)
[![release-version](https://img.shields.io/github/v/release/TheZoraiz/ascii-image-converter?label=Latest%20Version)](https://github.com/TheZoraiz/ascii-image-converter/releases/latest)
[![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/TheZoraiz/ascii-image-converter/blob/master/LICENSE.txt)
[![language](https://img.shields.io/badge/Language-Go-blue)](https://golang.org/)
![release-downloads](https://img.shields.io/github/downloads/TheZoraiz/ascii-image-converter/total?color=1d872d&label=Release%20Downloads)
[![ascii-image-converter-snap](https://snapcraft.io/ascii-image-converter/badge.svg)](https://snapcraft.io/ascii-image-converter)
ascii-image-converter is a command-line tool that converts images into ascii art and prints them out onto the console. Available on Windows, Linux and macOS.
@ -135,8 +135,7 @@ Now you can use ascii-image-converter in the terminal. Execute `ascii-image-conv
You will need to set an Environment Variable to the folder the ascii-image-converter.exe executable is placed in to be able to use it in the command prompt. Follow the instructions in case of confusion:
Download the archive for your Windows architecture [here](https://github.com/TheZoraiz/ascii-image-converter/releases/latest), extract it, and open the extracted folder. Now, copy the folder path from the top of the file explorer and follow these instructions:
* In Search, search for and then select: System (Control Panel)
* Click the Advanced System settings link.
* In Search, search for and then select: Advanced System Settings
* Click Environment Variables. In the section User Variables find the Path environment variable and select it. Click "Edit".
* In the Edit Environment Variable window, click "New" and then paste the path of the folder that you copied initially.
* Click "Ok" on all open windows.
@ -199,6 +198,19 @@ Example:
ascii-image-converter [image paths/urls] -b --threshold 170
```
#### --dither
Apply dithering on image to make braille art more visible. Since braille dots can only be on or off, dithering images makes them more visible in braille art.
Example:
```
ascii-image-converter [image paths/urls] -b --dither
```
<p align="center">
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/dither.gif">
</p>
#### --color-bg
If any of the coloring flags is passed, this flag will transfer its color to each character's background. instead of foreground. However, this option isn't available for `--save-img` and `--save-gif`
@ -492,6 +504,8 @@ You can fork the project and implement any changes you want for a pull request.
[github.com/asaskevich/govalidator](https://github.com/asaskevich/govalidator)
[github.com/makeworld-the-better-one/dither/v2](https://github.com/makeworld-the-better-one/dither/v2)
## License
[Apache-2.0](https://github.com/TheZoraiz/ascii-image-converter/blob/master/LICENSE.txt)

View File

@ -97,7 +97,7 @@ func pathIsGif(gifPath, urlImgName string, pathIsURl bool, urlImgBytes []byte, l
var imgSet [][]imgManip.AsciiPixel
imgSet, err = imgManip.ConvertToAsciiPixels(frameImage, dimensions, width, height, flipX, flipY, full, braille)
imgSet, err = imgManip.ConvertToAsciiPixels(frameImage, dimensions, width, height, flipX, flipY, full, braille, dither)
if err != nil {
fmt.Println("Error:", err)
os.Exit(0)

View File

@ -43,7 +43,7 @@ func pathIsImage(imagePath, urlImgName string, pathIsURl bool, urlImgBytes []byt
return "", fmt.Errorf("can't decode %v: %v", imagePath, err)
}
imgSet, err := imgManip.ConvertToAsciiPixels(imData, dimensions, width, height, flipX, flipY, full, braille)
imgSet, err := imgManip.ConvertToAsciiPixels(imData, dimensions, width, height, flipX, flipY, full, braille, dither)
if err != nil {
return "", err
}

View File

@ -60,6 +60,7 @@ func DefaultFlags() Flags {
SaveBackgroundColor: [3]int{0, 0, 0},
Braille: false,
Threshold: 128,
Dither: false,
}
}
@ -94,6 +95,7 @@ func Convert(filePath string, flags Flags) (string, error) {
saveBgColor = flags.SaveBackgroundColor
braille = flags.Braille
threshold = flags.Threshold
dither = flags.Dither
// Declared at the start since some variables are initially used in conditional blocks
var (

View File

@ -94,6 +94,10 @@ type Flags struct {
// be between 0 and 255. Ideal value is 128.
// This will be ignored if Flags.Braille is not set
Threshold int
// Apply FloydSteinberg dithering on an image before ascii conversion. This option
// is meant for braille art. Therefore, it will be ignored if Flags.Braille is false
Dither bool
}
var (
@ -117,4 +121,5 @@ var (
saveBgColor [3]int
braille bool
threshold int
dither bool
)

View File

@ -51,12 +51,13 @@ var (
saveBgColor []int
braille bool
threshold int
dither bool
// Root commands
rootCmd = &cobra.Command{
Use: "ascii-image-converter [image paths/urls]",
Short: "Converts images and gifs into ascii art",
Version: "1.8.0",
Version: "1.9.0",
Long: "This tool converts images into ascii art and prints them on the terminal.\nFurther configuration can be managed with flags.",
// Not RunE since help text is getting larger and seeing it for every error impacts user experience
@ -87,6 +88,7 @@ var (
SaveBackgroundColor: [3]int{saveBgColor[0], saveBgColor[1], saveBgColor[2]},
Braille: braille,
Threshold: threshold,
Dither: dither,
}
for _, imagePath := range args {
@ -126,13 +128,14 @@ func init() {
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ascii-image-converter.yaml)")
rootCmd.PersistentFlags().BoolVarP(&colored, "color", "C", false, "Display ascii art with original colors\n(Inverts with --negative flag)\n(Overrides --grayscale and --font-color flags)\n")
rootCmd.PersistentFlags().BoolVar(&colorBg, "color-bg", false, "If some color flag is passed, use that color\non character background instead of foreground\n(Inverts with --negative flag)\n(Doesn't work for --save-img or --save-gif)\n")
rootCmd.PersistentFlags().BoolVar(&colorBg, "color-bg", false, "If some color flag is passed, use that color\non character background instead of foreground\n(Inverts with --negative flag)\n(Only applicable for terminal display)\n")
rootCmd.PersistentFlags().IntSliceVarP(&dimensions, "dimensions", "d", nil, "Set width and height for ascii art in CHARACTER length\ne.g. -d 60,30 (defaults to terminal height)\n(Overrides --width and --height flags)\n")
rootCmd.PersistentFlags().IntVarP(&width, "width", "W", 0, "Set width for ascii art in CHARACTER length\nHeight is kept to aspect ratio\ne.g. -W 60\n")
rootCmd.PersistentFlags().IntVarP(&height, "height", "H", 0, "Set height for ascii art in CHARACTER length\nWidth is kept to aspect ratio\ne.g. -H 60\n")
rootCmd.PersistentFlags().StringVarP(&customMap, "map", "m", "", "Give custom ascii characters to map against\nOrdered from darkest to lightest\ne.g. -m \" .-+#@\" (Quotation marks excluded from map)\n(Overrides --complex flag)\n")
rootCmd.PersistentFlags().BoolVarP(&braille, "braille", "b", false, "Use braille characters instead of ascii\nTerminal must support braille patterns properly\n(Overrides --complex and --map flags)\n")
rootCmd.PersistentFlags().IntVar(&threshold, "threshold", 0, "Threshold for braille art\nValue between 0-255 is accepted\ne.g. --threshold 170\n(Defaults to 128)\n")
rootCmd.PersistentFlags().BoolVar(&dither, "dither", false, "Apply dithering on image for braille\nart conversion\n(Only applicable with --braille flag)\n(Negates --threshold flag)\n")
rootCmd.PersistentFlags().BoolVarP(&grayscale, "grayscale", "g", false, "Display grayscale ascii art\n(Inverts with --negative flag)\n(Overrides --font-color flag)\n")
rootCmd.PersistentFlags().BoolVarP(&complex, "complex", "c", false, "Display ascii characters in a larger range\nMay result in higher quality\n")
rootCmd.PersistentFlags().BoolVarP(&full, "full", "f", false, "Use largest dimensions for ascii art\nthat fill the terminal width\n(Overrides --dimensions, --width and --height flags)\n")

View File

@ -180,5 +180,10 @@ func checkInputAndFlags(args []string) bool {
return true
}
if dither && !braille {
fmt.Printf("Error: image dithering is only reserved for --braille flag\n\n")
return true
}
return false
}

BIN
example_gifs/dither.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

1
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gookit/color v1.4.2
github.com/magiconair/properties v1.8.5 // indirect
github.com/makeworld-the-better-one/dither/v2 v2.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d

2
go.sum
View File

@ -124,6 +124,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/makeworld-the-better-one/dither/v2 v2.2.0 h1:VTMAiyyO1YIO07fZwuLNZZasJgKUmvsIA48ze3ALHPQ=
github.com/makeworld-the-better-one/dither/v2 v2.2.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=

View File

@ -17,12 +17,8 @@ limitations under the License.
package image_conversions
import (
"fmt"
"image"
"image/color"
"github.com/TheZoraiz/ascii-image-converter/aic_package/winsize"
"github.com/disintegration/imaging"
)
type AsciiPixel struct {
@ -31,131 +27,26 @@ type AsciiPixel struct {
rgbValue [3]uint32
}
func resizeForBraille(asciiWidth, asciiHeight int) (int, int) {
return asciiWidth * 2, asciiHeight * 4
}
/*
This function shrinks the passed image according to passed dimensions or terminal
size if none are passed. Stores each pixel's grayscale and RGB values in an AsciiPixel
instance to simplify getting numeric data for ASCII character comparison.
This function shrinks the passed image according to specified or default dimensions.
Stores each pixel's grayscale and RGB values in an AsciiPixel instance to simplify
getting numeric data for ASCII character comparison.
The returned 2D AsciiPixel slice contains each corresponding pixel's values. Grayscale value
ranges from 0 to 65535, while RGB values are separate.
The returned 2D AsciiPixel slice contains each corresponding pixel's values
*/
func ConvertToAsciiPixels(img image.Image, dimensions []int, width, height int, flipX, flipY, full, isBraille bool) ([][]AsciiPixel, error) {
func ConvertToAsciiPixels(img image.Image, dimensions []int, width, height int, flipX, flipY, full, isBraille, dither bool) ([][]AsciiPixel, error) {
var asciiWidth, asciiHeight int
var smallImg image.Image
terminalWidth, terminalHeight, err := winsize.GetTerminalSize()
smallImg, err := resizeImage(img, full, isBraille, dimensions, width, height)
if err != nil {
return nil, err
}
if full {
asciiWidth = terminalWidth - 1
// We mainatin a dithered image literal along with original image
// The colors are kept from original image
var ditheredImage image.Image
// Passing 0 in place of width keeps the original image's aspect ratio
smallImg = imaging.Resize(img, asciiWidth, 0, imaging.Lanczos)
asciiHeight = smallImg.Bounds().Max.Y - smallImg.Bounds().Min.Y
// To fix aspect ratio in eventual ascii art
asciiHeight = int(0.5 * float64(asciiHeight))
if isBraille {
asciiWidth, asciiHeight = resizeForBraille(asciiWidth, asciiHeight)
}
smallImg = imaging.Resize(img, asciiWidth, asciiHeight, imaging.Lanczos)
} else if (width != 0 || height != 0) && len(dimensions) == 0 {
// If either width or height is set and dimensions aren't given
if width > terminalWidth-1 {
return nil, fmt.Errorf("set width must be lower than terminal width")
}
if width != 0 && height == 0 {
// If width is set and height is not set, use width to calculate aspect ratio
asciiWidth = width
smallImg = imaging.Resize(img, asciiWidth, 0, imaging.Lanczos)
asciiHeight = smallImg.Bounds().Max.Y - smallImg.Bounds().Min.Y
asciiHeight = int(0.5 * float64(asciiHeight))
if asciiHeight == 0 {
asciiHeight = 1
}
} else if height != 0 && width == 0 {
// If height is set and width is not set, use height to calculate aspect ratio
asciiHeight = height
smallImg = imaging.Resize(img, 0, asciiHeight, imaging.Lanczos)
asciiWidth = smallImg.Bounds().Max.X - smallImg.Bounds().Min.X
asciiWidth = int(2 * float64(asciiWidth))
if asciiWidth > terminalWidth-1 {
return nil, fmt.Errorf("width calculated with aspect ratio exceeds terminal width")
}
} else {
return nil, fmt.Errorf("both width and height can't be set. Use dimensions instead")
}
if isBraille {
asciiWidth, asciiHeight = resizeForBraille(asciiWidth, asciiHeight)
}
smallImg = imaging.Resize(img, asciiWidth, asciiHeight, imaging.Lanczos)
} else if len(dimensions) == 0 {
// This condition calculates aspect ratio according to terminal height
asciiHeight = terminalHeight - 1
smallImg = imaging.Resize(img, 0, asciiHeight, imaging.Lanczos)
asciiWidth = smallImg.Bounds().Max.X - smallImg.Bounds().Min.X
// To fix aspect ratio in eventual ascii art
asciiWidth = int(2 * float64(asciiWidth))
// If ascii width exceeds terminal width, change ratio with respect to terminal width
if asciiWidth >= terminalWidth {
asciiWidth = terminalWidth - 1
smallImg = imaging.Resize(img, asciiWidth, 0, imaging.Lanczos)
asciiHeight = smallImg.Bounds().Max.Y - smallImg.Bounds().Min.Y
// To fix aspect ratio in eventual ascii art
asciiHeight = int(0.5 * float64(asciiHeight))
}
if isBraille {
asciiWidth, asciiHeight = resizeForBraille(asciiWidth, asciiHeight)
}
smallImg = imaging.Resize(img, asciiWidth, asciiHeight, imaging.Lanczos)
} else {
asciiWidth = dimensions[0]
asciiHeight = dimensions[1]
if isBraille {
asciiWidth, asciiHeight = resizeForBraille(asciiWidth, asciiHeight)
}
smallImg = imaging.Resize(img, asciiWidth, asciiHeight, imaging.Lanczos)
}
// Repeated despite being in cmd/root.go to maintain support for library
//
// If there are passed dimensions, check whether the width exceeds terminal width
if len(dimensions) > 0 && !full {
if dimensions[0] > terminalWidth-1 {
return nil, fmt.Errorf("set width must be lower than terminal width")
}
if isBraille && dither {
ditheredImage = ditherImage(smallImg)
}
var imgSet [][]AsciiPixel
@ -177,6 +68,19 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int, width, height int,
g1 = uint32(g1 / 257)
b1 = uint32(b1 / 257)
if isBraille && dither {
// Change charDepth if image dithering is applied
// Note that neither grayscale nor original color values are changed.
// Only charDepth is kept from dithered image. This is because a
// dithered image loses its colors so it's only used to check braille
// dots' visibility
ditheredGrayPixel := color.GrayModel.Convert(ditheredImage.At(x, y))
charDepth, _, _, _ = ditheredGrayPixel.RGBA()
charDepth = charDepth / 257
}
// Get co1ored RGB values of original pixel for rgbValue in AsciiPixel
r2, g2, b2, _ := oldPixel.RGBA()
r2 = uint32(r2 / 257)
@ -200,22 +104,3 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int, width, height int,
return imgSet, nil
}
func reverse(imgSet [][]AsciiPixel, flipX, flipY bool) [][]AsciiPixel {
if flipX {
for _, row := range imgSet {
for i, j := 0, len(row)-1; i < j; i, j = i+1, j-1 {
row[i], row[j] = row[j], row[i]
}
}
}
if flipY {
for i, j := 0, len(imgSet)-1; i < j; i, j = i+1, j-1 {
imgSet[i], imgSet[j] = imgSet[j], imgSet[i]
}
}
return imgSet
}

151
image_manipulation/util.go Normal file
View File

@ -0,0 +1,151 @@
/*
Copyright © 2021 Zoraiz Hassan <hzoraiz8@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image_conversions
import (
"fmt"
"image"
"image/color"
"github.com/TheZoraiz/ascii-image-converter/aic_package/winsize"
"github.com/disintegration/imaging"
"github.com/makeworld-the-better-one/dither/v2"
)
func resizeForBraille(asciiWidth, asciiHeight int) (int, int) {
return asciiWidth * 2, asciiHeight * 4
}
func ditherImage(img image.Image) image.Image {
palette := []color.Color{
color.Black,
color.White,
}
d := dither.NewDitherer(palette)
d.Matrix = dither.FloydSteinberg
return d.DitherCopy(img)
}
func resizeImage(img image.Image, full, isBraille bool, dimensions []int, width, height int) (image.Image, error) {
var asciiWidth, asciiHeight int
var smallImg image.Image
terminalWidth, terminalHeight, err := winsize.GetTerminalSize()
if err != nil {
return nil, err
}
imgWidth := float64(img.Bounds().Dx())
imgHeight := float64(img.Bounds().Dy())
aspectRatio := imgWidth / imgHeight
if full {
asciiWidth = terminalWidth - 1
asciiHeight = int(float64(asciiWidth) / aspectRatio)
asciiHeight = int(0.5 * float64(asciiHeight))
} else if (width != 0 || height != 0) && len(dimensions) == 0 {
// If either width or height is set and dimensions aren't given
if width > terminalWidth-1 {
return nil, fmt.Errorf("set width must be lower than terminal width")
}
if width != 0 && height == 0 {
// If width is set and height is not set, use width to calculate aspect ratio
asciiWidth = width
asciiHeight = int(float64(asciiWidth) / aspectRatio)
asciiHeight = int(0.5 * float64(asciiHeight))
if asciiHeight == 0 {
asciiHeight = 1
}
} else if height != 0 && width == 0 {
// If height is set and width is not set, use height to calculate aspect ratio
asciiHeight = height
asciiWidth = int(float64(asciiHeight) * aspectRatio)
asciiWidth = int(2 * float64(asciiWidth))
if asciiWidth > terminalWidth-1 {
return nil, fmt.Errorf("width calculated with aspect ratio exceeds terminal width")
}
} else {
return nil, fmt.Errorf("both width and height can't be set. Use dimensions instead")
}
} else if len(dimensions) == 0 {
// This condition calculates aspect ratio according to terminal height
asciiHeight = terminalHeight - 1
asciiWidth = int(float64(asciiHeight) * aspectRatio)
asciiWidth = int(2 * float64(asciiWidth))
// If ascii width exceeds terminal width, change ratio with respect to terminal width
if asciiWidth >= terminalWidth {
asciiWidth = terminalWidth - 1
asciiHeight = int(float64(asciiWidth) / aspectRatio)
asciiHeight = int(0.5 * float64(asciiHeight))
}
} else {
asciiWidth = dimensions[0]
asciiHeight = dimensions[1]
}
// Repeated despite being in cmd/root.go to maintain support for library
//
// If there are passed dimensions, check whether the width exceeds terminal width
if len(dimensions) > 0 && !full {
if dimensions[0] > terminalWidth-1 {
return nil, fmt.Errorf("set width must be lower than terminal width")
}
}
if isBraille {
asciiWidth, asciiHeight = resizeForBraille(asciiWidth, asciiHeight)
}
smallImg = imaging.Resize(img, asciiWidth, asciiHeight, imaging.Lanczos)
return smallImg, nil
}
func reverse(imgSet [][]AsciiPixel, flipX, flipY bool) [][]AsciiPixel {
if flipX {
for _, row := range imgSet {
for i, j := 0, len(row)-1; i < j; i, j = i+1, j-1 {
row[i], row[j] = row[j], row[i]
}
}
}
if flipY {
for i, j := 0, len(imgSet)-1; i < j; i, j = i+1, j-1 {
imgSet[i], imgSet[j] = imgSet[j], imgSet[i]
}
}
return imgSet
}

View File

@ -1,6 +1,6 @@
name: ascii-image-converter
base: core18
version: "1.8.0"
version: "1.9.0"
summary: Convert images and gifs into ascii art
description: |
ascii-image-converter is a command-line tool that converts images into ascii art and prints