Michael Okoko Linux and Sci-Fi ➕ = ❤️

Working with Go images

4 min read 1271

workingwithgoimages

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.

Setup

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.

Create the image canvas

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.

Convert HEX code to RGBA 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.

Blank canvas generated by running the main.go file

Draw text on background

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.

[Avatar generated with initials set to “IM”

vatar generated with initials set to “LR” with purple background

Render generated images with chi router

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.

Firefox showing the generated avatar with initials “LR”

Conclusion

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.

Michael Okoko Linux and Sci-Fi ➕ = ❤️

Leave a Reply