LibreOffice Drawを位置・図形データ編集用GUIフロントエンドとして使う

これは [LibreOffice Advent Calendar 2019](https://adventar.org/calendars/4230) の 3日目の記事です！

※ 2019-12-03 に書いた記事をブログから引っ越してきました



<!--
LibreOffice Drawのodgファイルから図形の情報を抜き出して使う

// LibreOffice Drawをグラフィカルなデータを編集するための汎用エディタとして使う
// LibreOffice Drawを位置・図形データ編集用GUIフロントエンドとして使う
-->

# TL;DR

- プログラムに入力として与えるデータの編集をどうするか問題
- 位置情報などはテキストで管理すると直感的に修正できなくて辛い
- LibreOffice Draw で編集して odg ファイルから情報を抜き出して使う方法を試してみたらいい感じだった

<!--
  - ※ 逆（何らかのデータから odg ファイルを生成・編集する）はこの記事では扱いません
-->


# 動機

<!--
手短に。
-->

- プログラムに入力として与えるデータを用意したい
  - ゲームのマップ、オブジェクトの配置など
  - アルゴリズムや分析処理、作図ツールの検証に使うデータ
  - etc.
- ちょっとしたものならプログラム内に直接書いたりテキストデータとして用意したり
- 「ちょっとした」で済まなくなってくると辛い
  - 位置情報
  - 構造が複雑
  - データが多い
- どう辛いか
  - 直感的に編集できない
    - 一度2Dのグラフィックに変換しないと何がどうなっているのか分からない
      - 配置、要素同士の位置関係、サイズ、オブジェクトの種類、属性、etc.
    - 編集→表示させて確認→編集… を繰り返さないといけなくて手数が増えてめんどくさい

- こういう場合、WYSIWYG なエディタが欲しくなる
  - 出来合いのツールがあればそれを使えばいいが、ない場合は……
  - 自作する？
  - GUI自作は大変
    - コピペ、D＆D、アンドゥ/リドゥ、ズーム表示
  - 大変なので諦めてがんばりがち
    - 適当な可視化ツールだけ作ってお茶を濁したりしがち
  - エディタがあれば作業効率上がるはずなのに……コストが見合わない
  - 特にすばやくプロトタイプを作りたい場合、手間をかけずにサッと使いたい

そこで、LibreOffice Draw を汎用エディタとして使えないか？ と考えました。



# 矩形

さっそくやってみましょう。まずは基本ということで、矩形の位置とサイズを odg ファイルから抜き出してみます。

※ odg ファイルと書いてますが、以下では Flat XML な fodg ファイルを使います。odg でもだいたい同じだと思います。

<!--
// そのまま読めますし、バージョン管理もやりやすいでしょう。
-->

Draw でこんな図形を描きます。

<!-- sample_rectangle
[f:id:sonota88:20191111070209p:plain]
[f:id:sonota88:20191201055353p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/de673251-e9f4-a645-79e8-38215752f993.png)


fodgファイルの大まかな構造はこうなっています。

```xml
<office:document>
  <!-- メタデータ、スタイルの設定など -->
  <office:body>
    <office:drawing>
      <draw:page draw:name="page1" ... >
        ここに図形の記述が並ぶ
      </draw:page>
      <draw:page draw:name="page2" ... >
        ここに図形の記述が並ぶ
      </draw:page>
...
```

fodg ファイルには複数ページのデータが含まれていますが、今回は 1ページ目だけを使い、2ページ目以降は無視します。

「ここに図形の記述が並ぶ」の部分を見てみましょう。

```xml
<draw:custom-shape draw:style-name="gr1" draw:text-style-name="P1" draw:layer="layout"
  svg:width="9.5cm" svg:height="3.8cm"
  svg:x="1.9cm" svg:y="2.9cm"
>
  <text:p text:style-name="P1">box1<text:line-break/>aa</text:p>
  <text:p text:style-name="P1"/>
  <text:p text:style-name="P1">bb</text:p>
  <draw:enhanced-geometry svg:viewBox="0 0 21600 21600"
    draw:type="rectangle"
    draw:enhanced-path="M 0 0 L 21600 0 21600 21600 0 21600 0 0 Z N"
  />
</draw:custom-shape>

<draw:custom-shape draw:style-name="gr2" draw:text-style-name="P1" draw:layer="layout"
  svg:width="2.5cm" svg:height="7.1cm"
  svg:x="13.2cm" svg:y="1.7cm"
>
  <text:p text:style-name="P1">box2</text:p>
  <draw:enhanced-geometry svg:viewBox="0 0 21600 21600"
    draw:type="rectangle"
    draw:enhanced-path="M 0 0 L 21600 0 21600 21600 0 21600 0 0 Z N"
  />
</draw:custom-shape>
```

`draw:type="rectangle"` の部分を見ることで矩形であることが判別でき、`svg:width`, `svg:height`, `svg:x`, `svg:y` の部分から位置とサイズが抽出できそうですね。あとテキストも取れそうです。

---

Ruby と、標準ライブラリ REXML を使ってスクリプトを書きます。
（Ruby に馴染みのない方のためにここだけ `return` を省略しないスタイルで書いています）

<!--  -->
```ruby
# coding: utf-8
require "rexml/document"

def xpath_match(el, xpath)
  return REXML::XPath.match(el, xpath)
end

def extract_pages(doc)
  return xpath_match(doc, "//draw:page")
end

def extract_rectangles(page)
  custom_shape_els = xpath_match(page, "draw:custom-shape")

  rect_els = custom_shape_els.select { |el|
    geo_el = xpath_match(el, "draw:enhanced-geometry")[0]
    geo_el["draw:type"] == "rectangle"
  }

  return rect_els
end

# 手抜き実装。改行が失われます。
def extract_text(el)
  texts = []
  el.each_element_with_text { |el|
    texts << el.texts.join(" ")
  }

  return texts.join(" ")
end

def print_rectangle(rect_el)
  print "x="       , rect_el["svg:x"]
  print ", y="     , rect_el["svg:y"]
  print ", width=" , rect_el["svg:width"]
  print ", height=", rect_el["svg:height"]
  print ", text="  , extract_text(rect_el)
  print "\n"
end

# --------------------------------

xml = File.read("sample_rectangle.fodg")
doc = REXML::Document.new(xml)

pages = extract_pages(doc)

rect_els = extract_rectangles(pages[0])

rect_els.each { |rect_el|
  print_rectangle(rect_el)
}
```

実行結果:

```
$ ruby extract_rectangles.rb 
x=1.9cm, y=2.9cm, width=9.5cm, height=3.8cm, text=box1 aa bb
x=13.2cm, y=1.7cm, width=2.5cm, height=7.1cm, text=box2
```

抽出できました！ `x`, `y` はページ左端、上端の余白を含めた値になっているようです。

<!--
矩形だけでも、たとえばこういうファイルを作って機械学習の検証に使うとか、他にもいろんな用途に使えそう。

[f:id:sonota88:20191111071037p:plain]
-->



# コネクタ

次の例としてコネクタです。

Draw でこんな図を描きます。

<!--
sample_connector.fodg
// 画像 ★ 要修正
[f:id:sonota88:20191111071449p:plain]
[f:id:sonota88:20191201061646p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/f4da742c-5078-bd76-c189-2462a40df905.png)

ここから次のような情報が抜き出せればOK。

```
box1 => box3
box2 => box3
box3 => box4
```

XML を見るとこんな感じです。コネクタが繋がっている場合は矩形要素に id が振られます。

```xml
<draw:custom-shape draw:style-name="gr1" draw:text-style-name="P1"
  xml:id="id2" draw:id="id2"
  draw:layer="layout" svg:width="2.6cm" svg:height="5.7cm" svg:x="9.9cm" svg:y="1.8cm"
>
  <text:p text:style-name="P1">box3</text:p>
  <draw:enhanced-geometry svg:viewBox="0 0 21600 21600" draw:type="rectangle" draw:enhanced-path="M 0 0 L 21600 0 21600 21600 0 21600 0 0 Z N"/>
</draw:custom-shape>

<draw:connector draw:style-name="gr2" draw:text-style-name="P2" draw:layer="layout" draw:type="curve" svg:x1="6.6cm" svg:y1="2.55cm" svg:x2="9.9cm" svg:y2="4.65cm"
  draw:start-shape="id1"
  draw:start-glue-point="1"
  draw:end-shape="id2"
  svg:d="M6600 2550c2475 0 825 2100 3300 2100" svg:viewBox="0 0 3301 2101"
>
  <text:p/>
</draw:connector>
...
```

やってみます。同様の記述が多くなるのでコードは gist に貼りました。

<!--
`xml:id` と `draw:id` の使い分けがよく分かってないですが、
以下ではひとまず `draw:id` の方を使っています。
-->

[https://gist.github.com/sonota88/4a2221def064e675cabfce1a9266d48f#file-extract_connectors-rb](https://gist.github.com/sonota88/4a2221def064e675cabfce1a9266d48f#file-extract_connectors-rb)


実行結果:

```
$ ruby extract_connectors.rb 
(id1) box1 => (id2) box3
(id3) box2 => (id2) box3
(id2) box3 => (id4) box4
```

いけますね。



# 応用編

コネクタを同じ箇所に複数つなげるとこのような見た目になります。

<!--
[f:id:sonota88:20191112072421p:plain]
[f:id:sonota88:20191201062408p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/945885ea-60e6-c69f-5e0d-176b23e00f3e.png)


これ、矢印が重なると分かりにくいんですよね。この例でいえば、上から3番目のコネクタは両方向の矢印なのかな？ とか、矢印が両方ともないコネクタもあるのかな？ とか。

このように矢印がはっきり見えないと困るときや、コネクタの接続箇所の位置を調整したいとき、私はよくこういう描き方をします。

<!--
★要修正
[f:id:sonota88:20191112072429p:plain]
[f:id:sonota88:20191201062442p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/3c3b2cd5-b19f-7716-26a7-d86a8c8bb531.png)


<!--
たとえば、上から3番目のコネクタは双方向の矢印なのか？ とかが分かりにくいと思うんですよね。
-->

<!--
範囲選択でまとめて選択すれば一緒に移動できてちょっと便利。
-->

ちなみに、まとめて選択すれば一緒に移動できます。


<!--
★要修正
[f:id:sonota88:20191111072307p:plain]
[f:id:sonota88:20191201062750p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/b29b968b-e083-2504-7c50-2d146649b415.png)


この描き方を使ってさっきのコネクタの図を描き直してみました。今度はこの図から依存関係を抜き出してみましょう。

<!--
[f:id:sonota88:20191111072708p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/a23974b0-c963-e20a-3101-c9eae746be52.png)


こういうのが抜き出せればOK。上のコネクタの例と同じですね。

```
box1 => box3
box2 => box3
box3 => box4
```

この場合は単に抜き出すだけではなく、加工が必要です。

詳しくはコードを見ていただくとして、考え方としては

- 矩形の重なりを判定して、どの矩形がどの矩形と繋がっているかを調べる
- コネクタがテキストなし矩形に繋がっている場合は、
  そこから辿ってテキストあり矩形を探す

みたいな感じですね。

[https://gist.github.com/sonota88/4a2221def064e675cabfce1a9266d48f#file-extract_connectors_2-rb](https://gist.github.com/sonota88/4a2221def064e675cabfce1a9266d48f#file-extract_connectors_2-rb)


```
$ ruby extract_connectors_2.rb 
(id1) box1 => () box3
(id3) box2 => () box3
() box3 => (id6) box4
```

いいですね。

---

<!--
試しにもっとそれっぽい例で試してみましょう。
-->

もっとそれっぽい例で試してみましょう。達人プログラマー（ピアソン・エデュケーション版 p156）に載っている、ピニャ・コラーダの作り方を記述したアクティビティ図（UML の一種）です。

<!--
[f:id:sonota88:20191111073102p:plain]
[f:id:sonota88:20191112070228p:plain]
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/173f862d-ca55-7be7-29dd-9b0d6466245c.png)


要素は増えていますが、さっきの例と同じルールで描いているので、さっきのスクリプトで同じように抽出できるはず！

ここから抜き出した結果が下記です。

```
(id1) 2_ミックスを開ける => () join1
(id3) 1_ブレンダーを開ける => () join1
() join4 => (id6) 12_サーブする
(id3) 1_ブレンダーを開ける => (id7) 6_氷を2カップ入れる
(id8) 11_ピンクの傘を用意する => () join4
(id7) 6_氷を2カップ入れる => () join3
() join1 => (id12) 3_ミックスを入れる
(id12) 3_ミックスを入れる => () join3
(id14) 4_ラムを計る => () join2
(id16) 10_グラスの用意をする => () join4
(id18) 9_ブレンダーを開ける => () join4
(id20) 5_ラムを入れる => () join3
(id3) 1_ブレンダーを開ける => () join2
() join2 => (id20) 5_ラムを入れる
(id24) 8_かき混ぜる => (id18) 9_ブレンダーを開ける
(id25) 7_ブレンダーを閉める => (id24) 8_かき混ぜる
() join3 => (id25) 7_ブレンダーを閉める
```

<!--
アクティビティ図からタスクの依存関係を抜き出すツールの一丁あがりです。
ちょろい！
-->

アクティビティ図からタスクの依存関係を抜き出すツールができていました。ちょろい！



<!--
# 円の交点を求める

例をもうひとつ。

このような図を描いて、円の交点を探すというお題です。
「テキストで管理すると辛い」の例として分かりやすいかなと思って。

// TODO
-->



----

というわけで、矩形とコネクタの情報を抜き出す例を紹介しました。自分がよく使う図形と用途に合わせたやり方を把握しておくと低コストで汎用エディタが用意できそうですね（これをもっと早く思いついていればなあ〜）。

今回は矩形とコネクタだけを扱いましたが、線や円など他の図形を使ったり、レイヤーやスタイルの情報も利用するとさまざまな活用ができそうです。


# その他の図形

- [LibreOffice Drawのファイルから図形の情報を抜き出す（直線）](https://memo88.hatenablog.com/entry/2020/02/08/113757)




# （追記 2020-05-09） 例: 回路図エディタ

<!--
リレー式論理回路シミュレータを自作して1bit CPUまで動かした - memo88
https://memo88.hatenablog.com/entry/2020/05/03/132253
-->

<!--
[https://memo88.hatenablog.com/entry/2020/05/03/132253:embed:cite]
-->
https://memo88.hatenablog.com/entry/2020/05/03/132253

そうそう、こういうのがやりたかったんですよ、という具体例。こういうの作りたいなーと思った時にサッと作れるようにしたかったのです。

<!--
f:id:sonota88:20200411105424p:plain
-->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/4e9a573e-6ac5-3685-b244-8b3ec206974e.png)


こういう回路図を Draw で描いて、自作の論理回路シミュレータで読み込んで動かしてみました。見ての通りですが、使っているのは直線、矩形、矩形内のテキストだけです。





# 関連

<!-- Using LibreOffice draw as base for graphical programming of Embedded Control solutions :: LibreOffice Conference 2024 :: pretalx -->
https://events.documentfoundation.org/libreoffice-conference-2024/talk/3ANLAR/

- [発表の動画（YouTube）](https://www.youtube.com/watch?v=ntpdn5Ldkic)

> The tool reads the content.xml and translates it into a suitable target language (usually C/++).

とのことで、似た方向性のようです。




---

せっかくのアドベントカレンダーですのでいくつか宣伝ぽく LibreOffice 関連記事へのリンクを貼ってみます。

- 2019-12-02 [JavaでLibreOffice Calcのfodsファイルを読み書きするサンプル 2019](https://memo88.hatenablog.com/entry/2019/12/02/070925)
- 2019-12-02 [JRubyでLibreOffice Calcのfodsファイルを読み書きするサンプル 2019](https://memo88.hatenablog.com/entry/2019/12/02/230545)
- 2019-11-03 [LibreOffice Calcの入ったDockerイメージを作ってヘッドレスで動かす](https://memo88.hatenablog.com/entry/2019/11/03/090019)
  - Docker べんり
- 2014-12-16 [JavaScript（jrunscript/Rhino）でodsファイルからMySQLにデータ投入](https://memo88.hatenablog.com/entry/20141216/1418742273)
  - 5年前の LibreOffice アドベントカレンダー向けに書いたもの




# （追記 2019-12-07）テキスト抽出の改良

<!--
改行 `text:line-break` 要素を LF に変換して
段落を一つの文字列にし、文字列の配列として返すメソッドを書いてみました。
-->

図形内のテキストを文字列の配列として返すメソッドを書いてみました。改行（ `text:line-break` 要素）を LF に変換して段落を一つの文字列にしています。

`["box1\naa", "", "bb"]` のような配列を返すので、全部繋げて一つの文字列にしたい場合は `extract_paragraphs(el).join("\n")` のように使えばよいかと。

```ruby
def extract_paragraphs(el)
  para_els = xpath_match(el, "text:p")

  para_els.map { |para_el|
    para_el.children
      .map { |child_el|
        case child_el
        when REXML::Text
          child_el.value
        when REXML::Element
          if child_el.name == "line-break"
            "\n"
          else
            raise "unknown element"
          end
        else
          raise "unknown element"
        end
      }
      .join("")
  }
end
```

# 他に LibreOffice 関連で書いたもの

https://memo88.hatenablog.com/archive/category/LibreOffice

# 他に Ruby 関連で書いたもの

https://memo88.hatenablog.com/archive/category/Ruby