GIMP's pythonfu - unbound variable error

tutorial python gimp cli pythonfu

tl;dr

I was messing around with GIMP’s batch mode and I met with this unbound variable error. I tried to fix it, and went round and round before being able to solve it. This is the story about how I finally cracked the puzzle in the end. You can skip to the solution here.

omg… photo editing is tedious

I love to take photos, but when it comes to organizing and editing tons of photos from photo shoots, it gets a little tedious, especially with my opensource software combination: xnview (folder based photo organizer) and gimp (open source version of photoshop).

This is where professional photo management have this feature that my current xnview+gimp combo just cannot match: batch photo editing.

In Adobe’s lightroom, it possible to simply select a group of photos and apply a filter to all of them. This comes in very useful for things like white balance correction and mass brightening up of images.

When you have tons of photographs all requiring same photo enhancements, the batch editing feature becomes sorely missed.

Well, gimp is open source, I bet I could probably rig it to do something like this, couldn’t I?

what? You can do this?

According to the top few Google search results, you can create new plugins and scripts for gimp using its built in scripting environment. And the best part, you can now write your plugins in Python! (I saw the Scheme plugin at first, I was like AWWW… Then when i found PythonFu, I was like AWWW YEAH !!!)

GIMP, being open source and all, requires the help of cash-strapped developers/photographers all around the world (now I consider myself a member) to help write plugins so that it can level up it’s awesomeness and not let Photoshop catch up.

What the developers use is a Python module gimpfu that gives access to all of GIMP’s functions (from dialog to functions, brushs to gradients, it’s all there).

You mean you can use gimp like an engine to do whatever you want to the photographs!?!? Oh… This will be fun…

so… let’s get started

The first thing I did was to look at some of the plugins that others have written. It wasn’t hard to find one. Go on to gimp’s plugin registry and pick a random one… Do get one that is written in Python though (look under Python Fu), the Scheme plugins are a little confusing if you are not familiar with the Scheme syntax.

The basic idea behind the structure of the code is simple: you make a plugin, you register it into gimp, then you can access it via pdb.

If you are starting off, here are two tips that will save you a few hours of head scratching while you are scrolling through Google and StackOverflow. (read on to know more)

  1. You can view all the available functions under the Procedure Browsers (Help > Procedure Browser). It is like the documentation for using gimp plugins.
  2. To test out different functions, use the Console (Filters > Python Fu > Console).

what is this unbound error…

So everything was going well and fine. The function seemed to work like it should, I followed the registration parameters one by one. There seemed to be no problem. Let me just try running it on the command line. So I went: gimp -b '(my_function)'

And all I got was this error: unbound variable error

didn’t I have this working just now?

What is this unbound variable thing… Didn’t I register the function correctly? I looked at my messy and hackish code… “Maybe I should try a simple hello world program for this,” I thought to myself. So I went Googling again…

I found some basic scripts examples, which are basically just showing you how to register plugins into the gimp system. It wasn’t really helpful in solving my problems, but you could take a look here (or the github version) if you need some help with the syntax (download the files under attachments).

I spent some time pouring through the examples. I basically rewrote a hello world plugin for GIMP, and it took me quite a bit of time to check (and double check) through all the parameters to get it right. (Yeah… the GIMP plugin system is quite a tedious one)

And so I tried it again… And still it didn’t work… ARGHH!?!?

I really need some developer guide for this

For sample script, I tried running it from the GIMP GUI, it works. The dialog pops out, everything runs fine. But why can’t I get it to run in the “batch command” mode that GIMP promises me? Maybe I need some guide for GIMP batch mode, maybe there is a trick to it…

So I went reading through the official documentation and guides (bad idea, they weren’t very helpful… GIMP is really lacking in this area). I did an intensive search that taught me many different things: scriptfu (the Scheme plugin system), the gimpfu python library, the different components (pdb, etc). Then I found this tutorial from IBM DeveloperWorks.

In the tutorial, there is this section where they showed how they called the function using batch command mode. Then everything just clicked: all that I have read about the plugin system, Scheme language and ScriptFu.

And I tested out if I was right, I ran the following command in my terminal: gimp -i -b '(python-fu-my-function 0 0)' -b '(gimp-quit 0)'

Lo and behold… it works!

so what’s going on… what’s this unbound variable thing

So this is how GIMP’s plugin system works.

When you write your plugin in python, you use the gimpfu library (which is essentially a module that links to the GIMP existing functions) to register the plugin, so that the system will know that it needs to run the particular function when the plugin is called. (no idea what I am talking about? try reading some sample plugins, you will get it)

All plugins will appears in the Procedure Browser (accessible through Help > Procedure Browser). So you just have to check to make sure that yours is registered with all the right parameters. Parameters? Yes, that’s the next part I will be covering.

In the register function, you will notice that the last few parameters (one of those arrays ones) is supposed to be for the variable name, types and description. All these will be displayed for the documentation purposes in the Procedure Browser (view more on the available datatypes in the documentation). This is where you define all the variables that you will need to run your plugin, it can be an IMAGE, an PB_INT8 (if you need a numerical value), anything.

Where does all this comes in? When you want to call your plugin function, you need to put in the right number of variables or there will be an error. (yes, the unbound variable error)

Also, when you register your PythonFu plugin in GIMP’s system, it will automatically prefix python-fu in front of whatever command name you gave it. And because GIMP uses Scheme syntax to call the functions, your plugin will adopt the “kebab naming convention”. This means that my_function will now become python-fu-my-function. (Just search it up in the Procedure Browser to verify).

So this is the function I ran: gimp -i -b '(python-fu-my-function 0 0)' -b '(gimp-quit 0)'. What did the two zeros behind the function name stands for? What if my function requires no parameters? (This is where the fun part of the GIMP system comes in)

Not all plugins are equal. When you register your plugin, there is this parameter menu-path. For most plugins are image plugins (you run them on an image), so they will be registered under <Image>/whatever/menu/path. What that will do is prepend your parameter list with 2 arguements: an Image and a Drawable. (interesting fact: if you register under <Toolbox>, nothing will be passed into your function. Well, I will let you figure that one out on your own)

So if you don’t really have a need for it? No problem, just set it to 0 when you are running the function in batch mode so that you still provide the right number of parameters.

end of it all?

So that is the end to it. So why am I writing this post? Because when I am searching for a solution to my unbound variable error, I found this stack overflow thread.

“So… there is someone who didn’t manage to solve this problem…” I managed to get a quick answer out, but it’s not really my style of writing so I thought to wrote a whole post on how I managed to find the solution instead and here we are.

Hope this helps :)