Python-Fu #4 - Using Python-Fu in Gimp Batch Mode

A reader asked me a question about batch processing with Gimp. I've never used Gimp for batch processing (I usually go for ImageMagick), but I thought it might be interesting to have look at how you can batch-run your Python-Fu scripts, before we go on to the next chapter.

If you look at the official documentation for Gimp's batch feature, you might feel puzzled:

http://www.gimp.org/tutorials/Basic_Batch/

Our goal in this article is to achieve some sort of batch processing on a bunch of images, selected by giving a directory and some wildcard pattern, then applying the same python-fu script to process each image. There are a few ways I can see how to do this:

  • use Gimp's batch mode, using the non-interactive command line mode, and some script-fu in scheme (like in the doc above) to call your python-fu
  • use David's Batch Processor, a great plugin available with Gimp in many distros, and which is an all-in-one solution for batch processing from Gimp's menus
  • use an "interface" python-fu plugin that will wrap the batch processing and call to your base python-fu plugin, from standard Gimp menus, interactively
  • use the same as above, but not interactively, strictly from the command line, that will require a pinch of script-fu

First, we need a basic python-fu, that we'll use as the processing step in our batch processing.

The basic script

Let's start by building our Python script for a very silly plugin, that will take an image as input, flip it, and scale it down to a fixed size. We'll call it "flipscale.py". Save it in "~/.gimp-2.6/plug-ins" and don't forget to make it executable.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, re, glob
from gimpfu import *
from gimpenums import *

# our script
def my_script_function(image, drawable) :
    pdb.gimp_image_flip( image, ORIENTATION_HORIZONTAL )
    pdb.gimp_image_scale_full(image, 120, 120, INTERPOLATION_LANCZOS)
    return

# This is the plugin registration function
register(
    "python_fu_flipscale",    
    "Flip and scale image",   
    "A simple Python Script that flips the image.",
    "Michel Ardan",
    "Michel Ardan Company",
    "May 2011",
    "<Image>/MyScripts/Flip And Scale Image",
    "*",
    [],
    [],
    my_script_function,
)

main()

Restart Gimp, open an image, and run your script from the menu to check that it works ok.

First steps with Gimp's Batch Mode

Now, following the official Gimp's batch mode documentation, let's check out the options, running this in a terminal:

$ gimp --help

The full list of options should be:

Éditeur d'image GIMP

Options de l'aide :
  -h, --help                          Affiche les options de l'aide
  --help-all                          Affiche toutes les options de l'aide
  --help-gegl                         Show GEGL Options
  --help-gtk                          Affiche les options GTK+

Options de l'application :
  -v, --version                       Affiche l'information de version et quitte
  --license                           Affiche l'information de licence et quitte
  --verbose                           Être plus bavard
  -n, --new-instance                  Démarre une nouvelle instance de GIMP
  -a, --as-new                        Ouvre en tant que nouvelles images
  -i, --no-interface                  Lance sans interface utilisateur
  -d, --no-data                       Ne charge pas les brosses, dégradés, palettes, motifs...
  -f, --no-fonts                      Ne charge aucune police
  -s, --no-splash                     N'affiche pas l'écran de démarrage
  --no-shm                            N'utilise pas la mémoire partagée entre GIMP et ses greffons
  --no-cpu-accel                      N'utilise pas les accélérations propres aux CPU
  --session=<name>                    Utilise un autre fichier sessionrc
  -g, --gimprc=<filename>             Utilise un autre fichier système gimprc
  --system-gimprc=<filename>          Utiliser un autre fichier système gimprc
  -b, --batch=<command>               Commande batch à lancer (peut être utilisé plusieurs fois)
  --batch-interpreter=<proc>          La procédure pour exécuter les commandes batch
  -c, --console-messages              Envoie les messages dans la console au lieu d'utiliser une boîte de dialogue
  --pdb-compat-mode=<mode>            Mode compatibilité PDB (off|on|warn)
  --stack-trace-mode=<mode>           Débogage en cas d'arrêt brutal (never|query|always)
  --debug-handlers                    Active le débogage pour les signaux non fatals
  --g-fatal-warnings                  Rend tous les signaux fatals
  --dump-gimprc                       Créé un fichier gimprc avec les paramètres par défaut
  --display=AFFICHAGE                 Affichage X à utiliser

Which you probably have in english or another language than french on your computer. I have highlighted the options that interest us here.

First, "-i" means that Gimp will run with no interface. If you run:

$ gimp -i

you will see that nothing happens, Gimp started with no GUI, and does not react to any input, you have to quit with a "Ctrl-C".

What looks interesting is the "-b" flag, which says "Batch command to run (can be used multiple times)", and the "--batch-interpreter" which says "The procedure to process batch commands with". Mmm... can't go much further with that. Let's try what the doc says, first run from the command line:

$ gimp -b -

Gimp's usual interface will start, but if you looks a the command line you will see after a few seconds:

Welcome to TinyScheme, Version 1.38
Copyright (c) Dimitrios Souflis

>

Which is Gimp script-fu console! Enter this to quit Gimp:

> (gimp-quit 0)

You can run the script-fu console with no interface with:

$ gimp -i -b -

By the way, the last "-" character means "take input from the command line". The "--help" command told us that "-b" or "--batch" can be followed by a command. So let's try to send a command to Gimp from the console, we will forget about the "-i" flag for now so you can see Gimp opening a window:

$ gimp --batch='(gimp-message "Hello from the console")'

You can use the "-b" flag, which is the same but with a different syntax (no "="):

$ gimp -b '(gimp-message "Hello from the console")'

You can chain commands too, so lets try:

$ gimp -b '(gimp-message "Hello from the console")' -b '(gimp-message "I will quit now")' -b '(gimp-quit 0)'

You should see Gimp opening it's windows, display the two messages and quit.

Now, suppose we want do to some actual image processing, let's say you have an image at "/home/yourname/image.jpg", try this:

$ gimp -b '(gimp-display-new (car (gimp-file-load RUN-NONINTERACTIVE "/home/yourname/image.jpg" "/home/yourname/image.jpg")) )

So that's what you actually have to do to simply load an image from the command line. You quickly realise that loading the image is just the beginning, and you cannot do all your processing from the command line like that without going nuts. Sorry if you like script-fu, I respect that, but I don't like it at all. So you have to build special scripts dedicated to batch processing. That's what we will do now.

A Script-Fu / Python-Fu hybrid

I'm sorry to say, we will have to write a small Script-Fu first. Save this script as "~/.gimp-2.6/scripts/fubatchpy.scm", its content will look like this:

(define (call_py_flipscale filename)
  (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
     (drawable (car (gimp-image-get-active-layer image))))
        (python-fu-flipscale RUN-NONINTERACTIVE image drawable)
    (gimp-file-save RUN-NONINTERACTIVE image drawable filename filename)
    (gimp-image-delete image)
  )
)

Notice that:

  • our python procedure is "python-fu-flipscale" and not "python_fu_flipscale" when called from a script-fu

  • the script does not register any menu entry, so it will not be visible from the Gimp menu, though it can be called by other scripts.

Please note that this script overwrites the previous image, so take care to use it on test images.

Now, to use this "wrapper" script, call from the command line:

$ gimp -i -b '(call_py_flipscale "/home/yourname/image.jpg")' -b '(gimp-quit 0)'

Much easier, right ? Now, in the next step we will add 2 more elements: globbing and adding parameters to our script.

Extending our basic plugin, and globbing

First, let's modify our python script so that it handles 2 input parameters: the width and height of the scale image:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, re, glob
from gimpfu import *
from gimpenums import *

# our script
def my_script_function(image, drawable,  scale_w,  scale_h) :
    pdb.gimp_image_flip( image, ORIENTATION_HORIZONTAL )
    pdb.gimp_image_scale_full(image, scale_w, scale_h, INTERPOLATION_LANCZOS)
    return

# This is the plugin registration function
register(
    "python_fu_flipscale",    
    "Flip and scale image",   
    "A simple Python Script that flips the image.",
    "Michel Ardan",
    "Michel Ardan Company",
    "May 2011",
    "<Image>/MyScripts/Flip And Scale Image",
    "*",
    [
         (PF_INT, 'scale_w', 'New width for the image', 100),
         (PF_INT, 'scale_h', 'New height for the image', 100)
    ],  
    [],
    my_script_function,
)

main()

You can restart Gimp and test that it works like expected from the menus, asking you for the two values.

If you want to use it now from the command line, you have to change the "wrapper" script-fu to this:

(define (call_py_flipscale filename width height)
  (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
     (drawable (car (gimp-image-get-active-layer image))))
        (python-fu-flipscale RUN-NONINTERACTIVE image drawable width height)
    (gimp-file-save RUN-NONINTERACTIVE image drawable filename filename)
    (gimp-image-delete image)
  )
)

which will call the script with height and width, call this script with these new values:

$ gimp -i -b '(call_py_flipscale "/home/yourname/image.jpg" 120 150)' -b '(gimp-quit 0)'

A note on the "RUN-NONINTERACTIVE" flag. It is used to run procedures with no user interaction. You can change to "RUN-INTERACTIVE" in the script if you want, but it is not used in non-interactive command line, meaning you can't get any interface unless Gimp's interface is shown.

Now, back to globbing. Gimp includes some procedures to handle simple file globbing, which means it's very easy to tell a script the work on "*.jpg", or "base_*.png". First I'll show you how to do that with script-fu, and then a much easier way (in my opinion) with Python.

(define (call_py_flipscale pattern width height)
  (let* ((filelist (cadr (file-glob pattern 1))))
    (while (not (null? filelist))
      (let* ((filename (car filelist))         
        (image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
        (drawable (car (gimp-image-get-active-layer image))))
        (python-fu-flipscale RUN-INTERACTIVE image drawable width height)
        (gimp-file-save RUN-NONINTERACTIVE image drawable filename filename)
        (gimp-image-delete image)
        (set! filelist (cdr filelist))
      )
    )
  )
)

now, you can cd your terminal to a directory with some jpgs in a folder called "pics" and run:

$ gimp -i -b '(call_py_flipscale "pics/*.jpg" 120 150)' -b '(gimp-quit 0)'

and you'll have all your images processed!

Ouch! If you enjoy this, well good for you, I admit it's sort of "compact" and clean, but I'd rather go... all Python!

An interactive python-fu wrapper-batch-plugin

So, we will replace our "wrapper" script-fu, with this "wrapper" python-fu:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, re, glob
from gimpfu import *
from gimpenums import *

# our script
def pybatch(globpattern,  source) :
    pdb.gimp_message("Globbing: " + source)
    glob_result = pdb.file_glob(globpattern,  1)
    filecount = glob_result[0]
    for f in glob_result[1]:
        pdb.gimp_message("Found: " + f)
    return
    
# This is the plugin registration function
register(
    "python_fu_batchmyscript",    
    "A procedure to batch another script",   
    "A simple Python Script that can batch another script.",
    "Michel Ardan",
    "Michel Ardan Company",
    "May 2011",
   "<Toolbox>/MyScripts/Batch My Python cript...",
    "",
    [
        (PF_STRING, "glob_pattern", "Glob Pattern", "*.*"),
        (PF_DIRNAME, "source_directory", "Source Directory", ""),
    ],  
    [],
    pybatch,
)

main()

Note that I used a Toolbox menu entry field, and emptied the source image type, this way, our plugin appears in the menu, and you can select it even if no image is opened. This script will allow you to select a directory, a wildcard pattern, and will display the list of images found in the directory that matches the pattern.

Now applying our first script to each image is just a bit more work:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, re, glob
from gimpfu import *
from gimpenums import *

# our script
def pybatch2(globpattern,  source,  scale_w,  scale_h) :
    pdb.gimp_message("Globbing: " + source + os.sep + globpattern)
    glob_result = pdb.file_glob(source + os.sep + globpattern,  1)
    filecount = glob_result[0]
    for f in glob_result[1]:
        imagefile = f
        pdb.gimp_message("Opening: " + imagefile)
        try:
           image = pdb.gimp_file_load(imagefile,  imagefile)
           #pdb.gimp_display_new(image)
           drawable = pdb.gimp_image_get_active_layer(image)
           pdb.python_fu_flipscale(image,  drawable,  scale_w,  scale_h)
           pdb.gimp_file_save(image,  drawable,  imagefile,  imagefile)
           pdb.gimp_image_delete(image)

        except:
           pdb.gimp_message("Opening Error: " + imagefile)
    return
    
# This is the plugin registration function
register(
    "python_fu_batchmyscript2",    
    "A procedure to batch another script",   
    "A simple Python Script that can batch another script.",
    "Michel Ardan",
    "Michel Ardan Company",
    "May 2011",
   "<Toolbox>/MyScripts/Batch My Python flipscale script...",
    "",
    [
        (PF_STRING, "glob_pattern", "Glob Pattern", "*.*"),
        (PF_DIRNAME, "source_directory", "Source Directory", ""),
        (PF_INT, "scale_w", "Scale Width", "120"),
        (PF_INT, "scale_h", "Scale Height", "120"),
    ],  
    [],
    pybatch2,
)

main()

What's new ? Well we use gimp_file_load to load each image file. We could use gimp_display_new to make Gimp display the image, but I commented it as you may be running the script on a large amount of images. Then we need to get the drawable (main layer, the image itself if file isn't an XCF of animated GIF) with gimp_image_get_active_layer because our script needs both objects. Then we simply call our base script python_fu_flipscale with required parameters, and we end with gimp_file_save to overwrite the image with the new one (we could supply a different ouput directory, or rename the image too). And finally, we call gimp_image_delete to clean our mess (it just removes the image from memory). Notice the try/except mechanism here, if you're not familiar with this, this is called exception handling. Simply put, the code between the "try" and the "except" keywords is "monitored" for errors, typically this happen when a file that is not an image is encountered and gimp_file_load gives an error on it, we catch this error, do not process the file and go on processing the rest.

I guess that's not the best way to do it, in the documentation you will find some "temporary procedure" functions, that probably would allow us to remove the plugin registration part (you can empty the menu definition string, so that the wrapper is not available from the menus), but I have not taken the time to test that yet. So for now, we are registering our batch-oriented script as a plugin, this allows us to call it from Gimp's menus, and use it interactively to enter parameters (once, applied to each image).

Well my friends, we're not far from the Holy Grail...

 

The 99.9% percent Python-Fu command line version

With you terminal, go to a directory that contains a folder called "pics" filled with images. Then, call the previous script this way:

$ gimp -i -b '(python-fu-batchmyscript2 RUN-NONINTERACTIVE "*.*" "./pics" 512 512)' -b '(gimp-quit 0)'

And we're done!

Improvements

We could think about improvements. First, we may handle the progress bar at the bottom of Gimp's main window, which could give some feedback on the batch progress. The, we could do all the globbing in Python, and make the process recursive too. We could also try to build a more complex GUI than what Gimp offers. There are many GUI Library bindings in Python, and I guess we could even build a complex GUI for our scripts, and display it even when Gimp is run in command line mode!

See you next time for an article dedicated to adding a custom file format support to Gimp!

Comments

This tutorial have help me so much!!!! THX!!

Extremely helpful, thanks!

Really nice tuturial. I am working in window7, in gimp 2.8 I only could get it working when the script name started with "python" ! It took forever to find out, probably, I didn't read your manual carefully enough!

i.e. python-fu-watermark
no single quotes and escaping of double quotes inside...

"C:\tools\Graphics\GIMP 2\bin\gimp-2.8.exe" -i -b "(python-fu-watermark RUN-NONINTERACTIVE \"C:/Data/Dropbox/canada/customEffects/in.jpg\" \"C:/Data/Dropbox/canada/customEffects/out.jpg\")" -b "(gimp-quit 0)"

Hi, a bit off-topic, but just thought I would mention 2 important things in case they are useful to you or your readers:

1. if anyone is trying to use python fu to convert bmp to png, your dpi will be lost unless you make true the second-last arg in the save a png function. Took me ages to find this out.

2. if you are trying to use python fu to convert pdf to bmp/png etc with high quality, say 600dpi, you can use Popen function to call gswin32c.exe with appropriate options, and perform this conversion within your script that way. Use absolute paths for all program names and filenames. Cheers! (Also took "ages" to find this out...)

Add new comment