Web applications often need to render an avatar for users, and users aren’t always keen to upload their pictures. A popular fallback is to generate avatars for your users based on their name initials. In this tutorial, we will explore how to create such avatars in Go and serve it over HTTP using the chi router.
To follow along, you will need a reasonably recent version of Go installed (version 1.14 or higher).
Note: The shell commands used below are for Linux/macOS, feel free to use your operating system’s equivalent if it’s different.
First, create a folder for the project and initialize a new Go module:
$ mkdir go-avatars && cd go-avatars $ go mod init gitlab.com/idoko/go-avatars
Next, add the dependencies you’ll need by running the command below in your working directory (i.e go-avatars
):
$ go get golang.org/x/image github.com/golang/freetype github.com/go-chi/chi
The dependencies comprise of image
which provides methods for working with images in Go, freetype
for working with fonts (and writing on images), as well as the chi
router to serve the generated avatars over HTTP.
To prepare the background for our avatars, we create a main.go
file in the working directory and implement two functions – main
and createAvatar
. Open the main.go
file and add the code block below:
package main import ( "fmt" "image" "image/color" "image/draw" "image/png" "log" "os" "time" ) func main() { initials := "LR" size := 200 avatar, err := createAvatar(size, initials) if err != nil { log.Fatal(err) } filename := fmt.Sprintf("out-%d.png", time.Now().Unix()) file, err := os.Create(filename) if err != nil { log.Fatal(err) } png.Encode(file, avatar) } func createAvatar(size int, initials string) (*image.RGBA, error) { width, height := size, size bgColor, err := hexToRGBA("#764abc") if err != nil { log.Fatal(err) } background := image.NewRGBA(image.Rect(0, 0, width, height)) draw.Draw(background, background.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src) //drawText(background, initials) return background, err }
The createAvatar
function creates a square canvas whose width and height are the same as the size
parameter passed to the function. It converts a HEX color code to its RGBA equivalent, and the RGBA is then painted uniformly over the canvas. Next, we will implement the hexToRGBA
function to convert between colors.
Add the implementation of the hexToRGBA
function to the main.go
file:
func hexToRGBA(hex string) (color.RGBA, error) { var ( rgba color.RGBA err error errInvalidFormat = fmt.Errorf("invalid") ) rgba.A = 0xff if hex[0] != '#' { return rgba, errInvalidFormat } hexToByte := func(b byte) byte { switch { case b >= '0' && b <= '9': return b - '0' case b >= 'a' && b <= 'f': return b - 'a' + 10 case b >= 'A' && b <= 'F': return b - 'A' + 10 } err = errInvalidFormat return 0 } switch len(hex) { case 7: rgba.R = hexToByte(hex[1])<<4 + hexToByte(hex[2]) rgba.G = hexToByte(hex[3])<<4 + hexToByte(hex[4]) rgba.B = hexToByte(hex[5])<<4 + hexToByte(hex[6]) case 4: rgba.R = hexToByte(hex[1]) * 17 rgba.G = hexToByte(hex[2]) * 17 rgba.B = hexToByte(hex[3]) * 17 default: err = errInvalidFormat } return rgba, err }
The implementation uses the code from the gox
library and works by converting the HEX color code to a byte series. For hex triplets (i.e.,6-digits, three-bytes hexadecimal numbers), the first digit of each byte is multiplied by 16 (left shift by 4 bits) and added to the second digit of the same byte to get the RGB equivalent.
If the hex string is three digits (say #FFF), then we only need to multiply each of them by 17 to get the RGB equivalent.
You can learn more about conversions between web colors on this Wikipedia page.
Run the main.go
file with go run ./main.go
to generate a uniformly colored file like the one below (named out-xxxxxxxx.png
) in the project directory.
Next, we will implement the drawText
function responsible for writing the initials on the blank canvas. Open the main.go
file in your editor and add the code below to it:
func drawText(canvas *image.RGBA, text string) error { var ( fgColor image.Image fontFace *truetype.Font err error fontSize = 128.0 ) fgColor = image.White fontFace, err = freetype.ParseFont(goregular.TTF) fontDrawer := &font.Drawer{ Dst: canvas, Src: fgColor, Face: truetype.NewFace(fontFace, &truetype.Options{ Size: fontSize, Hinting: font.HintingFull, }), } textBounds, _ := fontDrawer.BoundString(text) xPosition := (fixed.I(canvas.Rect.Max.X) - fontDrawer.MeasureString(text)) / 2 textHeight := textBounds.Max.Y - textBounds.Min.Y yPosition := fixed.I((canvas.Rect.Max.Y)-textHeight.Ceil())/2 + fixed.I(textHeight.Ceil()) fontDrawer.Dot = fixed.Point26_6{ X: xPosition, Y: yPosition, } fontDrawer.DrawString(text) return err }
The function accepts an image.RGBA
pointer as a parameter, which ensures that it is modifying the same image passed to it. It uses the goregular font present in the Go standard library for rendering.
Drawing the text itself uses the font.Drawer
struct whose primary job is to write on images. We pass in the avatar canvas as drawer destination (Dst
) and a completely white image as the source (Src
) image, which renders the text in white.
We also calculate the horizontal starting point (xPosition
) for our text by first measuring how long the text is, subtracting the length from the overall canvas width, and dividing the result by 2 (to account for the left and right margins).
Similarly, the vertical position (yPosition
) is calculated by first, halving the difference between the canvas height and the text height, and adding the text height again to push the text into place.
Remember to uncomment the drawText(background, initials)
line in the createAvatar
function.
Also, you may need to import the dependencies manually if your editor hasn’t done that for you.
Run the main.go
file with go run ./main.go
and you should see the generated image in your working directory.
To make our avatars available in the browser, we will modify our main
function and make it render the generated images over HTTP. Replace your existing main
function with the code below:
router := chi.NewRouter() router.Use(middleware.Logger) router.Get("/avatar", func(w http.ResponseWriter, r *http.Request) { initials := r.FormValue("initials") size, err := strconv.Atoi(r.FormValue("size")) if err != nil { size = 200 } avatar, err := createAvatar(size, initials) if err != nil { log.Fatal(err) } w.Header().Set("Content-Type", "image/png") png.Encode(w, avatar) }) http.ListenAndServe(":3000", router)
The snippet above first sets up the /avatar
route using chi to process HTTP requests. The function then pulls the initials and size from the query parameters and uses it to generate the avatars.
Since png.Encode()
takes an io.Writer
interface (which is implemented by http.ResponseWriter
), our new main
function can directly serve the generated image for the browser to render.
Start the HTTP server by running go run ./main.go
in your terminal and visit http://localhost:3000/avatar?initials=JD&size=300 in your terminal to see the result.
I find dynamic avatars more aesthetically pleasing than static default ones, and in this article, we saw a way to implement it in Golang. You can take it a step further by using a background color determined by the initials – one way to do that is to create a slice or array of hexadecimal color strings, and pick a color from the slice based on the hash of the initials.
The complete source code for the tutorial is on GitLab. I wrote a mini package based on the code that you can use in your code here.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.