Added flip flags and made package available as a library

This commit is contained in:
Zoraiz 2021-05-31 18:28:09 +05:00
parent 8c537074aa
commit f3c90d36b7
8 changed files with 372 additions and 173 deletions

View File

@ -37,7 +37,7 @@ archives:
amd64: amd64_64bit amd64: amd64_64bit
arm64: arm64_64bit arm64: arm64_64bit
386: i386_32bit 386: i386_32bit
arm: arm32_32bit arm: arm_32bit
checksum: checksum:
name_template: 'sha256_checksums.txt' name_template: 'sha256_checksums.txt'

View File

@ -4,6 +4,8 @@
ascii-image-converter is a command-line tool that converts images into ascii art and prints them out onto the console. It is cross-platform so both Windows and Linux distributions are supported. ascii-image-converter is a command-line tool that converts images into ascii art and prints them out onto the console. It is cross-platform so both Windows and Linux distributions are supported.
It's also available as a package to be used in Go applications.
Image formats currently supported: Image formats currently supported:
* JPEG/JPG * JPEG/JPG
* PNG * PNG
@ -18,8 +20,9 @@ Image formats currently supported:
* [Go](#go) * [Go](#go)
* [Linux (binaries)](#linux) * [Linux (binaries)](#linux)
* [Windows (binaries)](#windows) * [Windows (binaries)](#windows)
- [Usage](#usage) - [CLI Usage](#cli-usage)
* [Flags](#flags) * [Flags](#flags)
- [Library Usage](#library-usage)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Packages used](#packages-used) - [Packages used](#packages-used)
- [License](#license) - [License](#license)
@ -72,7 +75,7 @@ Now, restart any open command prompt and execute "ascii-image-converter -h" for
<br> <br>
## Usage ## CLI Usage
Note: Decrease font size or increase terminal width (like zooming out) for maximum quality ascii art Note: Decrease font size or increase terminal width (like zooming out) for maximum quality ascii art
@ -211,12 +214,84 @@ ascii-image-converter [image paths/urls] --formats
ascii-image-converter [image paths/urls] -f ascii-image-converter [image paths/urls] -f
``` ```
#### --flipX OR -x
Flip the ascii art horizontally on the terminal.
```
ascii-image-converter [image paths/urls] --flipX
# Or
ascii-image-converter [image paths/urls] -x
```
<p align="center">
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/flipx.gif">
</p>
#### --flipY OR -y
Flip the ascii art vertically on the terminal.
```
ascii-image-converter [image paths/urls] --flipY
# Or
ascii-image-converter [image paths/urls] -y
```
<p align="center">
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/flipy.gif">
</p>
<br> <br>
You can combine flags as well. Following command outputs colored and negative ascii art, with fixed 100 by 30 character dimensions, custom defined ascii characters " .-=+#@" and saves the output in current directory as well. You can combine flags as well. Following command outputs colored and negative ascii art, with fixed 100 by 30 character dimensions, custom defined ascii characters " .-=+#@" and saves the output in current directory as well.
``` ```
ascii-image-converter [image paths/urls] -Cnd 100,30 -m " .-=+#@" -s ./ ascii-image-converter [image paths/urls] -Cnd 100,30 -m " .-=+#@" -s ./
``` ```
<br>
## Library Usage
First import the library with:
```
go get github.com/TheZoraiz/ascii-image-converter/aic_package
```
The library is to be used as follows:
```go
package main
import (
"fmt"
"github.com/TheZoraiz/ascii-image-converter/aic_package"
)
func main() {
// If image is in current directory. This can also be a URL to an image.
imagePath := "myImage.jpeg"
flags := aic_package.DefaultFlags()
// This part is optional. You can directly pass flags variable to ConvertImage() if you wish.
// For clarity, all flags are covered in this example.
flags["complex"] = true // Use complex character set
flags["dimensions"] = []int{50, 25} // 50 by 25 ascii art size
flags["savePath"] = "." // Saves to current directory
flags["negative"] = true // Ascii art will have negative color-depth
flags["colored"] = true // Keep colors from original image
flags["customMap"] = " .-=+#@" // Starting from darkest to brightest shades. This overrites "complex" flag
flags["flipX"] = true // Flips ascii art horizontally
flags["flipY"] = true // Flips ascii art vertically
// Return ascii art as a single string
asciiArt, err := aic_package.ConvertImage(imagePath, flags)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%v\n", asciiArt)
}
```
<br> <br>

View File

@ -0,0 +1,237 @@
/*
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 aic_package
import (
"bytes"
"fmt"
"image"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
// Image format initialization
_ "image/jpeg"
_ "image/png"
// Image format initialization
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
imgManip "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
"github.com/asaskevich/govalidator"
)
// Return default configuration for flags
func DefaultFlags() map[string]interface{} {
return map[string]interface{}{
"complex": false,
"dimensions": nil,
"savePath": "",
"negative": false,
"colored": false,
"customMap": "",
"flipX": false,
"flipY": false,
}
}
/*
ConvertImage takes an image path/url as its first argument
and a map of flags as the second argument, with which it alters
the returned ascii art string.
The "flags" argument should be declared as follows before passing:
flags := map[string]interface{}{
"complex": bool, // Pass true for using complex character set
"dimensions": []int, // Pass 2 integer dimensions. Pass nil to ignore
"savePath": string, // System path to save the ascii art string. Pass "" to ignore
"negative": bool, // Pass true for negative color-depth ascii art
"colored": bool, // Pass true for returning colored ascii string
"customMap": string, // Custom map of ascii chars e.g. " .-+#@" . Nullifies "complex" flag. Pass "" to ignore.
"flipX": bool, // Pass true to return horizontally flipped ascii art
"flipY": bool, // Pass true to return vertically flipped ascii art
}
*/
func ConvertImage(imagePath string, flags map[string]interface{}) (string, error) {
complex := flags["complex"].(bool)
var dimensions []int
if flags["dimensions"] == nil {
dimensions = nil
} else {
dimensions = flags["dimensions"].([]int)
}
savePath := flags["savePath"].(string)
negative := flags["negative"].(bool)
colored := flags["colored"].(bool)
customMap := flags["customMap"].(string)
flipX := flags["flipX"].(bool)
flipY := flags["flipY"].(bool)
numberOfDimensions := len(dimensions)
if dimensions != nil && numberOfDimensions != 2 {
return "", fmt.Errorf("requires 2 dimensions, got %v", numberOfDimensions)
}
// Declared at the start since some variables are initially used in conditional blocks
var (
pic *os.File
urlImgBytes []byte
urlImgName string = ""
err error
)
pathIsURl := govalidator.IsRequestURL(imagePath)
// Different modes of reading data depending upon whether or not imagePath is a url
if pathIsURl {
fmt.Printf("Fetching image from url...\r")
retrievedImage, err := http.Get(imagePath)
if err != nil {
return "", fmt.Errorf("can't fetching image: %v", err)
}
urlImgBytes, err = ioutil.ReadAll(retrievedImage.Body)
if err != nil {
return "", fmt.Errorf("failed to read fetched content: %v", err)
}
defer retrievedImage.Body.Close()
urlImgName = path.Base(imagePath)
fmt.Printf(" \r") // To erase "Fetching image from url..." text from console
} else {
pic, err = os.Open(imagePath)
if err != nil {
return "", fmt.Errorf("unable to open file: %v", err)
}
defer pic.Close()
}
var imData image.Image
if pathIsURl {
imData, _, err = image.Decode(bytes.NewReader(urlImgBytes))
} else {
imData, _, err = image.Decode(pic)
}
if err != nil {
return "", fmt.Errorf("error decoding %v. %v", imagePath, err)
}
imgSet, err := imgManip.ConvertToAsciiPixels(imData, dimensions, flipX, flipY)
if err != nil {
return "", fmt.Errorf("%v", err)
}
asciiSet := imgManip.ConvertToAscii(imgSet, negative, colored, complex, customMap)
ascii := flattenAscii(asciiSet, colored)
// Save ascii art before printing it, if --save flag is passed
if savePath != "" {
if err := saveAsciiArt(asciiSet, imagePath, savePath, urlImgName); err != nil {
fmt.Printf("Error: %v\n\n", err)
os.Exit(0) // Because this error will be thrown for every image passed to this function if we use "return"
}
}
result := strings.Join(ascii, "\n")
return result, nil
}
func checkOS() string {
if string(os.PathSeparator) == "/" && string(os.PathListSeparator) == ":" {
return "linux"
} else {
return "windows"
}
}
// flattenAscii flattens a two-dimensional grid of ascii characters into a one dimension
// of lines of ascii
func flattenAscii(asciiSet [][]imgManip.AsciiChar, color bool) []string {
var ascii []string
for _, line := range asciiSet {
var tempAscii []string
for i := 0; i < len(line); i++ {
if color {
tempAscii = append(tempAscii, line[i].Colored)
} else {
tempAscii = append(tempAscii, line[i].Simple)
}
}
ascii = append(ascii, strings.Join(tempAscii, ""))
}
return ascii
}
func saveAsciiArt(asciiSet [][]imgManip.AsciiChar, imagePath, savePath, urlImgName string) error {
// To make sure uncolored ascii art is the one saved
saveAscii := flattenAscii(asciiSet, false)
saveFileName, err := createSaveFileName(imagePath, urlImgName)
if err != nil {
return err
}
savePathLastChar := string(savePath[len(savePath)-1])
// Check if path is closed with appropriate path separator (depending on OS)
if savePathLastChar != string(os.PathSeparator) {
if checkOS() == "linux" {
savePath += "/"
} else {
savePath += "\\"
}
}
// If path exists
if _, err := os.Stat(savePath); !os.IsNotExist(err) {
return ioutil.WriteFile(savePath+saveFileName, []byte(strings.Join(saveAscii, "\n")), 0666)
} else {
return fmt.Errorf("save path %v does exist", savePath)
}
}
func createSaveFileName(imagePath string, urlImgName string) (string, error) {
if urlImgName != "" {
return urlImgName + "-ascii-art.txt", nil
}
fileInfo, err := os.Stat(imagePath)
if err != nil {
return "", err
}
currName := fileInfo.Name()
currExt := path.Ext(currName)
newName := currName[:len(currName)-len(currExt)] // e.g. Grabs myImage from myImage.jpeg
// Something like myImage.jpeg-ascii-art.txt
return newName + "." + currExt[1:] + "-ascii-art.txt", nil
}

View File

@ -16,14 +16,10 @@ limitations under the License.
package cmd package cmd
import ( import (
"bytes"
"fmt" "fmt"
"image"
"io/ioutil"
"net/http"
"os" "os"
"path"
"strings" "github.com/TheZoraiz/ascii-image-converter/aic_package"
// Image format initialization // Image format initialization
_ "image/jpeg" _ "image/jpeg"
@ -34,8 +30,6 @@ import (
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
imgMani "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
"github.com/asaskevich/govalidator"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -44,19 +38,21 @@ import (
var ( var (
// Flags // Flags
cfgFile string cfgFile string
compl bool complex bool
dimensions []int dimensions []int
savePath string savePath string
negative bool negative bool
formatsTrue bool formatsTrue bool
colored bool colored bool
customMap string customMap string
flipX bool
flipY bool
// Root commands // Root commands
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "ascii-image-converter [image paths/urls]", Use: "ascii-image-converter [image paths/urls]",
Short: "Converts images into ascii art", Short: "Converts images into ascii art",
Version: "1.2.5", Version: "1.2.6",
Long: "This tool converts images into ascii art and prints them on the terminal.\nFurther configuration can be managed with flags.", 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 // Not RunE since help text is getting larger and seeing it for every error impacts user experience
@ -67,12 +63,6 @@ var (
return return
} }
numberOfDimensions := len(dimensions)
if dimensions != nil && numberOfDimensions != 2 {
fmt.Printf("-d requires 2 dimensions, got %v\n\n", numberOfDimensions)
return
}
if len(args) < 1 { if len(args) < 1 {
fmt.Printf("Error: Need at least 1 image path/url\n\n") fmt.Printf("Error: Need at least 1 image path/url\n\n")
cmd.Help() cmd.Help()
@ -84,162 +74,33 @@ var (
return return
} }
flags := map[string]interface{}{
"complex": complex,
"dimensions": dimensions,
"savePath": savePath,
"negative": negative,
"colored": colored,
"customMap": customMap,
"flipX": flipX,
"flipY": flipY,
}
// fmt.Println(flags)
for _, imagePath := range args { for _, imagePath := range args {
convertImage(imagePath)
if asciiArt, err := aic_package.ConvertImage(imagePath, flags); err == nil {
fmt.Printf("%s", asciiArt)
} else {
fmt.Printf("Error: %v", err)
}
fmt.Println()
} }
}, },
} }
) )
func checkOS() string {
if string(os.PathSeparator) == "/" && string(os.PathListSeparator) == ":" {
return "linux"
} else {
return "windows"
}
}
func convertImage(imagePath string) {
// Declared at the start since some variables are initially used in conditional blocks
var pic *os.File
var urlImgBytes []byte
var urlImgName string = ""
var err error
pathIsURl := govalidator.IsRequestURL(imagePath)
// Different modes of reading data depending upon whether or not imagePath is a url
if pathIsURl {
fmt.Printf("Fetching image from url...\r")
retrievedImage, err := http.Get(imagePath)
if err != nil {
fmt.Printf("Error fetching image: %v\n\n", err)
return
}
urlImgBytes, err = ioutil.ReadAll(retrievedImage.Body)
if err != nil {
fmt.Printf("Failed to read fetched content: %v\n\n", err)
return
}
defer retrievedImage.Body.Close()
urlImgName = path.Base(imagePath)
fmt.Printf(" \r") // To erase "Fetching image from url..." text from console
} else {
pic, err = os.Open(imagePath)
if err != nil {
fmt.Printf("Unable to open file: %v\n\n", err)
return
}
defer pic.Close()
}
var imData image.Image
if pathIsURl {
imData, _, err = image.Decode(bytes.NewReader(urlImgBytes))
} else {
imData, _, err = image.Decode(pic)
}
if err != nil {
fmt.Printf("Error decoding %v. %v\n\n", imagePath, err)
return
}
imgSet, err := imgMani.ConvertToAsciiPixels(imData, dimensions)
if err != nil {
fmt.Printf("Error: %v\n\n", err)
return
}
asciiSet := imgMani.ConvertToAscii(imgSet, negative, colored, compl, customMap)
ascii := flattenAscii(asciiSet, colored)
// Save ascii art before printing it, if --save flag is passed
if savePath != "" {
if err := saveAsciiArt(asciiSet, imagePath, urlImgName); err != nil {
fmt.Printf("Error: %v\n\n", err)
os.Exit(0) // Because this error will be thrown for every image passed to this function if we use "return"
}
}
for _, line := range ascii {
fmt.Println(line)
}
}
// flattenAscii flattens a two-dimensional grid of ascii characters into a one dimension
// of lines of ascii
func flattenAscii(asciiSet [][]imgMani.AsciiChar, color bool) []string {
var ascii []string
for _, line := range asciiSet {
var tempAscii []string
for i := 0; i < len(line); i++ {
if color {
tempAscii = append(tempAscii, line[i].Colored)
} else {
tempAscii = append(tempAscii, line[i].Simple)
}
}
ascii = append(ascii, strings.Join(tempAscii, ""))
}
return ascii
}
func saveAsciiArt(asciiSet [][]imgMani.AsciiChar, imagePath string, urlImgName string) error {
// To make sure uncolored ascii art is the one saved
saveAscii := flattenAscii(asciiSet, false)
saveFileName, err := createSaveFileName(imagePath, urlImgName)
if err != nil {
return err
}
savePathLastChar := string(savePath[len(savePath)-1])
// Check if path is closed with appropriate path separator (depending on OS)
if savePathLastChar != string(os.PathSeparator) {
if checkOS() == "linux" {
savePath += "/"
} else {
savePath += "\\"
}
}
// If path exists
if _, err := os.Stat(savePath); !os.IsNotExist(err) {
return ioutil.WriteFile(savePath+saveFileName, []byte(strings.Join(saveAscii, "\n")), 0666)
} else {
return fmt.Errorf("Save path does not exist.")
}
}
func createSaveFileName(imagePath string, urlImgName string) (string, error) {
if urlImgName != "" {
return urlImgName + "-ascii-art.txt", nil
}
fileInfo, err := os.Stat(imagePath)
if err != nil {
return "", err
}
currName := fileInfo.Name()
currExt := path.Ext(currName)
newName := currName[:len(currName)-len(currExt)] // e.g. Grabs myImage from myImage.jpeg
// Something like myImage.jpeg-ascii-art.txt
return newName + "." + currExt[1:] + "-ascii-art.txt", nil
}
// Cobra configuration from here on // Cobra configuration from here on
func Execute() { func Execute() {
@ -254,12 +115,14 @@ func init() {
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ascii-image-converter.yaml)") // 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 the colors from original image\n(Can work with the -n flag)\n") rootCmd.PersistentFlags().BoolVarP(&colored, "color", "C", false, "Display ascii art with the colors from original image\n(Can work with the -n flag)\n")
rootCmd.PersistentFlags().BoolVarP(&compl, "complex", "c", false, "Display ascii characters in a larger range\nMay result in higher quality\n") rootCmd.PersistentFlags().BoolVarP(&complex, "complex", "c", false, "Display ascii characters in a larger range\nMay result in higher quality\n")
rootCmd.PersistentFlags().IntSliceVarP(&dimensions, "dimensions", "d", nil, "Set width and height for ascii art in CHARACTER length\ne.g. -d 100,30 (defaults to terminal height)\n") rootCmd.PersistentFlags().IntSliceVarP(&dimensions, "dimensions", "d", nil, "Set width and height for ascii art in CHARACTER length\ne.g. -d 100,30 (defaults to terminal height)\n")
rootCmd.PersistentFlags().BoolVarP(&formatsTrue, "formats", "f", false, "Display supported image formats\n") rootCmd.PersistentFlags().BoolVarP(&formatsTrue, "formats", "f", false, "Display supported image formats\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(Cancels --complex flag)\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(Cancels --complex flag)\n")
rootCmd.PersistentFlags().BoolVarP(&negative, "negative", "n", false, "Display ascii art in negative colors\n(Can work with the --color flag)\n") rootCmd.PersistentFlags().BoolVarP(&negative, "negative", "n", false, "Display ascii art in negative colors\n(Can work with the --color flag)\n")
rootCmd.PersistentFlags().StringVarP(&savePath, "save", "s", "", "Save ascii art in the format:\n<image-name>.<image-extension>-ascii-art.txt\nFile will be saved in passed path\n(pass . for current directory)\n") rootCmd.PersistentFlags().StringVarP(&savePath, "save", "s", "", "Save ascii art in the format:\n<image-name>.<image-extension>-ascii-art.txt\nFile will be saved in passed path\n(pass . for current directory)\n")
rootCmd.PersistentFlags().BoolVarP(&flipX, "flipX", "x", false, "Flip ascii art horizontally\n")
rootCmd.PersistentFlags().BoolVarP(&flipY, "flipY", "y", false, "Flip ascii art vertically\n")
defaultUsageTemplate := rootCmd.UsageTemplate() defaultUsageTemplate := rootCmd.UsageTemplate()
rootCmd.SetUsageTemplate("\nCopyright © 2021 Zoraiz Hassan <hzoraiz8@gmail.com>\n" + rootCmd.SetUsageTemplate("\nCopyright © 2021 Zoraiz Hassan <hzoraiz8@gmail.com>\n" +

BIN
example_gifs/flipx.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
example_gifs/flipy.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

View File

@ -36,7 +36,7 @@ type AsciiPixel struct {
// //
// The returned 2D AsciiPixel slice contains each corresponding pixel's values. Grayscale value // The returned 2D AsciiPixel slice contains each corresponding pixel's values. Grayscale value
// ranges from 0 to 65535, while RGB values are separate. // ranges from 0 to 65535, while RGB values are separate.
func ConvertToAsciiPixels(img image.Image, dimensions []int) ([][]AsciiPixel, error) { func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY bool) ([][]AsciiPixel, error) {
var asciiWidth, asciiHeight int var asciiWidth, asciiHeight int
var smallImg image.Image var smallImg image.Image
@ -79,7 +79,7 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int) ([][]AsciiPixel, er
defaultTermWidth, _ := consolesize.GetConsoleSize() defaultTermWidth, _ := consolesize.GetConsoleSize()
defaultTermWidth -= 1 defaultTermWidth -= 1
if dimensions[0] > defaultTermWidth { if dimensions[0] > defaultTermWidth {
return nil, fmt.Errorf("Set width is larger than terminal width") return nil, fmt.Errorf("set width is larger than terminal width")
} }
} }
@ -118,5 +118,29 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int) ([][]AsciiPixel, er
imgSet[y] = temp imgSet[y] = temp
} }
// This rarely affects performance since the ascii art 2D slice size isn't that large
if flipX || flipY {
imgSet = reverse(imgSet, flipX, flipY)
}
return imgSet, nil 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
}

View File

@ -1,6 +1,6 @@
name: ascii-image-converter name: ascii-image-converter
base: core18 base: core18
version: "1.2.5" version: "1.2.6"
summary: Converts images into ascii art summary: Converts images into ascii art
description: | description: |
This tool converts images into ascii format and prints them onto the terminal window. This tool converts images into ascii format and prints them onto the terminal window.