Draw Trees in AWS with Golang
Table of Contents
1. Tree Generator
The above widget is a simple binary tree generator that I created with the following AWS stack:
- API Gateway
- Lambda + Golang
- S3
2. Motivation
I love to write programs that generate interesting pictures. One way to achieve this is to recursively draw lines according to some rules.
In 2015 I did this with Rust. The graphics library was bare bones, so I implemented a function to draw a line, and another to draw a circle (source).
Here's a few of my favorites images that it generated:
click to expand or hide
A few years later I wrote a program in python/pygame that generated trees in real time. Eventually I made a front end for it on my website that let the user specify a few different parameters:
- depth: how many times to recursively draws more branches
- branches: 2 for a binary tree, 3 for a ternary tree, etc
- length: how long in pixels each branch should be
- angle: the angle that a new branch is drawn from its parent
Here's an example of a result and here's the source. This worked by using php to call the python program which was running pygame headless. Instead of showing the tree being built, it would just save the image. Unfortunately when I moved my website over to be statically hosted I didn't migrate it.
3. Create Images in AWS Lambda with Golang
I decided it would be fun to redo this app with Go and Lambda. I wanted to learn how to generate images in a serverless function, and then save them to a S3 bucket. The first step was to create a Go program that could generate and save an image. I decided to use the Go Graphics library.
Drawing and saving a simple image in go:
package main import "github.com/fogleman/gg" func main() { // gg - Go Graphics. c := gg.NewContext(1000, 1000) c.DrawCircle(400, 400, 400) c.SetRGB(0, 0, 0) c.Fill() c.SavePNG("circle.png") }
So I could save an image of a circle to my local file system. The next
challenge was finding a way to save it to a S3 bucket. First I looked
through the docs of aws-sdk-go
. I needed to find the type that an
S3 bucket expected to receive as the body of its upload. I found the
putObject
function in the github.com/aws/aws-sdk-go/service/s3
package.
This method takes a *PutObjectInput
as its only parameter.
It has has many options, but only three are required:
- Bucket - the name of the S3 bucket
- Body - the data to be uploaded
- Key - the name for the data in the bucket, basically a file name.
I would need to set the body to the generated image.
The type of body is a io.ReadSeeker
.
Next I looked in the go graphics package for a method that would work with this type.
I found the EncodePNG
method that can be called on the context. It has an io.Writer
type as its parameter. The built in bytes
package has types that implement these interfaces.
So I could convert the image context to a byte buffer, and then set this to the body of the S3 upload struct.
Here's a working example of creating an image in Go and then uploading it to a S3 bucket:
package main import ( "bytes" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/fogleman/gg" ) func draw() (buffer *bytes.Buffer, err error) { c := gg.NewContext(1000, 1000) // Create the image. c.SetRGB(200, 200, 0) c.DrawCircle(400, 400, 400) c.Fill() // Write the bytes from the image in the context to a buffer. buffer = new(bytes.Buffer) if err = c.EncodePNG(buffer); err != nil { fmt.Printf("failed to encode png %s", err.Error()) } return } func main() { buffer, err := draw() if err != nil { return } // Create a S3 client session := session.Must(session.NewSession(&aws.Config{ Region: aws.String("us-west-2"), })) svc := s3.New(session) // Read the bytes from the byte buffer that contains the image. reader := bytes.NewReader(buffer.Bytes()) putInput := s3.PutObjectInput{ Bucket: aws.String("nicolasknoebber.com"), Body: reader, Key: aws.String("test_upload.png"), } if _, err := svc.PutObject(&putInput); err != nil { panic(err) } }
It worked!. You may have noticed that there is no access key specified
here. This comes from the session
variable - it's reading the
access key that I set when I ran aws configure
in my terminal.
4. Draw User Defined Trees
I replaced draw()
with a function that generates binary trees
according to a few parameters / rules.
Rules
- A parent has a left and a right branch
- A branch stops growing when its width is 0 or its off the screen
- The length of each child should get less by some amount
- The angle of each branch should change by some amount
User Parameters
- Left / Right length change
- Left / Right angle change
const ( width = 400 height = 400 ) func polarLine(c *gg.Context, x0, y0, length, degrees float64) (x1, y1 float64) { theta := gg.Radians(degrees) x1 = length*(math.Cos(theta)) + x0 y1 = length*(math.Sin(theta)) + y0 c.DrawLine(x0, y0, x1, y1) c.Stroke() return } func tree(c *gg.Context, lineWidth, x0, y0, length, degrees float64, p TreeParam) { if lineWidth < 1 || x0 < 1 || y0 < 1 || x0 > width || y0 > height || length < 1 { return } c.SetLineWidth(lineWidth) lineWidth -= 2 x1, y1 := polarLine(c, x0, y0, length, degrees) tree(c, lineWidth, x1, y1, length-p.LeftLength, degrees-p.LeftAngle, p) tree(c, lineWidth, x1, y1, length-p.RightLength, degrees+p.RightAngle, p) } func createTree(p TreeParam) (buffer *bytes.Buffer, err error) { c := gg.NewContext(width, height) c.SetRGB(0, 0, 0) tree(c, 15, width/2, height, 100, 270, p) // Write the bytes from the image in the context to a buffer. buffer = new(bytes.Buffer) if err = c.EncodePNG(buffer); err != nil { fmt.Printf("failed to encode png %s", err.Error()) } return }
Next I created a public API endpoint to run this code. I put the above code in a lambda function and created a request object that has the parameters.
type TreeParam struct { LeftLength float64 `json:"leftLength"` LeftAngle float64 `json:"leftAngle"` RightLength float64 `json:"rightLength"` RightAngle float64 `json:"rightAngle"` }
Finally, I added the form that's at the top of this page, and a script to create the post request.