Release v1.4.0

This commit is contained in:
Zoraiz 2021-07-21 22:23:58 +05:00
parent 01ab53a4d3
commit 97f1d679b0
14 changed files with 971 additions and 354 deletions

View File

@ -1,4 +1,3 @@
project_name: ascii-image-converter
before:

145
README.md
View File

@ -1,12 +1,13 @@
# 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-brightgreen)](https://golang.org/)
[![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)
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)
}
}
```

252
aic_package/convert_gif.go Normal file
View File

@ -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
}

View File

@ -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
}

166
aic_package/convert_root.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}

140
aic_package/util.go Normal file
View File

@ -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)
}
}

View File

@ -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" +

View File

@ -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() {
}

View File

@ -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