In this blog post I will share how to generate text to image in Go programming language (Golang). I have previous and similar blog post using Python. You can check that post here.
The reasons why I created this application is for me to share text content like Linux configuration and source code snippet in WhatsApp or other messaging application. Another is to generate featured image for social media posts in Twitter, Facebook or LinkedIn.

Project Structure

Source code
The complete source code is available on github: https://github.com/johnpili/go-text-to-image
The file main.go contains the initialization and configuration codes to run this application.
package main import ( "flag" "github.com/johnpili/go-text-to-image/controllers" "github.com/johnpili/go-text-to-image/models" "io/ioutil" "log" "net/http" "os" "strconv" "time" rice "github.com/GeertJohan/go.rice" "github.com/go-zoo/bone" "github.com/psi-incontrol/go-sprocket/sprocket" ) var ( configuration models.Config ) func main() { pid := os.Getpid() err := ioutil.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() sprocket.LoadYAML(configLocation, &configuration) viewBox := rice.MustFindBox("views") staticBox := rice.MustFindBox("static") controllersHub := controllers.New(viewBox, nil, nil, &configuration) staticFileServer := http.StripPrefix("/static/", http.FileServer(staticBox.HTTPBox())) router := bone.New() router.Get("/static/", staticFileServer) controllersHub.BindRequestMapping(router) // CODE FROM https://medium.com/@mossila/running-go-behind-iis-ce1a610116df port := strconv.Itoa(configuration.HTTP.Port) if os.Getenv("ASPNETCORE_PORT") != "" { // get enviroment variable that set by ACNM port = os.Getenv("ASPNETCORE_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()) }
The page_controller.go handles both the page rendering and image generation.
package controllers import ( "bytes" "encoding/base64" "fmt" "golang.org/x/image/draw" "image" "image/color" "image/png" "io/ioutil" "log" "net/http" "strconv" "strings" "github.com/go-zoo/bone" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "github.com/psi-incontrol/go-sprocket/page" "github.com/psi-incontrol/go-sprocket/sprocket" "golang.org/x/image/font" ) // PageController ... type PageController struct { fontFile string } // RequestMapping ... func (z *PageController) RequestMapping(router *bone.Mux) { router.GetFunc("/", http.HandlerFunc(z.TextToImageHandler)) router.PostFunc("/", http.HandlerFunc(z.TextToImageHandler)) } func (z *PageController) loadFont() (*truetype.Font, error) { z.fontFile = "static/fonts/UbuntuMono-R.ttf" fontBytes, err := ioutil.ReadFile(z.fontFile) if err != nil { return nil, err } f, err := freetype.ParseFont(fontBytes) if err != nil { return nil, err } return f, nil } func (z *PageController) 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 := z.loadFont() if err != nil { 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 } // TextToImageHandler ... func (z *PageController) TextToImageHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: { page := page.New() page.Title = "Generate Text to Image in Go" renderPage(w, r, page, "base.html", "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 := z.generateImage(textContent, fgColorHex, bgColorHex, fontSize) if err != nil { log.Println(err) sprocket.RespondStatusCodeWithJSON(w, 500, nil) return } str := "data:image/png;base64," + base64.StdEncoding.EncodeToString(b) page := page.New() page.Title = "Generate Text to Image in Go" data := make(map[string]interface{}) data["generatedImage"] = str page.SetData(data) renderPage(w, r, page, "base.html", "text-to-image-result.html") } default: { } } }
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).

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

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