Generate text to image in Go

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.

social media post with feature image
Twitter post with featured image

Project Structure

text-to-image-in-go-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).

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)

text-positioning-offset

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)
}
text-positioning-newline

Demo

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

generate-text-to-image-in-go-demo
This is how the completed application looks like. Click to zoom in

Resources

https://blog.golang.org/image

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

Posted in GoTagged