Golang: Creating Images and Drawing — Simply Explained (Part 2)

Drawing functions to unleash your creativity

Mipsmonsta
4 min readApr 3, 2022

In the first part of the titular article (i.e. Part 1), we were introduced to the various data structs and the image.Image interface in the standard golang library.

In this part 2, let’s look at compositing.

Photo by Jessica Felicio on Unsplash

Compositing

Compositing is where you have a few images and you want to layer them onto each other. It’s not just so simple as putting one image on top of the other, or overlapping them. In the process of compositing you actually use the alpha channels of each image to describe not the opacity but the shape of the image to layer.

A seminal paper describing how the source image and the destination image can be composited together was put forth by Thomas Porter and Tom Duff. There are 12 compositing modes that can be invoked using Porter/Duff operations. However, in Golang’s official blog article, it’s stated:

The Porter-Duff paper presented 12 different composition operators, but with an explicit mask, only 2 of these are needed in practice: source-over-destination and source. In Go, these operators are represented by the Over and Src constants.

The Over operator performs the natural layering of a source image over a destination image: the change to the destination image is smaller where the source (after masking) is more transparent (that is, has lower alpha).

The Src operator merely copies the source (after masking) with no regard for the destination image’s original content. For fully opaque source and mask images, the two operators produce the same output, but the Src operator is usually faster.

The two functions in the small image/draw package that you must be familar to use in compositing are. But I would like to focus on the Draw function in this article.

func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point, op Op)

Draw function Dissected

The dst image is just a image.Image. Image structs such as pointer to image.RGBA are accepted since they implement the image.Image interface.

Note, it’s the pointer to image.RGBA that is implementing the image.Image interface. I say again so it will save you less grief.

Usually, you will use image.NewRGBA(image.Rect(0, 0, image_width, image_height) to give you a pointer to image.RGBA.

If you want to use a fixed sized and solid colored destination image, you could do the following

imgWidth = 276 // integer pixels
imgHeight = 174 //integer pixels
bgColor := image.Whitergba := image.NewRGBA(image.Rect(0,0, imgWidth, imgHeight))draw.Draw(rgba, rgba.Bounds(), bgColor, image.Pt(0, 0), draw.Src)//rgba will now be a white colored image of imgWidth width and imgHeight height

Recall the draw function signature:

func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)

Notice that for r, we use the rgba.Bounds() which return the bounds of the rgba rectangle. This is the destination bounding rectangle that will be drawn over. Parameter src (source) is the image.White, which is a image.Uniform struct representing a solid color that is infinite-sized.

You say infinited-sized? Then do we state the source image size to “draw” onto the “destination image”? No. The Draw function assumes that the destination bounding rectangle, where compositing is going to happen, is the same size as the rectangle slice of the source image that will be composited. Hence, in the parameter sp, which is an image.Point, we are setting where is the top-left point in source image that will correspond to minimum-x and minimum-y point of the destination bounding rectangle. This is the most confusing part of using the draw function. Re-read this part if you do not understand. Don’t just gloss over.

Finally, the op parameter accepts Src or Over operations. From the official golang blog:

The Over operator performs the natural layering of a source image over a destination image: the change to the destination image is smaller where the source (after masking) is more transparent (that is, has lower alpha).

The Src operator merely copies the source (after masking) with no regard for the destination image’s original content.

In the case of the draw function, since a nil mask is used (akin to using a fully opaque mask), Src and Over operations are the same, i.e. the source image will just be “drawn” over the destination fully (and you cannot see below the source for the destination pixels at all).

More complex example

Below is a piece of code I wrote to print a QR code at the top-left or top-right or bottom-left or bottom right of the a rectangular white image background.

func PrintQRCodeWithWhiteBgImageWithURL(url string, width, height int, corner int, offsetCorner int) (image.Image, error) {   rgba := image.NewRGBA(image.Rect(0, 0, width, height))   qr, err := generateQRCodeImageFromURL(url, 512)
if err != nil {
return nil, err
}
//resize QR code - half of the shortest end
var ss int
if width < height {
ss = width
} else {
ss = height
}
scaleTo := ss / 2
qr_scaled := ScaleImage(&qr, scaleTo, scaleTo, ScaleBestQ)
// f, err := os.Create("./test/test_qr.png")
//draw white background
bg := image.White
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{X: 0, Y: 0},
draw.Src)
//compute qr top left point in dst
var topLeftQR image.Point
switch corner {
case QRLowerRightCorner:
topLeftQR = image.Point{X: width - scaleTo -
offsetCorner, Y: height - scaleTo - offsetCorner}
case QRLowerLeftCorner:
topLeftQR = image.Point{X: offsetCorner, Y: height -
scaleTo - offsetCorner}
case QRUpperRightCorner:
topLeftQR = image.Point{X: width - scaleTo -
offsetCorner, Y: offsetCorner}
case QRUpperLeftCorner:
topLeftQR = image.Point{X: offsetCorner, Y:
offsetCorner}
case QRMiddle:
topLeftQR = image.Point{X: width/2 - scaleTo/2, Y:
height/2 - scaleTo/2}
default:
return nil, fmt.Errorf("wrong argument for parameter corner")
}
//draw QR on background
draw.Draw(rgba, image.Rect(topLeftQR.X, topLeftQR.Y,
topLeftQR.X+scaleTo, topLeftQR.Y+scaleTo), qr_scaled,
image.Point{X: 0, Y: 0}, draw.Src)

return rgba, nil
}

Okay. That’s all for today. Happy Golang coding! Do follow me if you like the article.

--

--

Mipsmonsta

Writing to soothe the soul, programming to achieve flow