GIMPの履歴機能に制限を加えてみた

はじめに

このブログは大学での実験課題である「大規模ソフトウェアを手探る」で我々が取り組んだことをまとめたブログです。この実験の目標は大規模な(十万行程度が目安)オープンソースソフトウェアのソースコードを変更して何らかの機能を加えるというもので、我々は題材となるソフトウェアとしてGIMPを選択しました。

 

何故GIMPか?

ソフトウェアに機能を加えるといっても、普段我々が使っているソフトウェアはオープンソースなものも含め数々の修正・変更が行われていて、そう簡単に改善案が浮かぶものではありません。しかし、筆者がそれなりに使っているGIMPは作業履歴がたまると高確率でフリーズするという経験則に基づく欠点(「gimp フリーズ」等のワードでググるとフリーズの回避方法として作業履歴をこまめに消すというのを挙げているページがいくつか見られます)があったため、これを修正するとよいのではないかという考えに至りました。

また、GIMPは我々が普段授業で作るプログラムに比べて製品らしい、あるいは実用的なものだと感じたのでこれを手探ることで何らかのノウハウを得ることを期待してこれをテーマとして選択しました。

 

何をするか

f:id:team-c:20161205231559p:plain

図1 作業履歴ダイアログ 

 

問題となる作業履歴ダイアログは図1のようになっています。我々の目的は履歴がたまりすぎてフリーズしてしまうのを回避することなので、履歴が一定数以上たまらないよう(今回は仮に10個とした)にし、10個以上になってしまった場合は古いものから随時破棄していくように修正を加えることを目標にしました。

 

GIMPのインストールからビルドまで

我々は修正作業を全てUbuntu環境で行いました。まず、GIMPのインストール方法について説明します。

$ apt-get source gimp

このように打つとgimpソースコードがインストールされます。我々はver3.6.2のものをインストールしました。しかしこのままだとパッケージが不足してconfigure出来ないので、

$ apt-get build-dep gnome-terminal
$ apt-get gnome-doc-utils

と追加でインストールしてから

$ CFLAGS="-O0 -g" ./configure --prefix=/home/hogehoge/gimp_install
$ make
$ make install

のようにすればconfigureとmakeが成功して、実行ファイルを用意することができます。

 

デバッグしてみる

実行ファイルが用意できたのでデバッグに移ります。emacs上でM-x gud-gdb と入力し

gdb --fullname gimp

 でデバッグを開始します。main関数にbreakpointを設定して、nextを押していくと途中でスレッドが立ちつつ最終的にmain.c440行目のapprunというメインループ関数 に到達し、GIMPが起動します。そしてデバッガ上ではコマンド入力が出来ず、GIMP上で操作してもデバッグが進行しないといった状態になります。

 

そこでデバッグをどう進めていけばいいのかということになるのですが、今回我々は

  1. grepを用いた検索で目的の機能を実現していそうな関数を絞っていき、それらにbreakpointを張って様子を見る
  2. Ctrl+cをemacs上で2度押してGIMPの実行を止めてから、デバッガ上で関数の動きを追跡する

という方針のもとでデバッグを進めることにしました。

 

第一手 〜履歴の全消去の追跡〜

f:id:team-c:20161207204807p:plain

図2 履歴全消去確認ウィンドウ

 

デバッグの進め方の方針がついたので、まずは履歴操作のうち理屈が最もわかりやすそうな履歴の全削除機能の追跡をすることにしました。図1の右下にあるほうきのようなボタンを押すと、図2のようなウィンドウが出て、本当に履歴を全消去していいか確認されます。ここで、Ctrl+cを2度押しGIMPを停止させ、バックトレース(デバッガ上でbtと入力)すると、

#0  g_main_context_iterate (context=0xdc5130, block=block@entry=1,
    dispatch=dispatch@entry=1, self=<optimized out>)
    at /build/buildd/glib2.0-2.40.2/./glib/gmain.c:3731
#1  0x00007ffff3e3230a in g_main_loop_run (loop=0xe3ac40)
    at /build/buildd/glib2.0-2.40.2/./glib/gmain.c:3928
#2  0x00007ffff7aca852 in gimp_dialog_run (dialog=0x195fa10)
    at gimpdialog.c:649
#3  0x0000000000498842 in edit_undo_clear_cmd_callback (action=0x2cf5f60,
    data=0x7fffd97e3670) at edit-commands.c:180
#4  0x00007ffff43033b8 in g_closure_invoke (closure=0x2cfbc90,
    return_value=0x0, n_param_values=1, param_values=0x7fffffffd210,
    invocation_hint=0x7fffffffd1b0)

このように表示され(#5以下#33までありますがあまり関係ないので省略しています)、#3のedit_undo_clear_cmd_callbackが”くさい”ように見えます。

 

そこでこの関数にbreakpointを張ると、期待通り履歴全消去の際に引っかかってくれます。このそこでこの関数にbreakpointを張ると、期待通り履歴全消去の際に引っかかってくれます。この関数はedit-command.c内にあります。

void
edit_undo_clear_cmd_callback (GtkAction *action,
                              gpointer   data)
{
  GimpImage     *image;
  GimpUndoStack *undo_stack;
  GimpUndoStack *redo_stack;
  GtkWidget     *widget;
  GtkWidget     *dialog;
  gchar         *size;
  gint64         memsize;
  gint64         guisize;
  return_if_no_image (image, data);
  return_if_no_widget (widget, data);

  dialog = gimp_message_dialog_new (_("Clear Undo History"), GIMP_STOCK_WARNING,
                                    widget,
                                    GTK_DIALOG_MODAL |
                                    GTK_DIALOG_DESTROY_WITH_PARENT,
                                    gimp_standard_help_func,
                                    GIMP_HELP_EDIT_UNDO_CLEAR,

                                    GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                    GTK_STOCK_CLEAR,  GTK_RESPONSE_OK,

                                    NULL);

  gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
                                           GTK_RESPONSE_OK,
                                           GTK_RESPONSE_CANCEL,
                                           -1);

  g_signal_connect_object (gtk_widget_get_toplevel (widget), "unmap",
                           G_CALLBACK (gtk_widget_destroy),
                           dialog, G_CONNECT_SWAPPED);

  g_signal_connect_object (image, "disconnect",
                           G_CALLBACK (gtk_widget_destroy),
                           dialog, G_CONNECT_SWAPPED);

  gimp_message_box_set_primary_text (GIMP_MESSAGE_DIALOG (dialog)->box,
                                     _("Really clear image's undo history?"));

  undo_stack = gimp_image_get_undo_stack (image);
  redo_stack = gimp_image_get_redo_stack (image);

  memsize =  gimp_object_get_memsize (GIMP_OBJECT (undo_stack), &guisize);
  memsize += guisize;
  memsize += gimp_object_get_memsize (GIMP_OBJECT (redo_stack), &guisize);
  memsize += guisize;

  size = g_format_size (memsize);
  gimp_message_box_set_text (GIMP_MESSAGE_DIALOG (dialog)->box,
                             _("Clearing the undo history of this "
                               "image will gain %s of memory."), size);
  g_free (size);

  if (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK)
    {
      gimp_image_undo_disable (image);
      gimp_image_undo_enable (image);
      gimp_image_flush (image);
    }

  gtk_widget_destroy (dialog);
}

この関数の中で

gimp_image_undo_disable (image);
gimp_image_undo_enable (image);

という箇所が見るからに怪しく、stepを何度か押して中身を追求していくとgimpimage.c内のgimp_image_undo_eventという関数にたどり着きます。

void
gimp_image_undo_event (GimpImage     *image,
                       GimpUndoEvent  event,
                       GimpUndo      *undo)
{
  g_return_if_fail (GIMP_IS_IMAGE (image));
  g_return_if_fail (((event == GIMP_UNDO_EVENT_UNDO_FREE   ||
                      event == GIMP_UNDO_EVENT_UNDO_FREEZE ||
                      event == GIMP_UNDO_EVENT_UNDO_THAW) && undo == NULL) ||
                    GIMP_IS_UNDO (undo));

  g_signal_emit (image, gimp_image_signals[UNDO_EVENT], 0, event, undo);
}

そしてこの中の

g_signal_emit (image, gimp_image_signals[UNDO_EVENT], 0, event, undo);

この関数こそ鍵なのではないかと考えました。g_signal_emitという関数自体はglibというライブラリ内の関数であり、さらに名前から推測できるように何らかの信号を発信している関数と推測されるので、これを詳しく見てもあまり意味はないと判断し、信号を発信しているのならどこかで受信しているはずだという考えで探索を続けます。調べたところg_signal_emitに対応するのはg_signal_connectという関数であるようで、g_singnal_emitの第二引数(gimp_image_signals[UNDO_EVENT])が信号名なのを手がかりに信号の受信場所を探索してみました。

 

そして信号を受信して動作する関数をいくつか見つけることができたのですが、それらは直接作業履歴スタックの操作に関与しておらず、作業履歴スタックの変更に伴うUIがらみの変更を管理する関数のようでした。今までやっていたことが的外れだということがわかったところで、後半戦へと続きます…。