Image Compression with Golang
Tags: go, tutorial, beginners, webdev
Published: 2021-08-28 21:41:42

I bet all of us at some point have had the need to compress an image. And many times we end up relying on third-party services to carry out this operation, when in fact it is easier to implement than we think.

For that I decided to create this article in which we will create a simple API to which an image will be sent from an http request, then the same image will be converted to a webp, then a compression will be made to make it lighter and finally let's save it in the indicated folder.

The framework I chose to create today's Api was [Fiber](https://gofiber.io/), if you're used to working with Express, Koa or Fastify you'll feel at home.

For image processing, I will use the [bimg](https://github.com/h2non/bimg) library because in my opinion it has a very intuitive API and is easy to use, in addition to being very fast.

Another library I'm going to use is [google/uuid](https://github.com/google/uuid), because I'm going to want to rename the image that we uploaded, in order to ensure that its name is unique.

## Prerequisites

However, this article has only one prerequisite, which is the installation of [libvips](https://libvips.github.io/libvips/), otherwise it will not be possible to process the images. If you happen to have [Homebrew](https://brew.sh/) installed on your computer, the process is simpler.

# let's code

First let's install the following packages:

```sh
go get github.com/gofiber/fiber/v2
go get github.com/h2non/bimg
go get github.com/google/uuid
```

Then let's create a simple API:

```go
package main

import "github.com/gofiber/fiber/v2"

func main() {
    app := fiber.New()

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello dev 👋")
    })

    app.Listen(":3000")
}
```

To run the API use the following command:

```sh
go run .
```

If you are testing our Api, you will receive the `Hello dev 👋` message in the body of the response.

If the app is working correctly, we can start by changing the http verb of our endpoint from `Get` to `Post`. Next we will create a file server to serve static files, which in this case will be our images, for that we will use `app.Static()` which will have two arguments, the first is the **prefix** (which will be used to get them) and the second is the **root** (folder from which they will be served).

```go
// ...

func main() {
    app := fiber.New()

    app.Static("/uploads", "./uploads")

    app.Post("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello dev 👋")
    })

    app.Listen(":3000")
}
```

Now we can get our image using the `c.FormFile()` function which will have a single argument which in this case is the field of the multipart/form-data corresponding to the image, which we will give the name of *picture*.

```go
app.Post("/", func(c *fiber.Ctx) error {
    fileheader, err := c.FormFile("picture")
    if err != nil {
        panic(err)
    }

    // ...

    return c.SendString("Hello dev 👋")
})
```

One of `c.FormFile()`'s returns is the file header, but we want the file data and not the header, so we will use the `.Open()` function.

```go
app.Post("/", func(c *fiber.Ctx) error {
    fileheader, err := c.FormFile("picture")
    if err != nil {
        panic(err)
    }

    file, err := fileheader.Open()
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // ...

    return c.SendString("Hello dev 👋")
})
```

After we get the data from the file we will want to get the file's buffer, for that we'll use `io.ReadAll()`, to which we'll pass the data from our file and one of the returns will be the file's buffer.

```go
app.Post("/", func(c *fiber.Ctx) error {
    fileheader, err := c.FormFile("picture")
    if err != nil {
        panic(err)
    }

    file, err := fileheader.Open()
    if err != nil {
        panic(err)
    }
    defer file.Close()

    buffer, err := io.ReadAll(file)
    if err != nil {
        panic(err)
    }

    // ...

    return c.SendString("Hello dev 👋")
})
```

We are already quite advanced, however I will now create two functions within `utils.go`. The first function will be called `createFolder()`, this function will be responsible for checking if the *uploads* folder exists in the root of our project, if it doesn't, it will create the folder.

This function will have a single argument which is the name we want to give the folder and returns the error if it exists.

```go
// @utils.go

package main

import "os"

// A new folder is created at the root of the project.
func createFolder(dirname string) error {
    _, err := os.Stat(dirname)
    if os.IsNotExist(err) {
        errDir := os.MkdirAll(dirname, 0755)
	if errDir != nil {
	    return errDir
	}
    }
    return nil
}

// ...
```

Now we can create the second function that will be responsible for processing our image. This function will be called `imageProcessing()` and will have three arguments, the first argument is the file **buffer**, the second is the image **quality** and the third will be the name of the folder where the image will be saved at the end.

This function will have two returns, one of them will be the image name and the second is the error, if it occurs.

```go
// @utils.go

package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/google/uuid"
    "github.com/h2non/bimg"
)

// ...

// The mime type of the image is changed, it is compressed and then saved in the specified folder.
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
    // ...
}
```

First let's create the filename for this we will use the google/uuid library and we will create a new uuid however I won't want the hyphens, to remove them I will use `strings.Replace()`, as follows:

```go
// @utils.go

// ...
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
    filename := strings.Replace(uuid.New().String(), "-", "", -1) + ".webp"

    // ...
}
```

Then we have to perform the image conversion, regardless of the image format we will want to change the mime type to webp. So let's use the `bimg.NewImage()` function to which we'll pass our image buffer and then we'll use the `.Convert()` function to change the mime type of the image to webp.

```go
// @utils.go

// ...
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
    filename := strings.Replace(uuid.New().String(), "-", "", -1) + ".webp"

    converted, err := bimg.NewImage(buffer).Convert(bimg.WEBP)
    if err != nil {
        return filename, err
    }

    // ...
}
```

After that we have to perform image compression. For that we will use the `bimg.NewImage()` function again, but this time we will pass the *converted image buffer*. Next we'll use the `.Process()` function to which we'll just pass the quality option.

```go
// @utils.go

// ...
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
    filename := strings.Replace(uuid.New().String(), "-", "", -1) + ".webp"

    converted, err := bimg.NewImage(buffer).Convert(bimg.WEBP)
    if err != nil {
        return filename, err
    }

    processed, err := bimg.NewImage(converted).Process(bimg.Options{Quality: quality})
    if err != nil {
        return filename, err
    }

    // ...
}
```

Now we need to save the image already processed in the uploads folder, for that we will use the `bimg.Write()` function, to which we will pass two arguments, the first is the name of the folder where the image will be saved and its name, the second is the buffer of the already processed image.

Finally, just return the filename and the null error.

```go
// @utils.go

// ...
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
    filename := strings.Replace(uuid.New().String(), "-", "", -1) + ".webp"

    converted, err := bimg.NewImage(buffer).Convert(bimg.WEBP)
    if err != nil {
        return filename, err
    }

    processed, err := bimg.NewImage(converted).Process(bimg.Options{Quality: quality})
    if err != nil {
        return filename, err
    }

    writeError := bimg.Write(fmt.Sprintf("./"+dirname+"/%s", filename), processed)
    if writeError != nil {
        return filename, writeError
    }
    
    return filename, nil
}
```

Now going back to `main.go`, let's now use our `createFolder()` function to check if the uploads folder already exists and if it doesn't, it is created.

```go
app.Post("/", func(c *fiber.Ctx) error {
    // ...

    errDir := createFolder("uploads")
    if errDir != nil {
        panic(errDir)
    }

    // ...

    return c.SendString("Hello dev 👋")
})
```

Next we will use our `imageProcessing()` function, passing its arguments, the first is the buffer, the second is the quality and the third is the folder where the image will be saved.

From this function we will receive the filename that will be used in the body of the response.

```go
app.Post("/", func(c *fiber.Ctx) error {
    // ...

    errDir := createFolder("uploads")
    if errDir != nil {
        panic(errDir)
    }

    filename, err := imageProcessing(buffer, 40, "uploads")
    if err != nil {
        panic(err)
    }

    return c.SendString("Hello dev 👋")
})
```

Now all that's left is to send the request response body, which will be a json with a single property that will be the link necessary for our image to be served. Like this:

```go
app.Post("/", func(c *fiber.Ctx) error {
    // ...

    errDir := createFolder("uploads")
    if errDir != nil {
        panic(errDir)
    }

    filename, err := imageProcessing(buffer, 40, "uploads")
    if err != nil {
        panic(err)
    }

    return c.JSON(fiber.Map{
        "picture": "http://localhost:3000/uploads/" + filename,
    })
})
```

Now when we test our API using a tool similar to [Postman](https://www.postman.com/) you should get a result similar to this:

![uploading file to api](https://res.cloudinary.com/dj5iihhqv/image/upload/v1630186890/Kapture_2021-08-28_at_22.39.53-min_ex0qg2.gif)

The final `main.go` code should look like this:

```go
package main

import (
	"io"

	"github.com/gofiber/fiber/v2"
)

func main() {
	app := fiber.New()

	app.Static("/uploads", "./uploads")

	app.Post("/", func(c *fiber.Ctx) error {
		fileheader, err := c.FormFile("picture")
		if err != nil {
			panic(err)
		}

		file, err := fileheader.Open()
		if err != nil {
			panic(err)
		}
		defer file.Close()

		buffer, err := io.ReadAll(file)
		if err != nil {
			panic(err)
		}

		errDir := createFolder("uploads")
		if errDir != nil {
			panic(errDir)
		}

		filename, err := imageProcessing(buffer, 40, "uploads")
		if err != nil {
			panic(err)
		}

		return c.JSON(fiber.Map{
			"picture": "http://localhost:3000/uploads/" + filename,
		})
	})

	app.Listen(":3000")
}
```

The final `utils.go` code should look like this:

```go
package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/google/uuid"
	"github.com/h2non/bimg"
)

// A new folder is created at the root of the project.
func createFolder(dirname string) error {
	_, err := os.Stat(dirname)
	if os.IsNotExist(err) {
		errDir := os.MkdirAll(dirname, 0755)
		if errDir != nil {
			return errDir
		}
	}
	return nil
}

// The mime type of the image is changed, it is compressed and then saved in the specified folder.
func imageProcessing(buffer []byte, quality int, dirname string) (string, error) {
	filename := strings.Replace(uuid.New().String(), "-", "", -1) + ".webp"

	converted, err := bimg.NewImage(buffer).Convert(bimg.WEBP)
	if err != nil {
		return filename, err
	}

	processed, err := bimg.NewImage(converted).Process(bimg.Options{Quality: quality})
	if err != nil {
		return filename, err
	}

	writeError := bimg.Write(fmt.Sprintf("./"+dirname+"/%s", filename), processed)
	if writeError != nil {
		return filename, writeError
	}

	return filename, nil
}
```

## Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧑🏻‍💻

Hope you have a great day! 😜