この記事は東京大学工学部電子情報工学科・電気電子工学科(通称eeic, cf.)の3年後期実験の一つである「大規模ソフトウェアを手探る」のレポートとして,書かれたものです。
概要を手探る。
人生で初めてOSSにContributeしたので、そのことについての記録を。
今回はOSSとしてドローソフトのInkscapeを取り上げた。
開発はGitLab上のリポジトリで行われており、メイン言語はC++となっている。
冒頭に書いたとおり、「大規模ソフトウェアを手探る」という大学の授業の一環で行ったことであるが、ずっとOSSに興味はあったので良い機会であった。
今回はバグ修正を1つと、機能追加を1つ行った。
バグ修正の方はMerge Requestを提出し、本リポジトリにマージされた。
この記事では、初めて見るソースコードに対して実際にどのように編集を加えていったのかを示す。
※ビルド方法などこの記事に書いていないことについては、同じく実験に参加したメンバーが別途記事を書いている。
- Inkscapeを手探る - Qiita(目次)
- Inkscapeの環境構築 - Qiita
- Visual Studio Codeを使ったデバッガ導入とデバッグ実行 - Qiita
- OSS開発初めかた (Issueの見つけ方) - Qiita
- OSS開発初めかた (GitLabにある本家にマージ) - Qiita
バグ修正編
バグの内容を手探る。
今回行った修正は以下のIssueへの解決である。
要はクリッピングマスクを適用した図形をpngエクスポートする際に、アンチエイリアスを無効にする設定にしても反映されないというバグである。
クリッピングマスクを適用した図形を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 argumentsdc
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
クラスは図形や文字・画像など、描画されるアイテムの基底クラスであるようであり、描画時に用いられる。DrawingShape
や DrawingGryphs
などのクラスから継承されていることがわかる。
たくさんのメソッドが記述されていたが、今回関係するメソッドだけを抽出すると以下のようになる。
上で記述されていた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
の値によって分岐し、アンチエイリアスの設定を切り替えている。 - クリップマスクが設定されている場合、メンバ変数
_clip
にDrawingItem
へのポインタが設定されている。(子要素のようなもの) - 描画ライブラリとしてcairoが用いられている。
これがわかったところで、まずは、どこで問題が発生しているのかを突き止める。
アンチエイリアスが反映されない原因として、以下のようなものが思いつく。
- 設定値が
_antialias
メンバ変数に反映されていない -
_antialias
メンバ変数をもとにcairoに設定値を正常に渡せていない - 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には、「キーオブジェクトに整列」機能がある。
これをInkscapeで実装した。([変更内容](2c47cfbda2d5ccbbc4052f77f0bad5837f52bd77...94caf0f50d4461d2eac6a8ba210cb514d5cf7545 · Yusuke Ebihara / inkscape · GitLab https://gitlab.com/ebiyuu1121/inkscape/-/compare/2c47cfbda2d5ccbbc4052f77f0bad5837f52bd77...94caf0f50d4461d2eac6a8ba210cb514d5cf7545))
- キーオブジェクトを保持する変数を作成
- 選択ツールの動作を変更
- 整列処理を変更
バグ修正で全体像を掴んでいたこともあり、機能追加自体はかんたんに行うことができた。
まだ未完成の状態で開発を進められていないが、機会があれば追記しようと思う。
感想を手探る。
初めてのOSSへの参加という経験であった。
「大規模ソフトウェアを手探る」というテーマの通り、たくさんのソースコードから該当箇所を見つけ出すのが最も大変であった。しかし、どんなソフトも結局それぞれの機能は普段使うような言語で書かれているということを実感できた。