├── .github └── workflows │ └── release.yml ├── .gitignore ├── .releaserc.json ├── LICENSE ├── README.md ├── arts ├── .gitignore ├── avatar_1.png ├── avatar_2.png ├── avatar_3.png ├── avatar_4.png ├── avatar_5.png ├── avatar_6.png └── goavatar-banner.png ├── example └── main.go ├── go.mod ├── goavatar.go └── goavatar_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Go Release and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - closed 10 | 11 | jobs: 12 | test: 13 | name: Run Go Tests 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.24 24 | 25 | - name: Run Tests 26 | run: go test -v 27 | 28 | release: 29 | name: Create Release 30 | needs: test 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: write # Required to create release 34 | 35 | steps: 36 | - name: Checkout Repository 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 # Fetch full history of semantic versioning 40 | 41 | - name: Install Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: 18 # Use the latest stable version 45 | 46 | - name: Install Semantic Release 47 | run: npm install -g semantic-release @semantic-release/github @semantic-release/changelog 48 | 49 | - name: Run Semantic Release 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub token for creating releases 52 | run: semantic-release 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | mise.toml 3 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | "@semantic-release/changelog", 9 | "@semantic-release/github" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Muhammad Saim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goavatar Identicon Generator in Go 2 | 3 |

4 | GoAvatar Banner 5 |

6 | 7 | This package provides a simple way to generate unique, symmetric identicons based on an input string (e.g., an email address or username). It uses an **MD5 hash** to create a deterministic pattern and color scheme, then mirrors the design for a visually appealing avatar. 8 | 9 | ## User Avatars 10 | 11 |

12 | 13 | Avatar 1
14 | QuantumNomad42 15 |
16 |      17 | 18 | Avatar 2
19 | EchoFrost7 20 |
21 |      22 | 23 | Avatar 3
24 | NebulaTide19 25 |
26 |      27 | 28 | Avatar 4
29 | ZephyrPulse88 30 |
31 |      32 | 33 | Avatar 5
34 | EmberNexus23 35 |
36 |      37 | 38 | Avatar 5
39 | nice__user__name 40 |
41 |

42 | 43 | ## Installation 44 | 45 | To use this package in your Go project, install it via: 46 | 47 | ```sh 48 | go get github.com/MuhammadSaim/goavatar 49 | ``` 50 | 51 | Then, import it in your Go code: 52 | 53 | ```go 54 | import "github.com/MuhammadSaim/goavatar" 55 | ``` 56 | 57 | ## Usage 58 | 59 | ### **Basic Example** 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "fmt" 66 | "image" 67 | "image/png" 68 | "os" 69 | 70 | "github.com/MuhammadSaim/goavatar" 71 | ) 72 | 73 | func main() { 74 | // empty slice. 75 | imgSlice := make([]image.Image, 0) 76 | 77 | // Generates a unique avatar based on "QuantumNomad42" with a custom width and height. 78 | // Saves the generated avatar as avatar_1.png 79 | image1 := goavatar.Make("QuantumNomad42", 80 | goavatar.WithSize(512), // Set custom image widthxheight (default is 64) 81 | ) 82 | 83 | // Generate the second avatar with a custom grid size with a 10x10 grid for more detail. 84 | // Saves the generated avatar as avatar_2.png 85 | image2 := goavatar.Make("EchoFrost7", 86 | goavatar.WithSize(512), // Set custom image widthxheight (default is 64) 87 | goavatar.WithGridSize(10), // Set custom grid size (default is 8), affects pattern complexity 88 | ) 89 | 90 | // Generate the third avatar with a custom brownish background color. 91 | // Saves the generated avatar as avatar_3.png 92 | image3 := goavatar.Make("NebulaTide19", 93 | goavatar.WithSize(512), // Set custom image widthxheight (default is 256) 94 | goavatar.WithBgColor(170, 120, 10, 255), // Change background color (default is light gray) 95 | ) 96 | 97 | // Generate the fourth avatar with a custom brownish background and white foreground. 98 | // Saves the generated avatar as avatar_4.png 99 | image4 := goavatar.Make("ZephyrPulse88", 100 | goavatar.WithSize(512), // Set custom image widthxheight (default is 64) 101 | goavatar.WithBgColor(170, 120, 10, 255), // Change background color (default is light gray) 102 | goavatar.WithFgColor(255, 255, 255, 255), // Change foreground color (default is extracted from hash) 103 | 104 | ) 105 | 106 | // Generate an avatar using default settings 107 | // Saves the generated avatar as avatar_5.png 108 | image5 := goavatar.Make("EmberNexus23") 109 | 110 | // Collect options dynamically 111 | var opts []goavatar.OptFunc 112 | 113 | // add size 114 | opts = append(opts, goavatar.WithSize(100)) 115 | opts = append(opts, goavatar.WithGridSize(10)) 116 | image6 := goavatar.Make("nice__user__name", opts...) 117 | 118 | // append all the images into the list 119 | imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6) 120 | 121 | // loop through the image slice and save the images 122 | for i, img := range imgSlice { 123 | 124 | filename := fmt.Sprintf("../arts/avatar_%d.png", i+1) 125 | 126 | // Create the file 127 | file, err := os.Create(filename) 128 | if err != nil { 129 | fmt.Println("Error creating file:", err) 130 | continue 131 | } 132 | defer file.Close() 133 | 134 | // Encode image as PNG and save 135 | err = png.Encode(file, img) 136 | if err != nil { 137 | fmt.Println("Error saving image:", err) 138 | } else { 139 | fmt.Println("Saved: ", filename) 140 | } 141 | 142 | } 143 | } 144 | ``` 145 | 146 | This will generate a unique identicons for the input string and save in the `arts` directory. 147 | 148 | ## Package Documentation 149 | 150 | ### **Generate Identicon** 151 | 152 | ```go 153 | func Make(input, ...optFunc) image.Image 154 | ``` 155 | 156 | - `input`: A string used to generate a unique identicon (e.g., email, username). 157 | - `...optFunc`: Functional options to override the default values. 158 | - `image.Image`: Function returns an `image.Image`, allowing the caller to handle image processing, encoding, and storage as needed. 159 | 160 | ## License 161 | 162 | This project is open-source under the MIT License. 163 | 164 | ## Contributing 165 | 166 | Contributions are welcome! Feel free to open a pull request or create an issue. 167 | -------------------------------------------------------------------------------- /arts/.gitignore: -------------------------------------------------------------------------------- 1 | !avatar_1.png 2 | !avatar_2.png 3 | !avatar_3.png 4 | !avatar_4.png 5 | !avatar_5.png 6 | !avatar_6.png 7 | !goavatar-banner.png 8 | -------------------------------------------------------------------------------- /arts/avatar_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_1.png -------------------------------------------------------------------------------- /arts/avatar_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_2.png -------------------------------------------------------------------------------- /arts/avatar_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_3.png -------------------------------------------------------------------------------- /arts/avatar_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_4.png -------------------------------------------------------------------------------- /arts/avatar_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_5.png -------------------------------------------------------------------------------- /arts/avatar_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/avatar_6.png -------------------------------------------------------------------------------- /arts/goavatar-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuhammadSaim/goavatar/b23df3ff0a12537396f2887572174961c44df8bd/arts/goavatar-banner.png -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "os" 8 | 9 | "github.com/MuhammadSaim/goavatar" 10 | ) 11 | 12 | func main() { 13 | // empty slice. 14 | imgSlice := make([]image.Image, 0) 15 | 16 | // Generates a unique avatar based on "QuantumNomad42" with a custom width and height. 17 | // Saves the generated avatar as avatar_1.png 18 | image1 := goavatar.Make("QuantumNomad42", 19 | goavatar.WithSize(512), // Set custom image widthxheight (default is 64) 20 | ) 21 | 22 | // Generate the second avatar with a custom grid size with a 10x10 grid for more detail. 23 | // Saves the generated avatar as avatar_2.png 24 | image2 := goavatar.Make("EchoFrost7", 25 | goavatar.WithSize(512), // Set custom image widthxheight (default is 64) 26 | goavatar.WithGridSize(10), // Set custom grid size (default is 8), affects pattern complexity 27 | ) 28 | 29 | // Generate the third avatar with a custom brownish background color. 30 | // Saves the generated avatar as avatar_3.png 31 | image3 := goavatar.Make("NebulaTide19", 32 | goavatar.WithSize(100), // Set custom image widthxheight (default is 64) 33 | goavatar.WithBgColor(170, 120, 10, 255), // Change background color (default is light gray) 34 | ) 35 | 36 | // Generate the fourth avatar with a custom brownish background and white foreground. 37 | // Saves the generated avatar as avatar_4.png 38 | image4 := goavatar.Make("ZephyrPulse88", 39 | goavatar.WithSize(50), // Set custom image widthxheight if size is less then 64 this will go to default (default is 64) 40 | goavatar.WithBgColor(170, 120, 10, 255), // Change background color (default is light gray) 41 | goavatar.WithFgColor(255, 255, 255, 255), // Change foreground color (default is extracted from hash) 42 | 43 | ) 44 | 45 | // Generate an avatar using default settings 46 | // Saves the generated avatar as avatar_5.png 47 | image5 := goavatar.Make("EmberNexus23") 48 | 49 | // Collect options dynamically 50 | var opts []goavatar.OptFunc 51 | 52 | // add size 53 | opts = append(opts, goavatar.WithSize(500)) 54 | opts = append(opts, goavatar.WithGridSize(13)) 55 | image6 := goavatar.Make("nice__user__name", opts...) 56 | 57 | // append all the images into the list 58 | imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6) 59 | 60 | // loop through the image slice and save the images 61 | for i, img := range imgSlice { 62 | 63 | filename := fmt.Sprintf("../arts/avatar_%d.png", i+1) 64 | 65 | // Create the file 66 | file, err := os.Create(filename) 67 | if err != nil { 68 | fmt.Println("Error creating file:", err) 69 | continue 70 | } 71 | defer file.Close() 72 | 73 | // Encode image as PNG and save 74 | err = png.Encode(file, img) 75 | if err != nil { 76 | fmt.Println("Error saving image:", err) 77 | } else { 78 | fmt.Println("Saved: ", filename) 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MuhammadSaim/goavatar 2 | 3 | go 1.24.0 4 | -------------------------------------------------------------------------------- /goavatar.go: -------------------------------------------------------------------------------- 1 | package goavatar 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "image" 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // option contains the configuration for the avatar generator. 12 | type options struct { 13 | size int 14 | gridSize int 15 | bgColor color.RGBA 16 | fgColor color.RGBA 17 | } 18 | 19 | // optFunc is a function that applies an option to the options struct. 20 | type OptFunc func(*options) 21 | 22 | // WithSize sets the width and height of the avatar minimum 64x64. 23 | func WithSize(s int) OptFunc { 24 | return func(o *options) { 25 | // insure that image should be at least 64x64 26 | if s >= 64 { 27 | o.size = s 28 | } 29 | } 30 | } 31 | 32 | // WithGridSize sets the grid size of the avatar. 33 | func WithGridSize(g int) OptFunc { 34 | return func(o *options) { 35 | // make sure grid is minimum 8 to make nice pattrens 36 | if g > 8 { 37 | o.gridSize = g 38 | } 39 | } 40 | } 41 | 42 | // WithBgColor sets the background color of the avatar. 43 | func WithBgColor(r, g, b, a uint8) OptFunc { 44 | return func(o *options) { 45 | o.bgColor = color.RGBA{r, g, b, a} 46 | } 47 | } 48 | 49 | // WithFgColor sets the foreground color of the avatar. 50 | func WithFgColor(r, g, b, a uint8) OptFunc { 51 | return func(o *options) { 52 | o.fgColor = color.RGBA{r, g, b, a} 53 | } 54 | } 55 | 56 | // defaultOptions provides the default value to generate the avatar. 57 | func defaultOptions(hash string) options { 58 | return options{ 59 | size: 64, // default size should be 64 to make sure images are perfect square 60 | gridSize: 8, // minimum size for the grid for make shape complexity 61 | bgColor: color.RGBA{240, 240, 240, 255}, // light gray color 62 | fgColor: color.RGBA{hash[0], hash[1], hash[2], 255}, // use the first three hash bytes as the foreground color 63 | } 64 | } 65 | 66 | // generateHash generates the MD5 hash of the input string. 67 | func generateHash(data string) string { 68 | hash := md5.Sum([]byte(data)) 69 | return hex.EncodeToString(hash[:]) 70 | } 71 | 72 | // drawPixel draws a single pixel block based on proportional scaling to avoid gaps. 73 | func drawPixel(img *image.RGBA, gridX, gridY int, c color.Color, gridSize, imageSize int) { 74 | // Calculate exact scaled bounds 75 | startX := int(math.Round(float64(gridX) * float64(imageSize) / float64(gridSize))) 76 | startY := int(math.Round(float64(gridY) * float64(imageSize) / float64(gridSize))) 77 | endX := int(math.Round(float64(gridX+1) * float64(imageSize) / float64(gridSize))) 78 | endY := int(math.Round(float64(gridY+1) * float64(imageSize) / float64(gridSize))) 79 | 80 | // Clamp to image size to avoid out-of-bounds 81 | if endX > img.Bounds().Dx() { 82 | endX = img.Bounds().Dx() 83 | } 84 | if endY > img.Bounds().Dy() { 85 | endY = img.Bounds().Dy() 86 | } 87 | 88 | // Fill the block 89 | for y := startY; y < endY; y++ { 90 | for x := startX; x < endX; x++ { 91 | img.Set(x, y, c) 92 | } 93 | } 94 | } 95 | 96 | // Make generates an avatar image based on the input string and options. 97 | func Make(input string, opts ...OptFunc) image.Image { 98 | // generate the hash of an input 99 | hash := generateHash(input) 100 | o := defaultOptions(hash) 101 | 102 | for _, opt := range opts { 103 | opt(&o) 104 | } 105 | 106 | // create a blank image 107 | img := image.NewRGBA(image.Rect(0, 0, o.size, o.size)) 108 | 109 | // generate colors 110 | avatarColor := o.fgColor 111 | bgColor := o.bgColor 112 | isOdd := o.gridSize%2 != 0 113 | 114 | // generate the pixel pattern 115 | // loop over each pixel in the grid 116 | for y := 0; y < o.gridSize; y++ { 117 | for x := 0; x < o.gridSize/2; x++ { 118 | // use bitwise operation to determine if a pixel should be colored 119 | pixelOn := (hash[y]>>(x%8))&1 == 1 120 | 121 | // image should 122 | if pixelOn { 123 | drawPixel(img, x, y, avatarColor, o.gridSize, o.size) 124 | drawPixel(img, o.gridSize-1-x, y, avatarColor, o.gridSize, o.size) // mirror the pixel 125 | } else { 126 | drawPixel(img, x, y, bgColor, o.gridSize, o.size) 127 | drawPixel(img, o.gridSize-1-x, y, bgColor, o.gridSize, o.size) // mirror the bg pixel 128 | } 129 | 130 | } 131 | // Draw the center column if gridSize is odd 132 | if isOdd { 133 | mid := o.gridSize / 2 134 | pixelOn := (hash[y]>>(mid%8))&1 == 1 135 | color := bgColor 136 | if pixelOn { 137 | color = avatarColor 138 | } 139 | drawPixel(img, mid, y, color, o.gridSize, o.size) 140 | } 141 | } 142 | 143 | return img 144 | } 145 | -------------------------------------------------------------------------------- /goavatar_test.go: -------------------------------------------------------------------------------- 1 | package goavatar 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | // expectedTopLeftPixel computes what color should appear at (0,0) by replaying the default options 9 | // and then using the same raw hash logic as in Make: for x=0, y=0, it tests if (hash[0] & 1) == 1. 10 | // 11 | // NOTE: generateHash returns a hex‑encoded string, so here we use its first character’s ASCII code. 12 | func expectedTopLeftPixel(input string, opts []OptFunc) (col color.Color) { 13 | // generate the hash of the input 14 | hash := generateHash(input) 15 | // get the default configuration; which sets fgColor to {hash[0], hash[1], hash[2], 255} 16 | conf := defaultOptions(hash) 17 | // apply all option functions to the default configuration 18 | for _, opt := range opts { 19 | opt(&conf) 20 | } 21 | // For the top‐left cell (x=0,y=0), the decision is based on the least‐significant bit of the raw hash character. 22 | // Using the raw ASCII value of hash[0] as in the current implementation. 23 | if (hash[0] & 1) == 1 { 24 | return conf.fgColor 25 | } 26 | return conf.bgColor 27 | } 28 | 29 | func TestMake(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | input string 33 | opts []OptFunc 34 | width int 35 | height int 36 | }{ 37 | { 38 | name: "Default settings", 39 | input: "test@example.com", 40 | opts: nil, // defaults 41 | width: 64, height: 64, 42 | }, 43 | { 44 | name: "Custom width and height", 45 | input: "custom-size", 46 | opts: []OptFunc{WithSize(512)}, 47 | width: 512, height: 512, 48 | }, 49 | { 50 | name: "Custom background color", 51 | input: "custom-bg", 52 | // override background color only 53 | opts: []OptFunc{WithBgColor(255, 0, 0, 255)}, 54 | width: 64, height: 64, 55 | }, 56 | { 57 | name: "Custom foreground color", 58 | input: "custom-fg", 59 | // override foreground color only 60 | opts: []OptFunc{WithFgColor(10, 20, 30, 255)}, 61 | width: 64, height: 64, 62 | }, 63 | { 64 | name: "QuantumNomad42", 65 | input: "QuantumNomad42", 66 | opts: []OptFunc{WithSize(512)}, 67 | width: 512, height: 512, 68 | }, 69 | { 70 | name: "EchoFrost7", 71 | input: "EchoFrost7", 72 | opts: []OptFunc{WithSize(512)}, 73 | width: 512, height: 512, 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | tt := tt // capture range variable 79 | t.Run(tt.name, func(t *testing.T) { 80 | img := Make(tt.input, tt.opts...) 81 | if img == nil { 82 | t.Fatalf("Make() returned nil for input %q", tt.input) 83 | } 84 | 85 | // Verify the image dimensions. 86 | bounds := img.Bounds() 87 | if bounds.Dx() != tt.width || bounds.Dy() != tt.height { 88 | t.Errorf("Unexpected image size for %q: got %dx%d, want %dx%d", 89 | tt.input, bounds.Dx(), bounds.Dy(), tt.width, tt.height) 90 | } 91 | 92 | // Compute the expected top-left pixel color. 93 | expected := expectedTopLeftPixel(tt.input, tt.opts) 94 | actual := img.At(0, 0) 95 | ar, ag, ab, aa := actual.RGBA() 96 | er, eg, eb, ea := expected.RGBA() 97 | if ar != er || ag != eg || ab != eb || aa != ea { 98 | t.Errorf("Unexpected top-left pixel color for %q: got %v, want %v", 99 | tt.input, actual, expected) 100 | } 101 | }) 102 | } 103 | } 104 | --------------------------------------------------------------------------------