Inkscapeを手探る

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

大規模ソフトウェアを手探るという実験は,2–3人のチームで著名で大規模なOSSを1つ選んで,何か変更したり機能拡張したりするという実験です。我々のチーム「ika」はInkscapeという画像編集ソフトを選んで,まだサポートされていないSVGアニメーションに対応させることを試みました。実験において何をやったか,それに至った過程などをここに記します。

ビルドをする

何らかの変更を加える前に,それがちゃんと動いていることを確認しておくのは重要です。 まずはInkscapeのソースをクローンしてきて,ちゃんとビルドし,実行できることを確認します。

gitlab.com

上のリポジトリをクローンして,CONTRIBUTING.mdのBuilding節に書いてあることをやればよいはずなのですが,いろいろ嵌った点がありました。(我々が参考にしていたのは2018-09-11の版です。また環境はUbuntu 18.04です。)

嵌った点その1としては,cmakeコマンドを実行したところで,あのライブラリがないこのライブラリがないと怒られてしまいました。 解決法は,無いと言われたライブラリをインストールすればよいです。といっても量がそれなりにあるので,一つ一つやっていては面倒です。そこで

sudo apt-get build-dep inkscape

を実行すると一発でインストールできます。しかし実はこれでもまだ数個足りなくて*1,仕方ないので一つずつ手でインストールしました。こうして無事ビルドすることができました。

嵌った点その2は,ビルドはできたけど,いざ起動してみると何もできない寂しいウィンドウがでてきてしまいました。唯一できることは,Quitです。

f:id:ika2018:20181102171913p:plain

解決法は,CONTRIBUTING.mdに書かれているビルド手順のうち

ln -s share share/inkscape

の代わりに

ln -s . share/inkscape

を実行したらうまくいきました(CONTRIBUTING.mdが間違っている)。というのも,Inkscapeを起動した端末に

** (inkscape:5693): WARNING **: 17:08:49.367: Failed to load resource: menus.xml from /home/denjo/.config/inkscape/ui/menus.xml or /home/denjo/repos/inkscape/build/../share/inkscape/ui/menus.xml

といった警告が出ていました。どうやらメニューに関係するファイルが見つからないというようなメッセージだったので,これが関係しているのではないかと推測した結果,上の解決法にたどり着けました。

手探る

起動して正しく動くことが確認できたところで,機能を追加していきます。といっても,ソースコードの規模が大きいのでどこをいじればよいかはすぐには分かりません。その手がかりを得るため,何も手を加えない状態でアニメーションを含んだSVGファイルをInkscapeで開いてみるとどうなるかを調べてみました。すると端末に下のようなメッセージがずらずらと出てきました。

WARNING: unknown type: svg:animate
WARNING: unknown type: svg:animateTransform
WARNING: unknown type: svg:animate
WARNING: unknown type: svg:animate
WARNING: unknown type: svg:animateTransform
WARNING: unknown type: svg:animate

やはりSVGアニメーションには対応していないということが分かります。でも,分かることはそれだけではありませんでした。

この警告がどこから出力されているかをgit grep "unknown type:"として調べてみると,src/object/sp-factory.cppというファイルから吐き出されているようだと分かりました。このファイルを覗いてみると,SVGの各種要素ごとに場合分けをして,対応するクラスのコンストラクタを呼んでいるようです。アニメーションを読み込めるようにするには,ここにアニメーション関連の要素の分岐を足して,対応するクラスを新たに追加すれば良さそうだという手がかりが得られました。

ここで,そもそもSVGアニメーションはどのようなものなのかについて簡単に説明します。(詳しくは仕様を確認したり調べたりしてみてください。)

まず幅50,高さ40の長方形があります。

<rect x="0" y="0" width="50" height="40" />

次のようにすると,幅が50→200→50と連続的に変化するアニメーションを10秒周期で無限回繰り返すようになります。

<rect x="0" y="0" width="50" height="40">
  <animate
    attributeName="width"
    values="50;200;50"
    dur="10"
    repeatCount="indefinite"
  />
</rect>

<animate>は親要素(上の場合<rect>)に対してアニメーションを行います*2。アニメーションの各種パラメータは<animate>の属性として指定します。

<animate>のほかにも,<set>とか<animateTransform>とか<animateMotion>とかもあるのですが,今回は(実験で与えられた期間が有限ということもあり)この<animate>要素に対象を絞ることにしました。

ここまで手探ってきたところで,与えられた10日のうち既に4日が経っていることに気づいたので,最終日(発表)を除いて残りの5日間で実装できそうな目標を設定することにしました。

  • アニメーションの編集機能は無く,単に画面上でアニメーションしている様子を表示できるだけとする
  • 再生ボタンを押すと画面上で再生できて,スライダー(シークバー)を操作すると好きな時刻の状態を見られる
  • 対応するのは<animate>に限定する(前述のとおり)
  • 仕様的にはアニメーションの開始・終了タイミングにイベント(要素のクリックなど)とか,実時間(2018年10月09日XX時XX分など)とかも指定できるようだが,複雑なので実装では文書の読み込みからの経過秒数に限定する
  • 仕様的には色のアニメーションもできるようだが,実装では単位のない数値の補間に限定する
  • 1つの要素の1つの属性に対して複数のアニメーションがある場合などはとりあえず考えない(優先順位などが複雑なため)
  • 離散アニメーション(calcMode="discrete")と線形アニメーション(linear)に限定する(spline, pacedは無いものとする)

以上を実装することを当面の目標として,余力があれば拡張していこうという感じになりました (余力があったとはいっていない)

実装

Inkscape全体の中で,今回実装したものを簡単な図に示すと下の赤い部分になります。(関係ない部分を省いているだけで,Inkscape自体はこんなに単純な構成ではないです。)

※この図は実装がすべて終わってこの記事を書くときに作ったのであって,始めからこう実装すれば良いと分かっていたわけではなく,この時点ではまだ手探り状態でした。

SPDesktopInkscapeのウィンドウ1つにインスタンス1つが対応するクラスみたいです。

InkscapeSVGファイルを開くと,SPDocumentが作られます。SVGファイルの木構造をもとに,要素と対応するクラスのインスタンスを生成していきます(根<svg>と対応するクラスはSPRoot<rect>と対応するクラスはSPRect,という具合で)。

それとは別に,Inkscapeの画面右側にあるパネル(ダイアログと呼ぶらしい)を管理しているDialogManagerがあります。ユーザーがメニューからダイアログを呼び出すと,DialogManagerが対応するクラスのインスタンスを生成します。

SPAnimateの実装

src/object/sp-factory.cppに分岐を追加して,<animate>に対応するクラスSPAnimateをsrc/object/sp-animate.cpp(と.h)に作ります。

sp-animate.cppを1から書くのはアレなので(というかまずどう書いていいか分からない),とりあえずsp-defs.cppをコピペしました。 コピペしたら忘れないうちに,sp-animate.cppをsrc/object/CMakeLists.txtに追記して,コンパイルの対象に追加しておきます。

次に,アニメーションの各パラメータをSPAnimateのメンバとして追加し,属性値を読み取るコードを追加しました。読み取る方法は,他のSPなんちゃらのクラスを参考にして,それっぽく書きました。(build(doc, repr)メソッドの中で属性ごとにreadAttr(属性名)を呼び,set(key, value)の中で属性値をパースしてメンバに格納するコードを書けばよい。)

<animate>の属性はいろいろありますが,実装対象の範囲をかなり限定したため以下のパラメータだけが残りました。

  • begin: 開始時刻(実数)
  • end: 終了時刻(実数)
  • dur: アニメーション周期(実数)
  • calcMode: lineardiscreteのいずれか
  • values: キーフレームでの値(実数のvector
  • keyTimes: キーフレームの割り方(0以上1以下の実数のvector
  • attributeName: アニメーションの対象の属性名

特に,<animate>に属性にあったrepeatDur, repeatCount属性はendメンバがあれば十分なのでメンバとして持たないようにしました。

というのも,本来タイミングはイベントや実時刻なども基準にできるので,例えば

<animate
  begin="0"
  dur="5" repeatCount="2"
  end="click"
  attributeName="..." from="..." to="..."
/>

とすると,読み込んで0秒後にアニメーションが開始して,「5秒間のアニメーションを2回再生し終わる」か,「その要素をクリックする」かどちらか早い方で停止する,ということが可能です。その場合,アニメーションが実際に停止する時刻はクリックされるまで分かりません。

しかし今回はタイミングは読み込みからの経過時間に限定したので,ファイルを開いた時点でアニメーションが開始・終了する時刻が実数として決定できます。したがって開始時刻と終了時刻だけを持つことができます。

最後に,経過秒数に応じて親要素に補間値をセットするメソッドsetAnimationTime(time)を生やしました。まず補間値をアニメーションのパラメータを元に計算します。親要素に対応するインスタンスthis->parentとして得られるので,それに対してsetKeyValue(key, value)メソッドを呼びます。

あとはsetAnimationTime(time)を呼ぶコードをどこか適切な場所に書けばアニメーションができるはずです。

Animationダイアログ

「再生ボタン」とか「スライダー(シークバー)」は新しいAnimationダイアログを作って,そこに実装しようということになりました。

ボタンとスライダーがあるダイアログを探したところ,「レイヤー」というダイアログを見つけました。

f:id:ika2018:20181104203821p:plain

レイヤー一覧の部分を消し,ボタンは「レイヤーを追加」ボタンを再生ボタンに変えてそれ以外を消し,Blend modeのドロップダウンの行を消し,BlurとOpacityの片方だけを残してあげれば,目的のダイアログに近いものが得られそうです。

そこで,「レイヤー」ダイアログを作っているコードを探すと,src/ui/dialog/layers.cppがそれっぽいと分かりました。

とりあえずはこれをコピーして,src/ui/dialog/animation.cpp(と.h)にします。 こちらも忘れないうちに,src/ui/CMakeLists.txtにdialog/animation.cppを追記して,コンパイルの対象に追加しておきます。

GTK+を使ったGUIプログラミングをしたことがなかったので知らなかったのですが,レイヤー一覧はTreeViewというウィジェットで実現されているようです(ツリー構造でない2次元の表を表示する場合でも有用なウィジェットらしい)。それで,コピペしたコードから「tree」が関係していそうな部分を容赦なく削除していきました。さらに,要らないボタンの部分も削った結果,1000行あまりあったcppファイルは300行ほどまで減りました。

次にBlend modeのドロップダウンの行を消し,BlurとOpacityの片方だけを残そうとしたのですが,コードを見るとこれらはInkscape::UI::Widget::ObjectCompositeSettingsという一つのウィジェットになっているようでした。その中身を見ていくと,目的のスライダーはInkscape::UI::Widget::SpinScaleというウィジェットであることが分かりました。このウィジェットに置き換えたところ,いい感じになりました。

f:id:ika2018:20181104220604p:plain

(この時点では,スライダーを操作しても何も起きないし,+ボタンを押すとレイヤーが追加されてしまう)

それから,まずスライダーの操作を処理する関数を書いて,スライダーが操作されたときに実際にそれが呼ばれるように設定します。

しかし,ここで問題が発生します。スライダーが操作されたら,SPAnimateインスタンスのそれぞれに対してさきほど作ったsetAnimationTime(time)を呼べばよいのですが,文書中のSPAnimateの全インスタンスをどうすれば取得できるのでしょうか。

ふと他のダイアログのソースコードを眺めていたところ,src/ui/dialog/document-properties.cppSP_ACTIVE_DOCUMENT->getResourceList("script")などのコードを見つけました。どうやら文書中の<script>の一覧を取得していそうです。ここから調べていくと,以下のことが分かりました。

  • SPScript<script>に対応するクラス)はbuildメソッドの中でdoc->addResource("script", this)として,SPDocumentインスタンス(が持っているvector)に自分への参照を登録している
  • DocumentProperties(「ドキュメントのプロパティ」ダイアログ)からgetResourceList("script")として呼ぶとSPScriptの一覧がvectorとして取得できる
  • SPScript::release()の中でdoc->removeResource("script", this)を呼んで,自分への参照を消去している(地味だけど大事)

ちょうど自分がやりたいことにうってつけな雰囲気がします。というわけで

  • SPAnimate::build(doc, repr)doc->addResource("animate", this)を呼ぶ
  • SPAnimate::release()doc->removeResource("animate", this)を呼ぶ
  • ダイアログ側ではSP_ACTIVE_DOCUMENT->getResourceList("animate")としてSPAnimateの一覧を取得する

というようにしてみました。そして,スライダーが操作されたら

  1. SPAnimateインスタンス一覧を取得する
  2. それぞれのインスタンスに対してsetAnimationTime(time)を呼ぶ

という処理を行うようにしました。その結果……

f:id:ika2018:20181104233628p:plain

動いた!!! 👏👏👏👏👏(動画を撮っていなかったためただの静止画です(何も伝わっていないと思います))

なお,例によって+ボタンを押すとレイヤーが追加されます(いじっていないので)。

というわけで最後に+ボタンをちゃんと再生ボタンにして機能させていきます。

アイコンについては,ダイアログのコンストラクタ内でINKSCAPE_ICON("list-add")と指定している部分を変えればよさそうです。そこで,list-addアイコンを探してコピペして再生アイコンを用意すればよいのかと思って,リポジトリ内を「list-add」で検索してみたところ,画像ファイルは見つかりませんでした。ググってみると,よく使われるアイコンはアプリケーションを超えて共通化されているみたいで,Icon Naming Specificationというページにアイコン名と説明文の一覧があったので,そこからmedia-playback-startというアイコンを選び,それに変更しました。

ボタンを押したときの挙動は,まずボタンをトグルボタンに変更し,オンの間は再生し続け,オフにすると停止するという挙動にしようということになりました。

アニメーション再生の処理をどうするかについては,Glib::signal_timeout()というGTK+シグナルを利用するとアニメーションができるという記事をググっていたら見つけたので,これを使えばできそうだと考えました。これを使うと,指定した関数を指定したミリ秒おきに実行することができます。

ボタンのオンオフを切り替えたときの処理をしている関数を書き換えて,オン・オフに応じてアニメーションの再生を開始・停止する関数を呼ぶようにします。

アニメーション再生を開始する関数_animationStart()では,タイマーを開始して,Glib::signal_timeout()を使って_animationNext()という関数が1ミリ秒おきに実行されるようにします。

_animationNext()では,開始してから経過した時刻をタイマーから取得してスライダーにセットします。

結果,ちゃんとアニメーションが再生できるようになりました!(今度はちゃんと動画です)

(再生できない場合はこちら)

バグ

最後に開発中に行き当たったバグとその解決を試みた方法について軽く書き残しておきたいと思います。

アニメーション対象を選択した状態で再生ボタンを押すとフリーズする

アニメーションを含むファイルを読み込んでいざダイアログを使って再生しようとしたところ,読み込んですぐ再生した場合はうまく再生しました。しかし,描画された図形を選択した(図形の周りに大きさを変更する黒い矢印が出ている)状態で再生をしようとするとアプリケーションがフリーズして,操作できない状態になってしまいました。

原因をgdbを使って調査してみることにしました。フリーズを発生させてgdb側からCtrl-Cを押してみると,ほとんど選択対象を示す四角い枠を描画していそうなところで止まっていました。 とりあえず,アニメーションの次のコマを表示する_animationNext()を1ミリ秒ごとに呼ぶよう設定する際に指定しているpriorityを1つ下げてみたところ,フリーズすることはなくなったので一応の解決は為し得ましたが,このあたりの詳しいことはよくわかりませんでした。

プレゼンテーション属性がアニメーションしない

SVGopacityという属性は透明度を指定できます。0を指定すると透明(その要素は完全に見えない)に,1を指定すると不透明(後ろは完全に見えない)になります。例えば

<rect x="0" y="0" width="50" height="40">
  <animate
    attributeName="opacity"
    from="0" to="1"
    begin="0" dur="5"
  />
</rect>

とすれば長方形がフェードインするアニメーションができます。実際Firefoxなどのブラウザで見ればフェードインする様子が確認できます。しかしこれを改造Inkscapeで読み込んでみると,アニメーションしてくれませんでした(x, y座標などのアニメーションは動くのに)。

SVGで要素に指定できる属性に,プレゼンテーション属性という分類があります。主に,色とか見た目とかに関係する属性はこれに当てはまります。opacityもこの1つです。

プレゼンテーション属性はCSSとしても指定できます。例えば

<rect opacity="0.5" />

と指定すると透明度50%の長方形ができますが,

<rect style="opacity:0.5" />

CSSで指定しても同じ図形になります。(座標や大きさは省略しています)

Inkscapeでは,どちらの指定方法でも正しく読み込むことができますが,保存するとどちらも後者の形式に変換されてしまいます。実は,Inkscape内部ではプレゼンテーション属性はCSSの属性に変換され,通常の属性とは別の場所で管理されているのです。

SPAnimate内で補間した値を設定するときにはsetKeyValue(key, value)というメソッドを呼び出して用いていましたが,CSSの属性はこのメソッドでは設定することができませんでした。opacityのアニメーションが動かないのはこれが原因でした。

他のソースを眺めた結果,CSSの属性に対してはsp_desktop_apply_css_recursive(o, css, skip_lines)という関数を使って設定するようにしたらうまく行きました。

クローンされた要素がアニメーションしない

SVGでは<use>要素を使うと図形を再利用することができます。例えば

<ellipse id="myEllipse" cx="10" cy="5" rx="6" ry="5" fill="#f66">
  <animate attributeName="cx" dur="10" values="6;204;6" />
</ellipse>
<use xlink:href="#myEllipse" y="15" />

とすれば,左右に振動する楕円#myEllipseとそれを下に15動かしたクローンの楕円ができます。(縦に並んだ2つの楕円が同時に左右に振動するイメージです。)

しかし改造Inkscapeでこれを読み込むと,上の楕円はちゃんと左右に振動するけれど,下のクローンの楕円は動かないという事象が発生しました。

gdbでコードの実行を追いかけてみたところ,SPAnimateインスタンスを登録しているSPDocument::addResource(key, object)は,objectがクローンである場合は登録しないという処理になっていることが判明しました。addResourceの内部を書き換えて,key"animate"であるときはクローンであっても追加するように変更してみたところ,クローン側もちゃんと動くようになりました。

これをもって,限定的ではありますが,アニメーション再生機能をInkscapeに持たせるという当初の目的を果たすことができました。

記録のため,我々の変更内容をhttps://pastebin.com/egC6B7Bjに残しておきます。(基準となるコミットは1747c77aです。)トータルで830行くらい追加したみたいです(※ただし大部分がコピペです)。

総評

今回この「大規模ソフトウェアを手探る」という実験を通して初めてOSSのソースに触れて変更をしてみました。大変チャレンジングな内容でしたが,プログラムを解析・改善する上で必要なスキルや考えを総合的に学ぶことができたのでとてもためになりました。マージリクエストを出せるほどのものはできませんでしたが,我々でもある程度は中身を知らない大規模なソースコードに手を出せるという自信につながったので実用的な実験だったかと思います。

*1:原因は調べていませんが,apt-getで入るのはstableな版が依存するもので,我々がビルドしようとしていたのはmasterだったので,そこで差があったのかもしれません(推測でいっているだけなので違うかも)

*2:targetElement属性,href属性が指定されていない場合。