Lessons

Automatically Create Cover Images with Golang

cover.png 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. unpolished.png This could be: sample1.png 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:

1 2 go mod init go mod tidy

I then installed a resize library and YAML parser.

1 2 go get github.com/nfnt/resize go get gopkg.in/yaml.v3

The YAML parser will be our configuration file, and it looks like:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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.
  • width and height :: The width and height of the final image.
  • overlayImageHeight :: The height of the icon image within the image.
  • iconPadding :: The padding between two icons.
  • leftPadding and rightPadding :: The padding on the outside of the image.
  • colorA and colorB :: This is what forms the gradient. They are rgb.
  • a :: The opacity of the final color.

What do I mean?

sample1_marked_up.png

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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.

1 2 3 4 5 6 7 8 9 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:

1 ./cover_image_gen config.yml
1 ./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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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.

1 2 3 4 5 6 7 8 9 10 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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:

  1. Grab the base iamge
  2. Determine the number of columns and the width of each.
  3. Draw a new output image with the base image
  4. Then iterate through each icon. The objective is to find the top left corner.
  5. Write all of it out.

That’s it! You can run it with:

1 go run ./ config.yml

Or you can build an executable to more easily add it to your CI/CD pipeline:

1 2 3 4 5 6 7 8 9 10 11 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:

sample1.png sample2.png sample3.png sample4.png

Resources