Release v1.4.0
This commit is contained in:
parent
01ab53a4d3
commit
97f1d679b0
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
project_name: ascii-image-converter
|
||||
|
||||
before:
|
||||
|
|
|
|||
145
README.md
145
README.md
|
|
@ -1,12 +1,13 @@
|
|||
|
||||
# ascii-image-converter
|
||||
|
||||
[](https://github.com/TheZoraiz/ascii-image-converter/releases/latest)
|
||||
[](https://github.com/TheZoraiz/ascii-image-converter/blob/master/LICENSE.txt)
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||

|
||||
[](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. 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. GIFs are now experimentally supported as well.
|
||||
|
||||
It's also available as a package to be used in Go applications.
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ Image formats currently supported:
|
|||
* BMP
|
||||
* WEBP
|
||||
* TIFF/TIF
|
||||
* GIF (Experimental)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
|
@ -88,7 +90,7 @@ Now, restart any open command prompt and execute "ascii-image-converter -h" for
|
|||
|
||||
Note: Decrease font size or increase terminal width (like zooming out) for maximum quality ascii art
|
||||
|
||||
The basic usage for converting an image into ascii art is as follows. You can also supply multiple image paths and urls.
|
||||
The basic usage for converting an image into ascii art is as follows. You can also supply multiple image paths and urls as well as a GIF.
|
||||
|
||||
```
|
||||
ascii-image-converter [image paths/urls]
|
||||
|
|
@ -110,10 +112,10 @@ Multiple images:
|
|||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/all.gif">
|
||||
</p>
|
||||
|
||||
Image from URL:
|
||||
GIF:
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/url.gif">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/gif-example.gif">
|
||||
</p>
|
||||
|
||||
### Flags
|
||||
|
|
@ -132,15 +134,6 @@ ascii-image-converter [image paths/urls] --color
|
|||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/color.gif">
|
||||
</p>
|
||||
|
||||
#### --complex OR -c
|
||||
|
||||
Print the image with a wider array of ascii characters for more detailed lighting density. Sometimes improves accuracy.
|
||||
```
|
||||
ascii-image-converter [image paths/urls] -c
|
||||
# Or
|
||||
ascii-image-converter [image paths/urls] --complex
|
||||
```
|
||||
|
||||
#### --dimensions OR -d
|
||||
|
||||
Note: Don't immediately append another flag with -d
|
||||
|
|
@ -153,7 +146,7 @@ ascii-image-converter [image paths/urls] --dimensions <width>,<height>
|
|||
```
|
||||
Example:
|
||||
```
|
||||
ascii-image-converter [image paths/urls] -d 100,30
|
||||
ascii-image-converter [image paths/urls] -d 60,30
|
||||
```
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/dimensions.gif">
|
||||
|
|
@ -195,42 +188,22 @@ ascii-image-converter [image paths/urls] --negative
|
|||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/negative.gif">
|
||||
</p>
|
||||
|
||||
#### --save-img OR -s
|
||||
|
||||
Note: Don't immediately append another flag with -s
|
||||
|
||||
Saves the ascii as a PNG image with the name `<image-name>-ascii-art.png` in the directory path passed to the flag. Can work with both --color and --negative flag.
|
||||
|
||||
Example for current directory:
|
||||
#### --complex OR -c
|
||||
|
||||
Print the image with a wider array of ascii characters for more detailed lighting density. Sometimes improves accuracy.
|
||||
```
|
||||
ascii-image-converter [image paths/urls] --save-img .
|
||||
ascii-image-converter [image paths/urls] -c
|
||||
# Or
|
||||
ascii-image-converter [image paths/urls] -s .
|
||||
ascii-image-converter [image paths/urls] --complex
|
||||
```
|
||||
|
||||
#### --save-txt
|
||||
|
||||
Similar to --save-img as but it creates a TXT file with the name `<image-name>-ascii-art.txt` in the directory path passed to the flag. Only saves uncolored text.
|
||||
|
||||
Example for current directory:
|
||||
#### --full OR -f
|
||||
|
||||
Print ascii art that fits the terminal width while maintaining aspect ratio.
|
||||
```
|
||||
ascii-image-converter [image paths/urls] --save-txt .
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/save-image-txt-demo.gif">
|
||||
</p>
|
||||
|
||||
#### --formats OR -f
|
||||
|
||||
Display supported image formats.
|
||||
|
||||
```
|
||||
ascii-image-converter [image paths/urls] --formats
|
||||
# Or
|
||||
ascii-image-converter [image paths/urls] -f
|
||||
# Or
|
||||
ascii-image-converter [image paths/urls] --full
|
||||
```
|
||||
|
||||
#### --flipX OR -x
|
||||
|
|
@ -252,10 +225,50 @@ ascii-image-converter [image paths/urls] --flipY
|
|||
ascii-image-converter [image paths/urls] -y
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### --save-img OR -s
|
||||
|
||||
Note: Don't immediately append another flag with -s
|
||||
|
||||
Saves the ascii as a PNG image with the name `<image-name>-ascii-art.png` in the directory path passed to the flag. Can work with both --color and --negative flag.
|
||||
|
||||
Example for current directory:
|
||||
|
||||
```
|
||||
ascii-image-converter [image paths/urls] --save-img .
|
||||
# Or
|
||||
ascii-image-converter [image paths/urls] -s .
|
||||
```
|
||||
|
||||
#### --save-txt
|
||||
|
||||
Similar to --save-img but it creates a TXT file with the name `<image-name>-ascii-art.txt` in the directory path passed to the flag. Only saves uncolored text.
|
||||
|
||||
Example for current directory:
|
||||
|
||||
```
|
||||
ascii-image-converter [image paths/urls] --save-txt .
|
||||
```
|
||||
|
||||
#### --save-gif
|
||||
|
||||
Note: This is an experimental feature and may not result in the finest quality GIFs, because all GIFs still aren't supported by ascii-image-converter.
|
||||
|
||||
Saves the passed GIF as an ascii art GIF with the name `<image-name>-ascii-art.gif` in the directory path passed to the flag.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/flipx-flipy.gif">
|
||||
<img src="https://raw.githubusercontent.com/TheZoraiz/ascii-image-converter/master/example_gifs/save.gif">
|
||||
</p>
|
||||
|
||||
#### --formats
|
||||
|
||||
Display supported input formats.
|
||||
|
||||
```
|
||||
ascii-image-converter --formats
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
You can combine flags as well. Following command outputs colored and negative ascii art, flips ascii art horizontally and vertically, with fixed 60 by 30 character dimensions, custom defined ascii characters " .-=+#@" and saves a generated image and .txt file in current directory as well.
|
||||
|
|
@ -267,7 +280,7 @@ ascii-image-converter [image paths/urls] -Cnxyd 60,30 -m " .-=+#@" -s . --save-t
|
|||
|
||||
## Library Usage
|
||||
|
||||
Note: The library may throw errors during Go tests due to some unresolved bugs with the [consolesize-go](https://github.com/nathan-fiscaletti/consolesize-go) package (Only during tests, not main program execution). Looking into this issue.
|
||||
Note: The library may throw errors during Go tests due to some unresolved bugs with the [consolesize-go](https://github.com/nathan-fiscaletti/consolesize-go) package (Only during tests, not main program execution). Furthermore, GIF conversion is not advised as it isn't fully library-compatible yet.
|
||||
|
||||
First, install the library with:
|
||||
```
|
||||
|
|
@ -275,6 +288,7 @@ go get github.com/TheZoraiz/ascii-image-converter/aic_package
|
|||
```
|
||||
|
||||
The library is to be used as follows:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
|
|
@ -285,30 +299,41 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
// If image is in current directory. This can also be a URL to an image.
|
||||
imagePath := "myImage.jpeg"
|
||||
// If file is in current directory. This can also be a URL to an image or gif.
|
||||
filePath := "myImage.jpeg"
|
||||
|
||||
flags := aic_package.DefaultFlags()
|
||||
|
||||
// This part is optional. You can directly pass flags variable to ConvertImage() if you wish.
|
||||
// This part is optional.
|
||||
// You can directly pass default flags variable to Convert() if you wish.
|
||||
// For clarity, all flags are covered in this example, but you can use specific ones.
|
||||
flags["complex"] = true // Use complex character set
|
||||
flags["dimensions"] = []int{50, 25} // 50 by 25 ascii art size
|
||||
flags["saveTxtPath"] = "." // Save generated text in same directory
|
||||
flags["saveImagePath"] = "." // Save generated PNG image in same 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
|
||||
flags.Complex = true // Use complex character set
|
||||
flags.Dimensions = []int{50, 25} // 50 by 25 ascii art size
|
||||
flags.SaveTxtPath = "." // Save generated text in same directory
|
||||
flags.SaveImagePath = "." // Save generated PNG image in same directory
|
||||
flags.SaveGifPath = "." // If gif was provided, save ascii art gif in same 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
|
||||
flags.Full = true // Display ascii art that fills the terminal width
|
||||
|
||||
// Return ascii art as a single string
|
||||
asciiArt, err := aic_package.ConvertImage(imagePath, flags)
|
||||
// For an image
|
||||
asciiArt, err := aic_package.Convert(filePath, flags)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
|
||||
fmt.Printf("%v\n", asciiArt)
|
||||
|
||||
// GIF CONVERSION IS AN EXPERIMENTAL FEATURE
|
||||
// For a gif. This function may run infinitely, depending on the gif
|
||||
// Work needs to be done on gif conversion to be more library-compatible
|
||||
_, err := aic_package.Convert(filePath, flags)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
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"
|
||||
"image/color/palette"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
imgManip "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
|
||||
)
|
||||
|
||||
type GifFrame struct {
|
||||
asciiCharSet [][]imgManip.AsciiChar
|
||||
delay int
|
||||
}
|
||||
|
||||
/*
|
||||
This function grabs each image frame from passed gif and turns it into ascii art. If SaveGifPath flag is passed,
|
||||
it'll turn each ascii art into an image instance of the same dimensions as the original gif and save them
|
||||
as an ascii art gif.
|
||||
|
||||
Multi-threading has been implemented in multiple places due to long execution time
|
||||
*/
|
||||
func pathIsGif(gifPath, urlImgName string, pathIsURl bool, urlImgBytes []byte, localGif *os.File) (string, error) {
|
||||
|
||||
var (
|
||||
originalGif *gif.GIF
|
||||
err error
|
||||
)
|
||||
|
||||
if pathIsURl {
|
||||
originalGif, err = gif.DecodeAll(bytes.NewReader(urlImgBytes))
|
||||
} else {
|
||||
originalGif, err = gif.DecodeAll(localGif)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't decode %v: %v", gifPath, err)
|
||||
}
|
||||
|
||||
var (
|
||||
asciiArtSet = make([]string, len(originalGif.Image))
|
||||
gifFramesSlice = make([]GifFrame, len(originalGif.Image))
|
||||
|
||||
counter = 0
|
||||
concurrentProcesses = 0
|
||||
wg sync.WaitGroup
|
||||
hostCpuCount = runtime.NumCPU()
|
||||
)
|
||||
|
||||
fmt.Printf("Generating ascii art... 0%%\r")
|
||||
|
||||
// Get first frame of gif and its dimensions
|
||||
firstGifFrame := originalGif.Image[0].SubImage(originalGif.Image[0].Rect)
|
||||
firstGifFrameWidth := firstGifFrame.Bounds().Dx()
|
||||
firstGifFrameHeight := firstGifFrame.Bounds().Dy()
|
||||
|
||||
// Multi-threaded loop to decrease execution time
|
||||
for i, frame := range originalGif.Image {
|
||||
|
||||
wg.Add(1)
|
||||
concurrentProcesses++
|
||||
|
||||
go func(i int, frame *image.Paletted) {
|
||||
|
||||
frameImage := frame.SubImage(frame.Rect)
|
||||
|
||||
// If a frame is found that is smaller than the first frame, then this gif contains smaller subimages that are
|
||||
// positioned inside the original gif. This behavior isn't supported by this app
|
||||
if firstGifFrameWidth != frameImage.Bounds().Dx() || firstGifFrameHeight != frameImage.Bounds().Dy() {
|
||||
fmt.Println("Error: GIF contains subimages smaller than default width and height\nProcess aborted because ascii-image-converter doesn't support subimage placement and transparency in GIFs\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var imgSet [][]imgManip.AsciiPixel
|
||||
|
||||
imgSet, err = imgManip.ConvertToAsciiPixels(frameImage, dimensions, flipX, flipY, full)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
asciiCharSet := imgManip.ConvertToAscii(imgSet, negative, colored, complex, customMap)
|
||||
gifFramesSlice[i].asciiCharSet = asciiCharSet
|
||||
gifFramesSlice[i].delay = originalGif.Delay[i]
|
||||
|
||||
ascii := flattenAscii(asciiCharSet, colored)
|
||||
|
||||
asciiArtSet[i] = strings.Join(ascii, "\n")
|
||||
|
||||
counter++
|
||||
percentage := int((float64(counter) / float64(len(originalGif.Image))) * 100)
|
||||
fmt.Printf("Generating ascii art... " + strconv.Itoa(percentage) + "%%\r")
|
||||
|
||||
wg.Done()
|
||||
|
||||
}(i, frame)
|
||||
|
||||
// Limit concurrent processes according to host's CPU count to avoid overwhelming memory
|
||||
if concurrentProcesses == hostCpuCount {
|
||||
wg.Wait()
|
||||
concurrentProcesses = 0
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Printf(" \r")
|
||||
|
||||
// Save ascii art as .gif image before displaying it, if --save-gif flag is passed
|
||||
if saveGifPath != "" {
|
||||
|
||||
// Storing save path string before executing the concurrent ascii art to gif conversion
|
||||
// This is done to avoid wasting time for invalid path errors
|
||||
|
||||
saveFileName, err := createSaveFileName(gifPath, urlImgName, "-ascii-art.gif")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fullPathName, err := getFullSavePath(saveFileName, saveGifPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't save file: %v", err)
|
||||
}
|
||||
|
||||
// Initializing some constants for gif. Done outside loop to save execution
|
||||
outGif := &gif.GIF{
|
||||
LoopCount: originalGif.LoopCount,
|
||||
}
|
||||
opts := gif.Options{
|
||||
NumColors: 256,
|
||||
Drawer: draw.FloydSteinberg,
|
||||
}
|
||||
|
||||
// Initializing slices for each ascii art image as well as delay
|
||||
var (
|
||||
palettedImageSlice = make([]*image.Paletted, len(gifFramesSlice))
|
||||
delaySlice = make([]int, len(gifFramesSlice))
|
||||
)
|
||||
|
||||
// For the purpose of displaying counter and limiting concurrent processes
|
||||
counter = 0
|
||||
concurrentProcesses = 0
|
||||
|
||||
fmt.Printf("Saving gif... 0%%\r")
|
||||
|
||||
// Multi-threaded loop to decrease execution time
|
||||
for i, gifFrame := range gifFramesSlice {
|
||||
|
||||
wg.Add(1)
|
||||
concurrentProcesses++
|
||||
|
||||
go func(i int, gifFrame GifFrame) {
|
||||
|
||||
img := originalGif.Image[i].SubImage(originalGif.Image[i].Rect)
|
||||
|
||||
tempImg, err := createGifFrameToSave(
|
||||
gifFrame.asciiCharSet,
|
||||
img,
|
||||
colored,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Following code takes tempImg as image.Image instance and converts it into *image.Paletted instance
|
||||
b := tempImg.Bounds()
|
||||
|
||||
palettedImg := image.NewPaletted(b, palette.Plan9[:opts.NumColors])
|
||||
|
||||
opts.Drawer.Draw(palettedImg, b, tempImg, image.Point{})
|
||||
|
||||
palettedImageSlice[i] = palettedImg
|
||||
delaySlice[i] = gifFrame.delay
|
||||
|
||||
counter++
|
||||
percentage := int((float64(counter) / float64(len(gifFramesSlice))) * 100)
|
||||
fmt.Printf("Saving gif... " + strconv.Itoa(percentage) + "%%\r")
|
||||
|
||||
wg.Done()
|
||||
|
||||
}(i, gifFrame)
|
||||
|
||||
// Limit concurrent processes according to host's CPU count to avoid overwhelming memory
|
||||
if concurrentProcesses == hostCpuCount {
|
||||
wg.Wait()
|
||||
concurrentProcesses = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
outGif.Image = palettedImageSlice
|
||||
outGif.Delay = delaySlice
|
||||
|
||||
gifFile, err := os.OpenFile(fullPathName, os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't save file: %v", err)
|
||||
}
|
||||
defer gifFile.Close()
|
||||
|
||||
gif.EncodeAll(gifFile, outGif)
|
||||
|
||||
fmt.Printf(" \r")
|
||||
}
|
||||
|
||||
// Display the gif
|
||||
loopCount := 0
|
||||
for {
|
||||
for i, asciiFrame := range asciiArtSet {
|
||||
clearScreen()
|
||||
fmt.Println(asciiFrame)
|
||||
time.Sleep(time.Duration((time.Second * time.Duration(originalGif.Delay[i])) / 100))
|
||||
}
|
||||
|
||||
// If gif is infinite loop
|
||||
if originalGif.LoopCount == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
loopCount++
|
||||
if loopCount == originalGif.LoopCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -20,232 +20,67 @@ 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.
|
||||
// Can be sent directly to ConvertImage() for default ascii art
|
||||
func DefaultFlags() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"complex": false,
|
||||
"dimensions": nil,
|
||||
"saveTxtPath": "",
|
||||
"saveImagePath": "",
|
||||
"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
|
||||
"saveTxtPath": string, // System path to save the ascii art string as a .txt file. Pass "" to ignore
|
||||
"saveImagePath": string, // System path to save the ascii art string as a .png file. 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
|
||||
}
|
||||
This function decodes the passed image and returns as an ascii art string, optionaly saving it as a .txt and/or .png file
|
||||
*/
|
||||
func ConvertImage(imagePath string, flags map[string]interface{}) (string, error) {
|
||||
func pathIsImage(imagePath, urlImgName string, pathIsURl bool, urlImgBytes []byte, localImg *os.File) (string, error) {
|
||||
|
||||
var dimensions []int
|
||||
if flags["dimensions"] == nil {
|
||||
dimensions = nil
|
||||
} else {
|
||||
dimensions = flags["dimensions"].([]int)
|
||||
}
|
||||
|
||||
complex := flags["complex"].(bool)
|
||||
saveTxtPath := flags["saveTxtPath"].(string)
|
||||
saveImagePath := flags["saveImagePath"].(string)
|
||||
negative := flags["negative"].(bool)
|
||||
colored := flags["colored"].(bool)
|
||||
customMap := flags["customMap"].(string)
|
||||
flipX := flags["flipX"].(bool)
|
||||
flipY := flags["flipY"].(bool)
|
||||
|
||||
// Declared at the start since some variables are initially used in conditional blocks
|
||||
var (
|
||||
pic *os.File
|
||||
urlImgBytes []byte
|
||||
urlImgName string = ""
|
||||
err error
|
||||
imData image.Image
|
||||
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 fetch 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)
|
||||
imData, _, err = image.Decode(localImg)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't decode %v: %v", imagePath, err)
|
||||
}
|
||||
|
||||
// Ascii art height and width are important for creating png image to save
|
||||
imgSet, imgWidth, imgHeight, err := imgManip.ConvertToAsciiPixels(imData, dimensions, flipX, flipY)
|
||||
imgSet, err := imgManip.ConvertToAsciiPixels(imData, dimensions, flipX, flipY, full)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
asciiSet := imgManip.ConvertToAscii(imgSet, negative, colored, complex, customMap)
|
||||
|
||||
// Save ascii art as .png image before printing it, if --save-img flag is passed
|
||||
if saveImagePath != "" {
|
||||
if err := createImageToSave(asciiSet, imgWidth, imgHeight, colored, saveImagePath, imagePath, urlImgName); err != nil {
|
||||
if err := createImageToSave(
|
||||
asciiSet,
|
||||
colored,
|
||||
saveImagePath,
|
||||
imagePath,
|
||||
urlImgName,
|
||||
); err != nil {
|
||||
|
||||
return "", fmt.Errorf("can't save file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save ascii art as .txt file before printing it, if --save-txt flag is passed
|
||||
if saveTxtPath != "" {
|
||||
if err := saveAsciiArt(asciiSet, imagePath, saveTxtPath, urlImgName); err != nil {
|
||||
if err := saveAsciiArt(
|
||||
asciiSet,
|
||||
imagePath,
|
||||
saveTxtPath,
|
||||
urlImgName,
|
||||
); err != nil {
|
||||
|
||||
return "", fmt.Errorf("can't save file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ascii := flattenAscii(asciiSet, colored)
|
||||
|
||||
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, colored bool) []string {
|
||||
var ascii []string
|
||||
|
||||
for _, line := range asciiSet {
|
||||
var tempAscii []string
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
if colored {
|
||||
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 as .txt
|
||||
saveAscii := flattenAscii(asciiSet, false)
|
||||
|
||||
saveFileName, err := createSaveFileName(imagePath, urlImgName, ".txt")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns new image file name along with extension
|
||||
func createSaveFileName(imagePath, urlImgName, newExtension string) (string, error) {
|
||||
if urlImgName != "" {
|
||||
currExt := path.Ext(urlImgName)
|
||||
newName := urlImgName[:len(urlImgName)-len(currExt)] // e.g. Grabs myImage from myImage.jpeg
|
||||
|
||||
return newName + "-ascii-art" + newExtension, 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
|
||||
|
||||
return newName + "-ascii-art" + newExtension, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
// 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"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
Dimensions []int
|
||||
Complex bool
|
||||
SaveTxtPath string
|
||||
SaveImagePath string
|
||||
SaveGifPath string
|
||||
Negative bool
|
||||
Colored bool
|
||||
CustomMap string
|
||||
FlipX bool
|
||||
FlipY bool
|
||||
Full bool
|
||||
}
|
||||
|
||||
var (
|
||||
dimensions []int
|
||||
complex bool
|
||||
saveTxtPath string
|
||||
saveImagePath string
|
||||
saveGifPath string
|
||||
negative bool
|
||||
colored bool
|
||||
customMap string
|
||||
flipX bool
|
||||
flipY bool
|
||||
full bool
|
||||
)
|
||||
|
||||
// Return default configuration for flags.
|
||||
// Can be sent directly to ConvertImage() for default ascii art
|
||||
func DefaultFlags() Flags {
|
||||
return Flags{
|
||||
Complex: false,
|
||||
Dimensions: nil,
|
||||
SaveTxtPath: "",
|
||||
SaveImagePath: "",
|
||||
SaveGifPath: "",
|
||||
Negative: false,
|
||||
Colored: false,
|
||||
CustomMap: "",
|
||||
FlipX: false,
|
||||
FlipY: false,
|
||||
Full: false,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Convert takes an image or gif path/url as its first argument
|
||||
and a Flags literal as the second argument, with which it alters
|
||||
the returned ascii art string.
|
||||
|
||||
The "flags" argument should be declared as follows before passing:
|
||||
|
||||
flags := aic_package.Flags{
|
||||
Complex: bool, // Pass true for using complex character set
|
||||
Dimensions: []int, // Pass 2 integer dimensions. Pass nil to ignore
|
||||
SaveTxtPath: string, // System path to save the ascii art string as a .txt file. Pass "" to ignore
|
||||
SavefilePath: string, // System path to save the ascii art string as a .png file. Pass "" to ignore
|
||||
SaveGifPath : string, // System path to save the ascii art gif as a .gif file. 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
|
||||
Full: bool, // Pass true to use full terminal as ascii height
|
||||
}
|
||||
*/
|
||||
func Convert(filePath string, flags Flags) (string, error) {
|
||||
|
||||
if flags.Dimensions == nil {
|
||||
dimensions = nil
|
||||
} else {
|
||||
dimensions = flags.Dimensions
|
||||
}
|
||||
complex = flags.Complex
|
||||
saveTxtPath = flags.SaveTxtPath
|
||||
saveImagePath = flags.SaveImagePath
|
||||
saveGifPath = flags.SaveGifPath
|
||||
negative = flags.Negative
|
||||
colored = flags.Colored
|
||||
customMap = flags.CustomMap
|
||||
flipX = flags.FlipX
|
||||
flipY = flags.FlipY
|
||||
full = flags.Full
|
||||
|
||||
// Declared at the start since some variables are initially used in conditional blocks
|
||||
var (
|
||||
localFile *os.File
|
||||
urlImgBytes []byte
|
||||
urlImgName string = ""
|
||||
err error
|
||||
)
|
||||
|
||||
pathIsURl := govalidator.IsRequestURL(filePath)
|
||||
|
||||
// Different modes of reading data depending upon whether or not filePath is a url
|
||||
if pathIsURl {
|
||||
fmt.Printf("Fetching file from url...\r")
|
||||
|
||||
retrievedImage, err := http.Get(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't fetch content: %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(filePath)
|
||||
fmt.Printf(" \r") // To erase "Fetching image from url..." text from console
|
||||
|
||||
} else {
|
||||
|
||||
localFile, err = os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to open file: %v", err)
|
||||
}
|
||||
defer localFile.Close()
|
||||
|
||||
}
|
||||
|
||||
if path.Ext(filePath) == ".gif" {
|
||||
return pathIsGif(filePath, urlImgName, pathIsURl, urlImgBytes, localFile)
|
||||
} else {
|
||||
return pathIsImage(filePath, urlImgName, pathIsURl, urlImgBytes, localFile)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
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 (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
_ "embed"
|
||||
|
||||
imgManip "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
)
|
||||
|
||||
/*
|
||||
Unlike createImageToSave(), this function is optimized to maintain original image dimensions and shrink ascii
|
||||
art font size to match it. This allows for greater execution speed, which is necessary since a gif contains
|
||||
multiple images that need to be converted to ascii art, and the potential loss of ascii art quality (since
|
||||
large ascii art instances will shrink the font too much).
|
||||
|
||||
Furthermore, maintaining original gif's width and height also allows for gifs of smaller size.
|
||||
*/
|
||||
func createGifFrameToSave(asciiArt [][]imgManip.AsciiChar, img image.Image, colored bool) (image.Image, error) {
|
||||
|
||||
// Original image dimensions
|
||||
x := img.Bounds().Dx()
|
||||
y := img.Bounds().Dy()
|
||||
|
||||
// Ascii art dimensions
|
||||
asciiWidth := len(asciiArt[0])
|
||||
asciiHeight := len(asciiArt)
|
||||
|
||||
// Iterators to move pointer on the image to be made
|
||||
var xIter float64
|
||||
var yIter float64
|
||||
|
||||
var fontSize float64
|
||||
|
||||
// Conditions to alter resulting ascii gif dimensions according to ascii art dimensions
|
||||
if asciiWidth > asciiHeight*2 {
|
||||
yIter = float64(y) / float64(asciiHeight)
|
||||
|
||||
xIter = yIter / 2
|
||||
x = int(xIter * float64(asciiWidth))
|
||||
|
||||
fontSize = xIter
|
||||
|
||||
} else {
|
||||
xIter = float64(x) / float64(asciiWidth)
|
||||
|
||||
yIter = xIter * 2
|
||||
y = int(yIter * float64(asciiHeight))
|
||||
|
||||
fontSize = xIter
|
||||
}
|
||||
|
||||
// 10 extra pixels on both x and y-axis to have 5 pixels of padding on each side
|
||||
x += 10
|
||||
y += 10
|
||||
|
||||
tempImg := image.NewRGBA(image.Rect(0, 0, x, y))
|
||||
|
||||
dc := gg.NewContext(x, y)
|
||||
|
||||
dc.DrawImage(tempImg, 0, 0)
|
||||
// Set image background as black
|
||||
dc.SetRGBA(0, 0, 0, 0)
|
||||
dc.Clear()
|
||||
|
||||
// Load embedded font
|
||||
tempFont, err := truetype.Parse(embeddedFontFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Font size increased during assignment to become more visible. This will not affect image drawing
|
||||
robotoBoldFontFace := truetype.NewFace(tempFont, &truetype.Options{Size: fontSize * 1.5})
|
||||
|
||||
dc.SetFontFace(robotoBoldFontFace)
|
||||
|
||||
// Font color of text on picture is white by default
|
||||
dc.SetColor(color.White)
|
||||
|
||||
// Pointer to track y-axis on the image frame
|
||||
yImgPointer := 5.0
|
||||
|
||||
// These nested loops print each character in asciArt 2D slice separately
|
||||
// so that their RGB colors can be maintained in the resulting image
|
||||
for _, line := range asciiArt {
|
||||
|
||||
// Pointer to track x-axis on the image frame
|
||||
xImgPointer := 5.0
|
||||
|
||||
for _, char := range line {
|
||||
|
||||
r := uint8(char.RgbValue[0])
|
||||
g := uint8(char.RgbValue[1])
|
||||
b := uint8(char.RgbValue[2])
|
||||
|
||||
if colored {
|
||||
// Simple put, dc.SetColor() sets color for EACH character before printing it
|
||||
dc.SetColor(color.RGBA{r, g, b, 255})
|
||||
}
|
||||
|
||||
dc.DrawStringWrapped(char.Simple, xImgPointer, yImgPointer, 0, 0, float64(x), 1.8, gg.AlignLeft)
|
||||
|
||||
// Incremet x-axis pointer character so new one can be printed after it
|
||||
// Set to the same constant as in line
|
||||
xImgPointer += xIter
|
||||
}
|
||||
|
||||
dc.DrawStringWrapped("\n", xImgPointer, yImgPointer, 0, 0, float64(x), 1.8, gg.AlignLeft)
|
||||
|
||||
// Incremet pointer for y axis after every line printed, so
|
||||
// new line can start at below the previous one on next iteration
|
||||
yImgPointer += yIter
|
||||
}
|
||||
|
||||
return dc.Image(), nil
|
||||
}
|
||||
|
|
@ -19,40 +19,41 @@ package aic_package
|
|||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
|
||||
_ "embed"
|
||||
|
||||
imgManip "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// This file mostly has spaghetti code lol.
|
||||
// Will work on organizing it into a readable format soon. Apologies...
|
||||
|
||||
// To embed font directly into the binary, instead of packaging it as a separate file
|
||||
//go:embed RobotoMono-Bold.ttf
|
||||
var embeddedFontFile []byte
|
||||
|
||||
func createImageToSave(asciiArt [][]imgManip.AsciiChar, x, y int, colored bool, saveImagePath, imagePath, urlImgName string) error {
|
||||
/*
|
||||
Unlike createGifFrameToSave(), this function is altered to ignore execution time and has a fixed font size.
|
||||
This creates maximum quality ascii art, although the resulting image will not have the same dimensions
|
||||
as the original image, but the ascii art quality will be maintained. This is required, since smaller provided
|
||||
images will considerably decrease ascii art quality because of smaller font size.
|
||||
|
||||
// IMPORTANT NOTE:
|
||||
// The raw numbers from here on are strictly experimental
|
||||
// and are used because they just happened to work with
|
||||
// the tallest and widest images I could find, which cover
|
||||
// the extreme cases of possible images encountered.
|
||||
Size of resulting image may also be considerably larger than original image.
|
||||
*/
|
||||
func createImageToSave(asciiArt [][]imgManip.AsciiChar, colored bool, saveImagePath, imagePath, urlImgName string) error {
|
||||
|
||||
// Multipying with respect to font size. I altered the numbers
|
||||
// until extreme cases' output images becaume right
|
||||
x = int(12.6 * float32(x))
|
||||
constant := 14.0
|
||||
|
||||
y = int(13.5 * float32(y))
|
||||
x := len(asciiArt[0])
|
||||
y := len(asciiArt)
|
||||
|
||||
// Multipying resulting image dimensions with respect to constant
|
||||
x = int(constant * float64(x))
|
||||
|
||||
y = int(constant * float64(y))
|
||||
y = y * 2
|
||||
|
||||
// To give small extra margins near the edges
|
||||
y += 5
|
||||
// 10 extra pixels on both x and y-axis to have 5 pixels of padding on each side
|
||||
y += 10
|
||||
x += 10
|
||||
|
||||
tempImg := image.NewRGBA(image.Rect(0, 0, x, y))
|
||||
|
|
@ -73,7 +74,7 @@ func createImageToSave(asciiArt [][]imgManip.AsciiChar, x, y int, colored bool,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
robotoBoldFontFace := truetype.NewFace(tempFont, &truetype.Options{Size: 21.0})
|
||||
robotoBoldFontFace := truetype.NewFace(tempFont, &truetype.Options{Size: constant * 1.5})
|
||||
|
||||
dc.SetFontFace(robotoBoldFontFace)
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ func createImageToSave(asciiArt [][]imgManip.AsciiChar, x, y int, colored bool,
|
|||
dc.SetColor(color.White)
|
||||
|
||||
// Pointer to track y-axis on the image frame
|
||||
yImgPointer := 2.5
|
||||
yImgPointer := 5.0
|
||||
|
||||
// These nested loops print each character in asciArt 2D slice separately
|
||||
// so that their RGB colors can be maintained in the resulting image
|
||||
|
|
@ -104,17 +105,18 @@ func createImageToSave(asciiArt [][]imgManip.AsciiChar, x, y int, colored bool,
|
|||
dc.DrawStringWrapped(char.Simple, xImgPointer, yImgPointer, 0, 0, float64(x), 1.8, gg.AlignLeft)
|
||||
|
||||
// Incremet x-axis pointer character so new one can be printed after it
|
||||
xImgPointer += 12.6
|
||||
// Set to the same constant as in line
|
||||
xImgPointer += float64(constant)
|
||||
}
|
||||
|
||||
dc.DrawStringWrapped("\n", xImgPointer, yImgPointer, 0, 0, float64(x), 1.8, gg.AlignLeft)
|
||||
|
||||
// Incremet pointer for y axis after every line printed, so
|
||||
// new line can start at below the previous one on next iteration
|
||||
yImgPointer += 27
|
||||
yImgPointer += float64(constant * 2)
|
||||
}
|
||||
|
||||
imageName, err := createSaveFileName(imagePath, urlImgName, ".png")
|
||||
imageName, err := createSaveFileName(imagePath, urlImgName, "-ascii-art.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -126,24 +128,3 @@ func createImageToSave(asciiArt [][]imgManip.AsciiChar, x, y int, colored bool,
|
|||
|
||||
return dc.SavePNG(fullPathName)
|
||||
}
|
||||
|
||||
// Returns path with the file name concatenated to it
|
||||
func getFullSavePath(imageName, saveImagePath string) (string, error) {
|
||||
savePathLastChar := string(saveImagePath[len(saveImagePath)-1])
|
||||
|
||||
// Check if path is closed with appropriate path separator (depending on OS)
|
||||
if savePathLastChar != string(os.PathSeparator) {
|
||||
if checkOS() == "linux" {
|
||||
saveImagePath += "/"
|
||||
} else {
|
||||
saveImagePath += "\\"
|
||||
}
|
||||
}
|
||||
|
||||
// If path exists
|
||||
if _, err := os.Stat(saveImagePath); !os.IsNotExist(err) {
|
||||
return saveImagePath + imageName, nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
imgManip "github.com/TheZoraiz/ascii-image-converter/image_manipulation"
|
||||
)
|
||||
|
||||
func saveAsciiArt(asciiSet [][]imgManip.AsciiChar, imagePath, savePath, urlImgName string) error {
|
||||
// To make sure uncolored ascii art is the one saved as .txt
|
||||
saveAscii := flattenAscii(asciiSet, false)
|
||||
|
||||
saveFileName, err := createSaveFileName(imagePath, urlImgName, "-ascii-art.txt")
|
||||
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) {
|
||||
savePath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
// 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 not exist", savePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns new image file name along with extension
|
||||
func createSaveFileName(imagePath, urlImgName, label string) (string, error) {
|
||||
if urlImgName != "" {
|
||||
currExt := path.Ext(urlImgName)
|
||||
newName := urlImgName[:len(urlImgName)-len(currExt)] // e.g. Grabs myImage from myImage.jpeg
|
||||
|
||||
return newName + label, 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
|
||||
|
||||
return newName + label, nil
|
||||
}
|
||||
|
||||
// flattenAscii flattens a two-dimensional grid of ascii characters into a one dimension
|
||||
// of lines of ascii
|
||||
func flattenAscii(asciiSet [][]imgManip.AsciiChar, colored bool) []string {
|
||||
var ascii []string
|
||||
|
||||
for _, line := range asciiSet {
|
||||
var tempAscii []string
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
if colored {
|
||||
tempAscii = append(tempAscii, line[i].Colored)
|
||||
} else {
|
||||
tempAscii = append(tempAscii, line[i].Simple)
|
||||
}
|
||||
}
|
||||
|
||||
ascii = append(ascii, strings.Join(tempAscii, ""))
|
||||
}
|
||||
|
||||
return ascii
|
||||
}
|
||||
|
||||
// Returns path with the file name concatenated to it
|
||||
func getFullSavePath(imageName, saveFilePath string) (string, error) {
|
||||
savePathLastChar := string(saveFilePath[len(saveFilePath)-1])
|
||||
|
||||
// Check if path is closed with appropriate path separator (depending on OS)
|
||||
if savePathLastChar != string(os.PathSeparator) {
|
||||
saveFilePath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
// If path exists
|
||||
if _, err := os.Stat(saveFilePath); !os.IsNotExist(err) {
|
||||
return saveFilePath + imageName, nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Following is for clearing screen when showing gif
|
||||
var clear map[string]func()
|
||||
|
||||
func init() {
|
||||
clear = make(map[string]func())
|
||||
clear["linux"] = func() {
|
||||
cmd := exec.Command("clear")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
clear["windows"] = func() {
|
||||
cmd := exec.Command("cmd", "/c", "cls") //Windows example, its tested
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func clearScreen() {
|
||||
value, ok := clear[runtime.GOOS]
|
||||
if ok {
|
||||
value()
|
||||
} else {
|
||||
fmt.Println("Error: your platform is unsupported, terminal can't be cleared")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
150
cmd/root.go
150
cmd/root.go
|
|
@ -18,8 +18,10 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/TheZoraiz/ascii-image-converter/aic_package"
|
||||
"github.com/TheZoraiz/ascii-image-converter/aic_package/winsize"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -33,64 +35,46 @@ var (
|
|||
dimensions []int
|
||||
saveTxtPath string
|
||||
saveImagePath string
|
||||
saveGifPath string
|
||||
negative bool
|
||||
formatsTrue bool
|
||||
colored bool
|
||||
customMap string
|
||||
flipX bool
|
||||
flipY bool
|
||||
full bool
|
||||
|
||||
// Root commands
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "ascii-image-converter [image paths/urls]",
|
||||
Short: "Converts images into ascii art",
|
||||
Version: "1.3.4",
|
||||
Short: "Converts images and gifs into ascii art",
|
||||
Version: "1.4.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
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if formatsTrue {
|
||||
fmt.Printf("Supported image formats: JPEG/JPG, PNG, WEBP, BMP, TIFF/TIF\n\n")
|
||||
if checkInputAndFlags(args) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
fmt.Printf("Error: Need at least 1 image path/url\nUse the -h flag for more info\n\n")
|
||||
return
|
||||
}
|
||||
|
||||
if customMap != "" && len(customMap) < 2 {
|
||||
fmt.Printf("Need at least 2 characters for --map flag\n\n")
|
||||
return
|
||||
}
|
||||
|
||||
numberOfDimensions := len(dimensions)
|
||||
if dimensions != nil && numberOfDimensions != 2 {
|
||||
fmt.Printf("Error: requires 2 dimensions, got %v\n\n", numberOfDimensions)
|
||||
return
|
||||
}
|
||||
|
||||
if dimensions != nil && (dimensions[0] < 1 || dimensions[1] < 1) {
|
||||
fmt.Printf("Error: invalid values for dimensions\n\n")
|
||||
return
|
||||
}
|
||||
|
||||
flags := map[string]interface{}{
|
||||
"complex": complex,
|
||||
"dimensions": dimensions,
|
||||
"saveTxtPath": saveTxtPath,
|
||||
"saveImagePath": saveImagePath,
|
||||
"negative": negative,
|
||||
"colored": colored,
|
||||
"customMap": customMap,
|
||||
"flipX": flipX,
|
||||
"flipY": flipY,
|
||||
flags := aic_package.Flags{
|
||||
Complex: complex,
|
||||
Dimensions: dimensions,
|
||||
SaveTxtPath: saveTxtPath,
|
||||
SaveImagePath: saveImagePath,
|
||||
SaveGifPath: saveGifPath,
|
||||
Negative: negative,
|
||||
Colored: colored,
|
||||
CustomMap: customMap,
|
||||
FlipX: flipX,
|
||||
FlipY: flipY,
|
||||
Full: full,
|
||||
}
|
||||
|
||||
for _, imagePath := range args {
|
||||
|
||||
if asciiArt, err := aic_package.ConvertImage(imagePath, flags); err == nil {
|
||||
if asciiArt, err := aic_package.Convert(imagePath, flags); err == nil {
|
||||
fmt.Printf("%s", asciiArt)
|
||||
} else {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
|
|
@ -108,6 +92,83 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// Check input and flags for any errors or interruptons
|
||||
func checkInputAndFlags(args []string) bool {
|
||||
|
||||
gifCount := 0
|
||||
gifPresent := false
|
||||
nonGifPresent := false
|
||||
for _, arg := range args {
|
||||
extension := path.Ext(arg)
|
||||
|
||||
if extension == ".gif" {
|
||||
gifPresent = true
|
||||
gifCount++
|
||||
} else {
|
||||
nonGifPresent = true
|
||||
}
|
||||
}
|
||||
|
||||
if gifPresent && nonGifPresent {
|
||||
fmt.Printf("Error: There are other inputs along with GIFs\nDue to the potential looping nature of GIFs, non-GIFs must not be supplied alongside\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
if gifCount > 1 {
|
||||
fmt.Printf("Error: There are multiple GIFs supplied\nDue to the potential looping nature of GIFs, only one GIF per command is supported\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
if formatsTrue {
|
||||
fmt.Printf("Supported input formats:\n\n" +
|
||||
"JPEG/JPG\n" +
|
||||
"PNG\n" +
|
||||
"WEBP\n" +
|
||||
"BMP\n" +
|
||||
"TIFF/TIF\n" +
|
||||
"GIF (Experimental)\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
fmt.Printf("Error: Need at least 1 input path/url\nUse the -h flag for more info\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
if customMap != "" && len(customMap) < 2 {
|
||||
fmt.Printf("Need at least 2 characters for --map flag\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
if dimensions != nil {
|
||||
|
||||
numberOfDimensions := len(dimensions)
|
||||
if numberOfDimensions != 2 {
|
||||
fmt.Printf("Error: requires 2 dimensions, got %v\n\n", numberOfDimensions)
|
||||
return true
|
||||
}
|
||||
|
||||
if dimensions[0] < 1 || dimensions[1] < 1 {
|
||||
fmt.Printf("Error: invalid values for dimensions\n\n")
|
||||
return true
|
||||
}
|
||||
|
||||
defaultTermWidth, _, err := winsize.GetTerminalSize()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n\n", err)
|
||||
return true
|
||||
}
|
||||
|
||||
defaultTermWidth -= 1
|
||||
if dimensions[0] > defaultTermWidth {
|
||||
fmt.Printf("Error: set width must be lower than terminal width\n\n")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Cobra configuration from here on
|
||||
|
||||
func Execute() {
|
||||
|
|
@ -121,19 +182,24 @@ func init() {
|
|||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.PersistentFlags().SortFlags = false
|
||||
rootCmd.Flags().SortFlags = false
|
||||
|
||||
// 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 --negative flag)\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().BoolVarP(&formatsTrue, "formats", "f", false, "Display supported image formats\n")
|
||||
rootCmd.PersistentFlags().BoolVarP(&colored, "color", "C", false, "Display ascii art with original colors\n(Can work with the --negative flag)\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")
|
||||
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(&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 that\nfill the terminal width\n(Cancels --dimensions 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(&flipX, "flipX", "x", false, "Flip ascii art horizontally\n")
|
||||
rootCmd.PersistentFlags().BoolVarP(&flipY, "flipY", "y", false, "Flip ascii art vertically\n")
|
||||
rootCmd.PersistentFlags().StringVarP(&saveImagePath, "save-img", "s", "", "Save ascii art as a .png file\nFormat: <image-name>-ascii-art.png\nImage will be saved in passed path\n(pass . for current directory)\n")
|
||||
rootCmd.PersistentFlags().StringVar(&saveTxtPath, "save-txt", "", "Save ascii art as a .txt file\nFormat: <image-name>-ascii-art.txt\nFile will be saved in passed path\n(pass . for current directory)\n")
|
||||
rootCmd.PersistentFlags().StringVar(&saveGifPath, "save-gif", "", "(Experimental)\nIf input is a gif, save it as a .gif file\nFormat: <image-name>-ascii-art.gif\nGif will be saved in passed path\n(pass . for current directory)\n")
|
||||
rootCmd.PersistentFlags().BoolVar(&formatsTrue, "formats", false, "Display supported input formats\n")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&saveImagePath, "save-img", "s", "", "Save ascii art in a .png file\nFormat: <image-name>-ascii-art.png\nImage will be saved in passed path\n(pass . for current directory)\n")
|
||||
rootCmd.PersistentFlags().StringVar(&saveTxtPath, "save-txt", "", "Save ascii art in the a .txt file\nFormat: <image-name>-ascii-art.txt\nFile will be saved in passed path\n(pass . for current directory)\n")
|
||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Help for "+rootCmd.Name()+"\n")
|
||||
rootCmd.PersistentFlags().BoolP("version", "v", false, "Version for "+rootCmd.Name())
|
||||
|
||||
defaultUsageTemplate := rootCmd.UsageTemplate()
|
||||
rootCmd.SetUsageTemplate(defaultUsageTemplate + "\nCopyright © 2021 Zoraiz Hassan <hzoraiz8@gmail.com>\n" +
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import (
|
|||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/TheZoraiz/ascii-image-converter/image_manipulation/winsize"
|
||||
"github.com/TheZoraiz/ascii-image-converter/aic_package/winsize"
|
||||
"github.com/nfnt/resize"
|
||||
)
|
||||
|
||||
|
|
@ -36,23 +36,34 @@ type AsciiPixel struct {
|
|||
//
|
||||
// The returned 2D AsciiPixel slice contains each corresponding pixel's values. Grayscale value
|
||||
// ranges from 0 to 65535, while RGB values are separate.
|
||||
func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY bool) ([][]AsciiPixel, int, int, error) {
|
||||
func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY, full bool) ([][]AsciiPixel, error) {
|
||||
|
||||
var asciiWidth, asciiHeight int
|
||||
var smallImg image.Image
|
||||
|
||||
if len(dimensions) == 0 {
|
||||
terminalWidth, terminalHeight, err := winsize.GetTerminalSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Following code in this condition calculates ratio according to terminal height
|
||||
if full {
|
||||
asciiWidth = terminalWidth - 1
|
||||
|
||||
terminalWidth, terminalHeight, err := winsize.GetTerminalSize()
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
// Passing 0 in place of width keeps the original image's aspect ratio
|
||||
smallImg = resize.Resize(uint(asciiWidth), 0, img, resize.Lanczos3)
|
||||
asciiHeight = smallImg.Bounds().Max.Y - smallImg.Bounds().Min.Y
|
||||
|
||||
// To fix aspect ratio in eventual ascii art
|
||||
asciiHeight = int(0.5 * float32(asciiHeight))
|
||||
|
||||
smallImg = resize.Resize(uint(asciiWidth), uint(asciiHeight), img, resize.Lanczos3)
|
||||
|
||||
} else if len(dimensions) == 0 {
|
||||
|
||||
// Following code in this condition calculates aspect ratio according to terminal height
|
||||
|
||||
asciiHeight = terminalHeight - 1
|
||||
|
||||
// Passing 0 in place of width keeps the original image's aspect ratio
|
||||
smallImg = resize.Resize(0, uint(asciiHeight), img, resize.Lanczos3)
|
||||
asciiWidth = smallImg.Bounds().Max.X - smallImg.Bounds().Min.X
|
||||
|
||||
|
|
@ -60,10 +71,11 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY bool)
|
|||
asciiWidth = int(2 * float32(asciiWidth))
|
||||
|
||||
// If ascii width exceeds terminal width, change ratio with respect to terminal width
|
||||
if asciiWidth > terminalWidth {
|
||||
smallImg = resize.Resize(uint(terminalWidth), 0, img, resize.Lanczos3)
|
||||
|
||||
if asciiWidth >= terminalWidth {
|
||||
asciiWidth = terminalWidth - 1
|
||||
|
||||
smallImg = resize.Resize(uint(asciiWidth), 0, img, resize.Lanczos3)
|
||||
|
||||
asciiHeight = smallImg.Bounds().Max.Y - smallImg.Bounds().Min.Y
|
||||
|
||||
// To fix aspect ratio in eventual ascii art
|
||||
|
|
@ -78,16 +90,18 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY bool)
|
|||
smallImg = resize.Resize(uint(asciiWidth), uint(asciiHeight), img, resize.Lanczos3)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if len(dimensions) > 0 && !full {
|
||||
defaultTermWidth, _, err := winsize.GetTerminalSize()
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultTermWidth -= 1
|
||||
if dimensions[0] > defaultTermWidth {
|
||||
return nil, 0, 0, fmt.Errorf("set width is larger than terminal width")
|
||||
return nil, fmt.Errorf("set width must be lower than terminal width")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +145,7 @@ func ConvertToAsciiPixels(img image.Image, dimensions []int, flipX, flipY bool)
|
|||
imgSet = reverse(imgSet, flipX, flipY)
|
||||
}
|
||||
|
||||
return imgSet, asciiWidth, asciiHeight, nil
|
||||
return imgSet, nil
|
||||
}
|
||||
|
||||
func reverse(imgSet [][]AsciiPixel, flipX, flipY bool) [][]AsciiPixel {
|
||||
|
|
@ -152,3 +166,7 @@ func reverse(imgSet [][]AsciiPixel, flipX, flipY bool) [][]AsciiPixel {
|
|||
|
||||
return imgSet
|
||||
}
|
||||
|
||||
func calculateDimensions() {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
name: ascii-image-converter
|
||||
base: core18
|
||||
version: "1.3.4"
|
||||
summary: Converts images into ascii art
|
||||
version: "1.4.0"
|
||||
summary: Converts images and gifs into ascii art
|
||||
description: |
|
||||
This tool converts images into ascii format and prints them onto the terminal window.
|
||||
Supported image formats are JPEG/JPG, PNG, WEBP, BMP and TIFF/TIF. Further configuration can be managed by flags.
|
||||
This tool converts images and gifs into ascii format and prints them onto the terminal window.
|
||||
Supported input formats are JPEG/JPG, PNG, WEBP, BMP TIFF/TIF and GIF. Further configuration can be managed by flags.
|
||||
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
|
|
|||
Loading…
Reference in New Issue