Working with Images in Go

Advertisement

Advertisement

Overview

Introduction

The Image interface is at the core of image manipulation in Go. No matter what format you want to import or export from, it ultimately ends up as an Image. This is where the beauty of Go interfaces really shines. Go comes with support for gif, jpeg, and png formats in the standard packages. These examples demonstrate how to programatically generate, encode, decode, write to file, and base64 encode images. We will also cover a little bit about interfaces.

The Image Interface

The Image interface has three elements. The color model, the dimensions, and the color at each pixel. Below is the actual interface definition. The interface is only an abstract thing though. We can't create an Image itself, we can only create things that satisfy the Image interface. Go provides a type that does that. The actual struct that implements the Image interface is the RGBA type. Now, to implement the interface we just have to have all of the data elements and functions that the interface expects. We don't have to specify that we are implementing the interface with any special keywords. Other languages might require somethign like class RGBA implements Image but that is not so in Go.

So, the key thing to remember is that Image is an interface that defines the minimum requirements for pieces interacting together. The RGBA type is the type of image that we will use. We can use the RGBA anywhere an Image interface is accepted.

// This is just an abstract interface. We can't actually instantiate an image
// directly. We can only use or implement types that implement these functions
// and contain these data elements. By doing so we satisfy the interface,
// and we can use that type anywhere an Image interface is passed
type Image interface {
    // ColorModel returns the Image's color model.
    ColorModel() color.Model

    // Bounds returns the domain for which At can return non-zero color.
    // The bounds do not necessarily contain the point (0, 0).
    Bounds() Rectangle

    // At returns the color of the pixel at (x, y).
    // At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
    // At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
    At(x, y int) color.Color
}

Parts of the Image Interface

The Image interface itself is pretty simple since it only has three elements. It is important to understand what each of those three elements represents too. Here is the source for those types.

// The Image interface is simple with only 3 elements. Each one
// of those elements has its own details though. Fortunately,
// they aren't very complex either. Here they are.
type Model interface {
    // Model can convert any Color to one from its
    // own color model. The conversion may be lossy.
    Convert(c Color) Color
}

// Stores two (x,y) coordinates called Points
// Those points mark the two corners of the rectangle
type Rectangle struct {
    Min, Max Point
}

// Color stores an RGB value and an Alpha(transparency)
type Color interface {
    // RGBA returns the alpha-premultiplied red, green, blue and alpha values
    // for the color. Each value ranges within [0, 0xFFFF], but is represented
    // by a uint32 so that multiplying by a blend factor up to 0xFFFF will not
    // overflow.
    RGBA() (r, g, b, a uint32)
}

Generating an Image

package main

import (
    "fmt"
    "image"
)

func main() {
    // Create a blank image 10 pixels wide by 4 pixels tall
    myImage := image.NewRGBA(image.Rect(0, 0, 10, 4))

    // You can access the pixels through myImage.Pix[i]
    // One pixel takes up four bytes/uint8. One for each: RGBA
    // So the first pixel is controlled by the first 4 elements
    // Values for color are 0 black - 255 full color
    // Alpha value is 0 transparent - 255 opaque
    myImage.Pix[0] = 255 // 1st pixel red
    myImage.Pix[1] = 0 // 1st pixel green
    myImage.Pix[2] = 0 // 1st pixel blue
    myImage.Pix[3] = 255 // 1st pixel alpha

    // myImage.Pix contains all the pixels
    // in a one-dimensional slice
    fmt.Println(myImage.Pix)

    // Stride is how many bytes take up 1 row of the image
    // Since 4 bytes are used for each pixel, the stride is
    // equal to 4 times the width of the image
    // Since all the pixels are stored in a 1D slice,
    // we need this to calculate where pixels are on different rows.
    fmt.Println(myImage.Stride) // 40 for an image 10 pixels wide
}

Writing Image to File

The Encode() function accepts a writer so you could write it to any writer interface. That includes files, stdout, tcp sockets, or any custom one. In this example we open a file writer.

package main

import (
    "image"
    "image/png"
    "os"
)

func main() {
    // Create a blank image 100x200 pixels
    myImage := image.NewRGBA(image.Rect(0, 0, 100, 200))

    // outputFile is a File type which satisfies Writer interface
    outputFile, err := os.Create("test.png")
    if err != nil {
    // Handle error
    }

    // Encode takes a writer interface and an image interface
    // We pass it the File and the RGBA
    png.Encode(outputFile, myImage)

    // Don't forget to close files
    outputFile.Close()
}

Reading Image From File

package main

import (
"fmt"
"image"
"image/png"
"os"
)

func main() {
// Read image from file that already exists
existingImageFile, err := os.Open("test.png")
if err != nil {
// Handle error
}
defer existingImageFile.Close()

// Calling the generic image.Decode() will tell give us the data
// and type of image it is as a string. We expect "png"
imageData, imageType, err := image.Decode(existingImageFile)
if err != nil {
// Handle error
}
fmt.Println(imageData)
fmt.Println(imageType)

// We only need this because we already read from the file
// We have to reset the file pointer back to beginning
existingImageFile.Seek(0, 0)

// Alternatively, since we know it is a png already
// we can call png.Decode() directly
loadedImage, err := png.Decode(existingImageFile)
if err != nil {
// Handle error
}
fmt.Println(loadedImage)
}

Base64 Encoding Image

Instead of writing the image data to a file, we could base64 encode it and store it as a string. This is useful if you want to generate an image and embed it directly in an HTML document. That is beneficial for one-time images that don't need to be stored on the file system and for creating stand-alone HTML documents that don't require a folder full of images to go with it.

package main

import (
    "bytes"
    "encoding/base64"
    "fmt"
    "image"
    "image/png"
)

func main() {
    // Create a blank image 10x20 pixels
    myImage := image.NewRGBA(image.Rect(0, 0, 10, 20))

    // In-memory buffer to store PNG image
    // before we base 64 encode it
    var buff bytes.Buffer

    // The Buffer satisfies the Writer interface so we can use it with Encode
    // In previous example we encoded to a file, this time to a temp buffer
    png.Encode(&buff, myImage)

    // Encode the bytes in the buffer to a base64 string
    encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())

    // You can embed it in an html doc with this string
    htmlImage := "<img src=\"data:image/png;base64," + encodedString + "\" />"
    fmt.Println(htmlImage)
}

Conclusion

With this knowledge, you should be able to handle basic image manipulation with Go. For further reading, refer to the official documentation on the image package at https://golang.org/pkg/image/.

Advertisement

Advertisement