Lessons
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:
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
andheight
:: 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
andrightPadding
:: The padding on the outside of the image.colorA
andcolorB
:: 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:
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:
- 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:
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: