Home About Contact
GIMP , Scheme

GIMP Script-Fu を使って 所定のディレクトリ内にある xcf ファイルを png にバッチ変換する

GIMPには、 Script-Fu というスクリプトが備わっていてバッチ処理などができます。

InDesign での ExtendScript / UXP Scripting に相当するものです。

今回は複数の xcf ファイルを一括で png ファイルに変換(保存)するスクリプトを書きます。

コマンドラインから Script-Fu を実行

現在のディレクトリ以下はこのような状態になっています。

.
├── main.scm
└── xcf
    ├── 1016841.xcf
    └── 1017466.xcf

main.scm に Script-Fu のコードがあり、これをコマンドラインから実行するには次のようにします。

$ cat main.scm | gimp -n -i -b -

-b オプションと - (ハイフン) を使うことで、標準入力からスクリプトを与える形でスクリプトを実行できます。

詳細はこちらを command line options 参照。

作動環境は Chromebook Linux ですが、macOS の場合は、 gimp コマンドをたとえば以下のようにフルパスで指定すると作動します。

/Applications/GIMP-2.10.app/Contents/MacOS/gimp

Chromebook Linux の GIMP ではエラーは出ないのですが、macOS では、なんやかんやエラーが出ます。それでも png 画像への変換自体はうまくいきました。

file-glob

./xcf/ ディレクトリ内にある xcf ファイルを全部列挙する Script-Fu は以下のようになります。

(print (file-glob "xcf/*.xcf" 1))
(gimp-quit 0)

file-glob の API 説明が見つからなかった。 この環境(Chromebook Linux) では file-glob の二番目の引数を 0 にしても同じ結果になる。 ネットにあるスクリプトはそこが 1 になっているので、とりあえず 1 にしてある。 https://www.gimp.org/tutorials/Basic_Batch/

実行すると以下のようになります。

$ cat main.scm | gimp -n -i -b -
(2 ("xcf/1017466.xcf" "xcf/1016841.xcf"))

「ファイル数」と「ファイルリスト」のリストが得られます。

二番目の要素のファイルリスト得たいので、cdr して car します。

(print (car (cdr (file-glob "xcf/*.xcf" 1))))

実行すると意図通りファイルリスト ("xcf/1017466.xcf" "xcf/1016841.xcf") を得ることができました。

リストの二番目の要素を取得するのに、 (car (cdr aList)) を一回で済ます関数 cadr があるので、それを使い、さらに let で filelist 変数に bind します。

(let ( 
    (filelist (cadr (file-glob "xcf/*.xcf" 1)))
  )
  (print filelist)
)
(gimp-quit 0)

while でファイルを一件ずつ取り出す

while で filelist の先頭から一件ずつファイルを取り出して print します。

(let ( 
    (filelist (cadr (file-glob "xcf/*.xcf" 1)))
  )
  (while (> (length filelist) 0)
         (print (car filelist))
         (set! filelist (cdr filelist)))
)
(gimp-quit 0)

toPNG 関数を定義

toPNG 関数を定義して、変換対象となるひとつのファイルを受け取りそれを処理します。 (今は単にファイルを print するだけです。)

(define (toPNG f)
    (print f))

(let ( 
    (filelist (cadr (file-glob "xcf/*.xcf" 1)))
  )
  (while (> (length filelist) 0)
    (toPNG (car filelist))
    (set! filelist (cdr filelist)))
)
(gimp-quit 0)

実行します。

cat main.scm | gimp -n -i -b -
"xcf/1017466.xcf"
"xcf/1016841.xcf"

xcf ファイルパスから 保存用の png ファイルパスを生成

substring や string-append 関数を使って、xcf/hoge.xcf から xcf/hoge.png 文字列を作り出します。

(string-append
  (substring f 0 (- (string-length f) (string-length ".xcf")))
  ".png")

xcf を png に変換する

ここまで来たらあとは、toPNG 関数の中で、xcf ファイルからイメージをロードして png として保存する処理を書けばよい。

(define (toPNG f)
  (let* (
    (pngf (string-append
      (substring f 0 (- (string-length f) (string-length ".xcf")))
      ".png"))
    (image (car (gimp-file-load RUN-NONINTERACTIVE f f)))
    (layer (car (gimp-image-merge-visible-layers image CLIP-TO-IMAGE)))
  )
  (gimp-file-save RUN-NONINTERACTIVE image layer pngf pngf)
  (gimp-image-delete image)
  (print pngf)))

let ではなく let* することで ローカル変数の有効範囲を拡大できる。

まとめ

完成したスクリプトを掲載します。

(define (toPNG f)
  (let* (
    (pngf (string-append
      (substring f 0 (- (string-length f) (string-length ".xcf")))
      ".png"))
    (image (car (gimp-file-load RUN-NONINTERACTIVE f f)))
    (layer (car (gimp-image-merge-visible-layers image CLIP-TO-IMAGE)))
  )
  (gimp-file-save RUN-NONINTERACTIVE image layer pngf pngf)
  (gimp-image-delete image)
  (print pngf)))

(let (
    (filelist (cadr (file-glob "xcf/*.xcf" 1)))
  ) 
  (while (> (length filelist) 0)
    (toPNG (car filelist))
    (set! filelist (cdr filelist)))
)
(gimp-quit 0)

実行。

$ cat main.scm | gimp -n -i -b -
"xcf/1017466.png"
"xcf/1016841.png"

xcf/ 以下に同じファイル名(ただし拡張子は png)で保存します。

もし、png/ ディレクトリ以下に保存したければ、保存用 png ファイルパス文字列を作り出す部分を工夫すればいい。 mkdir 関数もあったはず。

追伸 while の代わりに再帰関数を使う

while で filelist 変数を更新していくのが気持ち悪いので、代わりに再帰関数 each を定義してそれで処理できないか試す。

(define (each fs)
    (if (null? fs)
        '()
        (begin
          (toPNG (car fs))
          (each (cdr fs))
        )))

if の書き方は (if predicate then_value else_value) で、 then_valueelse_value を必ず記述する必要があること、そして そして何か値を返す必要があること。

このため、再帰の停止条件で空のリスト '() を返している。

たぶん、if に代えて unless を使えば良い可能性あり。 https://www.shido.info/lisp/scheme5.html

この each 関数は結局、toPNG 処理をする副作用を発生させるだけのもの。 値を作り出す再帰関数ではないので、無理やり最後に空リスト値を返す実装になった。

さらに then_value, else_value 部分で複数の関数を書き連ねたい場合は、 begin を使わないといけない。

この each 関数を使って書き直したスクリプトはこれ。

(define (toPNG f)
  (let* (
    (pngf (string-append
      (substring f 0 (- (string-length f) (string-length ".xcf")))
      ".png"))
    (image (car (gimp-file-load RUN-NONINTERACTIVE f f)))
    (layer (car (gimp-image-merge-visible-layers image CLIP-TO-IMAGE)))
  )
  (gimp-file-save RUN-NONINTERACTIVE image layer pngf pngf)
  (gimp-image-delete image)
  (print pngf)))

(define (each fs)
    (if (null? fs)
        '()
        (begin
          (toPNG (car fs))
          (each (cdr fs))
        )))

(let (
    (filelist (cadr (file-glob "xcf/*.xcf" 1)))
  ) 
  (each filelist)
)
(gimp-quit 0)

まあ、なんだかなぁ。

追伸 for-each を使う

for-each を試した。

while による元のコード(抜粋):

  (while (> (length filelist) 0)
    (toPNG (car filelist))
    (set! filelist (cdr filelist)))

for-each による新しいコード(抜粋):

  (for-each (lambda (f) (toPNG f)) filelist))

これは良い!最初から for-each を使うべきだった。

以上です。