Building an Image Compression Service with Golang, AWS S3, and AWS Lambda - Part 1
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.
Subscribe to my newsletter
Read articles from Rabinson Thapa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by