In this blog post, I’ll share how to generate text to image in Go programming language (Golang). I have a previous and similar blog post using Python. You can check that post here

I created this application to generate images of my Linux configuration files or source code snippets and share it via WhatsApp or other messaging platforms. Another reason is to generate featured images for my social media posts in Twitter, Facebook or LinkedIn.

Twitter post with featured image

Twitter post with featured image

Project Structure

go-text-to-image/ 
 ├──models/
 │   └──config_models.go
 ├──page/
 │   └──page.go
 ├──static/
 │   ├──css/
 │   ├──img/
 │   └──vendors/
 ├──views/
 │   ├──base.html
 │   ├──text-to-image-builder.html
 │   └──text-to-image-result.html
 ├──.gitignore
 ├──config.yml
 ├──go.mod
 ├──utils.go
 └──main.go

Source Code

The complete source code is available on github: https://github.com/johnpili/go-text-to-image

package main

import (
	"bytes"
	"embed"
	"encoding/base64"
	"flag"
	"fmt"
	"github.com/golang/freetype"
	"github.com/johnpili/go-text-to-image/models"
	"github.com/johnpili/go-text-to-image/page"
	"github.com/julienschmidt/httprouter"
	"golang.org/x/image/font"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io/fs"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

var (
	configuration models.Config
	fontFile      string

	//go:embed static/*
	static embed.FS

	//go:embed views/*
	views embed.FS
)

func main() {
	pid := os.Getpid()
	err := os.WriteFile("application.pid", []byte(strconv.Itoa(pid)), 0666)
	if err != nil {
		log.Fatal(err)
	}

	var configLocation string
	flag.StringVar(&configLocation, "config", "config.yml", "Set the location of configuration file")
	flag.Parse()

	loadConfiguration(configLocation, &configuration)

	f := fs.FS(static)
	v, _ := fs.Sub(f, "static")

	router := httprouter.New()
	router.ServeFiles("/static/*filepath", http.FS(v))
	router.HandlerFunc("GET", "/", indexHandler)
	router.HandlerFunc("POST", "/", indexHandler)

	port := strconv.Itoa(configuration.HTTP.Port)
	httpServer := &http.Server{
		Addr:         ":" + port,
		Handler:      router,
		ReadTimeout:  120 * time.Second,
		WriteTimeout: 120 * time.Second,
	}

	if configuration.HTTP.IsTLS {
		log.Printf("Server running at https://localhost/tools:%s/\n", port)
		log.Fatal(httpServer.ListenAndServeTLS(configuration.HTTP.ServerCert, configuration.HTTP.ServerKey))
		return
	}
	log.Printf("Server running at http://localhost/tools:%s/\n", port)
	log.Fatal(httpServer.ListenAndServe())
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		{
			p := page.New()
			p.Title = "Generate Text to Image in Go"
			renderPage(w, r, p, "views/base.html", "views/text-to-image-builder.html")
		}
	case http.MethodPost:
		{
			textContent := strings.Trim(r.FormValue("textContent"), " ")

			fontSize, err := strconv.ParseFloat(strings.Trim(r.FormValue("fontSize"), " "), 64)
			if err != nil {
				fontSize = 32
			}

			fgColorHex := strings.Trim(r.FormValue("fgColor"), " ")
			fgColorHex = strings.ToLower(fgColorHex)

			bgColorHex := strings.Trim(r.FormValue("bgColor"), " ")
			bgColorHex = strings.ToLower(bgColorHex)

			b, err := generateImage(textContent, fgColorHex, bgColorHex, fontSize)
			if err != nil {
				log.Println(err)
				return
			}

			str := base64.StdEncoding.EncodeToString(b)
			p := page.New()
			p.Title = "Generate Text to Image in Go"

			data := make(map[string]interface{})
			data["generatedImage"] = str

			p.SetData(data)
			renderPage(w, r, p, "views/base.html", "views/text-to-image-result.html")
		}
	default:
		{
		}
	}
}

func generateImage(textContent string, fgColorHex string, bgColorHex string, fontSize float64) ([]byte, error) {

	fgColor := color.RGBA{0xff, 0xff, 0xff, 0xff}
	if len(fgColorHex) == 7 {
		_, err := fmt.Sscanf(fgColorHex, "#%02x%02x%02x", &fgColor.R, &fgColor.G, &fgColor.B)
		if err != nil {
			log.Println(err)
			fgColor = color.RGBA{0x2e, 0x34, 0x36, 0xff}
		}
	}

	bgColor := color.RGBA{0x30, 0x0a, 0x24, 0xff}
	if len(bgColorHex) == 7 {
		_, err := fmt.Sscanf(bgColorHex, "#%02x%02x%02x", &bgColor.R, &bgColor.G, &bgColor.B)
		if err != nil {
			log.Println(err)
			bgColor = color.RGBA{0x30, 0x0a, 0x24, 0xff}
		}
	}

	loadedFont, err := loadFont()
	if err != nil {
		log.Println(err)
		return nil, err
	}

	code := strings.Replace(textContent, "\t", "    ", -1) // convert tabs into spaces
	text := strings.Split(code, "\n")                      // split newlines into arrays

	fg := image.NewUniform(fgColor)
	bg := image.NewUniform(bgColor)
	rgba := image.NewRGBA(image.Rect(0, 0, 1200, 630))
	draw.Draw(rgba, rgba.Bounds(), bg, image.Pt(0, 0), draw.Src)
	c := freetype.NewContext()
	c.SetDPI(72)
	c.SetFont(loadedFont)
	c.SetFontSize(fontSize)
	c.SetClip(rgba.Bounds())
	c.SetDst(rgba)
	c.SetSrc(fg)
	c.SetHinting(font.HintingNone)

	textXOffset := 50
	textYOffset := 10 + int(c.PointToFixed(fontSize)>>6) // Note shift/truncate 6 bits first

	pt := freetype.Pt(textXOffset, textYOffset)
	for _, s := range text {
		_, err = c.DrawString(strings.Replace(s, "\r", "", -1), pt)
		if err != nil {
			return nil, err
		}
		pt.Y += c.PointToFixed(fontSize * 1.5)
	}

	b := new(bytes.Buffer)
	if err := png.Encode(b, rgba); err != nil {
		log.Println("unable to encode image.")
		return nil, err
	}
	return b.Bytes(), nil
}

To start drawing, I created a rectangular area that will represent the boundary of my image. The code:

image.NewRGBA(image.Rect(0, 0, 1200, 630))

does that. This statement creates a rectangle from point(0,0) to point(1200, 630).

image.Rect(0,0,1200,630)

image.Rect(0,0,1200,630)

To insert line of text, I have to specify the x and y offset position for the text.

textXOffset := 50
textYOffset := 10+int(c.PointToFixed(fontSize)>>6) // Note shift/truncate 6 bits first

In the textYOffset, I needed to factor in also the font size (height)

Before proceeding with the text drawing, I replaced tab characters with spaces and split every newlines into slices ( array) of strings so that I can position every line of text properly.

code := strings.Replace(textContent, "\t", " ", -1)
text := strings.Split(code, "\n")

I iterate through the array of strings and draw each line

for _, s := range text {        
  _ , err = c.DrawString(strings.Replace(s, "\r", "", -1), pt)
  if err != nil {
    return nil, err
  }
  pt.Y += c.PointToFixed(fontSize * 1.5)
}

Demo

I made an online demo available here: https://johnpili.com/tools/text-to-image

generate-text-to-image-in-go-demo

Resources

https://blog.golang.org/image

https://pace.dev/blog/2020/03/02/dynamically-generate-social-images-in-golang-by-mat-ryer.html