DEV Community

Cover image for Image Compression with Golang
Francisco Mendes
Francisco Mendes

Posted on

Image Compression with Golang

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, if you're used to working with Express, Koa or Fastify you'll feel at home.

For image processing, I will use the 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, 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, otherwise it will not be possible to process the images. If you happen to have Homebrew installed on your computer, the process is simpler.

let's code

First let's install the following packages:

go get github.com/gofiber/fiber/v2
go get github.com/h2non/bimg
go get github.com/google/uuid
Enter fullscreen mode Exit fullscreen mode

Then let's create a simple API:

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")
}
Enter fullscreen mode Exit fullscreen mode

To run the API use the following command:

go run .
Enter fullscreen mode Exit fullscreen mode

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).

// ...

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

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

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

    app.Listen(":3000")
}
Enter fullscreen mode Exit fullscreen mode

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.

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

    // ...

    return c.SendString("Hello dev 👋")
})
Enter fullscreen mode Exit fullscreen mode

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.

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 👋")
})
Enter fullscreen mode Exit fullscreen mode

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.

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 👋")
})
Enter fullscreen mode Exit fullscreen mode

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.

// @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
}

// ...
Enter fullscreen mode Exit fullscreen mode

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.

// @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) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

// @utils.go

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

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// @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
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// @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
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// @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
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

    // ...

    return c.SendString("Hello dev 👋")
})
Enter fullscreen mode Exit fullscreen mode

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.

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 👋")
})
Enter fullscreen mode Exit fullscreen mode

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:

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,
    })
})
Enter fullscreen mode Exit fullscreen mode

Now when we test our API using a tool similar to Postman you should get a result similar to this:

uploading file to api

The final main.go code should look like this:

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")
}
Enter fullscreen mode Exit fullscreen mode

The final utils.go code should look like this:

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
}
Enter fullscreen mode Exit fullscreen mode

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! 😜

Discussion (0)