ZetCode

Go draw2d

last modified October 30, 2026

In this article we show how to work with graphics in Go using draw2d. The library draws vector shapes, curves and text with a Cairo-like API and writes the result to an image, a PDF document or an OpenGL surface.

draw2d uses the same coordinate conventions as the HTML canvas: the origin is in the top-left corner, the x axis grows to the right and the y axis grows downwards. Angles are in radians, and a path is built with MoveTo, LineTo and the curve methods before it is filled or stroked.

The examples draw on an image.RGBA canvas from the draw2dimg package. All were compiled and run with Go 1.27.0 and draw2d v0.0.0-20260422081035-c4331ac66734.

Installation

draw2d is a module dependency. The image backend is imported from github.com/llgcode/draw2d/draw2dimg, and the shape helpers from draw2dkit.

$ mkdir draw2d-demo
$ cd draw2d-demo
$ go mod init com.zetcode/draw2d
go: creating new go.mod: module com.zetcode/draw2d
$ go get github.com/llgcode/draw2d@latest
go: added github.com/llgcode/draw2d v0.0.0-20260422081035-c4331ac66734

Canvas and rectangle

A canvas is created with image.NewRGBA. The graphic context wraps the canvas and keeps the current colors, line width and transformation. draw2dkit.Rectangle adds a rectangle to the path, and FillStroke fills it and then draws its outline.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    gc.SetStrokeColor(color.Black)
    gc.SetLineWidth(3)

    draw2dkit.Rectangle(gc, 40, 40, 260, 160)
    gc.FillStroke()

    draw2dimg.SaveToPngFile("rect.png", dest)

    for _, p := range [][2]int{{150, 100}, {40, 100}, {10, 10}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

The rectangle arguments are the top-left and bottom-right corners. The pixels confirm the result: the center is white, the edge at x=40 is black and a point outside the rectangle is still transparent. The saved PNG keeps an alpha channel because the canvas was never filled completely.

$ go run main.go
[150 100]: {255 255 255 255}
[40 100]: {0 0 0 255}
[10 10]: {0 0 0 0}
$ file rect.png
rect.png: PNG image data, 300 x 200, 8-bit/color RGBA, non-interlaced

Lines and dashes

A line is a path with two points. SetLineWidth changes the thickness, SetLineDash splits the stroke into dashes, and SetLineCap controls the ends. Passing nil to SetLineDash restores a solid line.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"

    "github.com/llgcode/draw2d"
    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 200)
    gc.Fill()

    gc.SetStrokeColor(color.RGBA{0x00, 0x00, 0xcc, 0xff})
    gc.SetLineWidth(4)
    gc.MoveTo(20, 50)
    gc.LineTo(280, 50)
    gc.Stroke()

    gc.SetLineDash([]float64{15, 10}, 0)
    gc.MoveTo(20, 100)
    gc.LineTo(280, 100)
    gc.Stroke()

    gc.SetLineDash(nil, 0)
    gc.SetLineWidth(12)
    gc.SetLineCap(draw2d.RoundCap)
    gc.MoveTo(30, 160)
    gc.LineTo(270, 160)
    gc.Stroke()

    draw2dimg.SaveToPngFile("lines.png", dest)

    for _, p := range [][2]int{{150, 50}, {20, 100}, {40, 100}, {150, 160}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

The dash pattern alternates 15 pixels of ink and 10 pixels of gap, so the point at x=20 is drawn and the point at x=40 falls into a gap. The last line uses a round cap, which extends the stroke beyond its end points.

$ go run main.go
[150 50]: {0 0 204 255}
[20 100]: {0 0 204 255}
[40 100]: {255 255 255 255}
[150 160]: {0 0 204 255}
$ file lines.png
lines.png: PNG image data, 300 x 200, 8-bit/color RGB, non-interlaced

Circles and ellipses

draw2dkit.Circle takes a center and a radius. draw2dkit.Ellipse takes a center and two radii; equal radii make a circle.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 200)
    gc.Fill()

    gc.SetFillColor(color.RGBA{0xcc, 0x00, 0x00, 0xff})
    draw2dkit.Circle(gc, 80, 100, 50)
    gc.Fill()

    gc.SetFillColor(color.RGBA{0x00, 0x99, 0x00, 0xff})
    draw2dkit.Ellipse(gc, 220, 100, 60, 35)
    gc.Fill()

    draw2dimg.SaveToPngFile("circles.png", dest)

    for _, p := range [][2]int{{80, 100}, {80, 40}, {220, 100}, {220, 60}, {150, 100}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

The centers are filled, a point above the circle and a point above the ellipse are outside, and the middle of the canvas stays white. The ellipse is 120 pixels wide and 70 pixels high.

$ go run main.go
[80 100]: {204 0 0 255}
[80 40]: {255 255 255 255}
[220 100]: {0 153 0 255}
[220 60]: {255 255 255 255}
[150 100]: {255 255 255 255}
$ file circles.png
circles.png: PNG image data, 300 x 200, 8-bit/color RGB, non-interlaced

Curves

QuadCurveTo uses one control point and CubicCurveTo uses two. draw2dkit.RoundedRectangle combines straight edges with elliptical corners.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 300))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 300)
    gc.Fill()

    gc.SetStrokeColor(color.RGBA{0x00, 0x00, 0xcc, 0xff})
    gc.SetLineWidth(3)

    gc.MoveTo(20, 80)
    gc.CubicCurveTo(100, 20, 200, 140, 280, 80)
    gc.Stroke()

    gc.SetStrokeColor(color.RGBA{0xcc, 0x00, 0x00, 0xff})
    gc.MoveTo(20, 250)
    gc.QuadCurveTo(150, 150, 280, 250)
    gc.Stroke()

    gc.SetStrokeColor(color.RGBA{0x00, 0x99, 0x00, 0xff})
    draw2dkit.RoundedRectangle(gc, 40, 120, 260, 180, 20, 20)
    gc.Stroke()

    draw2dimg.SaveToPngFile("curves.png", dest)

    for _, p := range [][2]int{{150, 80}, {150, 200}, {40, 150}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

The midpoint of a cubic curve lies halfway between the end points on both axes when the control points are symmetric, so the pixel at (150, 80) is on the blue curve. The quadratic curve reaches its apex at (150, 200), and (40, 150) is on the left edge of the rounded rectangle.

$ go run main.go
[150 80]: {0 0 204 255}
[150 200]: {204 0 0 255}
[40 150]: {0 153 0 255}
$ file curves.png
curves.png: PNG image data, 300 x 300, 8-bit/color RGB, non-interlaced

Text

Text needs a TrueType font. The library registers fonts by name with draw2d.RegisterFont; the Go font from golang.org/x/image/font/gofont/goregular is embedded in the program, so no font file has to be shipped next to the binary.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"
    "log"

    "github.com/golang/freetype/truetype"
    "github.com/llgcode/draw2d"
    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
    "golang.org/x/image/font/gofont/goregular"
)

func main() {

    font, err := truetype.Parse(goregular.TTF)
    if err != nil {
        log.Fatal(err)
    }

    draw2d.RegisterFont(draw2d.FontData{
        Name:   "Go",
        Family: draw2d.FontFamilySans,
        Style:  draw2d.FontStyleNormal,
    }, font)

    dest := image.NewRGBA(image.Rect(0, 0, 400, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 400, 200)
    gc.Fill()

    gc.SetFontData(draw2d.FontData{Name: "Go", Family: draw2d.FontFamilySans})
    gc.SetFontSize(32)
    gc.SetFillColor(color.RGBA{0x22, 0x22, 0x22, 0xff})

    left, top, right, bottom := gc.GetStringBounds("Hello draw2d")
    fmt.Printf("bounds: %.2f %.2f %.2f %.2f\n", left, top, right, bottom)
    fmt.Printf("width: %.2f height: %.2f\n", right-left, bottom-top)

    gc.FillStringAt("Hello draw2d", 20, 100)

    draw2dimg.SaveToPngFile("text.png", dest)
}

The font data name must match the name used at registration. The baseline of the text is at y=100, and GetStringBounds returns the bounding box relative to that point; the negative top value means that most of the text lies above the baseline.

$ go run main.go
bounds: 3.30 -31.52 239.19 0.50
width: 235.89 height: 32.02
$ file text.png
text.png: PNG image data, 400 x 200, 8-bit/color RGB, non-interlaced

Transformations

Translate, Rotate and Scale modify the current transformation matrix. Save and Restore isolate a transformation so that later drawing is not affected.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"
    "math"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 300))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 300)
    gc.Fill()

    gc.Save()
    gc.Translate(150, 150)
    gc.Rotate(math.Pi / 4)
    gc.SetFillColor(color.RGBA{0x00, 0x00, 0xcc, 0xff})
    draw2dkit.Rectangle(gc, -60, -30, 60, 30)
    gc.Fill()
    gc.Restore()

    gc.SetFillColor(color.RGBA{0xcc, 0x00, 0x00, 0xff})
    draw2dkit.Rectangle(gc, 0, 0, 60, 60)
    gc.Fill()

    draw2dimg.SaveToPngFile("transform.png", dest)

    for _, p := range [][2]int{{150, 150}, {120, 120}, {150, 60}, {30, 30}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

The blue rectangle is moved to the center and rotated by 45 degrees, written as math.Pi / 4 because angles are in radians. After Restore the red square is drawn in the original coordinates at the top-left corner.

$ go run main.go
[150 150]: {0 0 204 255}
[120 120]: {0 0 204 255}
[150 60]: {255 255 255 255}
[30 30]: {204 0 0 255}
$ file transform.png
transform.png: PNG image data, 300 x 300, 8-bit/color RGB, non-interlaced

Transparency

A color with an alpha channel blends with what is already on the canvas. Use color.NRGBA to give straight, non-premultiplied components; color.RGBA expects the components to be premultiplied by alpha.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 200)
    gc.Fill()

    gc.SetFillColor(color.NRGBA{0xff, 0x00, 0x00, 0x80})
    draw2dkit.Rectangle(gc, 20, 40, 160, 160)
    gc.Fill()

    gc.SetFillColor(color.NRGBA{0x00, 0x00, 0xff, 0x80})
    draw2dkit.Rectangle(gc, 120, 40, 260, 160)
    gc.Fill()

    draw2dimg.SaveToPngFile("alpha.png", dest)

    for _, p := range [][2]int{{60, 80}, {200, 80}, {140, 80}} {
        fmt.Printf("%v: %v\n", p, dest.At(p[0], p[1]))
    }
}

Half-transparent red over white gives {255 127 127} and blue over white gives {127 127 255}. In the overlap the blue rectangle covers the red one, which produces {127 63 191}.

$ go run main.go
[60 80]: {255 127 127 255}
[200 80]: {127 127 255 255}
[140 80]: {127 63 191 255}
$ file alpha.png
alpha.png: PNG image data, 300 x 200, 8-bit/color RGB, non-interlaced

Saving PNG and JPEG

SaveToPngFile is a convenience wrapper around the PNG encoder. Other formats are written with the standard image packages, for example image/jpeg.

main.go
package main

import (
    "fmt"
    "image"
    "image/color"
    "image/jpeg"
    "os"

    "github.com/llgcode/draw2d/draw2dimg"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    dest := image.NewRGBA(image.Rect(0, 0, 300, 200))
    gc := draw2dimg.NewGraphicContext(dest)

    gc.SetFillColor(color.White)
    draw2dkit.Rectangle(gc, 0, 0, 300, 200)
    gc.Fill()

    gc.SetFillColor(color.RGBA{0x22, 0x66, 0xcc, 0xff})
    draw2dkit.Circle(gc, 150, 100, 70)
    gc.Fill()

    if err := draw2dimg.SaveToPngFile("drawing.png", dest); err != nil {
        fmt.Println(err)
        return
    }

    f, err := os.Create("drawing.jpg")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

    if err := jpeg.Encode(f, dest, &jpeg.Options{Quality: 90}); err != nil {
        fmt.Println(err)
    }
}

The PNG file keeps the exact colors, while JPEG is lossy and has no alpha channel. The quality option ranges from 1 to 100.

$ go run main.go
$ file drawing.png drawing.jpg
drawing.png: PNG image data, 300 x 200, 8-bit/color RGB, non-interlaced
drawing.jpg: JPEG image data, baseline, precision 8, 300x200, components 3

PDF output

The draw2dpdf backend draws the same paths into a PDF document. The page size is given in the constructor, and SaveToPdfFile writes the file.

main.go
package main

import (
    "image/color"

    "github.com/llgcode/draw2d/draw2dpdf"
    "github.com/llgcode/draw2d/draw2dkit"
)

func main() {

    pdf := draw2dpdf.NewPdf("L", "mm", "A5")
    gc := draw2dpdf.NewGraphicContext(pdf)

    gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff})
    gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff})
    gc.SetLineWidth(2)

    draw2dkit.Rectangle(gc, 10, 10, 100, 60)
    gc.FillStroke()

    draw2dpdf.SaveToPdfFile("rect.pdf", pdf)
}

The first argument selects landscape orientation, the second the unit and the third the page size. Shapes and text use the same calls as on an image canvas.

$ go get github.com/llgcode/draw2d/draw2dpdf
$ go run main.go
$ file rect.pdf
rect.pdf: PDF document, version 1.3, 1 pages

Source

Go draw2d - Github page
Go draw2d - package documentation

In this article we have worked with graphics in Go using draw2d. We drew rectangles, lines and dashes, circles and ellipses, curves and rounded rectangles, text with an embedded font, transformed shapes, blended transparent colors, and saved the canvas as PNG, JPEG and PDF.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.