Thursday, April 02, 2009

More Script-Fu - Practice Makes Perfect

Yesterday I had to tweak a handful of buttons adding a green'ish stroke to just the right position behind each button. I could have turned up the music and done the boring work, or I could take a few minutes and write out a recipe for The Gimp to follow. As a programmer, how could I resist not going with the second option?

In an effort to get more Script-Fu code out in the wild, I'd like to share what I wrote below. It's not rocket science, but like anything else, it takes practice to script The Gimp, and the more examples you have the better. In this case, I found René Nyffenegger's collection of Script-Fu exercises to be especially helpful.

While I was coding this up, and checking out various examples, I had the following observations:

  • Holy smokes there's a lot of ugly Script-Fu code out there. Apparently there are more C programmers writing Script-Fu than Lisp programmers. If you're going to write Script-Fu, you should check out a scheme style guide so your code can be especially clear.
  • The Procedural Database Browser that comes with The Gimp allows you easy access for viewing functions available to you. Because the functions are named consistently after what's shown on the interface, it's not as hard as one might thing to find the relevant procedure you're looking for.
  • I'm pretty convinced that I should lower the threshold of pain I require before I write a script. Sure, if I have 100 images to process, writing a script is a no-brainer. But what about 5 images? I'm thinking the answer is yes. Besides the reasons mentioned below, writing more scripts should make it easier to do and pay off in the long run.
  • Writing Script-Fu procedures are a nice way of codifying how graphics on your project are made. For example, rather than wondering what colors, brushes and fonts were used to make site buttons, you can just read the code.
  • A Script-Fu procedure represents an investment in your project, and when new images come in and you can reapply the same procedure, the investment will pay off. This happened to me this morning, as the customer wanted to swap out two of the images I had processed last night. Rather than having a little bit of boring work ahead of me, I was able to process them in seconds.
  • gimp-message is your friend. With the Tools » Error Console open, you can print a message there. This allows you at least a basic debugging facility.
  • Educators take note - you can teach users all the critical lessons of programming (recursion, debugging, building abstractions) by using the Scheme built in The Gimp. Drawing and tweaking photos has to be a better way to dive into programming than say Fibonacci sequences. This is definitely not a new concept.

Enough talk, here's the code:

;; A helper function to convert lists to C arrays.
;; Lets you say:
;;  (list->array 'double (1.2 2.4 2.5 10.0))
;; and get a sane array.
(define (list->array type contents)
  (let loop ((array (cons-array 4 type)) (index 0) (contents contents))
    (cond ((null? contents) array)
           (aset array index (car contents))
           (loop array (+ 1 index) (cdr contents))))))

;; Process a button by drawing a stroked line of a particular
;; color at just the right position under the image.
(define (bs-process-button img drawable)
  ;; A sub procedure for peeling the button image from the background.
  ;; I just duplciate the main layer and use fuzzy select + cut to get rid of
  ;; the background. Return back the button layer.
  (define (make-button)
    (let ((button (car (gimp-layer-copy drawable TRUE))))     
      (gimp-image-add-layer img button 0)
      (gimp-drawable-set-visible button TRUE)
      (gimp-fuzzy-select button 1 1 10 CHANNEL-OP-ADD TRUE FALSE 0 TRUE)
      (gimp-edit-cut button)
      (gimp-selection-none img)
  ;; Another sub procedure. Make a fresh background. This is just a white layer.
  (define (make-bg)
    (let ((bg (car (gimp-layer-copy drawable TRUE))))     
      (gimp-image-add-layer img bg 1)
      (gimp-drawable-set-visible bg TRUE)
      (gimp-context-set-foreground '(255 255 255))
      (gimp-bucket-fill bg FG-BUCKET-FILL NORMAL-MODE 100 255 FALSE 0 0)
  ;; Yet another sub procedure, add the right color stroke to the right
  ;; position in the image. Easy to describe in code, a pain to draw just right
  ;; for every image.
  (define (add-stroke layer)
    (gimp-context-set-foreground '(71 161 133))
    (gimp-context-set-background '(71 161 133))
    (gimp-context-set-brush "Circle (01)")
    (let ((w (car (gimp-drawable-width layer)))
          (h (car (gimp-drawable-height layer))))
      (gimp-pencil layer 4 (list->array 'double (list 0 (/ h 2)  w (/ h 2))))))

  ;; The code to build the button. First order of business, start an undo block.
  ;; Then make our new layers, remove the original one, marge the existing one
  ;; and flush it all to the screen. I could have included saving in here too,
  ;; but I just left it up to me, the user, to hit Control + S.
  (gimp-image-undo-group-start img)
  (let ((button (make-button))
        (bg (make-bg)))
    (add-stroke bg)
    (gimp-image-remove-layer img drawable)
    (gimp-image-merge-visible-layers img CLIP-TO-IMAGE)
    (gimp-image-undo-group-end img)

;; The procedure is written, now it's time to tell
;; The Gimp about it.
 "Bs-Process-Button an image"
 "Run some transformations that I need for a project has asked for"
 "Ben Simon"
 "Copyright 2009, i2x"
 "April 1, 2009"
 SF-IMAGE "Image to process" 0
 SF-DRAWABLE "Image to process" 0)

;; and add a menu item. I add it into the top level of filters
;; because I'm lazy.
(script-fu-menu-register "bs-process-button" "<Image>/Filters")


  1. Hi Ben,

    Nice post. You are right on with your claim that there are probably more C-programmers writing script-fu then lispniks: dangling parens, ugly loops with many temporary variables and "theImage"s abound...

    And now for a shameless self-plug:

    Last year I have written an
    emacs mode for writing script-fu, which you might want to check out.

    Keep up the good posts!

  2. Niels -

    I guess it's particularly ironic that there's always a debate of whether or not lisp is out there in wild, and here it is, and yet the lispers aren't using it. Amazing.

    Thanks for the link - considering I do my development in emacs, this is huge.