3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Inkscapeの開発に参加した

この記事は東京大学工学部電子情報工学科・電気電子工学科(通称eeic, cf.)の3年後期実験の一つである「大規模ソフトウェアを手探る」のレポートとして,書かれたものです。

概要を手探る。

人生で初めてOSSにContributeしたので、そのことについての記録を。
今回はOSSとしてドローソフトのInkscapeを取り上げた。
開発はGitLab上のリポジトリで行われており、メイン言語はC++となっている。

冒頭に書いたとおり、「大規模ソフトウェアを手探る」という大学の授業の一環で行ったことであるが、ずっとOSSに興味はあったので良い機会であった。

今回はバグ修正を1つと、機能追加を1つ行った。
バグ修正の方はMerge Requestを提出し、本リポジトリにマージされた。

この記事では、初めて見るソースコードに対して実際にどのように編集を加えていったのかを示す。
※ビルド方法などこの記事に書いていないことについては、同じく実験に参加したメンバーが別途記事を書いている。

バグ修正編

今回の修正内容

バグの内容を手探る。

今回行った修正は以下のIssueへの解決である。

clipping paths are antialiased even if export is set to no antialiasing (#1921) · Issues · Inkscape / inkscape · GitLab

要はクリッピングマスクを適用した図形をpngエクスポートする際に、アンチエイリアスを無効にする設定にしても反映されないというバグである。

クリッピングマスクを適用した図形をpngエクスポートする際に、アンチエイリアスを無効にしてもアンチエイリアスが適用されるバグ。

アンチエイリアスあり アンチエイリアスなし
image.png image.png

実際に試してみると、クリッピングパスを設定していないオブジェクト(黒)に関してはアンチエイリアスの設定が反映されているが、クリッピングパスを設定したオブジェクト(赤)に関してはアンチエイリアスの設定に関わらずアンチエイリアス処理が行われていることがわかる。

大量のファイルをを手探る。

ファイルが多すぎてどこをどういじれば良いのかわからない。

というわけで手がかりを探そうと思っていたところ、該当Issueに以下のような記述がなされていた。

Remarks to whoever wants to try to tackle this : in src/diplay/drawing-item.cpp please take the lines 711-726 and make it a static function taking arguments dc and _antialias, so that you can call it from other places (say, from the ::clip method, if that has chances to work)

(だいたい意訳)

このIssueをやる人へ:src/diplay/drawing-item.cpp の711-726行目を dc_antialias を引数にもつ static関数にすれば、色んな場所から呼び出せます。(そしてこれを ::clip メソッドから呼び出せばもしかしたらバグの解決に役立つかも!)

このコメント自体がそのまま解決に結びつくかどうかはまだわからないが、 src/diplay/drawing-item.cpp を見れば良いということはわかった。というわけで src/display/drawing-item.cpp をひもとく。

drawing-item.cpp を手探る。

drawing-item.cpp は Inkscape::DrawingItem というクラスの実装が記述されていた。
DrawingItem クラスは図形や文字・画像など、描画されるアイテムの基底クラスであるようであり、描画時に用いられる。DrawingShapeDrawingGryphs などのクラスから継承されていることがわかる。

image.png
(doxygenドキュメントより)

たくさんのメソッドが記述されていたが、今回関係するメソッドだけを抽出すると以下のようになる。
上で記述されていた711-726行目とは、switch(_antialias){ の行である。

...
void
DrawingItem::setAntialiasing(unsigned a)
{
    if (_antialias != a) {
        _antialias = a;
        _markForRendering();
    }
}
...
unsigned
DrawingItem::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, DrawingItem *stop_at)
{
    ... // 事前処理など

    switch(_antialias){
        case 0:
            cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_NONE);
            break;
        case 1:
            cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_FAST);
            break;
        case 2:
            cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_GOOD);
            break;
        case 3:
            cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_BEST);
            break;
        default: // should not happen
            g_assert_not_reached();
    }

    ... // 描画処理など

    if (_clip) {
        ict.pushGroup();
        _clip->clip(ict, *carea);
        ict.popGroupToSource();
        ict.setOperator(CAIRO_OPERATOR_IN);
        ict.paint();
    }

    ... // 描画処理など
}
...
void
DrawingItem::clip(Inkscape::DrawingContext &dc, Geom::IntRect const &area)
{
    ... // 処理
}
...

また、 Inkscape::DrawingItem クラスは以下のようなメンバ変数をもつ。(関連するもののみ抽出)

DrawingItem *_clip;
unsigned _antialias : 2; ///< antialiasing level (NONE/FAST/GOOD(DEFAULT)/BEST)

ソースコードから以下のようなことがわかる

  • render 関数で描画処理を行っている。
    • 実際にデバッガを用いてブレークポイントを貼りながら実行することで、描画処理時(画面の表示更新時や画像書き出しなどのラスタライズされるタイミング)に render メソッドが実行されることがわかった。
  • クリッピングパスが設定されたオブジェクトでは render メソッド内で clip メソッドが呼び出されている。(デバッガで調べると実際そうであることがわかる)
  • _render 関数の内部でメンバ変数 _antialias の値によって分岐し、アンチエイリアスの設定を切り替えている。
  • クリップマスクが設定されている場合、メンバ変数 _clipDrawingItem へのポインタが設定されている。(子要素のようなもの)
  • 描画ライブラリとしてcairoが用いられている。

これがわかったところで、まずは、どこで問題が発生しているのかを突き止める。
アンチエイリアスが反映されない原因として、以下のようなものが思いつく。

  1. 設定値が _antialias メンバ変数に反映されていない
  2. _antialias メンバ変数をもとにcairoに設定値を正常に渡せていない
  3. cairoが受け取った設定値を反映できていない(ライブラリのバグ)

clip() を手探る。

まず、3を検証するため、clip() メソッドを以下のように変更した。

  DrawingItem::clip(Inkscape::DrawingContext &dc, Geom::IntRect const &area)
  {
      // don't bother if the object does not implement clipping (e.g. DrawingImage)
      if (!_canClip()) return;
      if (!_visible) return;
      if (!area.intersects(_bbox)) return;


+     cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_NONE);
+ 
      dc.setSource(0,0,0,1);
      dc.pushGroup();

      ...
  }

クリッピングマスクに関して、設定値に関わらずアンチエイリアオフに固定してその結果を見る意図である。
これで正常にアンチエイリアスがオフになればcairoライブラリには問題がないということが分かる。

実際、ビルドして実行を行ったところ、アンチエイリアスがオフになったため、ライブラリの問題でないことはわかった。

そこで、render()メソッドに含まれるswitch文をclip()メソッドにも書いてみる。

  DrawingItem::clip(Inkscape::DrawingContext &dc, Geom::IntRect const &area)
  {
      // don't bother if the object does not implement clipping (e.g. DrawingImage)
      if (!_canClip()) return;
      if (!_visible) return;
      if (!area.intersects(_bbox)) return;


+     switch(_antialias){
+       case 0:
+           cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_NONE);
+           break;
+       case 1:
+           cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_FAST);
+           break;
+       case 2:
+           cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_GOOD);
+           break;
+       case 3:
+           cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_BEST);
+           break;
+       default: // should not happen
+           g_assert_not_reached();
+   } 
+ 
      dc.setSource(0,0,0,1);
      dc.pushGroup();

      ...
  }

これでバグ解決となればよかったが、解決しない。
デバッガで見てみると、 _antialias の値に設定値が反映されていないことが分かったため、この原因を調査する。

_antialiasを手探る。

_antialias はアンチエイリアスの設定によって変化するため、全DrawingItemで共通である。
(なぜグローバルなオブジェクトではなく各DrawingItemに持たせているのかはわからない)

DrawingItem(を継承しているクラス)のインスタンスは木構造となっていて、根の_antialiasを変更してrender()時に値を子に伝播させていく、という形式のようである。

この伝播の途中で処理が抜けていてクリッピングマスクの部分までで_antialiasの値が引き継がれていないのが原因のようだった。

render()関数内の該当箇所は以下のようになっている。

    // 3. Render object itself
    ict.pushGroup();
    render_result = _renderItem(ict, *iarea, flags, stop_at);

_renderItem()は仮想関数で、実態はDrawingItemを継承したクラスで定義されている。
このうちDrawingGroup::renderItem()の実装が問題であった。

  for (auto &i : _children) {
+     i.setAntialiasing(_antialias);
      i.render(dc, area, flags, stop_at);
  }

上記のように修正したところ、ちゃんと反映されるようになった。

ついでに

_clipが指すオブジェクトもDrawingItemなので、_antialiasを持っている。
クリッピングマスクが入れ子になっている場合は、_clip->_antialiasの値にも反映する必要があるため、render()関数の中で更新をおこなう。

  if (_clip) {
      ict.pushGroup();
+     _clip->setAntialiasing(_antialias); // propagate antialias setting
      _clip->clip(ict, *carea);
      ict.popGroupToSource();
      ict.setOperator(CAIRO_OPERATOR_IN);
      ict.paint();
  }

Merge Requestを手探る。

上記の変更を加え、Merge Requestを作成した。
Issueでも言及された共通部分の関数化なども合わせて実施した。

Fix clipping path antialias (!2401) · Merge Requests · Inkscape / inkscape · GitLab

約24時間後にmasterブランチへ無事マージされた。

おまけ:機能追加を手探る。

同じくドローソフトであるAdobe Illustratorには、「キーオブジェクトに整列」機能がある。

Illustratorでの整列機能

これをInkscapeで実装した。(変更内容)

Inkscapeで実装した整列機能

  • キーオブジェクトを保持する変数を作成
  • 選択ツールの動作を変更
  • 整列処理を変更

バグ修正で全体像を掴んでいたこともあり、機能追加自体はかんたんに行うことができた。
まだ未完成の状態で開発を進められていないが、機会があれば追記しようと思う。

感想を手探る。

初めてのOSSへの参加という経験であった。

「大規模ソフトウェアを手探る」というテーマの通り、たくさんのソースコードから該当箇所を見つけ出すのが最も大変であった。しかし、どんなソフトも結局それぞれの機能は普段使うような言語で書かれているということを実感できた。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?