Automate Resizing Bulk Images Using Libvips

— 6 minute read

With the grow­ing pop­u­lar­ity of ShopeeFood, many restau­rants were ap­ply­ing for part­ner­ship with them, in­cluded my rel­a­tive’s Padang restau­rant. But, their reg­is­tra­tion was not easy. They need a photo for each menu to be in a for­mat of 720x720 px. For a Padang restau­rant with over 50 menus and un­for­tu­nately the restau­ran­t’s menu pho­tos were in dif­fer­ent sizes.

I was too lazy to re­sized them man­u­ally. At first, I search for a ser­vice that could re­size an im­age. But they weren’t able to re­size so many im­ages at once. So I thought to write a pro­gram to au­to­mate this. Enter the lib­vips, lib­vips was a soft­ware that was used for im­age ma­nip­u­la­tion. Since I used Go and lib­vips was writ­ten in C, I searched for a Go lib­vips pack­age and found bimg.

The code was pretty sim­ple, we only need to it­er­ate all of the pho­tos in a di­rec­tory. Then, for each of photo, re­size it into 720x720 px. But, be­cause the pho­tos are not all squares, we can’t use the Resize method in­stead, we use ResizeAndCrop that will re­size the pho­tos into 720x720 px and crop it fill to cen­ter

import (
"fmt"
"path"

"github.com/h2non/bimg"
)

var root = "/menu-photos"
var outdir = "/menu-photos/720x720"

func resize(filepath, filename string) error {
buf, err := bimg.Read(filepath)
if err != nil {
return err
}

img := bimg.NewImage(buf)
size, err := img.Size()
if err != nil {
return err
}

newImage, err := img.ResizeAndCrop(720, 720)
if err != nil {
return err
}

size, err = bimg.NewImage(newImage).Size()
if err != nil {
return err
}

if size.Width != 720 || size.Height != 720 {
fmt.Printf("wrong size: '%s' %vx%v\n", filename, size.Width, size.Height)
}

fmt.Printf("resize & crop: %s\n", filename)
return bimg.Write(path.Join(outdir, filename), newImage)
}

To make the code faster, we can uti­lized gor­ou­tine. Here i spawned 4 gor­ou­tine that re­size the im­ages con­curently.

type resizeJob struct {
FilePath string
FileName string
}

func main() {
jobChan := make(chan resizeJob, 4)

wg := &sync.WaitGroup{}
nworker := 4
// spawn workers
for i := 0; i < nworker; i++ {
wg.Add(1)
go worker(wg, jobChan)
}

err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
// enqueue jobs
jobChan <- resizeJob{FilePath: path, FileName: info.Name()}
return nil
})
close(jobChan)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}

// wait for workers to finished
wg.Wait()
fmt.Println("done")
}

func worker(wg *sync.WaitGroup, jobChan chan resizeJob) {
defer wg.Done()
for file := range jobChan {
err := resize(file.FilePath, file.FileName)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
}
}