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
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")
}
To run the API use the following command:
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).
// ...
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.
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.
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.
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.
// @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.
// @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:
// @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.
// @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.
// @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.
// @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.
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.
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:
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 you should get a result similar to this:
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")
}
The final utils.go
code should look like this:
package main
import (
"fmt"
"os"
"strings"
<span class="s">"github.com/google/uuid"</span>
<span class="s">"github.com/h2non/bimg"</span>
)
// 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"
<span class="n">converted</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">bimg</span><span class="o">.</span><span class="n">NewImage</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span><span class="o">.</span><span class="n">Convert</span><span class="p">(</span><span class="n">bimg</span><span class="o">.</span><span class="n">WEBP</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">filename</span><span class="p">,</span> <span class="n">err</span>
<span class="p">}</span>
<span class="n">processed</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">bimg</span><span class="o">.</span><span class="n">NewImage</span><span class="p">(</span><span class="n">converted</span><span class="p">)</span><span class="o">.</span><span class="n">Process</span><span class="p">(</span><span class="n">bimg</span><span class="o">.</span><span class="n">Options</span><span class="p">{</span><span class="n">Quality</span><span class="o">:</span> <span class="n">quality</span><span class="p">})</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">filename</span><span class="p">,</span> <span class="n">err</span>
<span class="p">}</span>
<span class="n">writeError</span> <span class="o">:=</span> <span class="n">bimg</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"./"</span><span class="o">+</span><span class="n">dirname</span><span class="o">+</span><span class="s">"/%s"</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span> <span class="n">processed</span><span class="p">)</span>
<span class="k">if</span> <span class="n">writeError</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">filename</span><span class="p">,</span> <span class="n">writeError</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">filename</span><span class="p">,</span> <span class="no">nil</span>
}
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! π
Top comments (6)
ufff I got this error when I run this on the terminal (windows): go run .
pkg-config --cflags -- vips vips vips vips
pkg-config: exec: "pkg-config": executable file not found in %PATH%
pleace help!
Maybe you need to install the Windows binary libvips.org/install.html
thanks, but what will happend? if I export golang proyect to linux SO? there will be any problems?
I developing on windows PC, so I have to install libvips. then, I compile it for linux SO, this is it going to work?
Just Suggestion to keep using same filename.
name := fileheader.Filename
imageProcessing(name, buffer, 75, "uploads")
thank you i will use.