Building an Image Compression Service with Golang, AWS S3, and AWS Lambda - Part 1

Rabinson ThapaRabinson Thapa
5 min read

In this blog post, we'll create an image compression service using Golang. We'll explore the reasons for using Golang, understand image compression, and then delve into the step-by-step implementation. This includes creating routes, handling image compression, integrating with AWS S3, setting up AWS Lambda for serverless deployment, and testing the service locally. This post is divided into two parts.

What is Image Compression, and Why Do We Need It?

Image compression reduces an image's file size without significantly affecting its quality. This is crucial as compressed images take up less storage space, reduce the amount of data transferred over the network, and improve web application performance by reducing load times.

Why Golang?

Golang, or Go, is ideal for this project due to its simplicity, performance, and concurrency support. Its syntax is easy to learn, it handles compute-intensive tasks efficiently, and its rich standard library supports tasks like HTTP handling and image manipulation.

Project Initialization

Let's start by initializing our project:

mkdir goImageCompressor
cd goImageCompressor
go mod init goImageCompressor
touch main.go

Creating a Local Server

First, we will create a simple local server using Golang's core package net/http.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Create a new local server
    fmt.Println("Starting the server on localhost:3000")
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("TODO: Implement the image compressor")
    })

    // Start the local server
    err := http.ListenAndServe(":3000", nil)
    if err != nil {
        fmt.Println("Error in starting server", err)
    }
}

Implementing Image Compression

Next, we will create a function to handle image compression. Initially, this function will take an image from the local root, compress it, and save it. We will use the imaging package to compress our image.

package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"

    "github.com/disintegration/imaging"
)

func main() {
    // Create a new local server
    fmt.Println("Starting the server on localhost:3000")
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        compressImage()
    })

    // Start the local server
    err := http.ListenAndServe(":3000", nil)
    if err != nil {
        fmt.Println("Error in starting server", err)
    }
}

func compressImage() {
    // Decode a test image
    img, err := imaging.Open("enoshima.jpeg")
    if err != nil {
        log.Fatalf("failed to open image: %v", err)
    }

    // Resize the image to width and height of 128px
    img = imaging.Resize(img, 128, 128, imaging.Lanczos)

    // Encode the image to JPEG
    buf := new(bytes.Buffer)
    err = imaging.Encode(buf, img, imaging.JPEG)
    if err != nil {
        log.Fatalf("failed to encode image: %v", err)
    }

    // Save the resulting image as JPEG
    err = imaging.Save(img, "enoshima_small.jpg")
}

In the compressImage function, we first read the file imaging.Open and pass the file name as an argument. We then resize the image to 128px by 128px and save it locally.

Original Image - 2.5MB

Compressed Image - 7KB

Lanczos is a high-quality resampling filter for photographic images, yielding sharp results. You can find more resampling filters here.

To maintain the aspect ratio, we can update our code as follows:

// Resize the image to width 128px keeping the aspect ratio
img = imaging.Resize(img, 128, 0, imaging.Lanczos)

Updating the Route to Handle Requests

Finally, we will update our route to take an image from the request and a width size from the query, pass these to the compressImage function, and return the compressed image as a response. Our final code will look like this:

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "strconv"

    "github.com/disintegration/imaging"
)

func main() {
    // Create a new local server
    fmt.Println("Starting the server on localhost:3000")
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

        // Read the size to compress the image
        size, err := strconv.Atoi(r.URL.Query().Get("size"))
        if err != nil {
            fmt.Println(err)
            fmt.Println("Error in reading size")
            return
        }

        // Read the image file
        file, _, err := r.FormFile("image")
        if err != nil {
            fmt.Println("Error in reading image file")
            return
        }

        // Create a buffer to store the image
        var imageFile []byte
        buf := new(bytes.Buffer)
        io.Copy(buf, file)
        imageFile = buf.Bytes()

        // Compress the image
        compressedImage, err := compressImage(imageFile, size)
        if err != nil {
            fmt.Println("Error in compressing image")
            return
        }

        // Write the compressed image to the response
        w.Header().Set("Content-Type", "image/jpeg")
        w.Header().Set("Content-Length", fmt.Sprint(len(compressedImage)))
        w.Write(compressedImage)
    })

    // Start the local server
    err := http.ListenAndServe(":3000", nil)
    if err != nil {
        fmt.Println("Error in starting server", err)
    }
}

func compressImage(file []byte, size int) ([]byte, error) {
    // Decode the image
    img, err := imaging.Decode(bytes.NewReader(file))
    if err != nil {
        fmt.Println("Error in decoding image")
        return nil, fmt.Errorf("error in decoding image")
    }

    // Resize the image to width provided size keeping the aspect ratio
    img = imaging.Resize(img, size, 0, imaging.Lanczos)

    // Encode the image to JPEG
    buf := new(bytes.Buffer)
    err = imaging.Encode(buf, img, imaging.JPEG)
    if err != nil {
        fmt.Println("Error in encoding image")
        return nil, fmt.Errorf("error

 in encoding image")
    }

    return buf.Bytes(), nil
}

Once you have saved the file, you can start your server by running the following command and using Postman or another tool to check your API. I am using ThunderClient for testing.

go run main.go

Conclusion

In conclusion, we have successfully implemented a local server using Golang that can compress images to a specified width while maintaining the aspect ratio. This service reads an image from the request, processes it, and returns the compressed image as a response. You can view the GitHub repo from here.

Next Steps

In Part 2, we will enhance our service to fetch images from AWS S3, compress them, and save the compressed versions back to S3. We will then package this service as a Lambda function and learn how to deploy it on AWS Lambda. Stay tuned for the next part, in which we dive deeper into integrating with AWS and deploying our service in a scalable, serverless architecture.

0
Subscribe to my newsletter

Read articles from Rabinson Thapa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rabinson Thapa
Rabinson Thapa