Standard library image packages

The first step in working with images in Go is using the standard library package image. To use the package, you have to register an image decoder. The standard library provides decoders for only the most standard image types - jpg, png, and gif.

The standard library also provides subpackages image/color for handling colors and image/draw to layer images one over the other.

Loading images

Let’s start by writing a CLI for imageinfo. We want to take an input file, decode it (load the image), and then print image dimensions. Create the cmd/imageinfo path and create the following files in that location.

We will start with this simple entrypoint (main.go):

 1 package main
 2 
 3 import (
 4   _ "image/gif"
 5   _ "image/jpeg"
 6   _ "image/png"
 7   "log"
 8   "os"
 9 )
10 
11 func main() {
12   if len(os.Args) < 2 {
13     log.Fatalln("Usage: imageinfo [image.jpg/png/gif]")
14   }
15 
16   if err := printInfo(os.Args[1]); err != nil {
17     log.Fatalln(err)
18   }
19 }

The entrypoint takes care of loading the image drivers which we will support. By prefixing the import path with _, only the image drivers get registered, the rest of the packages are unused in this file.

The main() functions only takes care of ensuring that a single parameter was passed to our program. In case the parameter isn’t present, a descriptive error will be printed. We invoke printInfo with the filename to read.

Next, we need to create a helper function to load images. Let’s create load.go with the following contents:

 1 package main
 2 
 3 import (
 4   "image"
 5   "os"
 6 )
 7 
 8 func load(filename string) (image.Image, error) {
 9   f, err := os.Open(filename)
10   if err != nil {
11     return nil, err
12   }
13   defer f.Close()
14 
15   m, _, err := image.Decode(f)
16   return m, err
17 }

The load() function only takes care of loading the image from a file. It passes the file reader to image.Decode, and returns any error that might have occured.

Now, for each image, we would like to print two parts of information. We would like to know the image dimensions, and we would like to know what the average color of the image is.

Create a info.go to implement our imageInfo function:

 1 package main
 2 
 3 func printInfo(filename string) error {
 4   m, err := load(filename)
 5   if err != nil {
 6     return err
 7   }
 8 
 9   printDimensions(m)
10   printAverageColor(m)
11   return nil
12 }

Getting the dimensions of the image is done by retrieving the image bounds, and getting the information from the returned image.Rectangle. Go ahead and create a dimensions.go file:

 1 package main
 2 
 3 import (
 4   "fmt"
 5   "image"
 6 )
 7 
 8 func printDimensions(m image.Image) {
 9   bounds := m.Bounds()
10   fmt.Printf("Width:  %d\n", bounds.Dx())
11   fmt.Printf("Height: %d\n", bounds.Dy())
12 }

We used bounds.Dx() and bounds.Dy() as the shorthand calculations for width and height of the image. If we need image dimensions, we can use these built-in functions on image.Rectangle.

Reading image colors

For our next task, we want to calculate the average color of the image. This means reading all the image pixels, adding all the colors together, and finally dividing by the number of pixels in the image.

We will use another Image interface function for this:

1 func At(x, y int) color.Color

The Color interface provides a RGBA() (r,g,b,a uint32), with 16-bit accuracy, meaning each value ranges between [0, 0xFFFF] (16 bits). The uint32 type has been chosen to avoid an overflow when a color is multiplied by the blend factor up to 0xFFFF (16 bits). The full color is encoded in a 64 bit value, with 16 bits per component.

Let’s create a printAverageColor(m image.Image) function to print the average RGBA for the whole image. We need to read each pixel color value, add them together, and then divide by pixel count.

While the bounds have helpers for getting the width and height of the image, the actual image boundaries may start at values different from X=0,Y=0. This means we need to iterate the pixels by starting with the bounds Min.X and Min.Y values.

 1 package main
 2 
 3 import (
 4   "fmt"
 5   "image"
 6 )
 7 
 8 func printAverageColor(m image.Image) {
 9   var r, g, b, a uint64
10 
11   bounds := m.Bounds()
12   for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
13     for x := bounds.Min.X; x < bounds.Max.X; x++ {
14       cr, cg, cb, ca := m.At(x, y).RGBA()
15       r += uint64(cr)
16       g += uint64(cg)
17       b += uint64(cb)
18       a += uint64(ca)
19     }
20   }
21 
22   // multiply with 256 to get 8 bits per component after division
23   count := uint64(bounds.Dx() * bounds.Dy() * 256)
24 
25   fmt.Printf("R: %d\nG: %d\nB: %d\nA: %d\n", r/count, g/count, b/count, a/count)
26 }

We can invoke this function from printImageInfo, and look at the output:

1 Width:  640
2 Height: 427
3 R: 80
4 G: 75
5 B: 68
6 A: 255

As I’m loading a jpg file for testing, there is no alpha channel on it, meaning every color is opaque. If this was a png with a defined alpha channel, the story would be a little bit different:

A white pixel with 50% opacity results in the following:

1 Width:  1
2 Height: 1
3 R: 127
4 G: 127
5 B: 127
6 A: 127

Here you can note that the color returned is already multiplied by the alpha channel. The color package documents this:

RGBA returns the alpha-premultiplied red, green, blue and alpha values for the color. […] An alpha-premultiplied color component c has been scaled by alpha (a), so has valid values 0 <= c <= a.

This is dependant on what kind of image we have loaded.

Image color space

Depending on the image type, we may have loaded a paletted image (gif), or an 8-bit greyscale image. Each of these image examples has a particular color model, which we can inspect with the third and final function implemented by the Image interface.

1 func ColorModel() color.Model

In order to ensure that we’re dealing with a particular color model, we must create a new image of a particular type (NRGBA) and then draw the source image onto that. Think of it like copy-pasting a grey-scale image into a true-color empty image, and then saving it as true-color.

This brings us to the image/draw package, and it’s Draw() function.

Let’s implement converters to the NRGBA and NRGBA64 image types. By using the concrete image type, we can get other functions that also allow us to set/write pixels to the image, something that the Image interface doesn’t provide us. Create convert.go:

 1 package main
 2 
 3 import (
 4   "image"
 5   "image/draw"
 6 )
 7 
 8 func toNRGBA(in image.Image) *image.NRGBA {
 9   bounds := in.Bounds()
10   out := image.NewNRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
11   draw.Draw(out, out.Bounds(), in, bounds.Min, draw.Src)
12   return out
13 }
14 
15 func toNRGBA64(in image.Image) *image.NRGBA64 {
16   bounds := in.Bounds()
17   out := image.NewNRGBA64(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
18   draw.Draw(out, out.Bounds(), in, bounds.Min, draw.Src)
19   return out
20 }

Now, using the NRGBA64 image won’t give us NRGBA64 colors when calling the image At() function. The particular function will always return a premultiplied color. The NRGBA64 struct provides another function, NRGBA64At, which returns the color without premultiplication.

We need to read individual fields from the NRGBA64 color struct. If we would invoke RGBA(), it will return the multiplied color components. As we don’t need 16 bit precision, I’ll be using the NRGBA type here.

Let’s rewrite printAverageColor a bit, to split it into two functions. The updated function body to get the average color looks like this:

 1 func getAverageColor(in image.Image) *color.NRGBA {
 2   var r, g, b, a uint64
 3 
 4   m := toNRGBA(in)
 5 
 6   bounds := m.Bounds()
 7   for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
 8     for x := bounds.Min.X; x < bounds.Max.X; x++ {
 9       color := m.NRGBAAt(x, y)
10       r += uint64(color.R)
11       g += uint64(color.G)
12       b += uint64(color.B)
13       a += uint64(color.A)
14     }
15   }
16 
17   count := uint64(bounds.Dx() * bounds.Dy())
18   return &color.NRGBA{
19     R: uint8(r / count),
20     G: uint8(g / count),
21     B: uint8(b / count),
22     A: uint8(a / count),
23   }
24 }
25 
26 func printAverageColor(in image.Image) {
27   color := getAverageColor(in)
28   fmt.Printf("R: %d\nG: %d\nB: %d\nA: %d\n", color.R, color.G, color.B, color.A)
29 }

We can now re-use getAverageColor when we want to get the actual color. Re-running with the 50% white png image gives us the expected output now:

1 Width:  1
2 Height: 1
3 R: 255
4 G: 255
5 B: 255
6 A: 127

We can now read true colors, which can be used for HTML composed color entities. As RGB(A) colors in CSS aren’t pre-multiplied, relying on NRGBA image formats when doing color processing is a must. Whatever image processing APIs you will write, should use a concrete image type like NGRBA64 as soon as possible, do you don’t do unnecessary conversions.

I have chosen NRGBA as 8 bits per color component is the most standard image format used in JPEG/PNG files and HTML color codes as well.

Saving images

Saving images is done with their respective image drivers. The interface for saving the images isn’t generic, as each driver implements their own options. While JPEG images have a Quality parameter (percentage as int), PNG images only provide a CompressionRatio option.

Create a save.go with our needed imports:

 1 package main
 2 
 3 import (
 4   "errors"
 5   "image"
 6   "image/jpeg"
 7   "image/png"
 8   "io"
 9   "os"
10   "path"
11 )

We can abstract our image writers behind an interface:

 1 type imageWriter func(io.Writer, image.Image) error
 2 
 3 func writeJpeg(w io.Writer, img image.Image) error {
 4   opt := jpeg.Options{
 5     Quality: 90,
 6   }
 7   return jpeg.Encode(w, img, &opt)
 8 }
 9 
10 func writePng(w io.Writer, img image.Image) error {
11   encoder := &png.Encoder{
12     CompressionLevel: png.BestSpeed,
13   }
14   return encoder.Encode(w, img)
15 }

Based on the image filename we’re trying to save, we can decide which writer we need to use to save the image to disk:

 1 func saveImage(filename string, img image.Image, writer imageWriter) error {
 2   f, err := os.Create(filename)
 3   if err != nil {
 4     return err
 5   }
 6   defer f.Close()
 7 
 8   return writer(f, img)
 9 }
10 
11 func save(filename string, img image.Image) error {
12   ext := path.Ext(filename)
13   if ext == ".jpg" {
14     return saveImage(filename, img, writeJpeg)
15   }
16   if ext == ".png" {
17     return saveImage(filename, img, writePng)
18   }
19   return errors.New("save: unsupported extension " + ext)
20 }

And that’s all there is to it. Implementing a gif writer, or any other image format, is left as an exercise to the reader.