Generate text to image in Go

John Pili
5 min readFeb 12, 2021

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

Resources

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

Originally published at https://johnpili.com on February 12, 2021.

--

--

John Pili

I am currently working as a software development manager in Malaysia. I manage and help my team with technical software design and implementation.