Blog

Generating and Uploading Images to S3 with Golang

Sun 02/10/19


Left Branch Propagation

Length Change    Angle Change

Right Branch Propagation

Length Change    Angle Change

The above widget is a simple binary tree generator that I created with the following AWS stack:

In this post I explain my motivation for writing this, and the implementation details.

Disclaimer: I could of made a more interesting version of this in less time with client side JavaScript. I chose to do it this way because I wanted to practice this stack - particularly how I could upload arbitrary data to a S3 bucket from a Lambda handler.


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 did not reimplement it.

Implementation

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.

The circle isn't very interesting though. Next 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.

powered by  26.3 9.2.511/17/19