├── .github └── workflows │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yaml ├── README.md ├── go.mod ├── go.sum ├── main.go └── testdata └── image.png /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v5 23 | with: 24 | distribution: goreleaser 25 | version: latest 26 | args: release --clean 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | testdata/*.txt 2 | dist/ 3 | .idea/* -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 1 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | # you may remove this if you don't need go generate 16 | - go generate ./... 17 | 18 | builds: 19 | - env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | 26 | archives: 27 | - format: tar.gz 28 | # this name template makes the OS and Arch compatible with the results of `uname`. 29 | name_template: >- 30 | {{ .ProjectName }}_ 31 | {{- title .Os }}_ 32 | {{- if eq .Arch "amd64" }}x86_64 33 | {{- else if eq .Arch "386" }}i386 34 | {{- else }}{{ .Arch }}{{ end }} 35 | {{- if .Arm }}v{{ .Arm }}{{ end }} 36 | # use zip for windows archives 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - "^docs:" 46 | - "^test:" 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-img2ascii - An image-to-ascii converter 2 | This is a cli tool that converts any jpeg, jpg or png image to an ascii representation. 3 | 4 | ## Sample 5 | |Original|Ascii'd| 6 | |-|-| 7 | | ![image](https://github.com/codelikesuraj/go-img2ascii/assets/70463535/32a519a6-fd2d-4c0c-ba29-6f16430a7713) | ![image](https://github.com/codelikesuraj/go-img2ascii/assets/70463535/e9da71eb-97c6-4942-9419-4f33a67887db) | 8 | | ![image](https://github.com/codelikesuraj/go-img2ascii/assets/70463535/1e4f629b-f69b-48b4-a1a4-7dd1a857f377) | ![image](https://github.com/codelikesuraj/go-img2ascii/assets/70463535/55f79285-91b8-47ed-af4f-79247abf98ec) | 9 | 10 | ## Usage 11 | ```go run main.go [path-to-image] [character-width]``` 12 | 13 | ## Installation 14 | - [Click here to download the latest release for your platform](https://github.com/codelikesuraj/go-img2ascii/releases). 15 | - Extract the archive to a preferred folder/directory 16 | - Ensure this folder/directory is in your OS' PATH, otherwise add it to PATH 17 | - Now you can run ```go-img2ascii [path-to-image] [character-width]``` from anywhere 18 | 19 | ## Todo 20 | - ✅ load and decode image file 21 | - ✅ resize the image 22 | - ✅ convert image to grayscale 23 | - ✅ map grayscaled image to ascii characters 24 | - ✅ save ascii'd image to file 25 | - ✅ refactor with any cli library (https://github.com/urfave/cli) 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codelikesuraj/img2ascii 2 | 3 | go 1.22.0 4 | 5 | require github.com/urfave/cli/v2 v2.27.2 6 | 7 | require ( 8 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 9 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 10 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= 6 | github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= 7 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= 8 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "math" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | var executableName = filepath.Base(os.Args[0]) 20 | 21 | func main() { 22 | // print duration 23 | defer func(t time.Time) { 24 | fmt.Println("\nDuration:", time.Since(t).String()) 25 | }(time.Now()) 26 | 27 | app := cli.NewApp() 28 | app.Name = executableName 29 | app.Usage = "An image to ascii converter" 30 | app.UsageText = executableName + " [path-to-image] [character-width]" 31 | app.HideHelp = true 32 | app.Action = func(ctx *cli.Context) error { 33 | if ctx.NArg() < 1 { 34 | return errors.New("no image path provided") 35 | } 36 | 37 | if ctx.NArg() < 2 { 38 | return errors.New("no width is provided") 39 | } 40 | 41 | // load image 42 | imgPath := ctx.Args().First() 43 | imgFile, err := os.Open(imgPath) 44 | if err != nil { 45 | return err 46 | } 47 | defer imgFile.Close() 48 | 49 | // decode image 50 | img, _, err := image.Decode(imgFile) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // get width from args 56 | width, _ := strconv.Atoi(ctx.Args().Get(1)) 57 | if width < 1 { 58 | width = img.Bounds().Dx() 59 | } 60 | 61 | // calculate height while maintaining aspect ratio 62 | height := width * img.Bounds().Dy() / img.Bounds().Dx() 63 | 64 | // create new gray image 65 | newImage := image.NewGray(image.Rect(0, 0, width, height)) 66 | 67 | scale_x := float64(width) / float64(img.Bounds().Dx()) 68 | scale_y := float64(height) / float64(img.Bounds().Dy()) 69 | 70 | for y := 0; y < height; y++ { 71 | srcY := int(math.Round(float64(y) / scale_y)) 72 | for x := 0; x < width; x++ { 73 | srcX := int(math.Round(float64(x) / scale_x)) 74 | newImage.Set(x, y, img.At(srcX, srcY)) 75 | } 76 | } 77 | 78 | // save image to file 79 | fileName := strings.ReplaceAll(os.Args[1], ".", "_") + "_to_ascii.txt" 80 | newImageFile, err := os.Create(fileName) 81 | if err != nil { 82 | return err 83 | } 84 | defer newImageFile.Close() 85 | 86 | // map string to grayscale pixel value 87 | ascii, out := []byte{' ', '.', '-', '~', '+', '=', '%', '$', '#', '@'}, []byte{} 88 | 89 | bounds := newImage.Bounds() 90 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 91 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 92 | y := float64(newImage.GrayAt(x, y).Y) 93 | val := int(y / 255 * float64(len(ascii)-1)) 94 | out = append(out, ascii[val]) 95 | } 96 | out = append(out, '\n') 97 | } 98 | newImageFile.Write(out) 99 | 100 | fmt.Println("File conversion completed\n\nOutput file:", fileName) 101 | 102 | return nil 103 | } 104 | 105 | if err := app.Run(os.Args); err != nil { 106 | fmt.Println("ERROR:\n", err.Error()) 107 | fmt.Println("\nUSAGE:\n", app.UsageText) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /testdata/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codelikesuraj/go-img2ascii/8f7ac6674f827fa8c35d9d30f730121c37117ae6/testdata/image.png --------------------------------------------------------------------------------