Idiomdrottning’s homepage

Quick script for stacking images into an .ora

What is it?

When you have some images and you just want to throw them together as layers in an ora file, one of the easiest ways is to hit “open as layers” in the gimp image program and then export to .ora. But if you do this a lot you might want a fast command line program to do this, and that’s what ora_stack is.

It’s nothing very polished, it’s just something I threw together tonight, but it’s a start.

Getting it and installing it

It’s made in Chicken Scheme which means it’s a bit of a hassle to compile and run if you don’t usually use it. But it’s what it is.

sudo apt-get install chicken
sudo chicken-install imlib2 s sxml-serializer srfi-42 shell
git clone https://idiomdrottning.org/ora_stack
cd ora_stack
csc -O5 ora_stack.scm
sudo cp ora_stack /usr/local/bin

Using it

For example, type

ora_stack top_image.png middle-file2.png bottom-file3.png output.ora

in your shell. I’m probably going to call this from shell scripts and aliases if there are some common images or templates I like to stick in there.

Walking through the source code

(require-extension imlib2 s sxml-serializer (srfi 1 42) shell posix)

Just some libraries we use. I love srfi-42 and tend to use it for everything kinda gratuitously, even when a simple for-each or map would do the trick.

Then we parse the calling command line:

(define output-stem (s-chop-suffix ".ora" (last (argv))))
(define input-filenames (drop-right (cdr (argv)) 1))

Then some guard clauses. This version doesn’t check if the input filenames refer to real image files, let alone make sure they’re png:s. But it does attempt to try to avoid clobbering what you’ve got. If you do want clobbering (maybe you update your images often), just remove the clause and recompile.

(unless (<  2 (length (argv))) (error "Usage: ora_stack input-filenames... output-file"))
(unless (null? (glob output-stem)) (error "Exiting to prevent clobbering working directory"))
(unless (null? (glob (string-append output-stem ".ora"))) (error "Exiting to prevent clobbering output file"))

I’ll set the frame according to the first (top) layer. While mypaint, gimp and krita can all change the frame size easily enough, it’s even easier to reorder layers, so a tip is to make sure the layer you want to match the frame size is top layer, i.e. first filename on the command line.

(define w 0)
(define h 0)

(let* ((pic  (image-load (first input-filenames))))
  (set! w (image-width pic))
  (set! h (image-height pic))
  (image-destroy pic))

Then I’ll define a convenience function to get the new paths for the image layers.

(define (newp str)
  (s-prepend "data/" (last (s-split "/" str))))

Here is where I create a working directory for the ora’s contents, and add a stack file to it.

(let ((dir  (string-append output-stem "/data"))
      (stack `(*TOP* (*PI* xml "version='1.0' encoding='UTF-8'")
        (image (@ (http://mypaint.org/ns/openraster:frame-active "true")
              (w ,w)
              (h ,h))
           (version "0.0.5")
           (stack (@ (visibility "visible")
                 (opacity "1.0")
                 (isolation "isolate")
                 (composite-op "svg:src-over"))
              ,@(list-ec (: imname (index num) input-filenames)
                     `(layer (@
                          (composite-op "svg:src-over")
                          (opacity "1.0")
                          (src ,(newp imname))
                          ,@(if (zero? num)
                            '((selected "true"))
                            '())
                          (name ,(s-chop-suffix ".png" (last (s-split "/" imname))))
                          (x 0)
                          (y 0)))))))))
  (create-directory dir #t)
  (serialize-sxml stack output: (string-append output-stem "/stack.xml")
          ns-prefixes: '((mypaint . "http://mypaint.org/ns/openraster"))))

Adding #t to create-directory creates the parents so I can create the /data subdir along with creating the working directory.

I’ll just comment on a couple of excerpts from that larger block of code:

,@(if (zero? num)
      '((selected "true"))
      '())

Unquote-splicing a null list looks like a weird hack, but it’s a useful pattern for when you conditionally want to include something in a list. I sometimes quasiquote just so I can do this.

(x 0)
(y 0)

This means that each layer will be left-justified and top-justified. mypaint, gimp and krita all have easy tools for moving the layers where you want them. If you change this, remember to unquote:

(x ,(your-special-function arg arg))
(y 0)

OK, now we’re done with creating directory and the stack file, the rest is just pretty much Chicken’s equivalent of shell script basics. file-link looks weird but the posix unit doesn’t really have a “copy-file” as far as I know. But it saves on disk space and then the link is deleted as the corresponding image data is zipped up into the .ora file.

(with-output-to-file (string-append output-stem "/mimetype") (lambda () (display "image/openraster")))
(do-ec (: imname input-filenames)
       (file-link imname (string-append output-stem "/" (newp imname))))
(change-directory output-stem)
(print (current-directory))
(run ,(string-append "zip -r ../" output-stem ".ora *"))
(change-directory "..")
(delete-directory output-stem #t)