Automatically Create Cover Images with Golang

I’ve been trying to add more images through out my tutorials to make the site look better. The first thing I did was added a cover image to all tutorials. I create a basic Go script to do this and I’ll share it in this post.
Why? I’ve been using the same stolen image and it is the last 5% that makes a website that more polished.

This could be:

It doesn’t need to be complicated - gradient image as a background, then a few icons that related to the tutorial and are evenly spaced. I started by creating a new project:
go mod init
go mod tidy
I then installed a resize library and YAML parser.
go get github.com/nfnt/resize
go get gopkg.in/yaml.v3
The YAML parser will be our configuration file, and it looks like:
colorA:
- 207
- 237
- 237
colorB:
- 181
- 198
- 198
a: 255
leftPadding: 50
rightPadding: 50
iconPadding: 10
overlayImageHeight: 200
outputPath: output.png
width: 1300
height: 500
images:
- test_images/001-landing-page.png
- test_images/002-browser.png
- test_images/003-new-window.png
Each value here has some meaning, but the majority will remain static.
outputPath:: This is the file path where the output will be set.images:: A list of icons that will be overlayed.widthandheight:: The width and height of the final image.overlayImageHeight:: The height of the icon image within the image.iconPadding:: The padding between two icons.leftPaddingandrightPadding:: The padding on the outside of the image.colorAandcolorB:: This is what forms the gradient. They arergb.a:: The opacity of the final color.
What do I mean?

Creating an Image
The first step is to create an image with a gradient background. Thank you to this post for providing a sample. I created a file named create.go and the contents are:
package main
import (
"image"
"image/color"
"image/png"
"os"
)
// https://varunpant.com/posts/create-linear-color-gradient-in-go/
func createImageWithGradientBackground(config Config) {
width := config.Width
height := config.Height
dst := image.NewRGBA(image.Rect(0, 0, width, height)) //*NRGBA (image.Image interface)
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
r, g, b := linearGradient(config, float64(x), float64(y), width)
c := color.RGBA{
r,
g,
b,
uint8(config.A),
}
dst.Set(x, y, c)
}
}
img, _ := os.Create(config.OutputPath)
defer img.Close()
png.Encode(img, dst) //Encode writes the Image m to w in PNG format.
}
func linearGradient(config Config, x float64, y float64, max int) (uint8, uint8, uint8) {
colorA := config.ColorA
colorB := config.ColorB
d := x / float64(max)
r := colorA[0] + d*(colorB[0]-colorA[0])
g := colorA[1] + d*(colorB[1]-colorA[1])
b := colorA[2] + d*(colorB[2]-colorA[2])
return uint8(r), uint8(g), uint8(b)
}
Import Config
We want to create our configuration file, and decode the YAML file into a struct. I’ve added models.go and the contents are:
package main
import (
yaml "gopkg.in/yaml.v3"
"os"
)
type Config struct {
ColorA []float64 `yaml:"colorA"`
ColorB []float64 `yaml:"colorB"`
A float64 `yaml:"a"`
LeftPadding float64 `yaml:"leftPadding"`
RightPadding float64 `yaml:"rightPadding"`
IconPadding float64 `yaml:"iconPadding"`
OverlayImageHeight float64 `yaml:"overlayImageHeight"`
OutputPath string `yaml:"outputPath"`
Width int `yaml:"width"`
Height int `yaml:"height"`
Images []string `yaml:"images"`
}
func NewConfig(path string) Config {
// Read in the config yml file
f, err := os.ReadFile(path)
if err != nil {
panic(err.Error())
}
// Unmarshal the config yml file
var config Config
err = yaml.Unmarshal(f, &config)
if err != nil {
panic(err.Error())
}
return config
}
Overwrites
Next, I want to be able to easily swap in and out the output path and icons paths. I created a file called main.go and added this function.
func updateConfigWithAdditionalArgs(config Config) Config {
if len(os.Args) > 2 {
config.OutputPath = os.Args[2]
}
if len(os.Args) > 3 {
config.Images = os.Args[3:]
}
return config
}
You pass in the config, and it returns the config. This will allow us to run:
./cover_image_gen config.yml
./cover_image_gen config.yml output.png icon1.png icon2.png icon3.png
You may want to create a configuration for each configuration, or create a single config then adjust the inputs.
Overlay Images
Let’s add the remaining code to main.go. There are three methods: main , updateConfigWithAdditionalArgs (explained above), and overlayImages.
package main
import (
"fmt"
"github.com/nfnt/resize"
"image"
"image/draw"
_ "image/jpeg" // Register JPEG format
"image/png"
_ "image/png" // Register PNG format
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Printf("Usage: %s <config file>\n", os.Args[0])
os.Exit(1)
}
config := NewConfig(os.Args[1])
config = updateConfigWithAdditionalArgs(config)
createImageWithGradientBackground(config)
overlayImages(config)
}
Let’s explain this main function. First, we need a least 1 argument. The first argument is always the executable, so the second element will be the config. If not provided, exit.
Next, parse the config, and apply the overwrites (if applicable). We then create a gradient image. Finally, overlay the icons on top of the gradient image.
func updateConfigWithAdditionalArgs(config Config) Config {
if len(os.Args) > 2 {
config.OutputPath = os.Args[2]
}
if len(os.Args) > 3 {
config.Images = os.Args[3:]
}
return config
}
This code was explained above.
func overlayImages(config Config) {
baseImagePath := config.OutputPath
additionalImagePaths := config.Images
// Load in the base image
baseImageFile, err := os.Open(baseImagePath)
if err != nil {
panic(err.Error())
}
img, _, err := image.Decode(baseImageFile)
if err != nil {
panic(err.Error())
}
// Determine dimensions, and column widths
fmt.Printf("Base image is width=%d, height=%d\n", img.Bounds().Dx(), img.Bounds().Dy())
workingSpace := img.Bounds().Dx() - int(config.LeftPadding) - int(config.RightPadding)
fmt.Printf("Working space is %d\n", workingSpace)
colWidth := workingSpace / len(additionalImagePaths)
fmt.Printf("Column width is %d\n", colWidth)
// Create a new image to place the overlay images on
output := image.NewRGBA(img.Bounds())
draw.Draw(output, img.Bounds(), img, image.Point{0, 0}, draw.Src)
// Iterate through each image to be placed
for i, additionalImagePath := range additionalImagePaths {
// Read in the image being placed
additionalImageFile, err := os.Open(additionalImagePath)
if err != nil {
panic(err.Error())
}
additionalImage, _, err := image.Decode(additionalImageFile)
if err != nil {
panic(err.Error())
}
fmt.Printf("Additional image is orginally width=%d, height=%d\n", additionalImage.Bounds().Dx(), additionalImage.Bounds().Dy())
// Resize the image to make all equal height
additionalImage = resize.Resize(uint(config.OverlayImageHeight), 0, additionalImage, resize.Lanczos3)
fmt.Printf("Additional image is (after resize) width=%d, height=%d\n", additionalImage.Bounds().Dx(), additionalImage.Bounds().Dy())
// Determine the center position of the column. This would be half the remaining space after subtracting the
// overlay image.
leftX := ((int(config.IconPadding) * 2) + colWidth - additionalImage.Bounds().Dx()) / 2
// Math is remove padding. Take the column width, remove icon width. This leaves the remaining space. Divide
// that by 2 to get the amount of space on either side.
leftX += int(config.IconPadding) // Re-add the icon padding.
leftX += i * colWidth // Add the column width for each column to the left.
leftX += int(config.LeftPadding) // Add the left padding.
// Determine the center position of the image. This would be half the remaining space after subtracting the
// overlay image.
topY := (img.Bounds().Dy() - int(config.OverlayImageHeight)) / 2
fmt.Printf("Placing image %d at %d, %d\n", i+1, leftX, topY)
// Place the image on top of the page image
draw.Draw(output, additionalImage.Bounds().Add(image.Point{X: leftX, Y: topY}), additionalImage, image.ZP, draw.Over)
}
// Save the output
outputImg, _ := os.Create(config.OutputPath)
defer outputImg.Close()
png.Encode(outputImg, output) //Encode writes the Image m to w in PNG format.
}
I’ve done my best to comment throughout the code. The basic flow is:
- Grab the base iamge
- Determine the number of columns and the width of each.
- Draw a new output image with the base image
- Then iterate through each icon. The objective is to find the top left corner.
- Write all of it out.
That’s it! You can run it with:
go run ./ config.yml
Or you can build an executable to more easily add it to your CI/CD pipeline:
go build -o cover_image_gen .
GOOS=linux GOARCH=amd64 go build -o cover_image_gen_linux .
chmod +x ./cover_image_gen
./cover_image_gen sample.yml
# If you want to add it to your path
cp ./cover_image_gen /usr/local/bin/cover_image_gen
# Open a new terminal window
cover_image_gen sample.yml
Thanks for reading! Here are some samples:



