Scripting with Script-Fu in GIMP to compress photos in batch

I’m now trying out GIMP for photo editing. I know it supports scripting with Script-Fu, a programming language based on Scheme, a minimalist Lisp dialect. Today, I was trying to compress some photos to be uploaded to my blog. Instead of opening and editing the photos one by one, why don’t I try out Script-Fu to do the job in batch?

1. Running functions inside a *.scm script file from the console

Each of my script file ends with .scm, which stands for Scheme. To be loaded into GIMP, the file should be in

~/.config/GIMP/2.10/scripts

on my computer, which runs Linux and has GIMP 2.10 installed. Within each file, there are just a collection of functions written by myself, and those functions will be loaded into GIMP and can be called from the Script-Fu console.

For example, let’s say I have a function hello-world

(define (hello-world) (gimp-message "hello world"))

in the file, I can open the console in GIMP by finding Filter >> Script-Fu >> Console on the menu bar. If I call the function by typing

(hello-world)

in the prompt, it will prompt a message saying “hello world” (and return a true value #t).

We can also run the script from the operating system’s console:

gimp -i -b '(hello-world)'

where -i stands for no interface and -b stands for batch mode (which accepts function call). After the call was run, we can type ^C (Ctrl+C) to exit.

2. Getting help

I’m new to Scheme, but I know Emacs Lisp, another Lisp dialect, so reading Lisp code is fine with me. Still, I need to know (1) some basic functions and macros of Scheme (e.g. conditional statements) and (2) the APIs of GIMP (e.g. gimp-image-resize). I found that the Wikipedia page of Scheme has a good cheatsheet for the basics of Scheme, and right next to the prompt within the GIMP console, the Browse... button allows us to search the documentations of the GIMP API functions.

3. Now implement the code

I found a Script on Stackoverflow, so I only need to adapt the code for my own use (many thanks!). To be fair, the code should work almost out-of-box in my case, but I modified it a bit to satisfy my own taste (to make it functional instead of imperative) and to simplify the call from command line. Here it is:

(define (batch-resize dir)
  (let* ((filelist (cadr (file-glob (string-append dir "/*.JPG") 1))))
        (batch-resize-list filelist)
        (gimp-quit 1)))

(define (batch-resize-list filelist)
  (unless (null? filelist)
    (let* (
           (filename (car filelist))
           (size (file-size filename))
           (target-size 500000)  ; 500 KB
           (scale-factor (sqrt (/ size target-size)))
           (image (car (gimp-file-load 1 filename filename)))
           (drawable   (car (gimp-image-active-drawable image)))
           (cur-width  (car (gimp-image-width image)))
           (cur-height (car (gimp-image-height image)))
           (width      (round (/ cur-width scale-factor)))
           (height     (round (/ cur-height scale-factor)))
           )
      (gimp-message filename)
      (gimp-image-scale-full image width height INTERPOLATION-CUBIC)
      (gimp-file-save RUN-NONINTERACTIVE image drawable filename filename))
    (batch-resize-list (cdr filelist))))

where batch-resize is the function I should call and batch-resize-list is a helper function.

WARNING: This code will compress and REWRITE the original photo file, so make sure you have backups when testing it!

To call it from command line:

cd <my-photo-directory>
gimp -i -b "(batch-resize \"`pwd`\")"

Now you may see why I hard-coded the *.JPG pattern within the function — I don’t want to type in another argument when calling this function.