Writing an image-resizing server with go and libvips

PUBLISHED ON FEB 17, 2018

libvips is an image processing library that offers better performance than many other libraries, such as ImageMagick. libvips achieves this via a demand-driven design, which allows libvips to process regions of the images whenever they are required. In this post, we’ll use govips, a wrapper over libvips for go, to build a server that’ll resize images on the fly. This is very useful in scenarios such as serving low-resolutions assets to mobile devices which don’t have high bandwidth. By the end of this tutorial, we’ll be able to to do this conversion via an API.

(c) 2014 Grag Skidmore

Installing libvips

First, we’ll compile and install libvips. Its fairly quick, and shouldn’t take more than a few minutes for the build to complete. Run this in your shell:

# Install dependencies for libvips
sudo apt install build-essential pkg-config glib2.0-dev libexpat1-dev

cd /tmp/

# This assumed a UNIX based OS. Download the appropriate version from https://github.com/jcupitt/libvips/releases
# if you are running Windows
wget https://github.com/jcupitt/libvips/releases/download/v8.6.2/vips-8.6.2.tar.gz

tar xzf vips-8.6.2.tar.gz
cd vips-8.6.2
./configure
make
sudo make install
sudo ldconfig

Installing govips

Run this is in a shell to install govips:

# Wrapper over libvips for go
go get -u github.com/davidbyttow/govips

Note: If you get an error related to the -fopenmp flag while installing govips, run export CGO_CFLAGS_ALLOW='-fopenmp' and then install it again. (Relevant issue)

Building the service

We’ll be accessing the image resizing server via an API of the form /?width=100&height=100&url=https://example.com/image.png

The server will download the image, resize it to the given width and height, and return the resized image in the response. Ideally, we’d use an encoded URL so that ampersands and other special characters in the image url are properly handled, but its much easier to play around with the API without encoding. The default golang mux unescapes query parameters, so we don’t need to explicitly handle that case. The encoded version of the grumpy cat image would be

https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fe%2Fee%2FGrumpy_Cat_by_Gage_Skidmore.jpg

Routing

To follow along, start a new project and create a file called main.go. We’ll begin by spawning a server listening on the port 4444, with a single handler that is called for any URL.

package main

import (
	"log"
	"net/http"

	"github.com/davidbyttow/govips"
)

func main() {
	// Start vips with the default configuration
	vips.Startup(nil)
	defer vips.Shutdown()

	http.HandleFunc("/", resizeHandler)
	log.Fatal(http.ListenAndServe("localhost:4444", nil))
}

Next, let’s define resizeHandler. We shall obtain the query via query := r.URL.Query(), and subsequently calling query.Get() for every parameter we need. We’ll do some basic validation, and return 400 errors if the parameters were malformed. Make sure you add the imports for fmt and strconv.

func resizeHandler(w http.ResponseWriter, r *http.Request) {
	// Get the query parameters from the request URL
	query := r.URL.Query()
	queryUrl := query.Get("url")
	queryWidth := query.Get("width")
	queryHeight := query.Get("height")

	// Validate that all three required fields are present
	if queryUrl == "" || queryWidth == "" || queryHeight == "" {
		w.Write([]byte(fmt.Sprintf("url, width and height are required")))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Convert width and height to integers
	width, errW := strconv.Atoi(queryWidth)
	height, errH := strconv.Atoi(queryHeight)
	if errW != nil || errH != nil {
		w.Write([]byte(fmt.Sprintf("width and height must be integers")))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.Write([]byte(fmt.Sprintf("Gonna resize %s to %dx%d!", queryUrl, width, height)))
}

Run the server; either via running go install and then running the binary, or go run main.go.

Now, browse to

http://localhost:4444/?width=150&height=400&url=https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg

You should see: Gonna resize https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg to 150x400!

Fetching the source image

Get rid of the line that writes that to w, and proceed with the code to download the image. We perform a GET request and check for errors.

	// Start fetching the image from the given url
	resp, err := http.Get(queryUrl)
	if err != nil {
		w.Write([]byte(fmt.Sprintf("failed to get %s: %v", queryUrl, err)))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Ensure that a valid response was given
	if resp.StatusCode/100 != 2 {
		w.Write([]byte(fmt.Sprintf("failed to get %s: status %d", queryUrl, resp.StatusCode)))
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	defer resp.Body.Close()

Resizing the image

The next bit is where it gets interesting. We’ll create a vips Transform to perform all the operations we want. We specify the following sequence of operations:

  • Load the image from the response body
  • Use the resize strategy “stretch”. Alternatively, you can choose to embed or crop the resized image
  • Resize it to the specified width and height
  • Write the final image to the ResponseWriter so that the client receives the resized image

Once we specify this sequence, we call Apply() to perform the transformation. The transform returns a byte array, which we don’t need since the generated image has been written to the output stream.

	_, err = vips.NewTransform().
		Load(resp.Body).
		ResizeStrategy(vips.ResizeStrategyStretch).
		Resize(width, height).
		Output(w).
		Apply()
	if err != nil {
		w.Write([]byte(fmt.Sprintf("failed to resize %s: %v", imageUrl, err)))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

Now, browse to

http://localhost:4444/?width=194&height=149&url=https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg

again. You should see this:


Here is the complete source code:

package main

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

	"github.com/davidbyttow/govips"
)

func main() {
	// Start vips with the default configuration
	vips.Startup(nil)
	defer vips.Shutdown()

	http.HandleFunc("/", resizeHandler)
	log.Fatal(http.ListenAndServe("0.0.0.0:4444", nil))
}

func resizeHandler(w http.ResponseWriter, r *http.Request) {
	// Get the query parameters from the request URL
	query := r.URL.Query()
	queryUrl := query.Get("url")
	queryWidth := query.Get("width")
	queryHeight := query.Get("height")

	// Validate that all three required fields are present
	if queryUrl == "" || queryWidth == "" || queryHeight == "" {
		w.Write([]byte(fmt.Sprintf("url, width and height are required")))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Convert width and height to integers
	width, errW := strconv.Atoi(queryWidth)
	height, errH := strconv.Atoi(queryHeight)
	if errW != nil || errH != nil {
		w.Write([]byte(fmt.Sprintf("width and height must be integers")))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Start fetching the image from the given url
	resp, err := http.Get(queryUrl)
	if err != nil {
		w.Write([]byte(fmt.Sprintf("failed to get %s: %v", queryUrl, err)))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Ensure that a valid response was given
	if resp.StatusCode/100 != 2 {
		w.Write([]byte(fmt.Sprintf("failed to get %s: status %d", queryUrl, resp.StatusCode)))
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	defer resp.Body.Close()

	// govips returns the output of the image as a []byte object. We don't need
	// it since we are directly piping it to the ResponseWriter
	_, err = vips.NewTransform().
		Load(resp.Body).
		ResizeStrategy(vips.ResizeStrategyStretch).
		Resize(width, height).
		Output(w).
		Apply()

	if err != nil {
		w.Write([]byte(fmt.Sprintf("failed to resize %s: %v", queryUrl, err)))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

What next?

There are a lot more features that we can add to this. Such as caching images in redis for faster response times, or using a different ResizeStrategy. govips exposes many of libvips’ features, so there’s a lot more operations we can add via query parameters. Alternatively, if you are looking for a complete solution, take a look at imaginary.