2022-01-08 ,

WordPressのプラグインを作る

これまで画像の表示にEasy FancyBoxを使っていたのですが表示する情報を増やせず追加の拡張に課金するのもなぁと思ったので自分で作りました。

とりあえず表示するだけなら簡単だったのですが、スクロール出来るようにしようと思ったら案外大変。適当にoverflow: scrollでスクロールバー出しておけば良いだろうと思ったのですが、ホイールでスクロールするときに後ろのページがスクロールしてしまうことが判明。元々背景のdivでpreventDefaultしてホイールを抑制していたのですが、それだとホイールで画像をスクロール出来ないのでスクロール領域だけpreventDefaultしないように変更。しかしそうすると画像のスクロールが必要ない状況(高さが足りているとか上限に達しているとか)の時に後ろのページがスクロールするという。部分的にブラウザ既定の動作に任せるというのは思っていたよりも難しいらしく、どうすればよく分からなかったため、最終的にはスクロール機能は自前で実装することに。しかしそうするとタッチイベントでも同じ問題が発生。もうピンチイン/アウトも含めて対応してしまえ、そうするとホイールも拡大縮小に割り当ててマウスでパンするように修正。

というわけでCSSでちょろっとスクロール出来るようにしておけば良いだろうと思ったのが、思いのほか時間を取られてしまいました。他にも自分で作ってみると細かい改善点が沢山見えてきますね。ヤレヤレ。

動作例:

白馬大池

WordPress部分はheadにscriptを追加するだけなので簡単でした。しかし、deferにするのってこんなことしなきゃいけないの?? マジで??

2022-01-05

360Photo System

これまでに歴代Googleスマホ(Nexus/Pixel)のPhoto Sphereで撮った写真を共有する仕組みを作った。

必要な作業は次の通り。

  1. HDDの中からそれらしき画像をかき集める
  2. 画像ファイル名とタイトルの対応表を作る
  3. 8192x4096に変換
  4. 2~4MBくらいのJPEGに頑張って圧縮(mozjpegで-quality 40くらいに落ち着いた)
  5. 600x314のサムネイル画像を作る(ImageMagickで1800幅に縮小して中心600幅を切り抜く)
  6. ついでに、押せるマーク付きサムネイルも作る
  7. 画像毎にHTMLを生成する(中身はメタ情報と外部JS起動のみ。OGP、TwitterCard情報付き)
  8. サーバにアップロードする

1と2はある程度手動でやらざるを得ないとして、3以降は自動的に行う。変換処理のスクリプトはEmacs Lispで書いた。私はシェルスクリプトは何もわからんので。Emacs Lispでは directory-files して shell-command 呼べばいいだけ。後はある種のテンプレートエンジンというか、 {{{key}}} をalistを元に置換するような仕組みを作ってHTMLを生成する。わずかにMakefileも使用。必要な操作はtransientで作ったメニューにまとめたので忘れても安心。

一番手間がかかったのは圧縮の方法を決めるところ。元の画像は6~16MBくらいもあるので、転送量・転送時間的にもサーバ容量的にも厳しい。2MB程度に収まらないか色々試したが、あまり品質を低くすると空のグラデーションがはっきり帯状になってしまうので無理だった。最初はImageMagickで圧縮したが、Photoshopで保存した方が綺麗だった。最終的にはmozjpegを使った。

ブラウザでの表示は以前星空を描画するために作ったもの(misohena/drawstars)を転用。正距円筒図法(equirectangular)の画像をWebGLのテクスチャにして描画する。1枚のテクスチャで描画する仕組みになっていたが、8192ピクセルサイズのテクスチャは手元のAndroid端末ではエラーになったので、急遽複数のテクスチャに分割して描画するように変更した。こういうのがあるから私はどうにも3Dグラフィックスハードウェアというのが好きになれない。ただ、WebGLは素のOpenGL ESをいじるよりは(主に周辺的な事情により)幾分気が楽である。変なバグを発見。起動したときになぜか中途半端な方向を向いているなと思ったら現在の恒星時の方向を向いていた。機能を切り忘れていたらしい。内部的には北極に立って全宇宙を眺めているという扱いになっているので。

こうやってブログにも簡単に貼り付けられる。

20180715_074423_thb.jpg
20190104_120508_thb.jpg
20200826_080913_thb.jpg
20211105_130232_thb.jpg
20190121_110427_thb.jpg
20211106_104109_thb.jpg
20181002_123850_thb.jpg
2022-01-02 ,

org言語のソースブロックをエクスポートしたときにリンクをそのまま出力する

例えば次のようなソースブロックがあった時に……

リンクは次のように書きます。
#+begin_src org
[[https://example.com/][example.com]] へ行ってみよう。
#+end_src

エクスポートすると……

リンクは次のように書きます。

example.com へ行ってみよう。

みたいに出力されてしまいます。リンクの書き方を説明したいのに、リンクのブラケットやパスの部分が消えてしまうわけです。

昔からそうだったけ?とモヤモヤしながらもう長いこと経つのですが、org-modeの書き方を例示することが多い私はよくこの問題に引っかかります(最近ではこういうのとかこういうのとか)。

C-x 8 RET zero width space と打って見えない空白を[と[の間に入れて回避するのが常なのですが、そろそろ何とかしたいなぁと思って調べました。そのコードをコピペすると空白までコピーされて機能しませんからね。

結果、次のようなコードで回避できました。

(defvar-local my-org-in-html-fontify-code nil)

(advice-add 'org-html-fontify-code :around 'my-org-html-fontify-code-advice)
(advice-add 'org-html-htmlize-region-for-paste :around 'my-org-html-htmlize-region-for-paste)

(defun my-org-html-fontify-code-advice (old-func &rest rest)
  (cl-letf (((default-value 'org-link-descriptive) nil)
            ((default-value 'my-org-in-html-fontify-code) t))
    (apply old-func rest)))

(defun my-org-html-htmlize-region-for-paste (old-fun beg end &rest rest)
  ;; Erase htmlize-link properties
  (when (and my-org-in-html-fontify-code
             (eq major-mode 'org-mode))
    (remove-text-properties beg end '(htmlize-link nil)))
  ;; Call original
  (apply old-fun beg end rest))

ソースブロックがどのようにHTMLへ変換されるかは org-html-fontify-code を見ると良いです。テンポラリバッファを作って言語用のメジャーモードを起動し、ソースブロックの内容を挿入します。確実にfont-lockしたら、テキストプロパティを元にhtml化するわけです。

org-modeにはリンクを文字通りに表示する org-link-descriptive というオプションがあるので、この関数が呼ばれている間だけ nil になってもらうのが先のコードです。

org-link-descriptive はorg-modeが立ち上がるとバッファローカル変数になってしまいます。なのでエクスポート元のバッファでいくら let で nil にしても、新しく作られるテンポラリバッファでは nil になりません。そこでcl-letfでデフォルト値の方を nil にしています。

何かエクスポート中であることを判別する方法があるならorg-mode-hookでorg-toggle-link-displayでも呼んでしまおうかと思ったのですが、残念ながら見つかりませんでした。

まずはその辺りを修正してからエクスポートしたのがこちら。

リンクは次のように書きます。

[[https://example.com/][example.com]] へ行ってみよう。

リンクがクリックできるようになっているのがちょっと気持ち悪いです。この例では正しいURLになっていますが、URLとは解釈できないようなorg-mode独自のリンクパスも問答無用でHTMLのリンクにしてくれます。

これはhtmlize-linkというテキストプロパティの作用です。ソースのHTML化はhtmlizeというパッケージがバッファ内のテキストプロパティを元に行いますが、org-modeのfont-lockがhtmlize-linkというテキストプロパティを付けるので、それを見たhtmlizeが自動的にリンクに置き換えてしまうのです。

ソースブロックの中からリンクを張りたいなどとは思わないので、こちらも問答無用でhtmlize-linkというテキストプロパティを削除するようにしました。

最終結果は次の通りです。

リンクは次のように書きます。

[[https://example.com/][example.com]] へ行ってみよう。
2022-01-01

all-the-icons-diredを修正

dired-details-rをいじったついでに、前々から気になっていた挙動を手元で二点修正した。

  • ファイル操作でアイコンが崩れることがある
  • リモート(tramp)越しに沢山のファイルがあるディレクトリを開こうとすると待たされる

(ちなみに、アイコンの幅が不揃いでズレている件はフォントファイルをFontForgeで加工して解決した)

修正点

wyuenho/all-the-icons-dired at 5e9b097f9950cc9f86de922b07903a4e5fefc733 のバージョンからの修正、のはず。

ファイル操作でアイコンが崩れることがある

evaporate

オーバーレイは基本的にevaporateにした方が安全。diredはファイルを移動したり削除したりしたときに行を消すので、その時に自動的にオーバーレイも消えてくれる。

 (defun all-the-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'all-the-icons-dired-overlay t)
-    (overlay-put ov 'after-string string)))
+    (overlay-put ov 'after-string string)
+    (overlay-put ov 'evaporate t)))

after-readin-hookのみで更新

基本的にafter-readin-hookのタイミングで更新すれば十分なはず。……だけど、すでにdired-readinはadviceをかけてるな……。不十分だったらこれはキャンセル。

-(defun all-the-icons-dired--refresh-advice (fn &rest args)
-  "Advice function for FN with ARGS."
-  (prog1 (apply fn args)
-    (when all-the-icons-dired-mode
-      (all-the-icons-dired--refresh))))
-
-(defvar all-the-icons-dired-advice-alist
-  '((dired-aux     dired-create-directory       all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-create-files        all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-kill-lines          all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-rename              all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-insert-subdir          all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-kill-subdir            all-the-icons-dired--refresh-advice)
-    (dired         wdired-abort-changes         all-the-icons-dired--refresh-advice)
-    (dired         dired-internal-do-deletions  all-the-icons-dired--refresh-advice)
-    (dired-narrow  dired-narrow--internal       all-the-icons-dired--refresh-advice)
-    (dired-subtree dired-subtree-insert         all-the-icons-dired--refresh-advice)
-    (dired-subtree dired-subtree-remove         all-the-icons-dired--refresh-advice)
-    (dired         dired-readin                 all-the-icons-dired--refresh-advice)
-    (dired         dired-revert                 all-the-icons-dired--refresh-advice)
-    (find-dired    find-dired-sentinel          all-the-icons-dired--refresh-advice))
-  "A list of file, adviced function, and advice function.")
 
+(defun all-the-icons-dired--after-readin-hook ()
+  (when all-the-icons-dired-mode
+    (if (> (line-number-at-pos (point-max)) 1000)
+        ;; If there are many files, it will be very slow, so disable icons.
+        (all-the-icons-dired--remove-all-overlays)
+      (all-the-icons-dired--refresh))))
 
 (defun all-the-icons-dired--setup ()
   "Setup `all-the-icons-dired'."
   (setq-local tab-width 1)
-  (pcase-dolist (`(,file ,sym ,fn) all-the-icons-dired-advice-alist)
-    (with-eval-after-load file
-      (advice-add sym :around fn)))
-  (all-the-icons-dired--refresh))
+  (add-hook 'dired-after-readin-hook #'all-the-icons-dired--after-readin-hook nil t))
 
 (defun all-the-icons-dired--teardown ()
   "Functions used as advice when redisplaying buffer."
   (kill-local-variable 'tab-width)
-  (pcase-dolist (`(,file ,sym ,fn) all-the-icons-dired-advice-alist)
-    (with-eval-after-load file
-      (advice-remove sym fn)))
+  (remove-hook 'dired-after-readin-hook #'all-the-icons-dired--after-readin-hook t)
   (all-the-icons-dired--remove-all-overlays))

(2023-04-10追記: ファイル数が1000を超えたらアイコンを表示しないようにした。重いので)

現在ナローイングされている範囲だけ削除

上の修正をしたせいか(?)、iキーでサブディレクトリを追加したら追加したもの以外が消えてしまったので。いや、 all-the-icons-dired–remove-all-overlaysではwidenしているのに、all-the-icons-dired–refreshでwidenしていないからとも言えるかもしれない。

そもそもevaporateにしたから自動的に消えるので不要かもしれない(?)

 (defun all-the-icons-dired--remove-all-overlays ()
   "Remove all `all-the-icons-dired' overlays."
   (save-restriction
     (widen)
     (mapc #'delete-overlay
           (all-the-icons-dired--overlays-in (point-min) (point-max)))))

+(defun all-the-icons-dired--remove-narrowed-overlays ()
+  "Remove all `all-the-icons-dired' overlays."
+  (mapc #'delete-overlay
+        (all-the-icons-dired--overlays-in (point-min) (point-max))))

 (defun all-the-icons-dired--refresh ()
   "Display the icons of files in a dired buffer."
-  (all-the-icons-dired--remove-all-overlays)
+  (all-the-icons-dired--remove-narrowed-overlays)

リモート(tramp)越しに沢山のファイルがあるディレクトリを開こうとすると長時間待たされる

ファイル毎にファイル種別判別のための関数を呼んでいるのが原因。リモートの場合は避ける。

 (defun all-the-icons-dired--refresh ()
   "Display the icons of files in a dired buffer."
   (all-the-icons-dired--remove-narrowed-overlays)
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
       (when (dired-move-to-filename nil)
         (let ((case-fold-search t))
-          (when-let* ((file (dired-get-filename 'relative 'noerror))
+          (when-let* ((file (dired-get-filename nil 'noerror)) ;;フルパスで取得
-                      (icon (if (file-directory-p file)
-                                (all-the-icons-icon-for-dir file
-                                                            :face 'all-the-icons-dired-dir-face
-                                                            :v-adjust all-the-icons-dired-v-adjust)
-                              (apply 'all-the-icons-icon-for-file file
-                                     (append
-                                      `(:v-adjust ,all-the-icons-dired-v-adjust)
-                                      (when all-the-icons-dired-monochrome
-                                        `(:face ,(face-at-point))))))))
+                      (icon
+                       (if (save-excursion (forward-line 0) (looking-at-p dired-re-dir)) ;;file-directory-pはリモートアクセスを引き起こすので避ける
+                           (if (file-remote-p file)
+                               ;; all-the-icons-icon-for-dirの中でも file-*-p 関数を使っているので避ける
+                               (all-the-icons-octicon "file-directory"  :height 1.0 :v-adjust -0.1
+                                                      :face 'all-the-icons-dired-dir-face
+                                                      :v-adjust all-the-icons-dired-v-adjust)
+                             (all-the-icons-icon-for-dir file
+                                                         :face 'all-the-icons-dired-dir-face
+                                                         :v-adjust all-the-icons-dired-v-adjust))
+                         (apply 'all-the-icons-icon-for-file file
+                                (append
+                                 `(:v-adjust ,all-the-icons-dired-v-adjust)
+                                 (when all-the-icons-dired-monochrome
+                                   `(:face ,(face-at-point))))))))
             (if (member file '("." ".."))
-                (all-the-icons-dired--add-overlay (point) "  \t")
-              (all-the-icons-dired--add-overlay (point) (concat icon "\t"))))))
+                (all-the-icons-dired--add-overlay (point) "   ") ;;この辺は個人的な修正。タブを記号で表示しているので。どのみち位置は揃わないのでフォントの方を加工して揃えた。
+              (all-the-icons-dired--add-overlay (point) (concat icon " "))))
       (forward-line 1))))
2022-01-01

Emacsでdisplayプロパティを使って改行を置き換えると非常に遅くなる件

私はDiredをファイル名が一番左に来るように改造して使っているのですが、ファイル数が多いディレクトリを開くと動作が重くなって困ることが度々ありました(一時的に効果を切れば回避できます)。

オーバーレイが多いから仕方が無いくらいに思っていたのですが、今日少し調べたら原因は行末の "\n" を "文字列… \n" に置き換えているのが原因だと分かりました。オーバーレイでもテキストプロパティでも関係ありません。

次のコードは "\n" を "EOL\n" に置き換えるdisplayプロパティがついた文字列を20000行追加するものです(バッファにはオーバーレイではなくテキストプロパティのdisplayプロパティが設定されます)。

(dotimes (_ 20000)
  (insert "1234567890" (propertize "\n" 'display "EOL\n"))) ;;NG

結果の見た目は次のようになります。

1234567890EOL
1234567890EOL
...19997行略...
1234567890EOL

scratchバッファで実行した後バッファの末尾でprevious-line(C-p)してみると一行上に移動するのに1秒程度かかります。上に行けば行くほど時間は短くなり、バッファの冒頭付近では全く気がつかないくらいの時間になります。

"\n" を置換しなければこの現象は発生しません。例えば "0" を "0EOL" に置換しても(見た目は同じですが)全く遅くはなりません。

(dotimes (_ 20000)
  (insert "123456789" (propertize "0" 'display "0EOL") "\n")) ;;OK

オーバーレイのbefore-stringで"\n"の前に文字列を挿入しても(C-pは)遅くなりません(挿入自体の時間はテキストプロパティに比べてややかかります)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point)))) ;;\nのところを覆う
    (overlay-put ov 'before-string "EOL") ;;OK
    (overlay-put ov 'evaporate t)))

また、空の範囲のオーバーレイを許容するのであれば、"0"と"\n"の間にオーバーレイを挟むこともできます。この場合displayプロパティは効かないのでbefore-stringかafter-stringを使うことになります(evaporateが使えないので消すのが面倒になるので注意)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (1- (point))))) ;;\nの前の空の範囲!
    (overlay-put ov 'after-string "EOL"))) ;;OK: before-stringでも同じ。displayは空の範囲では表示されないので使えない
;; 消すときは (remove-overlays (point-min) (point-max)) あたりで。

displayプロパティで "\n" 込みの文字列で置換してしまうと、やっぱり遅くなるわけです(激重注意)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point)))) ;;\nのところを覆う
    (overlay-put ov 'display "EOL\n") ;;NG
    (overlay-put ov 'evaporate t)))

しかもテキストプロパティに比べて格段に遅いです。一行上に移動するのに何十秒もかかります。

私が"\n"を置換したかったのは、そうしないとカーソルをファイル名の末尾に置けないからです。例えば上の問題が起きないどのケースを使用しても"0"の直後にカーソルを置くことができません。"0"を指しているところでforward-charすると"0"の直後ではなく"EOL"の直後に飛んでしまいます。"0"を"0EOL"に置換した場合ならともかく、"\n"にbefore-stringをかけたときはbefore-stringの前にカーソルが来て欲しいものですが残念ながらそうはなりません。diredで表示を変えるだけならそれほど問題にはならないのですが、wdiredでファイル名を直接編集するときには問題になります(対策はwdiredが起動したら一時的に効果を消すくらいか)。

面白いのは一行下に移動するnext-line(C-n)は遅くならないこと。また、同じ一行上に移動するのでもM-: (forward-line -1)では遅くなりません。(previous-line)は(forward-line -1)に比べると色々な処理を追加で行っているので、そのどこかに原因があるのでしょう。previous-line → line-move → line-move-1 → vertical-motion と呼び出していて、vertical-motionはindent.cの中にあり細々とした処理をしているので追っていませんがdisplayとか'\n'とかが出てくるのでそのあたりで何かあるのでしょう。

ちなみに、連続した行でなければ問題は起きません。

(dotimes (_ 20000)
  ;; 最初に\nを入れる
  (insert "\n1234567890" (propertize "\n" 'display "EOL\n"))) ;;OK

1行空行を入れるととたんに問題は起きなくなります。

重いのは嫌なので結局一番速いテキストプロパティで改行の一つ前の文字を置き換えるように変更しました。

Improve performance · misohena/dired-details-r@c7699cb

(2022-01-02追記) before-stringの前にカーソルが置けないと書きましたが、cursorプロパティを使うと置けることに気がつきました。次のコードを使うと、previous-lineで遅くならず(\nをdisplayプロパティで置き換えていないので)、かつ、0とEOLの間にカーソルが置けて('cursor 1の部分の効果)、さらにそこで文字を入力するとEOLの前に挿入されます(make-overlayの第四引数の効果)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point) nil t))) ;;\nのところを覆う。直前に入力した文字はオーバーレイに含めない。
    (overlay-put ov 'before-string (propertize "EOL" 'cursor 1)) ;;EOLのテキストプロパティに1を付けるとなぜかEOLの直前にカーソルを置けるようになる。
    (overlay-put ov 'evaporate t)))

cursorテキストプロパティはマニュアルを読んでも正直意味が分からないので、なぜこうなるのかは不明です。

dired-details-rですが、大量のオーバーレイは移動こそ重くならないまでも追加と削除には時間がかかるので、テキストプロパティのままで行こうと思います。カーソルの移動に問題が残りますが我慢できないほどではないです。いや、行数で実装を切り替えるというのもアリですかね……?

(2022-01-06追記) wdiredでファイル名末尾にカーソルが置けないのがやっぱりストレスなので上記cursorプロパティを使う方法をdired-details-rに採用しました。オーバーレイはテキストプロパティよりも遅いので、1000行越えたらテキストプロパティに切り替える(+wdired起動時は表示を戻す)という荒技も組み合わせました。なおcursorプロパティの挙動は相変わらずよく分かっていません。

Fix issue can't move to the end of file names in wdired mode · misohena/dired-details-r@ae2f690

2022-01-01 ,

org-downloadで保存前にファイル名を入力する

私はこれまでorg-downloadを保存するファイル名が「orgファイル名_タイムスタンプ_ダウンロードファイル名」 になるように設定して使っていたのですが、十中八九後からファイル名を変更しなければならなくていちいちファイルとリンクの両方を修正しなければならず面倒くさいなーと思っていました。

よく考えたらorg-download-file-format-function変数に指定する関数内でread-file-nameを呼び出せば良いだけですね。この変数はなぜかdefcustomではなくdefvarなのですが、使っているのはfuncallする一箇所だけ。カスタマイズするのに使っても問題ないように見えます。私はファイル名にorgファイル名を入れるために既に書き替えていたのでそこを修正すれば良いだけでした。

(setq org-download-file-format-function 'my-org-download-file-format)
(defun my-org-download-file-format (filename)
  (read-file-name
   "File Name: "
   nil nil nil
   (concat
    (if-let ((fn (buffer-file-name)))
        (concat (file-name-base fn) "_"))
    (format-time-string "%Y%m%d_%H%M%S_")
    filename)))

ついでに前回作ったメニュー(org-cmenu)に登録。

これまでHydraでorg-download用のメニューを作っていたので、それをtransientに書き替えてorg-cmenuのInsertメニューに追加しました。

現在の設定はだいたいこんな感じ。

(autoload #'org-download-clipboard "org-download")
(autoload #'org-download-yank "org-download")
(require 'transient)
(transient-define-prefix my-org-download ()
  "Insert an image."
  ["Copy an image from:"
   ("c" "Clipboard" org-download-clipboard)
   ("y" "Full-path or URL on kill-ring" org-download-yank)
   ("a" "All monitors" my-org-download-screenshot-all)
   ("p" "Primary monitor" my-org-download-screenshot-primary)
   ("f" "Foreground window" my-org-download-screenshot-active-window)
   "or drop from a local image file."])

(with-eval-after-load "org-cmenu-setup"
  (transient-append-suffix
    'org-cmenu-insert
    '(0 -1 -1) ;;Insertメニューの一番最後に追加
    '("D" "org-download" my-org-download)))

(defun my-org-download-screenshot-all ()
  (interactive)
  (my-org-download-screenshot "screenshot-all.ps1"))

(defun my-org-download-screenshot-primary ()
  (interactive)
  (my-org-download-screenshot "screenshot-primary.ps1"))

(defun my-org-download-screenshot-active-window ()
  (interactive)
  (my-org-download-screenshot "screenshot-activewin.ps1"))

(defun my-org-download-screenshot (script-name)
  (require 'org-download)
  (let ((org-download-screenshot-method
         (format
          "powershell %s %%s"
          (expand-file-name
           (concat
            my-org-download-script-path ;;別途設定のこと
            script-name)))))
    (message "Waiting 3 seconds...")
    (sleep-for 3)
    (message nil)
    (org-download-screenshot)))

WindowsなのでPowerShellを使用してスクリーンショットを撮っていますが、スクリプトはWindowsのコマンドラインからスクリーンショットを撮る(PowerShell)で紹介したものです。

……あ、今気がついたのですが、org-download-rename-at-pointなんてコマンドがあるんですね。ファイル名とリンクを一緒にリネームしてくれるようです。まぁ、別に毎回ファイル名を確認してくれた方が良いんじゃないでしょうか。沢山の画像をドロップしまくるような使い方をするなら毎回確認されると困るでしょうけど、私はそういう使い方はしませんし。でもファイル名とリンクの同時リネームは便利だからorg-cmenuに加えておこっと。

(追記:追加しました。orgのリンクを書き替えただけでその先のファイル名も変わってるなんてマジカル!)

Add feature to rename linked file · misohena/org-cmenu@8805f27

2021-12-30 ,

org-mode で現在の構文要素に応じて適切なメニューを出す

org-cmenuというのを作りました。

misohena/org-cmenu: Context Sensitive Menu for Emacs Org Mode

org-modeって機能は沢山あるしキー割り当てももの凄く沢山あって大変です。とても全部は覚えられないし覚えたとしてもすぐに忘れます。それに何か新しい機能を作ったとして、それをどのキーに割り当てるのかを決めるのも一苦労です。そして自分で作った機能だとしても結局たまにしか使わなければ忘れます。私はorg-modeを使っていると、人間というのは何かを覚え続けてはいられない存在なのだと言うことを痛感させられるんです。

それはともかく、最近また作業を効率化すべく新しい機能を追加したいと思ったのですが、それを実行するキー割り当てを考えていくうちに、やはりこれは文脈に応じて切り替わるメニューのようなものが必要だろうと思うに至ったのでした。

コンテキストメニュー。一般的には対象を右クリックするとそれに対する操作がポップアップで表示されるあれです。まぁ、Emacsなのでマウス用のポップアップメニューにしても仕方が無いので画面下にテキストで表示するのですが、いずれにせよ対象を選びそれに対する操作の中から選ぶというインタフェースは記憶すべきことを大幅に減らしてくれる素晴らしい仕組みです。覚えておくべきなのはそのメニューを開く操作のみです。対象以外で使う操作はメニューに出てこないのでメニューの中から操作を探す手間も最小限です。

Emacsでキー操作用のメニューを作るパッケージとしてはHydraが有名ですが、Hydraは複数行にわたる巨大な文字列でレイアウトを指定するので固定のメニューを作るのには直感的で良いのですが、変化の大きいメニューを作るには向かない気がします(気がするだけで工夫次第で何とかなるのかもしれませんが)。なので最近良く耳にするようになったtransient.elを試してみたのが前回前々回のお話しでした。

transientを使ったとしても色々問題はあったのですが、それはひとまず置いておいて、まずはorg-modeで現在のポイントがある地点の構文要素を割り出す方法についてご紹介したいと思います。

org-elementの使い方

org-modeには標準でorg-elementというのが入っていて、現在位置の構文要素を簡単に割り出せるようになっています。

バッファ(文書)全体を解析する org-element-parse-buffer という関数もありますが、部分的な解析には org-element-at-point と org-element-context という関数が使いやすいです。この二つの関数は共にセクション(見出しと見出しで区切られた範囲)以上の解析は行わず、現在のポイントの近くにある要素を返す関数です。 org-element-at-point は行内要素を含まない大きな構造のみを返し、org-element-context は内部で org-element-at-point を使用しつつ、行内の細かい要素も解析して一番限定された狭い要素を返します。

なので結論から言えば、現在のポイントにある構文要素に関する情報を得たければ (org-element-context) だけでおしまいです。

org-element-context の結果は、例えば次ような リスト です。

(bold
 (:begin 1739
  :end 1745
  :contents-begin 1740
  :contents-end 1743
  :post-blank 1
  :parent (paragraph
           (:begin 1728
            :end 1750
            :contents-begin 1728
            :contents-end 1749
            :post-blank 1
            :post-affiliated 1728
            :parent nil))))

これは実際に上の太字になっているところで M-: (org-element-context) を実行した結果です。boldで始まるノードの親(:parent)としてparagraphで始まるノードが続いているのが見て取れます。その上はsection、headlineと続くはずなのですが、org-element-context(というかorg-element-at-point)はそこまでは解析しません。

ノードから情報を取得するには org-element-type や org-element-property 関数(アクセッサ)を使用します。次のように。

(let ((datum (org-element-context)))
  (list
    (org-element-type datum) ;; => bold
    (org-element-property :begin datum) ;; => 1739
    (org-element-property :end datum))) ;; => 1745

要素の開始点と終了点は必ず取得できるので、これだけで全ての要素に適用出来るコマンドを記述できます。例えば要素全体のマーク(選択)やカット、コピーを実装するのは簡単です。

(defun my-org-kill-element (datum)
  (interactive (list (org-element-context)))
  (kill-region (org-element-property :begin datum)
               (org-element-property :end datum))) ;;本当は色々細かい問題があるのだけどまぁいいや……

org-element-type関数が返すboldやparagraphといったシンボルは type と呼ばれていて構文要素の種類を表します。

構文要素の種類

ところで皆さんはorg-modeにどのくらいの種類の構文要素があるかご存じですか?

org-elements.elの中には次のように書かれている部分があります。

(defconst org-element-all-elements
  '(babel-call center-block clock comment comment-block diary-sexp drawer
               dynamic-block example-block export-block fixed-width
               footnote-definition headline horizontal-rule inlinetask item
               keyword latex-environment node-property paragraph plain-list
               planning property-drawer quote-block section
               special-block src-block table table-row verse-block)
  "Complete list of element types.")

(defconst org-element-all-objects
  '(bold citation citation-reference code entity export-snippet
         footnote-reference inline-babel-call inline-src-block italic line-break
         latex-fragment link macro radio-target statistics-cookie strike-through
         subscript superscript table-cell target timestamp underline verbatim)
  "Complete list of object types.")

リストの中のシンボルがtypeの種類を表しています。org-element-all-elementsの中にあるのが30、org-element-all-objectsの中にあるのが24で計54種類にもなります(org-version 9.5.1 時点)。

org-element.elでは、bold等の行内に現れる要素のことを object と呼んでいて(HTMLで言えばインライン要素?)、それ以上の大きな要素のことを element と呼んでいるようです(HTMLで言えばブロックレベル要素?)。両者を合わせたもの(+α)はdatumと呼んでいる箇所が多いですが、普通にelementと呼んでいる場所もありややこしいです。

シンボルの名前だけだと何を意味しているのか分かりにくいので、全てのtypeを使用したorg-mode文書を作成して理解を深めました。

https://raw.githubusercontent.com/misohena/org-cmenu/main/examples/all-types.org

Emacsの中で見ないと構文がハイライトされないので見づらいですね……。参考までにtypeとそれに関連したマニュアルへのリンクも載せておきます。

all-elements
keyword headline section planning drawer property-drawer node-property clock center-block quote-block verse-block example-block comment-block dynamic-block export-block special-block src-block babel-call paragraph footnote-definition fixed-width comment plain-list item table table-row diary-sexp horizontal-rule inlinetask latex-environment
all-objects
bold underline italic code strike-through verbatim subscript superscript entity inline-babel-call inline-src-block line-break link target radio-target statistics-cookie footnote-reference table-cell timestamp macro export-snippet citation citation-reference latex-fragment

沢山あって大変ですが、やりたいことはこの全要素を網羅したorg-mode文書の各地点で適切なメニューを表示するということになります。

org-elementとtransientをつなぐ

transientには条件毎にメニューの項目を無効化する仕組みがあり、それを使ってメニューの内容を現在の文脈に合わせることもできます。つまり一つのメニュー(transient prefix)を定義して、その中に全部のコマンドを追加して所々述語で無効化していくわけです。

しかしそれよりは、まずtypeとそれに対して使えるコマンドの一覧表があって、そこからtypeごとのメニューを生成してしまった方がずっと楽な気がします。雑然とコマンドが並んでも分かりづらいので、カテゴリー分けするための情報はある程度持たせる必要はありますが。

なので次のような構造を考えました。

type -> group -> ( command[key desc func] | string | group(入れ子) )

まず事前にtypeとgroupに対してcommandを登録していきます。commandが使用できるtypeに対してのみ登録していきます。例えばリンクを開くコマンドはlinkというidを持つtype内の適当なグループに追加します。上のmy-org-kill-elementのように沢山の種類の要素に適用出来るコマンドもあるので、追加先はリストやallのような別名でも指定できるようにしておきます。

そしてメニューを開くときに、org-elementで現在のtypeを割り出し、そのtypeに応じたメニューをgroupから組み立ててtransient-define-prefixで定義して実行します。

org-cmenu

そんな感じのやり方で作ったのがorg-cmenuです。

misohena/org-cmenu: Context Sensitive Menu for Emacs Org Mode

org-cmenu-setup.elというファイルが前述の「typeとgroupに対してcommandを登録していく」処理になり、ここがメニューの内容を決めています。既存のorg-modeのコマンドを分類して追加しているほか、新たに欲しい機能はorg-cmenu-tools.elの方に書いてから追加しています。

メニューの構造を管理したり実際にメニューを表示する部分はorg-cmenu.elに入っています。コアの部分ですがこれだけでは何も表示されません。

これらを使うための設定は次のようになります。

;; メニューを開くorg-cmenuコマンド実行時にorg-cmenu.elを読み込む
(autoload 'org-cmenu "org-cmenu")
;; org-mode内にorg-cmenuコマンドを起動するキーを割り当てる
(add-hook 'org-mode-hook
          (lambda ()
            ;; お好きなキーを設定してください
            (define-key org-mode-map (kbd "C-^") #'org-cmenu)))

;; org-cmenu.elが読み込まれたらorg-cmenuコマンドが起動する前にメニュー内容を登録する
(with-eval-after-load "org-cmenu"
  (require 'org-cmenu-setup) ;;自分専用のsetupファイルに差し替えることもできる
  ;; ここで自分専用のコマンドを追加することもできる
)

リストに対するメニューを開く例

実際にall-types.orgのplain-listがあるところでメニューを起動すると次のようになります。

リストの段落の中でメニューを起動した様子
図1: リストの段落の中でメニューを起動した様子

現在の構文要素はparagraphです。段落に対してできることはまだそれほど多くはありません。何かobjectを挿入したり、全体をカット・コピーしたりするくらいでしょうか。

^キーを押すと一つ上の親要素が選択されます。2回押してplain-listを選択したところが次です。

親要素を選択したところ
図2: 親要素を選択したところ

リスト全体に対しては段落よりももう少し色々な操作ができるようになっています。 リスト全体をチェックボックス化したりLispのリストにしてコピーしたり。

リンクに対するメニュー

次は画像リンクに対して #+CAPTION#+ATTR_HTML を追加する例。

画像リンクに対して属性を追加する例
図3: 画像リンクに対して属性を追加する例

実は今回のメニューはこれがやりたいが為に作ったものでした。何か専用のキーを割り当てるのもバカバカしいちょっとしたことでした。でもCAPTIONやHTML属性(主にwidthやclass)の設定はこの記事を書くためだけでも既に何度も使用しています。

ちなみに #+CAPTION:#+ATTR_HTML:#+NAME: といった部分はorg-elementではaffiliated keywordsと呼ばれていて、原則的には全てのelement(非object)に付加できるようになっています(現実的には例外あり。table-cell等には付けようがない)。org-elementが返すノードの:beginプロパティはこのaffiliated keywordを含む要素全体の先頭になります。affiliated keywordを除いた先頭は:post-affiliatedプロパティで取得できます。

その他、リンクに対してはパス・説明部分の編集、リンク先を開く、パス・ファイル名のコピー、ファイル情報表示等を行えるようにしました。

画像リンクに対する操作一覧
図4: 画像リンクに対する操作一覧

これを作成しているときに初めて知ったのですが、 C-c C-o (org-open-at-point)によるリンクのオープンは、C-uを一回付けるとEmacs優先で、C-uを二回付けると外部アプリ優先(Windowsだとw32-shell-execute)で開くようになっていたんですね。知りませんでした……。

私は画像リンクに対してはプライベートな設定で撮影位置の地図表示やコピー、撮影日時のコピーなんかも加えています。この間やっていたことの続きですね。

表に対するメニュー

次はtable、table-row、table-cellに対する操作の例。

表に対して色々な操作をする例
図5: 表に対して色々な操作をする例

通常のカーソル移動の操作(C-f, C-b, C-p, C-n, C-a, C-e, M-<, M->)だけでセル単位で移動できるようにしてみました。

そのままC-SPCでリージョンを作成すれば複数のセルを矩形で選択、カット・コピー、ペーストできます。

TABやS-TABによるカラム幅の伸縮も非常に直感的になりました。

tableやtable-rowに対する操作をtable-cell選択時にも表示するかは迷ったのですが、あると便利なものは表示しておくことにしました。現在はファイルエクスポートやS式コピーといった頻繁には使わない機能は^でtableを選択しないと出てこないようにしてあります。

Insertメニュー

sectionやparagraph等で表示されるInsertメニュー。まだ発展途上ですが、objectは一通り挿入できるようになっています。elementはまだまだです。

Insertメニュー
図6: Insertメニュー

特に注目したいのがentityの挿入。entityについては以前org-modeで文字をエスケープする方法でも触れましたが、狙った文字を探すのが案外面倒なんですよね。なので、正引き・逆引きの両方に対応した補完入力を付けることで簡単に狙った文字をentityとして追加できるようにしました。C-^ i e & RETで \amp{} と入力されます。

Export Snippetなんかも(普段使わないので)地味に書き方を忘れたりするので便利です。もう書き方を検索する必要はありません!

上付き文字、下付き文字、entityに対するメニュー

superscript、subscript、entityに対しては org-toggle-pretty-entities が候補に出るようになっています。

prettyを有効にしたところ
図7: prettyを有効にしたところ

使っているところでC-^ pを押せば切り替えられるので多少は便利かもしれません。こういう地味な機能を盛り込むのも躊躇無くできるのがorg-cmenuの良い所です。

マニュアルを開く

ところで全てのメニューに ? キーとして Manual というのが書いてあるのにお気づきでしょうか。?キーを押すとその構文要素について書かれているorg-modeマニュアルの該当部分がブラウザで開きます。

これで覚えておかなければならない事がさらに減りますね。

最後に

まだまだ手つかずの構文要素が残っていますが全て埋める必要は無いでしょう。

考えていくといくらでもアイデアが出てきてキリがありません。必要になったときにちょくちょく追加していくことにします。

スニペットの挿入のような個人的なものも後から簡単に追加できるのが良いですね。

2021-12-27

transient.elで同じdescriptionを持つ二つの無名コマンドが衝突する件

前回に引き続きtransient.elをいじっているのですが、prefixの定義において別のキーに割り当てたコマンド(関数)が呼ばれてしまう現象に遭遇しました。

再現するコードは次の通りです。

(transient-define-prefix talk ()
  "Let's talk to animal."
  ["Dog" ("d" "Talk" (lambda () (interactive) (message "bowwow")))]
  ["Cat" ("c" "Talk" (lambda () (interactive) (message "meow")))])

(talk)

実行してみれば分かりますが、犬も猫のように鳴いてしまいます(dを押してもmeowと表示されます)。

実際のコードはもう少し複雑で、既存のコマンドを呼び出し規約に合致するようにラッピングする関数が挟まっていてそのあたりを調べたのですが原因が分からず、仕方ないのでtransient.elの中を追ってみたら原因が見つかりました。

原因は transient.el の次の部分です。

https://github.com/magit/transient/blob/51c50d8c828b5fac2878b651e2188ad0c6f44184/lisp/transient.el#L1024

transient.elの中には無名の関数が渡されたときに内部で関数名を付ける処理があって、その関数名が transient:<prefix>:<description> の形式になっていました。上の例だと transient:talk:Talk という名前の関数が定義されます(C-h fでも確認できます)。d(Dog)もc(Cat)もdescriptionが"Talk"で同じです。従って同じ関数名になってしまうので、後に定義するc(Cat)の関数だけが使われてしまうわけです。ちなみにdescriptionが無い場合はkey(割り当てキー)が使われます。

せめて (format "transient:%s:%s:%s" prefix (or (plist-get args :description) "") (plist-get args :key)) くらいだったらなと思いますが、keyも重複が許されている(述語でどれかを無効化するのを前提に)みたいなので、それも完全では無いのかもしれません。gensymみたいにカウンターで数字を割り当てていくというのも手ですが定義のたびにどんどん増えていくのは嫌かもしれません(消せばいいだけ?)。

仕方が無いので自分でfsetで関数名を付けてそれを渡すような実装にしました。上にも書いたようにラッピングする関数を通しているので、元の関数名に何らかのprefixを付ければ大丈夫です。

上の例のような場合、ドキュメントの例にあるようにinfixを使えということになるのかもしれません(最初にdかcを選んでからtを押すというような)。infixについてはまだ理解が十分ではないのですみません。

2021-12-21

Transientでメニューを作る

ちょっと興味があってTransientを見ています。MagitのUIに使われているというアレです。set-transient-map関数とも近いですね。text-scale-adjust…ほら、文字の大きさを+/-で変えられるやつ…なんかで使われていて一時的なキーマップを実現するやつです。あれにコマンドメニュー表示の仕組みを付け加えたような感じ? コマンドメニューを作るならHydraが便利なのですが、カスタマイズ性や動的な要素が必要になったのでTransientを調べてみることにしました。

最初にGitHub内のプロジェクトトップページを見てよく分からず探し回ってしまったのですが、まずはWikiのクイックスタートガイドをやれって感じみたいですね。

Developer Quick Start Guide

はい、後で苦手な英語と格闘して集中力が続くところまでやっておきます……。

他にも長いマニュアルの方には例えば次のような例が載っていて

(require 'transient)
(transient-define-prefix outline-navigate ()
  :transient-suffix     'transient--do-stay
  :transient-non-suffix 'transient--do-warn
  [("p" "previous visible heading" outline-previous-visible-heading)
   ("n" "next visible heading" outline-next-visible-heading)])

これだけで後は M-x outline-navigate を実行するとメニューが出て、outline-mode(org-modeも含む!)における見出しの前後移動がpキーとnキーで出来て、その状態はC-gで止めるまで続きます。

こんなちょっとしたことでも0から実装しようと思うと結構なコード量が必要ですからね。ありがたい話です。いや、 (read-key) とかでもいいならある意味簡単ではあるんですけどね……。

資料:

2021-12-19

Emacsのimage-map(画像に対するキーマップ)をカスタマイズする

insert-image 関数や put-image 関数で画像を作成すると、その場所(テキストプロパティなりオーバーレイなり)には image-map というキーマップが設定されて画像にカーソル(ポイント)を当てて何かキーを押すと画像に対する操作が実行されるようになっています。

image-map の内容はEmacs27の時点では次のようになっています。

image-map
(keymap
 (111 . image-save) ;;o
 (114 . image-rotate) ;;r
 (C-mouse-4 . image-mouse-increase-size)
 (C-wheel-up . image-mouse-increase-size)
 (C-mouse-5 . image-mouse-decrease-size)
 (C-wheel-down . image-mouse-decrease-size)
 (43 . image-increase-size) ;;-
 (45 . image-decrease-size)) ;;+

できるのは保存したり、回転したり、小さくしたり、大きくしたりといった程度です。

もう少し色々できても良いのではないでしょうか。

というわけで、思いついたものを少し追加してみました。

  • 関連付けられた外部アプリで開く、編集する(Windows版のみ実装。他のOSではopenコマンドとかを使うらしいです)
  • 画像に関する何かを開く
    • 画像情報を別バッファで表示する (Exif ToolやImageMagickのidentify等で)
    • 撮影場所の地図をブラウザで開く (先日のこれこれを使用。exif.elはまた新しいバグを見つけてしまったので追記しておきました)
    • 画像がある場所のディレクトリを開く (Diredで開いたらファイルの位置へジャンプ)
  • 画像に関する情報を表示・コピーする
    • パス
    • ファイル名
    • 緯度,経度
    • 撮影日時

どうせ覚えられないのでHydraにしてiキーでメニューが表示されるようにしました。

画像にカーソルを合わせてiを押したところ
図1: 画像にカーソルを合わせてiを押したところ
;;;  -*- lexical-binding: t; -*-
(require 'image)
(require 'hydra)
(require 'my-exif) ;;https://misohena.jp/blog/attach/20211219_my-exif.el
(require 'my-location) ;;https://github.com/misohena/my-location

;;;; Modify Image Key Map

(defun my-image-menu-setup ()
  (define-key
    image-map
    (kbd "i")
    (defhydra hydra-image-action (:hint nil :exit t)
      "
Image Menu:
^ExternalApp^  ^Open^          ^Copy^
------------------------------------------------------
_o_: Open      _i_: Info       _p_: Path
_e_: Edit      _m_: Map        _f_: File Name
^ ^            _d_: Directory  _l_: Latitude,Longitude
^ ^            ^ ^             _t_: Time

(ImageMap i:This Menu r:Rotate o:Save -:Decrease +:Increase)
"
      ("q" nil)
      ("i" my-image-info-at-point)
      ("m" my-image-open-map-at-point)
      ("o" my-image-open-by-app-at-point)
      ("e" my-image-edit-by-app-at-point)
      ("d" my-image-open-directory-at-point)

      ("p" my-image-copy-path-at-point)
      ("f" my-image-copy-file-name-at-point)
      ("l" my-image-copy-latlng-at-point)
      ("t" my-image-copy-time-at-point)
      )))

(my-image-menu-setup)

;;;; Get File Name at Point

(defun my-image-file-at-point ()
  ;; I referred to the image-save function defined in image.el
  (plist-get (cdr (image--get-image)) :file))

;;;; Get Image Information

(defun my-image-info (file)
  (interactive "fImage File: ")
  (when (and file (file-exists-p file))
    (let* ((fmt (pcase (file-name-extension file)
                  ;;("jpg" "")
                  ;;(_ "identify -verbose %s")
                  (_ "exiftool %s")))
           (cmd (format fmt file)))
      (my-shell-command-popup cmd "*Image Info*" "*Image Info Error*"))))

(defun my-image-info-at-point ()
  (interactive)
  (my-image-info (my-image-file-at-point)))

;;;; Get Image Date Time and Latitude/Longitude

(defun my-image-guess-time-from-file-name (file)
  (when (and (stringp file)
             (string-match "\\(20[0-9][0-9]\\|19[0-9][0-9]\\)-?\\(0[1-9]\\|1[0-2]\\)-?\\([0-3][0-9]\\)[ _]?\\(0[0-9]\\|1[0-2]\\)\\([0-5][0-9]\\)\\([0-5][0-9]\\)?" file))
    (encode-time
     (make-decoded-time
      :year (string-to-number (match-string 1 file))
      :month (string-to-number (match-string 2 file))
      :day (string-to-number (match-string 3 file))
      :hour (string-to-number (match-string 4 file))
      :minute (string-to-number (match-string 5 file))
      :second (string-to-number (or (match-string 6 file) "0"))))))

(defun my-image-timelatlng (file)
  (when file
    (let* (;; FILEからExifを読み込む。
           (exif (and (member (file-name-extension file) '("jpg" "jpeg"))
                      (my-exif-parse-file file)))
           ;; 撮影日時を取得する。
           (time (or (and exif (my-exif-date-time-original exif))
                     (my-image-guess-time-from-file-name file)))
           ;; 撮影位置を取得する。
           (latlng (or (and exif (my-exif-latlng exif)) ;;From GPS Info
                       (and time (my-location-latlng-at-time time))))) ;From GPX File
      (cons time latlng))))

(defun my-image-latlng (file)
  (cdr (my-image-timelatlng file)))

;;;; Open Map of Image Shooting Location

(defun my-image-open-map (file)
  (interactive "fImage File: ")
  (when (and file (file-exists-p file))
    (when-let ((ll (my-image-latlng file)))
      (my-location-browse-map ll)
      ll)))

(defun my-image-open-map-at-point ()
  (interactive)
  (my-image-open-map (my-image-file-at-point)))

;;;; Open Directory Containing Image

(defun my-image-open-directory-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point)))
    (find-file (file-name-directory file))
    (when (eq major-mode 'dired-mode)
      (dired-jump nil file))))

;;;; Open Image by External App

(defun my-image-open-by-app-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point)))
    ;;@todo support other platforms
    (w32-shell-execute "open" file)))

;;;; Edit Image by External App

(defun my-image-edit-by-app-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point)))
    ;;@todo support other platforms
    (w32-shell-execute "edit" file)))

;;;; Copy Image Information

(defun my-image-copy-and-show (str)
  (when str
    (kill-new str)
    (message "%s" str)))

(defun my-image-copy-path-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point)))
    (my-image-copy-and-show file)))

(defun my-image-copy-file-name-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point)))
    (my-image-copy-and-show (file-name-nondirectory file))))

(defun my-image-copy-latlng-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point))
             (latlng (my-image-latlng file)))
    (my-image-copy-and-show (format "%.6f,%.6f" (car latlng) (cdr latlng)))))

(defun my-image-copy-time-at-point ()
  (interactive)
  (when-let ((file (my-image-file-at-point))
             (time (car (my-image-timelatlng file))))
    (my-image-copy-and-show (format-time-string "%Y-%m-%d %H:%M:%S" time))))

;;;; Execute Shell Command

(defun my-shell-command-popup (command output-buffer error-buffer)
  "Execute COMMAND and pop up the resulting buffer."

  (let* ((kill-buffers ;;lexical binding
          (lambda ()
            (when (get-buffer output-buffer) (kill-buffer output-buffer))
            (when (get-buffer error-buffer) (kill-buffer error-buffer))))
         (quit
          (lambda ()
            (interactive)
            (quit-window)
            (funcall kill-buffers)))
         (init-buffer
          (lambda (buffer-name)
            (when (get-buffer buffer-name)
              (with-current-buffer buffer-name
                (read-only-mode)
                (local-set-key "q" quit)))))
         (result-code
          (progn
            (funcall kill-buffers)
            (shell-command command output-buffer error-buffer))))
    (funcall init-buffer output-buffer)
    (funcall init-buffer error-buffer)
    (pop-to-buffer (if (equal result-code 0) output-buffer error-buffer))
    result-code))

Org-modeでバリバリインライン画像を使っているともっと色々な操作(例えば画像にキャプションを追加したり、属性を設定したり)が欲しくなるのですが、それはここじゃないほうが良いのでしょうね。