Monthly Archives: 6月 2024

2024-06-22 ,

org-gotoで日本語で検索(インクリメンタルサーチ)できない不具合を修正

org-modeで C-c C-j (org-goto) を実行し、日本語(マルチバイト文字)を入力すると次のようなエラーが出てインクリメンタルサーチできない(Org9.7.3時点)。

funcall-interactively: Wrong type argument: stringp, [12375]

consult-org-heading なり consult-outline なりを使えば良いような気もするけど気持ち悪いので一応直しておく(org-goto-local-auto-isearchに対する修正)。

;; org-gotoで日本語検索が出来ない問題を修正
(with-eval-after-load "org-goto"
  ;; 関数を置き換える。
  (defun org-goto-local-auto-isearch ()
    "Start isearch."
    (interactive)
    (let ((keys (this-command-keys)))
      (when (eq (lookup-key isearch-mode-map keys) 'isearch-printing-char)
        (isearch-mode t)
        ;; ここから修正
        ;; 元: (isearch-process-search-char (string-to-char keys))
        (cond
         ((vectorp keys)
          (when (< 0 (length keys))
            (let ((ch (aref keys 0)))
              (when (integerp ch)
                (isearch-process-search-char ch)))))
         ((stringp keys)
          (isearch-process-search-char (string-to-char keys))))
        ;; ここまで修正
        (font-lock-ensure)))))

this-command-keys はキーシーケンスを文字列で返すこともあればvectorで返すこともあるのだとか(Strings of Events (GNU Emacs Lisp Reference Manual))。

2024-06-19

xrefでメソッドへジャンプできなくなる問題に対処する

Emacs Lispをいじっているときに、cl-defmethodで定義したメソッドへM-.(xref-find-definitions)でジャンプできなくなることがたまにあったので次のように対処しました。(Emacs29時点)

;; xrefでメソッドに飛べなくなるのを回避するハック。
;; defgenericを使わずにdefmethodをloadし直すと起きる問題に対処する。
;; `elisp--xref-find-definitions:around'から`find-lisp-object-file-name'を
;; 呼び出したときの挙動を変更する。
(defun my-elisp--xref-find-definitions:around (old-fun &rest args)
  (cl-letf* ((flofn-old (symbol-function 'find-lisp-object-file-name))
             ((symbol-function 'find-lisp-object-file-name)
              (lambda (object &rest flofn-args)
                (or (apply flofn-old object flofn-args)
                    ;; 本来のfind-lisp-object-file-nameの結果がnullでかつ
                    ;; OBJECTがgeneric関数シンボルなら
                    ;; 空のファイル名を返すことで
                    ;; elisp--xref-find-definitions内の
                    ;; メソッド列挙部に到達させるハック。
                    ;; 本来ならその部分のFIXMEにも書いてある通り
                    ;; その部分を`elisp-xref-find-def-functions'に分離
                    ;; すべきだと思われる。
                    (when (and (symbolp object) (cl--generic object))
                      "")))))
    (apply old-fun args)))
(advice-add 'elisp--xref-find-definitions :around
            'my-elisp--xref-find-definitions:around)

根本的な原因は私がcl-defgenericせずにcl-defmethodしているからなのですが、まぁ、そこは良いんです。かったるくてやってられないから省いているだけので。それがダメというならmy-defmethodでも作らにゃなりません。しかし、まぁ、そのツケが回ってきたというだけの話ではあります。

xref等はsymbol-file関数を使ってシンボルを定義したファイルを特定しますが、そのsymbol-fileload-history変数に記録されたファイルとシンボル定義の対応表(alist)を参照します。elファイルをloadするとその過程で定義されたシンボルがこの変数に記録されるわけですが、cl-defmethodは既にgeneric関数が定義されているときには新たにgeneric関数を定義しないので、同じelファイルを2回ロードすると暗黙的に作成されたgeneric関数のファイル名が消えてしまいます。elisp--xref-find-definitionsはgeneric関数のファイル名が取得できないとメソッドのファイル名も列挙しないので、結果定義されているはずのメソッドに飛べなくなるわけです。

上の変更では、generic関数のファイル名が特定できなかった場合に空文字列のファイル名を返すことで特定できたことにして、メソッドの列挙部分に無理矢理処理を通します。幸いなことにxrefは空文字列のファイル名を無視してくれる(ジャンプ先候補に出さない)ようです。

ろくなもんじゃありませんけど、とりあえずはこれで。

ちゃんとやるならelisp--xref-find-definitionsのFIXMEコメントに書かれているようにgeneric&method列挙部分をelisp-xref-find-def-functions変数に登録する関数としてくくりだした上で、その中でgeneric関数のファイル名が特定できなくてもメソッドのファイル名を列挙するようにするのが良さそうです。

今回はシンボルと定義ファイル名の割り出し方法に関するお勉強でした。

2024-06-08 ,

org-inline-image-fixのorg-mode 9.7対応

org-modeでインライン画像化する画像形式を限定するの続き。

org-mode 9.7がリリースされたので関係する変更点を調べてorg-inline-image-fixに必要な修正を加えました。

9.7のインライン画像周辺の変更点としては次のものが見つかりました:

いくつかは前回対応しましたし、取りこむ必要が無いものもあります。

  • org-image-max-widthの追加
  • org-image-alignの追加

の二つは一応取りこんでおくことにしました。

org-image-max-width は org-limit-image-size と機能が被りますが、それぞれ独立して機能するのでどちらを使っても問題ありません。私は高さの制限が出来る後者を使い続けます。まれに縦長の画像に出くわすことがあるので。

org-image-align の方は、私は中央寄せや右寄せの指定を普段しないので多分使わないと思います。一応試したらこんな感じになりました:

Org9.7のインライン画像alignプレビュー機能を使ってみたところ
図1: Org9.7のインライン画像alignプレビュー機能を使ってみたところ

一箇所、インライン画像の右側にある空白を画像のオーバーレイに含めてしまう(空白を消して表示する)修正に疑問があったので、本家とは違う修正をしました。おそらく中央寄せや右寄せにするときに右側に空白があると完全な位置に寄らないのでそれを解消する意図があるのだと思いますが、個人的には空白が(存在するのに)勝手に消えて表示されるのはあまり好ましくないような気がします。とりあえず左寄せの時は従来通りに必ず空白を残すようにしておきました。

2024-06-08 ,

インポートとジェネレータ

el-easydraw(以下edraw)の最近の変更の中で比較的大きかったのはインポート機能でしょうか。

これは元々edrawが一部のSVGを正しく読み込むことが出来ないという問題を指摘されたことから開発に至った機能です。

元々edrawは膨大なSVG仕様の全てに対応する気はさらさら無く、edrawによって作成・出力したSVGのみが再編集可能です。edrawが出力したSVGには <g id="edraw-body">...</g> という形のグループ要素がありその中が唯一編集可能な領域になっています。idがedraw-bodyなグループ要素(つまりg#edraw-body)の中にはedrawが対応しているものしか入っていない前提で作られているため、無理矢理そこに何かを入れたとしても動作は保証されません(いや、まぁ、どのみち誰も何も保証しませんが)。とは言えその方は他のツールで作成したSVGを持ってきたかったらしく、そのための仕組みとして最終的にインポート機能を作ることになったわけです。

特にedraw-modeを使うと任意のSVGファイル(を開いたバッファ)で作図エディタを起動することが可能だったのですが、そんなことをしても当然そのSVGの中にあった図形を編集できるわけも無く、せいぜいその上に新しい図形を乗せていくことくらいしか出来ませんでした。

現在では、他で作った(g#edraw-bodyが存在しない)SVGでM-x edraw-modeを実行するとエラーが出て、諦めるか自己責任で M-x edraw-convert-buffer-to-edraw-svg-xml を実行して変換するよう勧めるメッセージが表示されます。実際に edraw-convert-buffer-to-edraw-svg-xml を使用すると、バッファ内のSVGがedrawが扱えそうな形に変換されます。対応していない要素は除去されたり警告が出たりします。そして再度 M-x edraw-mode を実行すると、運が良ければ編集できることでしょう。

作図エディタの中からでもメインメニューの「ドキュメント」→「ファイルからインポート...」を選択して任意のSVGファイルを選べばそれを変換・取りこむことができます。

試しにInkscapeで作成したSVGを読み込んでみましたが、まぁ、思ったよりは取りこめる感じでした。他にもGraphvizで作成したSVGを取りこんだりも出来ました(ただしグラフ構造を手軽に再編集できるようなものではありません)。その方はdvisvgmの出力を取りこみたかったようですが、それも最終的にはうまく取りこめるようになりました。

dvisvgmを使用すればlatexで生成したものをSVGに変換できます。つまり、例えば数式をSVGに変換することが可能です。そしてそれを取りこめるようになったわけです。

しかしこのままでは数式を図の中に取りこむには手間がかかりすぎます。まずlatexのソースを書いて、latexでdviを作成し、それをdvisvgmでsvgへ変換し、edrawでインポートしなければなりません。もっと効率よく出来る仕組みが必要です。

私も数式を図に取りこめたら良いだろうなと思ったことはあったので、そのための仕組みを作ることにしました。

「数式ツール」のようなものを作っても良かったのですが、それでは直接的すぎて面白くありません。そこで考えたのが「ジェネレータ」です。

ジェネレータは何らかの設定から図形を生成するような(図形として配置可能な)オブジェクトです。ジェネレータツール(生成ツール)で配置できます。

ジェネレータにはプロパティとして生成の元(ソース)となるような情報を指定出来ます。まず第一に生成タイプがあり、今のところlatexかgridが指定出来ます。次に生成ソースを指定するプロパティがあり、タイプがlatexの時はそこにLaTeXのソースコードを記述できます。最後に生成オプションがあり、これはタイプによって異なる追加のプロパティを色々設定できます。

そして生成ボタンを押すとそれらのソース情報を元に図形が生成されるという寸法です。latexの場合は、preamble部分(カスタマイズ変数で変更可能)などと合成されてlatex、dvisvgmコマンドが呼び出され、生成されたSVGがedraw用に変換されてジェネレータの子要素として挿入されます(もちろんlatexとdvisvgmが必要です。私はTeX Liveでインストールしましたが、MSYS2にPATHが通っていると色々ハマるのでご注意を)。

latexジェネレータで数式を生成したところ
図1: latexジェネレータで数式を生成したところ

生成タイプのgridは格子状の線を生成するものです。生成タイプがlatexだけでは格好が付かないので適当に作ってみました。線が太くて気に入らないという方は位置を0.5ピクセルずらしてみて下さい。「これもうソースコード要らないじゃん!」ということが発覚してちょっと後悔しています。

数式をSVGに入れるならMathMLを使うという手もあります。実際latexからMathMLに変換してforeignObject要素として取りこむような生成タイプを作ることもできるでしょう。しかし一番のネックはlibrsvgが対応していないのでEmacsの中で表示されないことでしょうね。

latexの中で(tikzで)グラフを生成することもできるみたいです。

latexの図を生成したところ
図2: latexの図を生成したところ

これらの機能で簡単に複雑な図を表現できるようになったのは良いところですが、反面データサイズが膨らみがちなことが困りそうなところでしょうか。特に [[edraw:data= 形式のリンクはデータがOrgファイル内(特に一行に)埋め込まれるので、あまり大きいと何か問題を引き起こすかもしれません。単純にインライン画像をoffにしたときに見栄えが悪いというのもあります(今更ですが)。そういう場合はインライン画像を右クリックして [[edraw:file= 形式へ変換して下さい。

2024-06-07 ,

複数サブパス(複合パス)への対応

別のプロジェクトで簡単な絵が必要になったので自作の作図ツールを使って描いていたら色々不満があったので最近はちょくちょくいじっていました。そのプロジェクトはそっちのけで(笑)。

misohena/el-easydraw: Embedded drawing tool for Emacs

特に複数のサブパスを含むパスへの対応をいい加減やらなきゃな、と。

複数のサブパスとは

複数のサブパスというのは、一つのパスデータ(path要素のd属性:<path d=>)の中に複数の内部的なパス(サブパス)が表現されているような状況のことです。例えば <path d="M0,-100 L100,100 L-100,100 Z M0,-50 L-50,50 L50,50 Z" /> と書くと一つのpath要素で大小二つの三角形が表現できます。実際に表示してみるとこんな感じになります。

図1: d="M0,-100 L100,100 L-100,100 Z M0,-50 L-50,50 L50,50 Z"

複数のパスを表現したいなら複数のpath要素を使えばいいじゃないかと思うかもしれませんが、上のような「穴あき」を表現するには一つのパスデータの中の複数のパス(サブパス)が必要になります。複数のpath要素を重ねて配置しただけでは塗りつぶした大きな三角形の上に小さな三角形を塗りつぶすことしか出来ません。一つのパスの中に大小の三角形が一緒に入っていてはじめてこのような「穴あき」が作れます。

一つのパスの中に二つのパスが並んでいるだけでなぜ中が抜けるんだろうかと疑問に思う方もいると思いますが、これはこの図形をレンダリングすることを考えれば分かります。レンダリングの基本は画像を上から下へ一行ずつ、そして一行の中を左から右へ1ピクセルずつ色を決めて点を打っていくことです。例えばこの図形の真ん中らへんの一行(1ライン)をレンダリングするとします。画像の左端から右端へ向かって1ピクセルごと処理していきます。このとき各線分(segment)との交差判定をしながら進んでいくことがポイントです。一番最初は図形の外なので点を打ちません。右へ向かっていくと、最初の線分を跨ぎます。なので、それ以降は灰色のfill色を打ちます。さらに進むとまた線分を跨ぎます。なので、それ以降は図形の外にあると認識して点を打ちません。さらに進むとまた線分を跨ぐ(奇数回目)ので色を塗り始めます。そしてまた線分を跨いだら(偶数回目)塗るのを止めます。と、このように図形の内外を判定しながらレンダリングしていくのですが、このアルゴリズムで真ん中に穴あきがあるドーナツ形を作るには外側の線も内側の線も同じ判定対象(線分集合)の中に存在していなければなりません。凹形なら一つのパスでも作れるのでその上側をくっつけてしまうという回避策もありますが、strokeを指定するとボロが出たりと問題もあります。

ということで、一つのパスの中に複数のパス(サブパス)が必要になるわけです。

対応してこなかった理由

これまで対応してこなかった理由はひとえにデータ構造の悪さからでした。SVGのd=属性を解析して内部的な表現に変換してから編集するのですが、その内部的な表現の構造が悪すぎました。その表現は、d属性を解析したほぼそのままのコマンドリストでした。なので、一つアンカーを打てばMコマンド(最初の点を指定する)、Lコマンド(直線)、Cコマンド(カーブ)、Zコマンド(閉じる)を前後の状況に応じて判別して追加するような複雑なことをしなければなりませんでした。もちろんその複雑さを緩和するような層があって、上の層は下の層に処理を投げるだけなのですが、複数のサブパスに対応するには下の層を改善しなければならず、そしてそれは複雑なのでやりたくなかったわけです。

この構造のまま複数のサブパスにも(ユーザーからの様々な操作に対してちゃんと)対応しようとすると細かい条件分けが複雑すぎて大変でした。実は実際最後まで書き切ったのですが、どこかにバグがあっても不思議ではない、これからの改善も全くしたくないようなコードにしかなりませんでした。ウンザリしてすぐにその辺りのを構造(edraw-path.el)を書き直すことにしました。

新しい構造と内部・外部表現の変換

新しい構造はパス(パスデータ)、サブパス、アンカー、ハンドルを素直に表現したものとなりました。これによって様々な処理が驚くほどシンプルに素直に書けるようになりました。

そして新しい構造とSVGのd属性(コマンド文字列)との間の変換処理も用意して、必要に応じて変換します。これは従来の構造(コマンドリスト)でもそのような作りになっていました。

ただし、内部的な構造と実際のd属性の文字列(外部表現)との変換は必要最小限にする必要があります。それはどちらの方向でも失う情報があるからです。

SVGのd属性で表現できないもの

内→外で失うものとしては、開サブパスの端点の外側のハンドルがあります。何を言っているのかよく分からないかもしれませんが、次図の通りです。

閉(サブ)パス開(サブ)パス端点端点の外側のハンドル端点を持たない
図2: 開サブパスの端点の外側のハンドル

つまり、線の端っこのさらに外にあるハンドルです。実際のところこれは描画には全く影響を及ぼしません。なのでSVGのd属性で表す方法がありません。しかし編集においては、その端点の次にアンカー点を打ったときに、その端点と新しい端点とを結ぶ曲線の曲がり具合に影響します。描画側から見たら「まだ存在しない曲線の属性なんて知らん」ですが、編集側から見たら「いやいや、線がその端点を通過したときの出て行く先を示すんだからその端点の属性だろ」というわけです。というわけで、編集用の内部表現をd属性へ変換するとそのハンドルは失われます。まぁ、独自の属性に持たせるといった方法もありますが。

新しい構造で表現できないもの

外→内で失うものとしては、各コマンドの細かいニュアンスだと思います。

SVGのパスデータには沢山のコマンドが用意されています。

M m Z z L l H h V v C c S s Q q T t A a

これだけのコマンドがあります(Paths ― SVG 2を参照のこと)。

これだけあると同じ形を描画するのにも沢山の表現方法があります。これらのコマンドの使い分けは、ほとんどの場合データサイズを削減することが目的だと思われますが、誰かがそこにそれ以外の意図を込めていないと言い切れるでしょうか。相対表現(小文字)は前の点からの相対関係を維持してほしいという意図があったり、水平線や垂直線はその性質を維持してほしいと考えてはいないでしょうか。Aコマンドは特に意図が現れやすいです。しかし内部表現に変換すれば、それは全て無味乾燥な三次ベジェ曲線のアンカーとハンドルに集約されてしまいます。

私が一番頭を悩ませていたのは一つのMコマンドが複数のサブパスで共有されうることです。例えば次のようなd属性があった場合:

M0,0 L40,-20 L40,20 Z L20,40 L-20,40 Z L-40,20 L-40,-20 Z L0,-40

(ちなみにこれは分かりやすくするためにあえて無駄な書き方をしていますが、実際には次のように書けます)

M0 0 40-20V20ZL20 40H-20ZL-40 20V-20ZV-40
図3: 一つのMコマンドが複数のサブパスで共有されている例

これは上のような図形ですが、中央の点、つまり0,0の表記はd属性中に一つしか現れていません。このd属性の中には4つのサブパスが含まれています。しかしその開始点は最初に現れる M0,0 ただ一つにまとめられています。

これは単なるケチ表現なのかもしれません。しかし同じ点になっていることには何か必然的な理由があるのかもしれません。その点をドラッグしたとき、4つ全てのサブパスの開始点が動いた方が親切かもしれません。

まぁ、そんなことを全てのコマンドに対してやっていたら大変なので諦めることにしたわけです。

そして今

今では上のようなデータも正しく編集できるようになりました。もちろんバラバラの4つのサブパスとしてです。

私が間違った設計判断をしてしまった原因はこの割り切りをためらったからかもしれません。SVGパスデータの元々の表現の性質をできるだけ残しておきたかったわけです。しかしすでに相対表現やら二次ベジェ、水平線、垂直線などは絶対表現のLとCに変換してしまっていましたから、今更なわけですが。

実はこの問題は作り始めのかなり初期から気がついていました。でも頑張れば何とかなるだろうと思っていたわけです。

私は近年山を歩くことがありますが、登山道を歩いていると不意に道がおかしいな? と気がつくことがあります。妙に道が荒れている、障害となる草木や岩石が多い、越えられないことはないが一般登山道のレベルとは思えない、など。もちろんそこまで至るまでには「分岐など見当たらなかった」「正しい道を進んでいる」と思っているものです。しかしそういった異変に気がついたら地図を確認してすぐに引き返すことです。頑張れば行ける。そう思って先に行くと取り返しの付かないことになるかもしれません。