Inkscapeでレイヤの出力を便利にした~実装編~

この記事は「大規模ソフトウェアを手探る」という授業の最終レポートとして書かれました。

記事を複数まとめて1つのレポートとなっており、この記事の親記事はこちら、一つ前の記事はこちらです。 これが一連のうち最後の記事となります。

ベクタ画像編集ソフトであるInkscapeでは、CUIから画像をエクスポートできます。それに際して、2つ機能を追加しました。どちらもレイヤに関する機能追加となっています。 レイヤについての説明はコードを読むや、公式ページを参照してください。

以下に実際に追加した機能について説明していきます。

追加機能1: エクスポートの際に、レイヤ名で出力するレイヤを指定できるようにする。

実装結果

bash inkscape <ファイル名> --export-layer=レイヤ名1;レイヤ名2 のようにすると、それぞれ指定したレイヤが出力される。

使用例

説明のために、下図のようなInkscapeのファイルを用意しました。これはInkscapeでポチポチして作ったもので、mydrawing.svgという名前で保存しています。 これは下図のような構造になっており、レイヤの中に四角形や円などの図形がはいっていること、レイヤが入れ子になることができることが確認できます。レイヤ名に空白や記号が入っていても動作することを示すために、レイヤ名にそのような文字を含めています。

レイヤは全てで5つあり、それぞれ名前が"レイヤーー", "レイヤの 中のレイヤ", "L$Star", "L Circle", "LRect"となっています。

<sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.2282038" inkscape:cx="400.58497" inkscape:cy="657.46415" inkscape:window-width="1850" inkscape:window-height="1016" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer4" showguides="true" /> <inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="-12.170835 : 148.5 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="197.82909 : 148.5 : 1" inkscape:persp3d-origin="92.829132 : 99 : 1" id="perspective3" /> Layer 1 Layer2 Layer3 レイヤの中のレイヤ レイヤーー

構成

  • レイヤーー [Layer]

    • ピンクの四角形
    • "レイヤの中のレイヤ"という名前のレイヤ [Layer]
      • 緑の五角形
      • "レイヤの中のレイヤ"というテキスト
  • L$Star [Layer]

    • "Layer3"というテキスト
    • 黄色の星
  • L Circle [Layer]

    • "Layer2"というテキスト
    • 水色の円
  • LRect [Layer]

    • "Layer 1"というテキスト
    • 赤色の長方形

使用法

  • "LRect"という赤色の長方形を含むレイヤを出力したいときには、
    inkscape mydrawing.svg --export-layer="LRect" -o ./output/rect.svg
    とすることで出力できます。
    mydrawing.svgというのは、Inkscapのファイル名で-o ./output/rect.svgは出力するファイルを指定しています。これは、適当に変更することができます。
    実際のrect.svgは以下のようになります。

Layer 1 Layer2 Layer3 レイヤの中のレイヤ レイヤーー

  • 現在存在する他の--export-plain-svgなどといったオプションと同時に使うことができます。

  • "L\$Star"と"レイヤーー"という2つのレイヤを同時に出力したいときには、レイヤ名を";" (セミコロン)で区切ることで1コマンドで実行できます。 改善点の章でも説明しますが、この際は出力フォーマットを--export-typeで与える必要があります。
    inkscape mydrawing.svg --export-layer="L\$Star;レイヤーー" --export-type=svg

    "$"をエスケープする必要がありますが、記号やマルチバイト文字にも対応しています。

この機能を追加した背景

InkscapeCUIからファイルを出力する際、出力するオブジェクトを選択するには、--export-id="hoge"のようにオブジェクトのIDを指定する必要がありました。IDは、InkscapeGUIから指定・変更できるものではないため、ファイルをテキストエディタで開いて確認する必要があります。ユーザが容易に変更・確認できるレイヤ名から出力するオブジェクトを指定できれば便利になります。

実装方針

ここからは少し細かい話になりますが、どのように実装していったかを説明します。適宜飛ばしてください。
詳しい話はコードを読むに譲りますが、gdbなどを駆使してエクスポートの際の挙動を確認していきました。
主要なポイントは、エクスポートの際に、--export-idで指定されたオブジェクトのIDから実際のオブジェクトを見つけ、save()という関数を読んでファイルに出力しているということです。
save()関数の実装を変更するということも考えられますが、実装が容易で変更点が少なくて済む方法があります。それが、レイヤ名に対応するオブジェクトのIDを見つけ出し、既存のIDから出力する仕組みを利用するという方法です。

実装内容

実際のコードはこちらのgithubレポジトリから確認できます。 以下で簡単に説明していきます。実装の肝である、1.)どのようにレイヤ名を保持するか、2.)どのようにレイヤ名からIDを見つけるかという2点を説明します。

  1. InkFileExportCmdクラスにexport_layerというメンバ変数を追加する。
    このInkFileExportCmdクラスはCUIからのエクスポートに関する情報をすべて持っています。コマンドラインで与えたオプションはこのクラスのメンバ変数に対応するようになっています。
// src/io/file-export-cmd.h
class InkFileExportCmd {
public:
    InkFileExportCmd();
    void do_export(SPDocument* doc, std::string filename_in="");
private:
    ExportAreaType export_area_type{ExportAreaType::Unset};
    Glib::ustring export_area{};
    guint32 get_bgcolor(SPDocument *doc);
    std::string get_filename_out(std::string filename_in = "", std::string object_id = "");
    int do_export_svg(SPDocument *doc, std::string const &filename_in);
    int do_export_vector(SPDocument *doc, std::string const &filename_in, Inkscape::Extension::Output &extension);
    int do_export_png(SPDocument *doc, std::string const &filename_in);
    int do_export_ps_pdf(SPDocument *doc, std::string const &filename_in, std::string const &mime_type);
    int do_export_ps_pdf(SPDocument *doc, std::string const &filename_in, std::string const &mime_type,
   // <中略>
    double        export_dpi;
    bool          export_ignore_filters;
    bool          export_text_to_path;
    int           export_ps_level;
    Glib::ustring export_pdf_level;
    bool          export_latex;

   /* ここが変更点 */
    Glib::ustring export_layer; 

2. レイヤ名からIDを見つける。 これは3段階に分かれます。

2.1. レイヤを全て取得する。
幸運なことに、指定した種類のオブジェクトを全て取得する関数getResourceList()がすでに存在していました。この関数にkey="layer"を渡すことで、レイヤ全てを取得することができます。 詳しくはこちらへ。src/document.cpp)

// src/document.cpp
std::vector<SPObject *> const SPDocument::getResourceList(gchar const *key)
{
    std::vector<SPObject *> emptyset;
    g_return_val_if_fail(key != nullptr, emptyset);
    g_return_val_if_fail(*key != '\0', emptyset);

    return resources[key];
}
// src/inkscape-application.cpp L1027
  auto layers = document->getResourceList("layer");

2.2. レイヤから、レイヤ名とレイヤのオブジェクトIDを取得し、std::mapに格納する。
  愚直にfor文を回します。レイヤ名はlayerをレイヤとして、layer->_label、オブジェクトIDは、layer->getId()とすることで容易に得られます。

// src/inkscape-application.cpp L1028
        std::map<std::string, std::string> label_id_map;

        for (auto &layer: layers) {
            std::string label = layer->_label;
            std::string id = layer->getId();
            label_id_map.insert(std::make_pair(label,id));
        }  for (auto &layer: layers) {
            std::string label = layer->_label;
            std::string id = layer->getId();
            label_id_map.insert(std::make_pair(label,id));
        }

2.3. レイヤ名をパースした後、export-layerとして保持したレイヤ名から、レイヤIDを探索し、export_idという出力するオブジェクトのIDを持つ、InkFileExportCmdクラスのメンバ変数に格納する。

// src/inkscape-application.cpp 
// Concatinates layer-ids with ";" as a separator so that multiple layers can be exported.
// export_id is not empty when export_id is specified by the user.
  if(_file_export.export_id != "") {
      _file_export.export_id += ";";
  }
  for (auto &layer_label: export_layer_label_lists) {
      try{
          _file_export.export_id += label_id_map.at(layer_label);
      } catch (const std::out_of_range& ex) {
          std::cerr << "Layer name: '" << layer_label << "' not found.\r\nExport failed." << std::endl;
          return;
      }

      if (&layer_label != &export_layer_label_lists.back()) {
          _file_export.export_id += ";";
      }
  }

3. コマンドラインからexport-layer=としてレイヤ名を受け取れるようにする。 src/actions/actions-output.cppと、src/inkscape-application.cppに他のオプションの受け取り方の実装を参考にコードを追加します。

追加機能2: エクスポートの際に、すべてのレイヤを一括で出力できるようにする。

実装結果

bash inkscape <ファイル名> --export-all-layers --export-type=svg とすると、全てのレイヤがそれぞれ別のsvgファイルで出力される。svgに限らずpngなどInkscapeのエクスポートで対応している出力フォーマットに対応します。参考: man page

使用例

追加機能1と同じmydrawing.svgを使います。
inkscape mydrawing.svg --export-all-layers --export-type=svg とすることで、全ての5つのレイヤが出力されます。レイヤが入れ子になっていても、全て出力されます。--export-type=のように出力フォーマットを指定することは必須になっています。

この機能を追加した背景

Layerを多数使用して、例えばアニメーションを作っているときに、それぞれ1つずつ出力するのは面倒であってCUIで一発で出力できると便利であるからです。

実装方針・内容

追加機能1の内容をほとんど使いまわすことができます。したがって実装も追加機能1の際とほとんど変わりません。
1. InkFileExportCmdクラスにexport_all_layersというメンバ変数を追加する。
2. 全レイヤのIDを見つける。
2.1.レイヤを全て取得する。
2.2. レイヤのオブジェクトIDを全て取得する。
2.3. export_idに";"(セミコロン)区切りで連結し格納する。という流れです。

// src/inkscape-application.cpp
    if (_file_export.export_all_layers) {
        auto layers = document->getResourceList("layer");

        _file_export.export_id = "";
        for (auto it = layers.begin(); it != layers.end(); ++it) {
            _file_export.export_id += (*it)->getId();
            if (std::next(it) != layers.end()) {
                _file_export.export_id += ";";
            }
        }

3. あとは、コマンドラインから--export-all-layersとしてオプションを指定できるようにするだけです。

改善点

ここまで追加したエクスポートの機能のさらなる改善点を挙げていきます。改善点は主にエクスポートの実装の際に--export-id=の機能を利用したことに由来します。

  • 複数レイヤを出力する時に、出力先ファイル名の指定ができない。
    --export-layer="hoge;fuga"としたときや、--export-all-layersとしたときに、-o ./output/hoge.svg,fuga.svgのように出力ファイルを指定することができない。
    これは、--export-idでIDを指定して複数したときに、-o オプションなどで出力ファイルを指定できないことに由来します。
  • 複数レイヤを出力する際は、--export-type=svgなどと、出力形式を指定する必要がある。
  • 対象のレイヤのみを重なりなしで出力する際は、--export-id-onlyとつけなければならない。
    これはややわかりにくいので例を挙げて説明します。例えば上のmydrawing.svgの中で、赤の長方形と水色の円は一部重なって存在しています。--export-id-onlyなしでは、赤の長方形を含むLRectのみを出力したいときに、円の一部が重なって表示されてしまいます。長方形のみを出力したいときには、このオプションが必要です。   改善点となるのは、idを指定しているわけではないのに、idがオプションに入っているため分かりづらいという点です。

FAQ

  • export-idとexport-layerを両方指定した場合は?

    • どのように指定したかにかかわらず、指定したオブジェクトすべてが出力されます。
  • export-idとexport-layerで同じレイヤを2重に指定したら?

    • 指定したレイヤのファイルが1つだけ出力されます。おそらく同じファイルを重複して指定しただけ上書きされているはずです。[要検証]
  • export-layerが間違っている場合は?

    • std::map::at()で例外が発生し、プログラムが終了します。
  • export-layerに;が入っている場合は?

    • 現在のところ非対応です。該当するレイヤがないというエラーになります。
      パースの方法を工夫すると、対応できるかもしれません。

リンク