Skip to content

Entries tagged "script-fu".

A new bug in an old script

Today while writing a new Script-fu for GIMP, I discovered that a script that I'd written many years ago had a bug in it. The Anti-crop script works quite well, however, I took a shortcut in writing it which can cause saved selections to be corrupted when the script is run.

The shortcut I took was to resize the image to match the size of the layer being "anti-cropped", thus simplifying all of my selection calculations by making the layer coordinates match the image coordinates. For example, instead of having to specify offset-x and (+ offset-x layer-width), I could just use 0 and width. After the script had finished with all of these calculations and processing, I restored the image back to its original size.

The problem is that when the image is resized, all of the channels will be cropped to the new size; and when the original image boundaries are restored, none of those cropped regions will be recovered -- those parts of the channel outside the boundaries of the processed layer will be filled with black. For saved selections (the most typical usage of channels), this means regions that were originally selected may become unselected. This is, of course, unacceptable and I have submitted a bug report.

I have not yet fixed the problem as I was in the middle of writing a new script at the time I discovered this flaw. Nonetheless, I will correct things soon and will be sure to keep this potential pitfall of resizing images in mind when writing scripts and plug-ins in the future.

Regards.

(mis-)Adventures in scripting

The GIMP script I briefly mentioned in yesterday's entry is completed, or at least it is now working as I need it to work. I am not particularly proud of the coding as it is both inefficient and inelegant. I suspect that I will completely re-write it at some point in the future.

The script, Expand Strip, is quite simple in concept: if a vertical or horizontal strip of a layer is selected, that region is doubled in size, with the rest of the layer being repositioned to accommodate the enlarged region. (The script is the converse of the Anti-crop script, which is intended for removing strips from a layer.)

Graphic 1

The steps to accomplish this strip expansion are fairly obvious:

  • Select the Top region, float the selection, and move it up by half the height of the Middle region.
  • Select the Bottom region, float the selection, and move it down by half the height of the Middle region.
  • Scale the Middle region to twice its original height.

When floated selections are anchored onto their original layers, they get cropped to the boundaries of that original layer. Therefore it is necessary to resize the layer to acommodate the repositioned Top and Bottom regions before performing the above steps. An alternative would be to convert the floated selections to new layers ('gimp-floating-sel-to-layer') and then later on merge them with the original (using the EXPAND-AS-NECESSARY option), however, that might require repositioning the new layers in the layerstack because those new layers are always placed at the top of the layerstack (not above the original layer).

When I scaled the Middle region, I originally used the 'gimp-drawable-transform-scale' procedure (actually, the -full version) but encountered some anomalous behavior with that function. When scaling a fully opaque region, under some circumstances (dependent upon the size of the original layer and the scale factor) the edges of the scaled result may become partially transparent. Fortunately, 'gimp-layer-scale' does not suffer from this malady.

I had encountered this problem with 'gimp-drawable-transform-scale' when writing my Extend Layer script. I did not report it as bug back then because I am not really sure it qualifies (should an edge pixel be interpolated with transparency or retain its original opacity?). I did mention it on the mailing list (or was it in IRC) but at the time the transform tools were going through a rather extensive re-write. For the present, I would recommend 'gimp-layer-scale-full' for all scripts which depend upon such pixel-accurate scaling and if you object to the behavior of the transform-scale procedure, bring it up on the GIMP developers mailing list.

The only other difficulties encountered in writing the script were keeping track of all of the changes in offsets and sizes, handling selections along the edges of the layer, and positioning things properly for the final result. I also wanted to have the script finish with the scaled region being selected so that it would be easy to re-run the script to quadruple the expansion, or more (a single-pixel strip could quickly be stretched to whatever size needed). I admit to having done a pretty poor job of handling this from a coding standpoint and the script is a prime candidate for a major re-write. Nonetheless, the script does what it is supposed to and so I will leave it as is for the present.

Regards.

Fixed a long-standing bug with my Anti-crop script

I have fixed the bug that I mentioned in a previous article. If you have the Anti-crop script installed on your system then it would be best if you were to upgrade to the latest version.

Regards.

Variable Arguments in Script-fu (Scheme)

I thought I'd briefly describe how one can define Script-fu procedures which accept a variable number of arguments and how those arguments are handled.

There are two basic ways of defining a new procedure in Scheme:

(define (somefunction x y z)
  ...
  )

or

(define somefunc
  (lambda (x y z)
    ...
    )
  )

The two forms are effectively equivalent; in both cases a lambda closure is created and then bound to a symbol. In fact the former is generally considered merely syntactic sugar for the latter. Nonetheless, there are potential advantages to the second ("lambda") form in certain situations.

One advantage obtains from the fact that operations can be inserted before the lambda is created. For example, one might check to see if a desired dependency is available.

(define somefunc
  (let ((align-down-missing? 
          (zero? (car (gimp-procedural-db-proc-exists 
                        "script-fu-sg-align-down" )))))
    (lambda (x y z)
      ...
      )
    )
  )

The test for the existence of the Align Down script only has to be performed once when the function is first defined. Only the lambda part will be executed when 'somefunc' is invoked, yet the variable representing the missing status will be retained forever. In Scheme terminology, the 'align-down-missing?' binding exists in the environment captured by the lambda (the environment which existed at the time the lambda was created), but not in the global environment. Of course, one could assign 'align-down-missing?' as a variable in the global environment (by definining it outside the 'somefunc' definition), but global variables tend to clutter things up and can introduce potential problems with namespace conflicts. There are other interesting aspects of this "capturing" of environments by a lambda binding, but I will leave those for a later discussion.

The main point I wish to cover is passing a variable number of arguments to procedures. This is implemented in Scheme by using a notation rather similar to that of dotted pairs. For example, if we have the definition

(define (somefunc . z)
  ...
  )

then within the definition, the variable 'z' will contain a list of all the arguments that were passed to the procedure. For example, if you invoked the above using "(somefunc 1 2 3 4)" then the variable 'z' would be assigned the list '(1 2 3 4). You can access individual elements of that list using the traditional 'car' and 'cdr' functions.

If you included parameters before the dot, for example,

(define (somefunc x y . z)
  ...
  )

These "explicit" parameters would be assigned their passed values and the rest of the arguments would be assigned (as a list) to 'z'. If you only provide the explicit arguments 'x' and 'y' then the list 'z' will be empty. Note that when calling your procedure, you must provide all explicit arguments.

It should be noted that while the first example above works fine for defining a procedure with no explicit arguments, this notation does not work in Scheme when the lambda form is employed. In other words, the following code will NOT work:

(define somefunc
  (lambda ( . z)
    ...
    )
  )

Instead, to employ the lambda form with no explicit arguments, omit the parentheses and period altogether, leaving only the identifier to which the list of arguments is assigned.

(define somefunc
  (lambda z
    ...
    )
  )

Variable arguments can provide a nice way of expanding the functionality of a procedure without increasing the complexity of the default behavior. Consider the following procedure whose main purpose is to duplicate an object -- whether that object be a number, a string, a character, or a list. If a second argument is supplied, that argument should be the number of copies that result (if omitted, this is assumed to be "2"). If a third argument is supplied (and has a non-false value), it indicates that cloning of characters or strings should result in a single, concatenated string (not a list).

; Make list comprising N copies of the item (default N = 2)
; ex:  (clone 9)   ==> (9 9)
;      (clone 9 4) ==> (9 9 9 9)
;      (clone "abc" 3) ==> ("abc" "abc" "abc")
; chars and strings accept a third arg which if not false,
; will result in a string instead of a list.      
; ex:  (clone #\a 4)     ==> (#\a #\a #\a #\a)
;      (clone #\a 4 #t)  ==> "aaaa"
;      (clone "ab" 3 #t) ==> "ababab"

(define (clone x . args)
  (let ((count (if (null? args)
                 2
                 (car args) ) ))
    (if (and (pair? args) 
             (pair? (cdr args))
             (cadr args) )
      (let loop ((result "")
                 (count count))
        (if (zero? count)
          result
          (if (char? x)
            (loop (string-append result (make-string 1 x)) (- count 1))
            (loop (string-append result x) (- count 1)) )))
      (let loop ((result '())
                 (count count) )
        (if (zero? count)
          (if (null? result)
            '()
            (reverse result) )
          (loop (cons x result) (- count 1)) )))))

Combining the functionality of variable arguments with Script-fu's dynamic typing of variables, it is possible to write some rather elegant procedures. For example,

(define (paint-xy drawable x y . args)
  (let ((color (cond
                 ((null? args) ;; color not specified, use FG
                   (list->vector (car (gimp-context-get-foreground))) )
                 ((vector? (car args)) ;; color is specified as a vector
                   (car args) )
                 ((pair? (car args)) ;; color is specified as a list
                   (list->vector (car args)) )
                 ((string? (car args)) ;; color specified using its CSS name
                   (let ((color '(0 0 0)))
                     (gimp-context-push)
                     (gimp-context-set-foreground (car args))
                     (set! color (list->vector (car (gimp-context-get-foreground))))
                     (gimp-context-pop)
                     color ))
                 (else ;; red, green, and blue values provided inline
                   (list->vector args) ))))
     (gimp-drawable-set-pixel drawable x y (vector-length color) color) ))

This single function could be invoked in any of the following ways:

(paint-xy layer x y)                ;; using current foreground color setting
(paint-xy layer x y #(255 255 255)) ;; white
(paint-xy layer x y '(0 255 0))      ;; green
(paint-xy layer x y "aliceblue"))   ;; CSS named color
(paint-xy layer x y 255 165 0)      ;; orange

For those interested, the color names recognized by GIMP are listed in the gimprgb-parse.c file in the source tree.

While it is somewhat rare for GIMP scripts to attain the level of complexity where the full power of the Scheme programming language can be exploited, it is nonetheless useful to learn of its capabilities.

Regards.

Script-fu/Tinyscheme Macros

The Tinyscheme interpreter upon which GIMP Script-fu relies is to a large degree compliant with the R5RS specification, surprisingly so, given its small memory footprint (<100Kb). One of the few areas where it deviates from the standard is with regard to macros -- Tinyscheme does not implement the "syntax-rules" pattern-matching functionality of R5RS, nor does it directly implement "hygienic" macros. I will postpone any discussion of these two concepts until after we've addressed the macro facilities that Tinyscheme(/Script-fu) does provide.

What is a "macro"?

In Scheme, it is a general rule that all arguments get evaluated before they are passed to a procedure. For example, in the following statement, the mathematical expressions "(- 14 4)" and "(* 3 5)" each get evaluated (to "10" and "15", respectively) before being passed to the addition function, which in turn gets evaluated (to "25") and passed to the display procedure.

(display (+ (- 14 4) (* 3 5)))

This general behavior is quite adequate for most Scheme programming, however, it is often convenient to NOT evaluate the arguments being passed, but instead leave the decision whether or not to evaluate the arguments to the processing within the procedure. Consider the statement

(set! x 10)

In this situation, we do not want the first argument to be evaluated (i.e., we don't want to access the original value assigned to 'x'), but instead want to treat it as a symbol.

(if (< x 0) (display "X is a negative number"))

Here we don't want the message to be displayed before the 'if' routine is invoked -- we only want it to be displayed after the 'if' code has evaluated the expression "(< x 0)" and found it to be true. In other words, we don't want to evaluate the third argument until after the second argument has been evaluated and ensured to be true.

Both of these operations, "set!" and "if", are core to the Scheme language but they demonstrate how evaluation of arguments can be postponed, or even skipped altogether. They are not properly called "procedures" or "functions" but instead are called "special forms" because they follow special evaluation rules (i.e., their arguments are not always evaluated).

Scheme macros permit you the programmer to define your own "special forms" and thereby increase the expressiveness and flexibility of your programs. As is the case with variable arguments discussed in my previous article, you will rarely find it necessary to use macros when writing GIMP scripts, however, it is a powerful technique that can enhance your scripting skills.

The 'macro' statement

The basic functionality of macros in Tinyscheme are implemented with a 'macro' operator. I have not found very much documentation on this, but through examination of the source code and some experimentation have figured out its behavior (at least I think so).

A macro definition has the basic following form:

(macro (NAME FORM)
  *BODY* 
  )

When a macro call is encountered (while reading your script), the BODY of the macro is executed (evaluated) and the output (a Scheme expression) substituted for the macro invocation. The entire macro invocation expression is available with the body as the variable "FORM".

(macro (foo form)
  (list 'quote form)
  )
(foo (* (+ 5 2) 3)) ==> (foo (* (+ 5 2) 3))

Note that if we'd defined 'foo' as a procedure then the formula would have been evaluated (to "21") before the procedure code was called. With a macro, the arguments are passed directly without any evaluation taking place.

Also, notice that the entire expression, including the macro name, is passed to the body. This threw me at first because I would've expected just the unevaluated arguments to be passed. It ends up that there is a good reason for doing this (which comes into play when creating nested or recursive macros), but by and large most macros will just skip over the macro name and only use the arguments.

In addition, all of the arguments are included in the 'form' variable. There is no need to specify separate names for each expected argument when defining your macro. In fact, if you were to define a macro with "(macro (foo x y z)" it would generate an error everytime it was invoked. (You can apparently leave out the FORM variable when defining a macro, but that would be useless.)

The beauty of Scheme macros arises from the fact that all code is in the form of symbolic expressions. In other words, any block of code is just a list, and you can access any part of that code using just the 'car' and 'cdr' functions (no need for complicated parsing, token evaluation, lookaheads, or checking of indentation levels). The job of a body of a macro is to examine the code and produce new code by re-arranging and/or adding to those elements. This new code is, of course, a list; and so the output of the body of a macro is a list.

An Example

Most new-comers to Scheme struggle a little bit with the syntax of its IF statement.

(if predicate
    consequent
    alternative )

It is not uncommon to forget that the 'consequent' (what to do if the 'predicate' test is true) is a single expression. If you wish to do more than one thing then you generally need to combine all of your actions into a single "block", otherwise only the first action will be performed and the second one treated as the 'alternative' (what to do if the 'predicate' is false).

Using Tinyscheme's macro facility, we can easily introduce a new conditional which allows multiple expressions to be evaluated (i.e., actions to be taken) when the 'predicate' is true.

(macro (when form)
  (list 'if (cadr form) 
            (cons 'begin (cddr form)) ))

We now have a new command (actually, Tinyscheme already offers this macro, but it serves as a fine example of a simple macro).

(when (< x 0)
  (display "Negative numbers are not allowed")
  (newline)
  (display "Please enter a number greater than or equal to zero")
  (newline) )

Quasiquoting

Scheme offers a very powerful facility called "quasiquoting" for creating lists that is especially useful for creating macros -- because the output of a macro body is expected to be a list. Quasiquoting is quite well-documented and so I won't go into too much detail about it, but the general premise is that code that has been quasiquoted will by default not be evaluated (just as with quoting), however, you can optionally 'unquote' parts of the expression.

This form of list generation generally results in macros that are much more recognizable with regard to the code being generated. For example, the body of the preceding 'when' macro would appear as follows in quasiquote notation:

  `(if ,(cadr form)
     (begin
       ,@(cddr form) ))

NOTE: this is precisely how the 'when' macro is defined in GIMP's "script-fu.init" file.

What about GIMP?

Just as in the case of variable number of arguments, it will rarely be worthwhile to create macros for typical Script-fus. The problem is that while macros can make your code more readable, the reader needs to learn what the macro does. Unless your macro is going to be made available globally to all Script-fus (and this introduces the potential of namespace collisions), it is usually just better to "brute force" your code.

Nonetheless, I will present a few examples of macros that might assist in making Script-fu code more readable, or at least a bit more "Scheme-y". (Maybe in the future, some useful macros can be introduced into the default GIMP installation so that script writing will become easier.)

;; (with-context
;;   *BODY* ) 
;
(macro (with-context form)
  `(begin
     (gimp-context-push)
     ,@(cdr form)
     (gimp-context-pop) ))

;; (with-undo image
;;   *BODY* )
;
(macro (with-undo form)
  `(begin
      (gimp-image-undo-group-start ,(cadr form))
      ,@(cddr form)
      (gimp-image-undo-group-end ,(cadr form)) ))

Wrap-up

This article is already getting overly long so I will leave to future installments any further coverage of Tinyscheme macros and the potential they present to improving GIMP scripting.

Regards.

More On Script-fu/Tinyscheme Macros

If you read my previous article on writing Script-fu macros, you probably were somewhat disappointed with the rather peculiar syntax of Tinyscheme's 'macro' form. But despair not, things aren't as bad as they seem.

In addition to the 'macro' form discussed in the previous article, GIMP's Script-fu also supports a 'define-macro' statement, which combined with Scheme's quasiquoting permits writing macros that are both useful and readable (and even largely hygienic, though not to the degree that Scheme purists might desire).

The basic form for 'define-macro' is:

(define-macro (NAME PARAM1 PARAM2 . FORM)
  *BODY*
  )

This allows you to provide meaningful names to your macro's parameters and avoid having to use such constructs as 'caddr' when accessing them in the BODY.

Note that if no parameters are present in the definition, 'define-macro' basically devolves into a simple 'macro' definition (but with slightly different handling of the FORM parameter). This can be seen by contrasting the 'with-context' definition from the previous article:

(macro (with-context form)
  `(begin
     (gimp-context-push)
     ,@(cdr form)
     (gimp-context-pop) ))

with the equivalent 'define-macro' implementation (note that we no longer take the 'cdr' because the macro's name is not part of this FORM):

(define-macro (with-context . form)
  `(begin
     (gimp-context-push)
     ,@form
     (gimp-context-pop) ))

Here is the 'with-undo' macro from the previous article, written using 'define-macro':

(define-macro (with-undo image . form)
  `(begin
      (gimp-image-undo-group-start ,image)
      ,@form
      (gimp-image-undo-group-end ,image) ))

Here is another example which "automatically" saves and restores the selection:

(define-macro (with-selection image . form)
  `(let ((channel (car (gimp-selection-save ,image))))
    ,@form
    (gimp-selection-load channel)
    (gimp-image-remove-channel ,image channel) ))      

This would permit you to do something such as a filling an entire layer with red, even though only part of it is selected; all the while keeping the original context and selection, and being undo-able:

(with-undo image
  (with-selection image
    (gimp-selection-none image)
    (with-context
      (gimp-context-set-foreground '(255 0 0))
      (gimp-edit-fill layer FOREGROUND) )))

When I wrote my original article about TinyScheme macros, I was thinking that they wouldn't be that useful for GIMP Scripts because of the potential naming conflicts if the macros appeared in multiple scripts; however, I was pleasantly surprised to discover that macros can be defined locally within a procedure. Considering how much more readable code can become, I think I will be making much greater use of macros in my future scripts.

Regards.

Using Script-fu scripts with LiVES

This week I spent some time with the LiVES video editor, mainly trying to interface LiVES with GIMP's Script-fu capabilities. Now LiVES already has a powerful scripting/plug-in capability, however, it is mainly targeted at invocations of command line programs such as ImageMagick or its fraternal twin GraphicsMagick.

The problem with using this approach with GIMP is that GIMP startup times are typically about 5-10 seconds, even on faster systems. Processing a thousand frames of video at ten seconds per frame would tax even my patience.

Script-fu Server

Fortunately, GIMP provides a network interface through which commands and scripts can be sent so this startup time can be avoided. This interface is known as "Script-fu server" and can be found in the GIMP menus under "Filters->Script-fu". Once the Script-fu server is running, it waits for messages containing Script-fu code on TCP port 10008 (by default).

The message starts with a 3-byte header: an ASCII 'G' followed by a 16-bit 'length' value (in big endian order). 'length' bytes of code follow the message header. I am not sure if this protocol can handle UTF-8 encoding, so for now am planning on restricting myself to plain ASCII (LiVES currently suggests that RFX scripts limit themselves to the C locale, which suggests a similar limitation).

The Script-fu server will respond with a similarly encoded response after the code has been executed. This response merely states whether execution of the code completed successfully.

LiVES RFX

Now on the LiVES side there is, as already mentioned, support for adding rendered and/or realtime effects by way of RFX scripts. My mission therefore, was to write a RFX script which communicated with GIMP's Script-fu server. This ended up being surprisingly trivial to do, my only hindrances were my lack of hardly any Perl programming experience and a dearth of coverage of my project in the RFX documentation.

The RFX documentation is actually very good, it just presumes command line programs will provide the processing. There is even a nice utility called RFX Builder that greatly simplifies the task of writing a LiVES plug-in (documentation for RFX Builder in ODT).

RFX Builder

To start using RFX Builder, open up the "New Test RFX Script" dialog from the "Advanced->RFX Effects, Tools, and Utilities" menu. Fill in (at a minimum) the Name, Author, Menu Text, Action Text fields. For my first Script-fu test RFX, I created a simple "Invert" script (that merely negates the color of the selected video).

Screenshot

My Test Script

My script does not need any parameters, and I am postponing learning how to pass them as a future project (it would seem fairly trivial), so I ignored all of those settings, and focused mainly on the Pre-loop and Loop code editors.

Pre-Loop (setup)

The Pre-loop code gets executed once before any frames are processed (whereas the Loop code gets executed once for each frame in the current selected range of frames). It is very fortunate that this capability is available, since I wanted my RFX to start a Script-fu server if one wasn't already running.

Here is the code I came up with. While I've done some programming in just about every language there is, this is my first foray into Perl and it is quite possible that there are betters ways to accomplish this simple task of testing if a server is available and if not, starting GIMP with a new server. (Since GIMP takes a few seconds to load, I then have to wait for the server to be available.)

use IO::Socket; 
use Text::Balanced;
use Time::HiRes qw( usleep );

$sf_socket = 10008; 

$start_gimp_command= qq{ gimp -b "(plug-in-script-fu-server 1 $sf_socket \\\"\\\")" & };
$sock = new IO::Socket::INET ( PeerAddr => 'localhost', 
                               PeerPort => '10008', 
                               Proto => 'tcp', 
                               );
if ( not defined $sock ) {
  system ( $start_gimp_command );
  while (not defined $sock) {
    sleep (1);
    $sock = new IO::Socket::INET ( PeerAddr => 'localhost', 
                                   PeerPort => '10008', 
                               Proto => 'tcp', 
                               );
    };
  };

This Pre-loop code will be the same for any RFX/Script-fu scripts, though there may be some additional code to pre-process parameters for scripts which accept those.

Loop Code (executed once for each frame)

Once the Pre-loop code has connected to the server, LiVES will execute the Loop code once for each frame in the selected range of the current clip. Each time through, the code is expected to load the original frame, process it, and then save it (with ".mgk" extension).

LiVES provides several variables ($in, $out, $frame, etc.) that are helpful to the script writer, and I found it useful to use the quoting abilities offered by the Perl Text::Balanced module so that some of these variables were available to the Script-fu code. I also assigned this code to a string variable so that its length could be determined and an appropriate TCP message header created.

Here is the code I used to invert the colors in the frame. Most of it is boilerplate that would be used by any RFX/Script-fu -- only the middle part containing '(gimp-invert layer)' would be changed.

$code= qq{
  (let* ((input-file (string-append "$curtmpdir" DIR-SEPARATOR "$in"))
         (output-file (string-append "$curtmpdir" DIR-SEPARATOR "$out" "$img_ext"))
         (image (car (gimp-file-load RUN-NONINTERACTIVE input-file input-file)))
         (layer (car (gimp-image-get-active-layer image))) )

    (gimp-invert layer)

    (gimp-file-save RUN-NONINTERACTIVE image layer output-file output-file)
    (gimp-image-delete image) )
  };

First note that LiVES RFX normally works by changing to the directory containing the input files, so the '$in' variable contains only the basename of the file; however, since we are using a server connection, GIMP needs the fully qualified path in order to open the input file and to save the output. Fortunately, the directory containing files is specified by the '$curtmpdir' variable. The '$curtmpdir' doesn't appear in the documentation, so it took me some time to find it in the LiVES source code.

I also ran into a slight problem with the naming of the output file. The 'gimp-file-save' procedure only saves to files whose names have a recognized file extension. The RFX variable '$out' however, uses the ".mgk" extension (for example, if $in is "000003.png" then $out would be "$000003.mgk"). While I could use GIMP's 'file-png-save' or 'file-jpg-save' and then use whatever extension I wanted, it was easier to just append appropriate extension to the '$out' string. This name change later has to be undone, which I will get in a minute.

Sending The Message

Now that our Script-fu code is assigned to a variable, we need to affix a header to the code and send the resulting message to the Script-fu server. My original approach to doing this was to use Perl "print $sock" commands to send this message. I then wrote a Perl loop that waited for the output file to be created. This method worked fine, even though I was ignoring the response received from the server.

However, I later came across Alan Stewart's Script-fu Perl client which included some code that actually monitored the response messages; so I basically copied that (Mr Stewart kindly released his code under GNU's General Public License).

Here is the resulting Perl code that should be included after the Script-fu string has been assigned.

my $len = length ($code);
die "ERROR: script is too long for one server request: $len > 65535\n" if $len > 65535;
# send script to GIMP
my $header = pack( 'an', 'G', $len);
syswrite( $sock, $_ ) for ($header, $code);

# wait for response
my $rin = '';
vec( $rin, fileno($sock), 1 ) = 1;
select( $rin,  undef, undef, undef );    # wait (forever) for response start
select( undef, undef, undef, .1 );       # wait a bit for response to finish
                                         #  increase wait if INVALID/INCOMPLETE RESPONSE occurs

# response
my $len = sysread( $sock, $header, 4 ) or die "INVALID RESPONSE: empty response\n";
( $len == 4 and $header =~ /^G/ ) or die "INVALID RESPONSE: bad header\n";
my $status;
my ( $status, $len ) = unpack( 'xCn', $header );
my $response;
( sysread( $sock, $response, $len ) == $len ) or die "INCOMPLETE RESPONSE: $response\n";

# exit if response is not "Success"
if ( $status and $response =~ /^Error: Success\n/i ) {
  die "UNSUCCESSFUL EXECUTION: Script-fu error\n";
  }
rename "$out" . "$img_ext", "$out";

Again, like the Pre-loop code, this code is all pretty much boiler plate and should be included unaltered in the Loop code of any RFX/Script-fu that is created. Also, note that the last line of the code renames the temporary output file (e.g., "000003.mgk.png") to match the expectation of LiVES RFX processor.

Building And Running The Script

After you've successfully edited the Pre-loop and Loop code, save and build the script by pressing OK in the RFX Builder dialog. Next, you build the script by running the "Rebuild all RFX plugins" command in the "Advanced->RFX Effects, Tools, and Utilities" menu.

You can then run the script on the selected range of a clip by choosing the "Run Test Rendered Effect/Tool/Generator" command from the "Advanced->RFX Effects, Tools, and Utilities" menu. Start with a fairly short selected range (~100 frames or so), since the process can take some time.

After you've fully tested your plugin, you can add it permanently to LiVES (the details of which I will cover in the future).

Speed

Though I did not do too much in the way of speed benchmarking, my preliminary experiences left me pleasantly surprised to find that my simple RFX/Script-fu "Invert" script compared somewhat favorably to the LiVES built-in "Negate" function (which, like many of the LiVES effects, is based upon ImageMagick's 'convert' utility).

On my fairly puny 2 GHz dual-core machine, the script processed about two frames per second using PNG's highest compression level, but this increased to over five FPS with a PNG compression factor of "3". The lower compression level will result in larger files, but only by about 15%.

Conclusion

This method of adding visual effects opens up a wide range of opportunities for accessing all of GIMP's powerful capabilities from within LiVES. While LiVES approach to video editing -- wherein individual frames are handled as image files -- is not particular fast, it offers a great benefit with regard to flexibility and intuitiveness. Interfacing LiVES RFX scripts with GIMP's scripting capabilities is a powerful means to maximize this benefit.

This is still a work in progress and I hope to improve things as I learn more about Perl and the LiVES RFX system. Also, based on the benefit of dropping PNG compression, I will be wanting to switch the Script-fu code from using 'gimp-file-save' to 'file-png-save' so that the compression can be explicitly specified ('gimp-file-save' uses the default value that was set in GIMP).

Regards.

RFX-GIMP (or using Script-fu with LiVES continued)

I've made some decent progress with running GIMP Script-fu scripts from the LiVES video editor (see my previous article for background).

I've offloaded much of the "loop code" to the "pre-loop code" section, such that the loop code of the Invert script can be written as follows.

&rfx_sendmsg (
  qq{
    (let* ((input-file (string-append "$curtmpdir" DIR-SEPARATOR "$in"))
           (image (car (gimp-file-load RUN-NONINTERACTIVE input-file input-file)))
           (layer (car (gimp-image-get-active-layer image))) )

      (gimp-invert layer)

      (rfx-save-frame image "$out") )
    }
  );

Everything but the 'gimp-invert' line is basically boilerplate code that will be used in the loop code for most all RFX-GIMP effects (minor modifications may be needed for handling parameters, or for transition and generator effects).

The 'rfx-save-frame' procedure handles saving the frame to either a PNG or a JPG file (depending upon what LiVES asks) and also removes the image. Any new entities (images, layers, masks, buffers, etc) created by loop code should be removed or else GIMP memory usage will grow each time a frame is processed (and one might very well expect thousands of frames to be processed).

I won't go into greater detail here because I have started an "official" RFX-GIMP project where I will be tracking progress and adding documentation.

Regards.

RFX-GIMP Part Deux

I've made pretty good progress with my RFX-GIMP project. The biggest improvement is that now GIMP will be started as a background process so as not to clutter up the user interface.

GIMP - A 20Mb Daemon

I had originally intended to have the choice between a foreground versus background GIMP selected through the use of an environment variable, RFXGIMP_FLAGS. No value would open GIMP normally, while setting the value to "-i" would mean GIMP would open in the background.

This approach didn't work out as well as I'd thought because if GIMP was already running, then an invocation with a line such as "gimp -b '...'" did not result in the batch code being executed -- and since using the --new-instance/-n flag would result in a second instance of GIMP anyway, it might as well run in the background. Besides, I originally had it open in the foreground mainly for debugging purposes, but now that things appear to be working fairly well, a GIMP "daemon" seemed the better overall approach.

Logging

I also removed support for the RFXGIMP_LOGFILE environment variable that would enable Script-fu logging and specify the file path where it should be saved. Again, this was mainly done for debugging purposes, and once the software was in suitable shape these log files weren't very instructive. Logging also seemed to have a just noticeable effect on execution speed, slowing things down by about 10%.

Logging -- and running GIMP in the foreground -- are both still available if you start the Script-fu server from within a running GIMP.

Forking

When I wanted the pre-loop Perl code (being executed by LiVES) to start GIMP, I was presented with two options: 1) start GIMP and wait for it to finish, or 2) start GIMP and detach it from LiVES. Obviously, the first choice would not work; I of course wanted GIMP to keep running for as long as LiVES was using it.

Running GIMP detached was simple enough; I just needed to include an "&" on the end of the command line being passed to Perl's 'system()' function. However, this was not ideal because there was no way to then know the process ID of the GIMP process, and I wanted to know this so that at some time in the future, the background instance of GIMP could be terminated (for example, after LiVES has been closed).

The solution was to use the Unix forking capability. When a process is forked, a second copy of the running program is created, with all the same variable values and executing at the very same point in the code (the return point of the 'fork' command). The only difference between these two processes is the value being returned by 'fork'.

The value returned by fork to the cloned copy of the process is zero. The value returned to the original process is the process ID of the cloned process. Yay! that is what I needed to know.

Process Groups

Forking was simple enough, The original process continued as normal, while the cloned process saves its own process ID to a file ("$tmpdir/rfxgimp.pid") for later access, starts the GIMP, and goes into hibernation (waking up when messages have been sent to the Script-fu server).

However, knowing the process ID of the GIMP process was not enough. The problem was that GIMP started some of its own detached processes, namely its own Script-fu processor and our Script-fu server. Terminating the GIMP process fails to kill these detached processes that were spawned by GIMP.

Unix provides a way for grouping processes such that killing the PID of the group terminates all of the process in the group; however, our GIMP process, and all of its child processes, belong to the same group as the LiVES process. If I terminated GIMP then LiVES would shutdown.

The solution to this was rather trivial: I just needed to have the child process re-assign itself to a new group before starting GIMP. This was done with the 'setpgid' function. The value of the PID of this new process group is what is saved in $tmpdir (it is actually the same value as the spawned process).

I Am Not A Zombie (I'm a feature)

Now that GIMP is running in the background as a detached process (i.e., a daemon), the question arises as to when is the appropriate time to terminate it. The whole point of using Script-fu server is so that GIMP didn't have to be restarted for each frame being processed, but the GIMP "daemon" does take up some bit of memory (about 10-20Mb depending). So should it be shutdown down when it is not being used?

Doing so is, of course, possible; we could have the RFX Post-loop code shut down the process after the filter has finished. But this would mean the next time an RFX-GIMP filter was run there'd be a five second or so delay while GIMP was restarted.

The best place to kill the GIMP process would be when LiVES shut down; but that would require modifying LiVES itself. Maybe at some point in the future the LiVES developers can be petitioned to add this operation to their exit code, but for now the spawned GIMP process will be left sleeping in the background until killed explicitly by the user, or the system is rebooted.

To kill the GIMP process explicitly, you need to know where your user's LiVES $tmpdir is. By default this directory is named "livestmp" and is located in the user's home directory (the actual value can be found in the $HOME/.lives configuration file).

Once you know where $tmpdir is, you can terminate the GIMP daemon with the 'kill' command as follows:

kill -TERM -- -$(cat $HOME/livestmp/rfxgimp.pid)

The double-hyphen is required so that the minus sign in front of the process ID is not interpreted as a command line switch. The minus sign in front of the process ID indicates that the entire process group should be killed, not just the process.

Of course, there's nothing wrong with just leaving the GIMP process running. In fact, if it is left running then the next time you use LiVES then you will not wait for GIMP to initialize when you first run an RFX-GIMP script (it's a feature, not a bug). After all, 20Mb is not really that much to lose if you have giga-bytes of memory.

Regards.

Command Line Batch Processing With GIMP

If you save the following macro to a file in your scripts directory (I use the name "with-files.scm"), it will greatly simplify using GIMP from the command line to process a bunch of files.

(define-macro (with-files pattern . body)
  (let ((loop (gensym))
        (filenames (gensym))
        (filename (gensym)) )
    `(begin
       (let ,loop ((,filenames (cadr (file-glob ,pattern 1))))
         (unless (null? ,filenames)
           (let* ((,filename (car ,filenames))
                  (image (catch #f (car (gimp-file-load RUN-NONINTERACTIVE
                                                        ,filename 
                                                        ,filename ))))
                  (layer (if image (car (gimp-image-get-active-layer image)) #f))
                  (basename (unbreakupstr (butlast (strbreakup ,filename ".")) ".")) )
             (when image
               ,@body
               (gimp-image-delete image) ))
           (,loop (cdr ,filenames)) ))
       (gimp-quit 0) )))

This macro allows you to invoke GIMP with a command line such as:

gimp -i -b '(with-files "*.png" <body>)'

where body is the code that handles whatever processing you want to perform on the files. There are three variables that are available within the body: basename, image, and layer. The basename is the name of the file with its extension removed, while the other two variables are self-explanatory. You basically write your code as though it were processing a single 'image' and the 'with-files' macro applies it to all of the files matching the pattern.

For example, if I want to invert the colors of all of the PNG files in my home directory, I would use the following command line:

gimp -i -b '(with-files "/home/saul/*.png" (gimp-invert layer) (gimp-file-save 1 image layer (string-append basename ".png") (string-append basename ".png")))'

For more involved processing tasks, using a HERE document might make things easier. The following commands will create JPEG thumbnails no larger than 120x120 pixels for every PNG file in my home directory (note that JPEG files need to be flattened RGB images). This example also shows how an environment variable can be used to specify the thumbnail size.

export THUMBNAILSIZE=120
gimp -b - <<HERE
(with-files "/home/saul/*.png" 
  (let* ((h (car (gimp-image-height image)))
         (w (car (gimp-image-width image)))
         (aspect (/ h w)) )
    (unless (= (car (gimp-image-base-type image)) RGB)
      (gimp-image-convert-rgb image) )
    (if (> w h)
      (gimp-image-scale image $THUMBNAILSIZE (* aspect $THUMBNAILSIZE))
      (gimp-image-scale image (/ $THUMBNAILSIZE aspect) $THUMBNAILSIZE) )
    (set! layer (car (gimp-image-flatten image)))
    (gimp-file-save RUN-NONINTERACTIVE 
                    image 
                    layer 
                    (string-append basename ".jpg") 
                    (string-append basename ".jpg") )))
HERE

Regards.