├── .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 |
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 | 
14 | QuantumNomad42
15 |
16 |
17 |
18 | 
19 | EchoFrost7
20 |
21 |
22 |
23 | 
24 | NebulaTide19
25 |
26 |
27 |
28 | 
29 | ZephyrPulse88
30 |
31 |
32 |
33 | 
34 | EmberNexus23
35 |
36 |
37 |
38 | 
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 |
--------------------------------------------------------------------------------