GIMPのPython-Fuでオレオレフィルタを書くためのあれこれ

# はじめに

- 対象
 - GIMPのオレオレフィルタを*python*で書かきたいヒト
 - pythonがなんとなく読み書きできるレベル
 - なんとなく英語がよめるレベル

- まず [GIMP Scripts and Plug-ins](http://gimpbook.com/scripting/slides30/gimpscripting.html) を読もう！
- [5 分で始める GIMP Python-Fu](https://qiita.com/dhomma/items
/f33ecd1c4d4c6ece7685) も合わせて読もう！
- これでPython-Fuの基本は完璧だ！

## 環境
- GIMP 2.8.18 Python Console
- Python 2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)]

## Hello World
```python:helloworld.py
#!/usr/bin/env python

from gimpfu import *

def plugin_main(message):
    gimp.message("Message: " + message)

register("python_fu_hello_world", "", "", "", "", "",
	"python_fu_hello_world", 
	"RGB*",
	[
(PF_STRING, "string", "Text string", 'Hello, world!')],
	[],
	plugin_main,
	menu = "<Image>/Filters")

main()
```

- `%USERPROFILE%/.gimp-2.8/plug-ins/helloworld.py`みたいな感じで保存する。[^1]
- 直下に置く必要がある気がする。ディレクトリに含めると認識されない。
- SCM(git)やIDE(VS)などを使うときは `編集` → `設定` → `フォルダ` → `プラグイン` で別途専用のディレクトリを設定しておくのが良いのかも。

# 動かす
1. GIMPを閉じる
2. GIMPを起動するときにパラメタに ` --verbose --console-messages ` をつけて起動するとプラグインの読み込みでミスったとき教えてくれる。
3. フィルターの最後に項目が増えていれば成功。無いときはコンソールをよく見て問題を特定しよう。案外躓く。


# 書く
1. **https://www.gimp.org/docs/python/ にほぼすべてが書いてある。**がんばって読もう。ただし古い。
1. **http://gimpbook.com/scripting/notes.html** も参考になる
1. 現状に即したフォーマットが知りたい場合は、プレインストールされているPython-Fuが参考になる。
`C:\Program Files\GIMP 2\lib\gimp\2.0\plug-ins\foggify.py` あたりを読んでみよう。

## register()
>-   name
-    blurb
-   help
-   author
-  copyright
- date
- menupath
- imagetypes
-  params
-   results
-    function

適当なサンプルを参考にして埋めよう。
ただし、menupathに直接メニューの位置を書くのは古いそうなので、menu引数にメニューの場所を渡そう。

## main関数
昔はデフォで第一引数と第二引数に`image`と`layer`が渡されていたみたいだが、いまはそうじゃない。これらを取るときはregister()で以下のように明示的に渡す必要がある。

```python
        (PF_IMAGE, "image",       "Input image", None),
        (PF_DRAWABLE, "drawable", "Input drawable", None),
```

## printデバッグ
デバッガが使える気がしないのでprintデバッグ中心になるが、Windowsでデバッグ出力をとるのは意外とめんどいみたい。いくつかやり方はあり。Linuxを使おう！
### `--console-messages` をつけて `gimp.message()` を使う。

```
gimp.message("Message: " + message)
```
なお、`gimp.message()`は普通に起動した状態だとエラーダイアログが出て処理が止まる。
 
### sys.stdoutをリダイレクトする。

```
sys.stdout = open("stdout.txt", 'a')
```
`%USERPROFILE%`にできる。がWindowsに`tail -f`はない。Linuxを使おう！[^2]

## 固有なオブジェクト

|名前|概要|
|--:|:--|
|Image|カンバスそのもの。自分で作れる。|
|Channel|色チャンネル。|
|Layer|レイヤー。|
|Drawable|書き込み可能なもの。LayerとChannelsの共通部分を表す。|
|Tile|画像データのブロック（Tile）単位での読取を提供する。|
|Pixel Regions|範囲のピクセル操作（読み書き）を提供する。|

## ピクセル操作
- Tileは画像データのGIMPの内部的な表現。カンバス全体をメモリに展開すると重いので、部分ごとに読み取るための仕組み。読み取り専用。
- **書く**場合はPixel Regionsを使う必要がある。

### 読み込み
画像のピクセルとその成分をすべて出力する例を見てみる。

```py
	w = layer.width
	h = layer.height
	src_rgn = layer.get_pixel_rgn(0, 0, w, h, False, False)
	for x in xrange(0, w):
		for y in xrange(0, h):
			p = map(ord, src_rgn[x, y])
			print x, ":", y, " -- ", "".join(map((lambda x: format(x, '02x')), p))
```

- `layer.get_pixel_rgn()`で画像内の領域を読み込み、Pixel Regionsを得る。
- `src_rgn[x, y]`でその1要素（ピクセル）を取り出す。 
 - 今回はRGBAレイヤーに対して処理しているのでその前提とする。
 - たぶんグレスケとかAなしだと変わる。`len(src_rgn[0, 0])`で検査しよう
- strで格納されているので、1文字づつを取り出して`ord()`で数値に変換できる。
- こうなればタダの要素数4の整数配列なので煮るなり焼くなりできるようになった。

```text:出力の一例
2 : 1  --  ff33337d
2 : 2  --  ffcc00ff
           ^^^^^^^^
         * RRGGBBAA
```

### 書き込み

```py
	dst_rgn = dest_drawable.get_pixel_rgn(0, 0, w, h, False, True)
	dst_rgn[x, y] = array("B", [ red, green, blue, 255 ]).tostring()
```
でピクセルをいじっていけばOK。

- `get_pixel_rgn()`の第5引数dirtyはdrawableにpixel regionの変更を反映するかどうからしいが、正直Falseでも違いがわからない。だれか教えて。
- 第6引数shadowはpixel regionsへの変更をshadow tilesに反映するかどうか。多分そのほうが早い気がする。その場合、`merge_shadow()`を呼ぶ必要がある。

書き込んだあとは、スクリプトの最後に*おまじない*を差し込む必要がある

```py
	# 書き込みデータのflush。らしいが正直なにをしているのかよくわからない。
	dest_drawable.flush()
	
	# 上記のとおり、shadow tileを使う場合はこれを呼ぶ必要がある。
	dest_drawable.merge_shadow()

	# 忘れると、レイヤーのサムネイルが表示されない残念なことになる。
	dest_drawable.update(0, 0, w, h)
```

## スクリプト内でundoをまとめておく

```py
	pdb.gimp_image_undo_group_start(image)
```
と

```py
	pdb.gimp_image_undo_group_end(image)
```
を開始と最後に読んでおくと、行った操作がひとつの「もとに戻す」になるのでいい感じ。
ただし、Exceptionなどが発生して呼びそこねるとGIMPから怒られる。try except finallyを駆使してちゃんとハンドリングしよう。

# 最後に

- 遅い。（192x192を操作してupdateするのに5秒くらい待たされる。）
- pixelそのものの型がstrな点が非常にわかりにくい。[^3]
- この先は公式の既存のスクリプトを読んで勉強するのが良いと思う。
  - https://github.com/akkana/gimp-plugins
  - Progressの更新、Tileの活用方法、Custom Dialog、などなど

成果が[ここ](https://github.com/Nanashia/python_fu_pixel_border)にあるので興味があればどうぞ[^4]

[^1]: Script-Fuはscripts、Python-Fuはplug-insなので間違えないように。プラグイン・スクリプト・モジュールといろいろあってややこしい。
[^2]: 個人的にはこのやり方+ Windows Subsystem for Linuxのtailで監視が一番ラク
[^3]: PythonはVisual Studo Community 2017にPythonインテグレーションをいれて書いていたが、Python標準の関数はしっかり保管してくれて書きやすかった。ただ、どうもGIMP側のpydが正しく読み込まれず、GIMPまわりの機能はリファレンスにらめっこと試行錯誤をせざるをえなかったので、あと一歩というところだった。うまくいけば補完付きでゴリゴリ書けるかもしれないので情報募集中
[^4]: Python初めて書いたけど動的型付けはやっぱりクソ（過激派）