Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 1 | # The Go image/draw package |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 2 | 29 Sep 2011 |
Andrew Gerrand | b316fcd | 2013-06-05 09:59:16 +1000 | [diff] [blame] | 3 | Tags: draw, image, libraries, technical |
Russ Cox | faf1e2d | 2020-03-14 09:44:01 -0400 | [diff] [blame] | 4 | Summary: An introduction to image compositing in Go using the image/draw package. |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 5 | OldURL: /go-imagedraw-package |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 6 | |
| 7 | Nigel Tao |
| 8 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 9 | ## Introduction |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 10 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 11 | [Package image/draw](https://golang.org/pkg/image/draw/) defines only one operation: |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 12 | drawing a source image onto a destination image, |
| 13 | through an optional mask image. |
| 14 | This one operation is surprisingly versatile and can perform a number of |
| 15 | common image manipulation tasks elegantly and efficiently. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 16 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 17 | Composition is performed pixel by pixel in the style of the Plan 9 graphics |
| 18 | library and the X Render extension. |
| 19 | The model is based on the classic "Compositing Digital Images" paper by Porter and Duff, |
| 20 | with an additional mask parameter: |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 21 | `dst = (src IN mask) OP dst`. |
| 22 | For a fully opaque mask, this reduces to the original Porter-Duff formula: `dst = src OP dst`. |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 23 | In Go, a nil mask image is equivalent to an infinitely sized, |
| 24 | fully opaque mask image. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 25 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 26 | The Porter-Duff paper presented [12 different composition operators](http://www.w3.org/TR/SVGCompositing/examples/compop-porterduff-examples.png), |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 27 | but with an explicit mask, only 2 of these are needed in practice: |
| 28 | source-over-destination and source. |
| 29 | In Go, these operators are represented by the `Over` and `Src` constants. |
| 30 | The `Over` operator performs the natural layering of a source image over |
| 31 | a destination image: |
| 32 | the change to the destination image is smaller where the source (after masking) |
| 33 | is more transparent (that is, has lower alpha). |
| 34 | The `Src` operator merely copies the source (after masking) with no regard |
| 35 | for the destination image's original content. |
| 36 | For fully opaque source and mask images, the two operators produce the same output, |
| 37 | but the `Src` operator is usually faster. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 38 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 39 | ## Geometric Alignment |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 40 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 41 | Composition requires associating destination pixels with source and mask pixels. |
| 42 | Obviously, this requires destination, source and mask images, |
| 43 | and a composition operator, but it also requires specifying what rectangle |
| 44 | of each image to use. |
| 45 | Not every drawing should write to the entire destination: |
| 46 | when updating an animating image, it is more efficient to only draw the |
| 47 | parts of the image that have changed. |
| 48 | Not every drawing should read from the entire source: |
| 49 | when using a sprite that combines many small images into one large one, |
| 50 | only a part of the image is needed. |
| 51 | Not every drawing should read from the entire mask: |
| 52 | a mask image that collects a font's glyphs is similar to a sprite. |
| 53 | Thus, drawing also needs to know three rectangles, one for each image. |
| 54 | Since each rectangle has the same width and height, |
| 55 | it suffices to pass a destination rectangle `r` and two points `sp` and `mp`: |
| 56 | the source rectangle is equal to `r` translated so that `r.Min` in the destination |
| 57 | image aligns with `sp` in the source image, |
| 58 | and similarly for `mp`. |
| 59 | The effective rectangle is also clipped to each image's bounds in their |
| 60 | respective co-ordinate space. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 61 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 62 | .image image-draw/20.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 63 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 64 | The [`DrawMask`](https://golang.org/pkg/image/draw/#DrawMask) function |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 65 | takes seven arguments, |
| 66 | but an explicit mask and mask-point are usually unnecessary, |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 67 | so the [`Draw`](https://golang.org/pkg/image/draw/#Draw) function takes five: |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 68 | |
| 69 | // Draw calls DrawMask with a nil mask. |
| 70 | func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op) |
| 71 | func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point, |
| 72 | mask image.Image, mp image.Point, op Op) |
| 73 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 74 | The destination image must be mutable, so the image/draw package defines |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 75 | a [`draw.Image`](https://golang.org/pkg/image/draw/#Image) interface which has a `Set` method. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 76 | |
| 77 | type Image interface { |
| 78 | image.Image |
| 79 | Set(x, y int, c color.Color) |
| 80 | } |
| 81 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 82 | ## Filling a Rectangle |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 83 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 84 | To fill a rectangle with a solid color, use an `image.Uniform` source. |
| 85 | The `ColorImage` type re-interprets a `Color` as a practically infinite-sized |
| 86 | `Image` of that color. |
| 87 | For those familiar with the design of Plan 9's draw library, |
| 88 | there is no need for an explicit "repeat bit" in Go's slice-based image types; |
| 89 | the concept is subsumed by `Uniform`. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 90 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 91 | // image.ZP is the zero point -- the origin. |
| 92 | draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 93 | |
| 94 | To initialize a new image to all-blue: |
| 95 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 96 | m := image.NewRGBA(image.Rect(0, 0, 640, 480)) |
| 97 | blue := color.RGBA{0, 0, 255, 255} |
| 98 | draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 99 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 100 | To reset an image to transparent (or black, |
| 101 | if the destination image's color model cannot represent transparency), |
| 102 | use `image.Transparent`, which is an `image.Uniform`: |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 103 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 104 | draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 105 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 106 | .image image-draw/2a.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 107 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 108 | ## Copying an Image |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 109 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 110 | To copy from a rectangle `sr` in the source image to a rectangle starting |
| 111 | at a point `dp` in the destination, |
| 112 | convert the source rectangle into the destination image's co-ordinate space: |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 113 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 114 | r := image.Rectangle{dp, dp.Add(sr.Size())} |
| 115 | draw.Draw(dst, r, src, sr.Min, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 116 | |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 117 | Alternatively: |
| 118 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 119 | r := sr.Sub(sr.Min).Add(dp) |
| 120 | draw.Draw(dst, r, src, sr.Min, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 121 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 122 | To copy the entire source image, use `sr = src.Bounds()`. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 123 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 124 | .image image-draw/2b.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 125 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 126 | ## Scrolling an Image |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 127 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 128 | Scrolling an image is just copying an image to itself, |
| 129 | with different destination and source rectangles. |
| 130 | Overlapping destination and source images are perfectly valid, |
| 131 | just as Go's built-in copy function can handle overlapping destination and source slices. |
| 132 | To scroll an image m by 20 pixels: |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 133 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 134 | b := m.Bounds() |
| 135 | p := image.Pt(0, 20) |
| 136 | // Note that even though the second argument is b, |
| 137 | // the effective rectangle is smaller due to clipping. |
| 138 | draw.Draw(m, b, m, b.Min.Add(p), draw.Src) |
| 139 | dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y)) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 140 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 141 | .image image-draw/2c.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 142 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 143 | ## Converting an Image to RGBA |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 144 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 145 | The result of decoding an image format might not be an `image.RGBA`: |
| 146 | decoding a GIF results in an `image.Paletted`, |
| 147 | decoding a JPEG results in a `ycbcr.YCbCr`, |
| 148 | and the result of decoding a PNG depends on the image data. |
| 149 | To convert any image to an `image.RGBA`: |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 150 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 151 | b := src.Bounds() |
| 152 | m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) |
| 153 | draw.Draw(m, m.Bounds(), src, b.Min, draw.Src) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 154 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 155 | .image image-draw/2d.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 156 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 157 | ## Drawing Through a Mask |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 158 | |
| 159 | To draw an image through a circular mask with center `p` and radius `r`: |
| 160 | |
| 161 | type circle struct { |
| 162 | p image.Point |
| 163 | r int |
| 164 | } |
| 165 | |
| 166 | func (c *circle) ColorModel() color.Model { |
| 167 | return color.AlphaModel |
| 168 | } |
| 169 | |
| 170 | func (c *circle) Bounds() image.Rectangle { |
| 171 | return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r) |
| 172 | } |
| 173 | |
| 174 | func (c *circle) At(x, y int) color.Color { |
| 175 | xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r) |
| 176 | if xx*xx+yy*yy < rr*rr { |
| 177 | return color.Alpha{255} |
| 178 | } |
| 179 | return color.Alpha{0} |
| 180 | } |
| 181 | |
| 182 | draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over) |
| 183 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 184 | .image image-draw/2e.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 185 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 186 | ## Drawing Font Glyphs |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 187 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 188 | To draw a font glyph in blue starting from a point `p`, |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 189 | draw with an `image.ColorImage` source and an `image.Alpha mask`. |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 190 | For simplicity, we aren't performing any sub-pixel positioning or rendering, |
| 191 | or correcting for a font's height above a baseline. |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 192 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 193 | src := &image.Uniform{color.RGBA{0, 0, 255, 255}} |
| 194 | mask := theGlyphImageForAFont() |
| 195 | mr := theBoundsFor(glyphIndex) |
| 196 | draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over) |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 197 | |
Russ Cox | 972d42d | 2020-03-15 15:50:36 -0400 | [diff] [blame] | 198 | .image image-draw/2f.png |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 199 | |
Russ Cox | af5018f | 2020-03-09 23:54:35 -0400 | [diff] [blame] | 200 | ## Performance |
Andrew Gerrand | db9a09f | 2013-03-08 11:17:09 +1100 | [diff] [blame] | 201 | |
Russ Cox | 482079d | 2020-03-09 22:11:04 -0400 | [diff] [blame] | 202 | The image/draw package implementation demonstrates how to provide an image |
| 203 | manipulation function that is both general purpose, |
| 204 | yet efficient for common cases. |
| 205 | The `DrawMask` function takes arguments of interface types, |
| 206 | but immediately makes type assertions that its arguments are of specific struct types, |
| 207 | corresponding to common operations like drawing one `image.RGBA` image onto another, |
| 208 | or drawing an `image.Alpha` mask (such as a font glyph) onto an `image.RGBA` image. |
| 209 | If a type assertion succeeds, that type information is used to run a specialized |
| 210 | implementation of the general algorithm. |
| 211 | If the assertions fail, the fallback code path uses the generic `At` and `Set` methods. |
| 212 | The fast-paths are purely a performance optimization; |
| 213 | the resultant destination image is the same either way. |
| 214 | In practice, only a small number of special cases are necessary to support |
| 215 | typical applications. |