省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

GIMPプラグイン Layer via Copy の解読

 GIMPプラグイン作成法のお勉強として、以前以下の記事で紹介した、Dmitry Dubyaga氏が作成した、layer-via-copy-cut.py という プラグインを解読してみます。

yasuo-ssi.hatenablog.com

 最初は、Pythonを使う宣言です。引き続きエンコードUTF-8の仕様を宣言します。なお、以下、プログラムは原則斜体ので表記し、変数名(オブジェクト変数名)はで、定数名は赤紫で表記します。緑の部分、および "" に囲まれた部分はユーザが任意に変更可能な部分です。プログラムの初心者ですと、必ずその通り書かなければならない手続き名と、プログラマーが任意につけられる変数名の区別が難しいと思いますのでこのように書いています。例えば、このプログラムでは、イメージオブジェクトを格納するオブジェクト変数名として image という変数名を使用していますが、img でも構わないということです。一方、gimp_image_... というような手続き名(API名)を、gimp_img_... と書いてはいけないということです。また定数名は決まった定数名から選択して使うことになります。

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-

(中略)

from gimpfu import *

 プログラムの冒頭にgimpのライブラリが呼び出されます。

 

 以下、プログラムの順番は逆になりますが、まずRegister関数から見ていきます。

register (
"python-fu-layer-via-copy",
"Copy and move the selected area to a new layer in the same position.",
"Copy and move the selected area to a new layer in the same position.",
"Dmirty Dubyaga",
"Dmitry Dubyaga <dmitry.dubyaga@gmail.com>",
"09.01.2014",
"Layer via Copy",
"*",
[
(PF_IMAGE, "image", "", None),
(PF_DRAWABLE, "drawable", "", None)
],
,
python_fu_layer_via_copy, menu="<Image>/Layer/"
)

  

上の、

[
(PF_IMAGE, "image", "", None),
(PF_DRAWABLE, "drawable", "", None)
],

というところで、imageという変数に、現在アクティブなimageオブジェクトを、そして、drawableという変数に現在アクティブなdrawableオブジェクトを読み込むことを指定しています。なお、ダイアログに表示するメッセージが、"" で省略されているので、入力ダイアログは表示されません。

 

次は、このプログラムの実質的なメイン部分です。

def python_fu_layer_via_copy(image, drawable):

 ここで、定義を行っていますが、上のRegster関数でアクティブなimageおよびdrawableを代入した、image, drawable 変数を引数にしています。python_fu_layer_via_copy は、作者が命名した手続き名(ユーザ定義関数名)になります。

  gimp.context_push()

 ここで使っている gimp-context-push というAPIですが、ここでいうcontextとは、ネットで検索した限りでは、どうやら現在の作業状態や設定状態のことらしいです。そしてこれから行うことを新しい作業履歴として記録し、現在の設定状態等を一つ前の作業履歴に押し出すようです。そして、後にgimp-contxt-popを実行したときに、gimp-context-pushで一つ前の作業履歴に押し出しておいた、設定状態等を回復する、という役割を果たすようです。他に例を探ったところ、描画色と背景色を保存するために使われているケースもありましたが、どうもgimp-contxt-popを実行して、元の描画色/背景色を回復しているようですので、やはり基本機能は作業履歴を記録し次のステップに進み、その後記録しておいた状態に戻す、という機能が基本のようです。
  image.undo_group_start()

 gimp-image-undo-group-start というAPIは、後にgimp-image-undo-group-end を指定したところまでを、もし、「元に戻す」を実行したときに、一挙に元に戻す命令のまとまりとして指定するAPIです。

 ところで、この部分本来ならば、gimp_image_undo_group_start(image) と書くはずではないかと思いますが、どうやら imageオブジェクトのメソッドとしてこのAPIを使う場合は、gimp_image_ という部分を省略して上記のような使い方ができるのではないかと思います。知りませんでした。以下同様な使い方をしているケースが頻出します。

  pdb.gimp_message_get_handler(ERROR_CONSOLE)

 この命令は、エラーメッセージをどこに表示するかを指定するAPIで、この場合はエラーコンソールに表示するよう指定しています。

  if not pdb.gimp_item_is_layer(drawable):
    pdb.gimp_message("The layer or layer group is not selected.")
    image.undo_group_end()
    gimp.context_pop()
    return

 この部分はdrawableがレイヤーではない場合、レイヤーが選ばれていないとのメッセージを出して終了します。
  if pdb.gimp_selection_is_empty(image):
    pdb.gimp_message("The selection is empty.")
    image.undo_group_end()
    gimp.context_pop()
    return

 この部分は選択範囲がない場合、エラーメッセージを出して終了します。
  selection = pdb.gimp_selection_save(image)

 選択範囲を変数 selection に保存します。

  if pdb.gimp_item_is_group(drawable):

 drawableがレイヤーグループである場合、以下の処理を実行します。

    parent = pdb.gimp_item_get_parent(drawable)

 レイヤーグループの「親」(おそらくレイヤーグループ自体を示すポインタ)を変数parentに保存します。
    position = pdb.gimp_image_get_item_position(image, drawable)

 レイヤーグループのimageオブジェクト上の位置(何番目のレイヤーにあるのか)をposition変数に保存します。
    group_new = pdb.gimp_layer_copy(drawable, TRUE)

 このレイヤーグループを group_newにコピーします。その際、アルファチャンネルを付加します(gimp_layer_copyの2番目の引数)。
    pdb.gimp_image_insert_layer(image, group_new, parent, position)

 新しいレイヤーグループgroup_newをレイヤーとして追加します。そのさい、parentに指定されているレイヤーグループの中に追加します。つまり既存のレイヤーグループの中に追加する形で、group_newが追加されます。挿入場所を指定するのに、上で保存した変数 position が使われています。
    group_new.name = "Group via Copy"

 group_newの名前を"Group via Copy"と名付けます。なお、nameはメソッドではなくプロパティです。

    layers_list = find_layers_in_group(group_new, elements = )

 ユーザ定義関数、find_layers_in_groupを使って、group_newの中にあるレイヤーの一覧をlayers_listに取得します。
    for element in layers_list:

 以下、group_newに含まれるレイヤーごとに以下の処理を行っていきます。
      element.add_alpha()

 各レイヤーにアルファチャンネルを付加します。これも本来、gimp-layer-add-alpha というAPIです。
      pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE, selection)

 各レイヤーにselectionに従い、範囲指定を付加します。gimp_image_select_itemの2番目の引数ですが、以下の選択肢があり得ます。

{ CHANNEL-OP-ADD (0), CHANNEL-OP-SUBTRACT (1), CHANNEL-OP-REPLACE (2), CHANNEL-OP-INTERSECT (3) }

 これは、範囲選択の以下の4つのモードに対応します。

f:id:yasuo_ssi:20210617141701p:plain

選択範囲の追加、選択範囲の差引き、選択範囲の新規作成 or 置き換え、既存の選択範囲との交差を範囲選択

 ここでは選択範囲の新規作成が選ばれています。

      pdb.gimp_image_select_item(image, CHANNEL_OP_INTERSECT, element)

 さらに、既存の選択範囲と、各レイヤーの交差範囲を選択しなおします(=各レイヤーごとにselectionの範囲が選択範囲とされるということ。elementは各レイヤー)。
      created_mask = element.create_mask(ADD_SELECTION_MASK)
      element.add_mask(created_mask)

 各レイヤーごとに、選択範囲をマスクとして付加します。ここも本来は、gimp-layer-create-mask(layer, mask-type) および、gimp-layer-add-mask(layer, mask) というAPIです。
      element.remove_mask(MASK_APPLY)

 ここで、gimp-layer-remove-maskというマスクを除去するAPIが適用されています。ここも本来ならば

   pdb.gimp_layer_remove_mask(element, MASK_APPLY)
         ※最初の引数はレイヤー、2番目はモード

と書くべきところかと思いますが、レイヤーオブジェクトにある elements に付加されたメソッドとしてAPIが書かれているので、このような省略形になっているものと思われます。また最初の引数も省略され、モードのみが引数として参照されているようです。ここでモードとしてMASK_APPLYが指定されているため、マスクを除去する際に、マスク画像をオリジナルのレイヤーの画像データに適用したのち、マスクを除去しているようです。このため、結果としてオリジナルの画像から範囲指定された部分だけを切り抜いた画像が、新規レイヤーとして残るのです。
      selection_bounds = pdb.gimp_selection_bounds(image)

 選択範囲のx, yのそれぞれ最大値、最小値を取得します。この場合、selection_bounds [0]は選択範囲があるかどうか(あればTrue、なければFalse)、selection_bounds [1]はxの最小値、selection_bounds [2]はyの最小値、selection_bounds [3]はxの最大値、selection_bounds [4]はyの最大値になります。
      selection_width = selection_bounds[3] - selection_bounds[1]
      selection_height = selection_bounds[4] - selection_bounds[2]

 選択範囲の幅と高さを計算します。
      element_offset_x, element_offset_y = pdb.gimp_drawable_offsets(element)

 各レイヤーのオフセット値を取得します。
      element.resize(
          selection_width,
          selection_height,
          element_offset_x - selection_bounds[1],
          element_offset_y - selection_bounds[2]
          )

 これは、gimp-layer-resizeが適用されているのだと思いますが、これはおそらく各レイヤーが、選択範囲より小さかったり、選択範囲がレイヤーをはみ出す場合を考えてリサイズを掛けているのではないかと思います。
    image.remove_channel(selection)

 選択範囲 selection を削除します。ここも本来は、gimp-image-remove-channel(layer, channnel) というAPIです。

    pdb.gimp_selection_none(image)

 選択範囲を消します。
    pdb.gimp_image_set_active_layer(image, group_new)

 group_newをアクティブなレイヤーグループにします。

    gimp.displays_flush()

 表示を描画しなおします。
    image.undo_group_end()

 undo範囲をここまでとします。
    gimp.context_pop()

 作業履歴を戻します。
    return

 ここから下は、drawableがレイヤーグループではなく単独のレイヤーだった時の処理です。上の処理とかぶる内容の説明は省略します。

  parent = pdb.gimp_item_get_parent(drawable)
  position = pdb.gimp_image_get_item_position(image, drawable)
  layer_new = pdb.gimp_layer_copy(drawable, TRUE)
  pdb.gimp_image_insert_layer(image, layer_new, parent, position)

 選択範囲を新しいレイヤーとしてコピーし挿入します。
  layer_new.name = "Layer via Copy"

 新しいレイヤーにレイヤー名"Layer via Copy"をつけます。

  layer_new.add_alpha()
  pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE, selection)
  pdb.gimp_image_select_item(image, CHANNEL_OP_INTERSECT, layer_new)
  created_mask = layer_new.create_mask(ADD_SELECTION_MASK)
  layer_new.add_mask(created_mask)
  layer_new.remove_mask(MASK_APPLY)
  selection_bounds = pdb.gimp_selection_bounds(image)
  selection_width = selection_bounds[3] - selection_bounds[1]
  selection_height = selection_bounds[4] - selection_bounds[2]

  layer_offset_x, layer_offset_y = pdb.gimp_drawable_offsets(layer_new)
  layer_new.resize(
      selection_width,
      selection_height,
      layer_offset_x - selection_bounds[1],
      layer_offset_y - selection_bounds[2]
      )
  image.remove_channel(selection)
  pdb.gimp_selection_none(image)

  gimp.displays_flush()
  image.undo_group_end()
  gimp.context_pop()
  return

 

 最後は、レイヤーグループの中のレイヤーを取得する、find_layers_in_groupというユーザ定義関数です。group (レイヤーグループのポインタ)とelemantsが引数になっていますが、上の実質メイン部分でelementsに関しては空き配列として宣言され引かれています。elementsにはレイヤーグループの各要素、すなわち各レイヤーが格納されます。

def find_layers_in_group(group, elements):
  num_children, child_ids = pdb.gimp_item_get_children(group)

 これはレイヤーグループの子レイヤーのidを取得する関数です。最初は子レイヤーの数、そしてその次のchild_idsに子レイヤーのIDが格納されます。
  for id in child_ids:
    child = gimp.Item.from_id(id)

 Item.from_idにより、idからレイヤーの中身がchildに取得されます。前にも言及しましたが、gimp.Item.from_id はプロシジャーブラウザに出てこない非正規(?)のAPIです。おそらくPython-fuのみに有効なAPIです。
    if pdb.gimp_item_is_group(child):
      find_layers_in_group(child, elements)

 ここは孫レイヤーグループがある場合(つまりchildもレイヤーではなくレイヤーグループの場合)、再度自身を呼び出します。
    else:
      elements.append(child)

 elementsに各レイヤーの画像の中身を取得します。
  return elements

最後に、各レイヤーの画像を返します。

 

 なお、本来ならば pdb.gimp_xx_ ... と書くべきところを、gimp.xx_ ... と書いているケースがあります。

 pdb.gimp_... で呼び出すモジュールを、The Procedural Database (PDB)、gimp.xx_... で呼び出すモジュールをGIMP Module Procedures と解説しているサイトもありますが*1、どうも必ずしもそうではないようです。というのは、GIMP Python Documentation ( https://www.gimp.org/docs/python/ ) にGIMP Module Proceduresとして載っていないAPIでもgimp.xx_ という呼び出しを許容している手続きがありますので。これも言わば gimpというアプリケーション自体をオブジェクトとして呼び出し、それに対するメソッドとしてAPIを使用しているものと思われます。但し、この場合、gimpというオブジェクト名は変えることができないと思います。また、どうやら、APIによっては gimp.xx_ ... という呼び出し方を許さないものもあるようです。そのあたりどのような違いになっているのかよく分かりません。

 

 

*1:以下のサイトです。

lendl.sakura.ne.jp