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.
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
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)
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
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
You should see:
Gonna resize https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg to 150x400!
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()
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:
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
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
}
}
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.