Docker container as an executable to process images using Go (golang)
By: Uday Hiwarale
Tags: Image Processing, Dockerfiles, Go, Docker Image, Docker

Image Processing Program

In this lesson, we are going to write a Go program that takes a source image on the file system (using the path of the image) and crops the image in a square shape. It also tweaks the contrast and brightness. Then it finally saves the grayscale format of the image on disk. The results look like below.

We won’t be writing a program that has complicated logic to manipulate every aspect of the source image to achieve this functionality. Instead, we are going to use the sweet little imaging library written in Go that provides us functions to manipulate the image and save it to the disk.

docker-image-processor/

├── .dockerignore

├── .gitignore

├── Dockerfile

├── bin/

| └── avatar

├── go.mod

├── go.sum

├── main.go

├── process.go

├── process_test.go

├── shared/

├── test.jpg

├── tmp/

| ├── out/

| | ├── cmd_out.jpg

| | ├── model-1.jpg

| | └── model-2.jpg

| └── src/

| | ├── model-1.jpg

| | └── model-2.jpg

└── utils.go

Out project structure will look something like the above. Do not worry about creating these files just now, some of them will be generated afterward. First of all, let’s convert this project to a Go module so that we can install and track dependencies.

$ go mod init docker-image-processor

This command will create the go.mod file in the project directory. We are not concerned about publishing this Go module so docker-image-processor module name would be alright for us. Now, let’s install the imaging module.

$ go get -u github.com/disintegration/imaging

This command will download the imaging module from GitHub and register it as a dependency inside go.mod file. It will also generate go.sum file to hold cryptographic hashes of the installed modules and their own dependencies.

The process.go file contains the actual logic to transform the source image and save the output image to disk. It has the makaAvatar function that does the job of producing the image.

func makaAvatar(srcPath string, outPath string) error

This function accepts two arguments. The first argument srcPath is the path of the source image on the disk and the second argument outPath is the path at which the output image will be saved.

The package main declaration suggests that this Go project would be compiled into a binary executable file. We will discuss more while working on the Dockerfile and the Docker image.

The utils.go file contains some utility functions such as fileExists to aid other programs in the project. Now that we have the makeAvatar() function ready, let’s test it out. For this, the best approach would be to create a unit test and execute makeAvatar() function to produce some sample outputs.

A unit test file in Go ends with _test suffix and the unit test function must start with the Test prefix. In the process_test.go test file, we have written the TestMakeAvatar function that tests the makeAvatar function for sample images located in the ./tmp/src directory and saves the output inside ./tmp/out directory. It then checks if the output files exist to conclude the results of the test. Let’s see the result of this test.

$ go test -v

The go test -v command instructs Go to look for test files ( *_test.go ) in the module directory and execute the test functions. From the result above, the TestMakeAvatar test has been passed. After running this test, you would be able to see the output images in the ./tmp/out directory which would look something like below (right).

Now let’s work on the main.go file which would be the entrypoint of the application. The application we are trying to build is a binary executable that accepts two arguments as shown below.

$ ./avatar <srcPath> <outPath>

The ./avatar file is the executable we will build from this Go project. The srcPath and outPath arguments are the path to the source image and output image on the disk. When we execute this command, the main() function inside the main.go file would be executed with these arguments.

But before building an executable file, we need to work on the main.go file and make a call to makeAvatar() function with srcPath and outPath received in the command. We can use go run . to run the module which executes the main() function just like an executable would.

$ go run . <srcPath> <outPath>

We can access the arguments passed to the command that executed the main() function using os.Args variable. The Args is a slice (array) of strings and the first element of this slice contains the path of the executable.

// main.go

package main import "fmt"

import "os" func main() {

fmt.Println(os.Args)

}

This simple program produces the following result when executed with the command $ go run . ./test.jpg ./test_out.jpg .

[

/var/folders/xx/..../b001/exe/docker-image-processor

./test.jpg

./test_out.jpg

]

When we run a Go program or an entire module in this case, Go first creates a binary executable file on the fly, stores it at a temporary location, and then executes that binary executable file. The binary executable execution starts by executing the main() function.

The command-line arguments received from os.Args we are interested in are the 2nd and 3rd ones. So let’s modify the main() function and make a call to makeAvatar() function with these two values.

In the program above, first, we are checking if the length of os.Args is less than 3 and exiting the program with an error if that’s the case. Then we are extracting the 2nd and 3rd argument which would be the srcPath and outPath for the makeAvatar() function.

$ go build -o ./bin/avatar .

The command above will build a binary executable file avatar from this module and put it inside the ./bin directory of the project. Then we can execute this binary executable using the ./bin/avatar <args> command.

$ ./bin/avatar ./tmp/src/model-1.jpg ./tmp/out/cmd_out.jpg

Success! Image has been generated at ./tmp/out/cmd_out.jpg.