Writing an Image Manipulation Library in Go — Part 1

Koray Göçmen
The Startup
Published in
4 min readOct 14, 2020

--

Part 1 — Utility Functions

I have been interested in image manipulation for some time. Last year I worked on a small package that does simple image manipulation using Go’s standard image package: github.com/KorayGocmen/image

Let’s look at how to read an image from a provided file path and creating an image object. This is what the structs will look like.

// GrayscaleAverage, GrayscaleLuma, GrayscaleDesaturation
// are used by grayscale to choose between algorithms
const (
GrayscaleAverage = 0
GrayscaleLuma = 1
GrayscaleDesaturation = 2
)

// Pixel is a single pixel in 2d array
type Pixel struct {
R int
G int
B int
A int
}

// Image is the main object that holds information about the
// image file. Also is a wrapper around the decoded image
// from the standard image library.
type Image struct {
Pixels [][]Pixel
Width int
Height int
_Rect image.Rectangle
_Image image.Image
}

An image is basically a matrix of pixels. There are different image encoding techniques. I am using RGBA colour space, it stands for Red, Green, Blue and Alpha. Red, Green, Blue is pretty self-explanatory and Alpha is basically opacity of the pixel.

I am going to need 3 helper functions to quickly get/set a certain pixel and to transform the decoded pixel to my pixel format. This is how they are going to look:

// Get pixel value with key name
func (pix *Pixel) Get(keyName string) int {
switch keyName {
case "R":
return pix.R
case "G":
return pix.G
case "B":
return pix.B
case "A":
return pix.A
default:
return -1
}
}

// Set pixel value with key name and new value
func (pix *Pixel) Set(keyName string, val int) Pixel {
switch keyName {
case "R":
pix.R = val
case "G":
pix.G = val
case "B":
pix.B = val
case "A":
pix.A = val
}
return *pix
}

// rgbaToPixel alpha-premultiplied red, green, blue and alpha values
// to 8 bit red, green, blue and alpha values.
func rgbaToPixel(r uint32, g uint32, b uint32, a uint32) Pixel {
return Pixel{
R: int(r / 257),
G: int(g / 257),
B: int(b / 257),
A: int(a / 257),
}
}

`rgbaToPixel` function converts the pixels image package returns into pixels I defined. The RGBA values in standard package are uint32 type, I preferred int type. Therefore I need to divide the values by 257 and cast them as int.

Now, I am ready to read the image from a provided `filepath` and create my image object. This code can decode “jpeg”/”jpg” and “png” formats. I am going to keep the native image packages “img” object for future reference under my own image object via the key “_Image”. This function will return the image object or maybe an error.

// New reads an image from the given file path and return a
// new `Image` struct.
func New(filePath string) (*Image, error) {
s := strings.Split(filePath, ".")
imgType := s[len(s)-1]

switch imgType {
case "jpeg", "jpg":
image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig)
case "png":
image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
default:
return nil, errors.New("unknown image type")
}

imgReader, err := os.Open(filePath)
if err != nil {
return nil, err
}

img, _, err := image.Decode(imgReader)
if err != nil {
return nil, err
}

bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y

var pixels [][]Pixel
for y := 0; y < height; y++ {
var row []Pixel
for x := 0; x < width; x++ {
pixel := rgbaToPixel(img.At(x, y).RGBA())
row = append(row, pixel)
}
pixels = append(pixels, row)
}

return &Image{
Pixels: pixels,
Width: width,
Height: height,
_Rect: img.Bounds(),
_Image: img,
}, nil
}

Now I can read an image from a file but we also need to rewrite our manipulated image back to a file. This is pretty much doing everything I did in the `New` function in reverse as one might have guessed.

// WriteToFile writes iamges to the given filepath.
// Returns an error if it occurs.
func (img *Image) WriteToFile(outputPath string) error {
cimg := image.NewRGBA(img._Rect)
draw.Draw(cimg, img._Rect, img._Image, image.Point{}, draw.Over)

for y := 0; y < img.Height; y++ {
for x := 0; x < img.Width; x++ {
rowIndex, colIndex := y, x
pixel := img.Pixels[rowIndex][colIndex]
cimg.Set(x, y, color.RGBA{
uint8(pixel.R),
uint8(pixel.G),
uint8(pixel.B),
uint8(pixel.A),
})
}
}

s := strings.Split(outputPath, ".")
imgType := s[len(s)-1]

switch imgType {
case "jpeg", "jpg", "png":
fd, err := os.Create(outputPath)
if err != nil {
return err
}

switch imgType {
case "jpeg", "jpg":
jpeg.Encode(fd, cimg, nil)
case "png":
png.Encode(fd, cimg)
}
default:
return errors.New("unknown image type")
}

return nil
}

Alright, I have the utility functions ready, now it is time for the fun part.

--

--

Koray Göçmen
The Startup

University of Toronto, Computer Engineering, architected and implemented reliable infrastructures and worked as the lead developer for multiple startups.