I recently read a great article by Mat Ryer about and got inspired by it to solve a repetitive task I usually do every month. programmatically generating images in Go My dad is the head of an organization called that promotes reading to kids and adults in Cornellà de Llobregat, Spain. Once a month they organize a meet-up: either a literary dinner with a book author to talk about a book or a storytelling session. He usually asks me to create an image to illustrate the event and sends the details via email to all the members of the organization. The illustration looks like this: Projecte LOC It's quite annoying to do it every single month by hand, importing the image, aligning text, adjusting everything... I usually make mistakes copying the day, the time and the title from the email he sends to me. I end up re-doing it entirely after he finds typos/mistakes. So when I saw Mat's article I immediately thought that I could do something to improve this tedious process both for myself and my dad. Wouldn't it be great to have something where he would enter a few variable data about the meet-up (title, guest, image and date) and I would programmatically generate the whole image? I needed something really simple and user friendly for him to enter the info. That was an easy decision: Typeform. He's used to typeforms and I know the API really well, after all, I helped build it. Then, I would take advantage of Typeform webhooks to get the response details from the form submission, process the info about the meet-up, generate the corresponding image and provide a link for him to download it. 💪 1. Creating the Typeform 🛠 First, I created the typeform with all the fields that would change for each meet-up. Each field has a unique identifier called , which is useful to identify the fields on the response payload that we'll receive on the webhook. By default, this is a long alphanumeric string, but I modified it on the to look a bit more human-readable so I could identify each answer quickly in the code. reference reference Create Panel Next, I created the webhook on the Connect Panel pointing it to an endpoint that will handle each typeform submission. 2. Handling the webhook ⚓️ 2.1. Signature verification ✍️ When creating a webhook we are exposing the endpoint URL to the internet, which is not . Potentially anyone with bad intentions could request to it with any data. I wanted to be sure that I was only processing webhooks coming from Typeform, so I added a on my webhook configuration. Then, Typeform would use it to sign the webhook payload and add it as a header on the request. secure secret To accept an incoming webhook request, the first thing the handler has to do is verify that Typeform sent it. This is done with the function that takes multiple parameters: the request body, the shared with Typeform, and the value of the header. verifySignature secret Typeform-Signature Then, it computes the signature for the received payload with the and compares the result with the . secret receivedSignature { signature, err := computeSignature(payload, secret) err != { , err } signature == receivedSignature, } { h := hmac.New(sha256.New, [] (secret)) _, err := h.Write(payload) err != { , err } + base64.StdEncoding.EncodeToString(h.Sum( )), } func verifySignature (payload [] , secret, receivedSignature ) byte string ( , error) bool if nil return false return nil func computeSignature (payload [] , secret ) byte string ( , error) string byte if nil return "" return "sha256=" nil nil If the comparison succeeds we are sure the request is coming from Typeform so the execution can proceed, otherwise, the handler stops and returns an error. { body, err := ioutil.ReadAll(r.Body) err != { w.WriteHeader(http.StatusInternalServerError) } r.Body.Close() ok, err := verifySignature(body, os.Getenv(secretToken), r.Header.Get( )) err != || !ok { w.WriteHeader(http.StatusBadRequest) } ... } func generateHandler (w http.ResponseWriter, r *http.Request) // 1. Read the request body if nil return defer // 2. Verify the signature "Typeform-Signature" if nil return // 3. The verification succeeded so we can process the webhook 🙌 2.2. Creating a Poster from the submission 🖼 At this point, we are sure the request is perfectly safe. The next step is actually reading and parsing the JSON body of the request and storing it somewhere. To do that, we create a variable of and unmarshal the request body into it. type Webhook Next, we want to convert the Webhook into a variable holding only the answers to each of the typeform questions. This will simplify the rendering task, we won't have to work anymore with a complicated Webhook struct with full of non relevant data. Poster { ... ... wh poster.Webhook err = json.Unmarshal(body, &wh) err != { log.WithFields(log.Fields{ : err}).Error( ) w.WriteHeader(http.StatusInternalServerError) } p := wh.ToPoster() ... func generateHandler (w http.ResponseWriter, r *http.Request) // 1. Read the request body // 2. Verify the signature // 3. Parse the webhook from the request body var if nil "error" "could not unmarshal webhook" return // 4. Convert the webhook data to a Poster // 5. Generate the image The function loops over the answers of the webhook using the attribute (that we previously set when creating the form) to identify to which poster field it corresponds. ToPoster() reference Poster { Title Guest Date time.Time Time PicURL Type } { poster := Poster{} _, answer := w.FormResponse.Answers { answer.Field.Ref { : poster.Title = answer.Text : poster.Guest = answer.Text : date, _ := time.Parse( , answer.Date) poster.Date = date : poster.Time = answer.Text : poster.Type = answer.Choice.Label : poster.PicURL = answer.PicURL } } poster } type struct string string string string string func (w Webhook) ToPoster () Poster for range switch case "title" case "guest" case "date" "2006-01-02" case "time" case "type" case "pic" return 3. Generating the image 👩🎨 Finally we have our Poster ready to be rendered! 👏 { ... ... ... ... err = p.Render() err != { log.WithFields(log.Fields{ : err}).Error( ) w.WriteHeader(http.StatusInternalServerError) } } func generateHandler (w http.ResponseWriter, r *http.Request) // 1. Read the request body // 2. Verify the signature // 3. Parse the webhook from the request body // 4. Convert the webhook data to a Poster // 5. Generate the image if nil "error" "could not generate poster" return Let's dive into the method. We will use the rendering library to generate the final image. Render Go Graphics { ctx := gg.NewContext(width, height) err := drawBackground(ctx, ) err != { err } err = drawBackground(ctx, ) err != { err } err = drawPicture(ctx, p) err != { err } err = drawText(ctx, p) err != { err } err = ctx.SavePNG( ) err != { err } } func (p Poster) Render () error "assets/images/background.png" if nil return "assets/images/logos.png" if nil return if nil return if nil return "poster.png" if nil return return nil The poster always has a similar format with the same background and sponsors logos at the bottom, so that's the first part we will render with the and methods. drawBackground drawLogos Drawing the picture poster is a bit more interesting since it's uploaded through the typeform and we don't really know the size it will have. In the Poster variable, we have the url of the picture. First we will download it to a temporary file with the method, resize it to fit into the poster and position it in the right image coordinates. poster.Picture() { filepath, err := poster.Picture() err != { err } pic, err := gg.LoadImage(filepath) err != { err } resizedPic := resize.Thumbnail( (pic.Bounds().Dx()), , pic, resize.Lanczos3, ) contentWidth := ctx.Width()/ - margin ctx.DrawImageAnchored(resizedPic, margin+contentWidth/ , , , ) err = os.Remove(filepath) err != { err } } func drawPicture (ctx *gg.Context, poster Poster) error // Download the picture to a local file if nil return // Load it if nil return // Resize it uint 250 // Position and draw it 2 2 185 0.5 0 // Delete the temporary file if nil return return nil The only part left now is drawing all the info about the meet-up (title, guest, image and date) with the method. The challenge here is making sure all the text lines fit in our image, since it comes from a user input we have no idea how long those lines could be. We need to change the font size depending on the length of the line. drawText To simplify the task we create an array of structs holding all the info of the poster. Each has the text to render, the margin to position it, the font name, and the default font size. If the font size is too big causing the text to overflow the image, it will be decreased until it fits. Line Line { ctx.SetColor(color.White) lines := []Line{ ... { text: fmt.Sprintf( , poster.Title), marginTop: , fontSize: , fontPath: RobotoBold, }, { text: , marginTop: , fontSize: , fontPath: RobotoLight, }, { text: fmt.Sprintf( , poster.Guest), marginTop: , fontSize: , fontPath: RobotoBold, }, { text: poster.When(), marginTop: , fontSize: , fontPath: RobotoLight, }, { text: poster.Where(), marginTop: , fontSize: , fontPath: RobotoLight, }, ... } contentWidth := (ctx.Width()/ - margin) positionX := margin + contentWidth/ positionY := margin _, line := lines { err := ctx.LoadFontFace(line.fontPath, line.fontSize) err != { err } err = adjustFontSize(ctx, line, contentWidth) err != { err } positionY = calculatePositionY(ctx, line, positionY) ctx.DrawStringAnchored(line.text, positionX, positionY, , ) } } func drawText (ctx *gg.Context, poster Poster) error // Lines with all the info to render `"%s"` 290 45 "amb" 25 25 "%s" 20 45 35 45 20 45 float64 2 2 // Loop through each line adjusting the font and drawing it for range if nil return if nil return 0.5 0 return nil And we have our poster! We just need to save it as a PNG file. { ... err = ctx.SavePNG( ) err != { err } } func (p Poster) Render () error // Draw everything // Store the poster in a file "poster.png" if nil return return nil 4. Downloading the poster ⬇️ Lastly, the most important part. You must be thinking how my dad is going to access this beautiful poster file and download it. 🤔Let me explain: As you saw in the last step, we always save the image with the same name, so we know for sure that after the typeform submission, we'll have the poster available in the same path. We'll take advantage of that fact and create another handler on the endpoint that shows the poster on the browser. /download { image, err := os.Open( ) err != { log.WithFields(log.Fields{ : err}).Error( ) w.WriteHeader(http.StatusInternalServerError) } image.Close() w.Header().Set( , ) _, err = io.Copy(w, image) err != { log.WithFields(log.Fields{ : err}).Error( ) w.WriteHeader(http.StatusInternalServerError) } } func download (w http.ResponseWriter, r *http.Request) "poster.png" if nil "error" "could not open image" return defer "Content-Type" "image/png" if nil "error" "could not write image" return Now, we go back to our typeform and add that link into the Thank You Screen button. With this setup, after the form is submitted, the Thank You Screen will be shown and we'll be able to download the poster by clicking on the button. Create Panel 5. Deploying it 🚀 I wanted to practice my Docker skills a bit so I decided to use Docker and Heroku to deploy the application. 5.1. Dockerize it 📦 The first thing to do was to the app. I did it with a multi-stage build. In the first part, we are using image to build the app into a binary called . Then, in the second step we use the image to copy the binary from the previous stage and run it. dockerize golang:1.14-alpine poster alpine:latest golang: -alpine AS builder alpine:latest FROM 1.14 ADD . /poster WORKDIR /poster RUN go mod download RUN CGO_ENABLED=0 go build -ldflags -o poster cmd/poster/*.go "-s -w" FROM RUN apk --no-cache add ca-certificates COPY --from=builder /poster ./ RUN chmod +x poster CMD ./poster 5.2. Deploy to Heroku I choose to go with for the deployment, because they support Docker image deploy with their CLI app. Heroku I created a free account and installed the . After that, creating the application was just logging in and running a simple command: CLI app heroku create As a result of that command a new app is created in our account and it displays a generated URL to access it. Once your app is created, you need to tell Heroku which Docker file to use to run your app. We create a at the root of our application. heroku.yml build: docker: web: Dockerfile Finally, our app is ready to be deployed using git: git push heroku master Now that the app has been deployed. We can access the handler at https://{something}.herokuapp.com/download The last step is to update our webhook and Thank You screen buttons to point to the new URLs. Now it works! 💥 I took the opportunity to change the design a bit, this is what it looks like now! 😁 Here you can find the full code: https://github.com/albarin/nit-del-llop Thanks for reading, any comments or questions are welcomed 😊 Previously published at https://medium.com/typeforms-engineering-blog/how-i-used-go-to-generate-images-for-my-dads-meetups-c54519bea9a0