2023-08-17

image-diredで不要になったサムネイルを削除する

前回の続き。image-diredが生成するサムネイルを元の画像の場所が分かるような形式にしたのでした(image-dired-thumbnail-storage'image-dired-dir のときの話)。

image-diredのサムネイルを元の画像の場所が分かる形式にする(2023-08-17修正あり)

今回はこの仕組みを使って、不要になったサムネイルを削除するコマンドを作ります。

まずサムネイルのパスから元の画像の場所を求める関数を作成します。

;;;; サムネイルファイル名のデコード

(defun my-image-dired--decode-thumb-name (str)
  "STR内の%xx表記を元の文字に戻します。"
  (replace-regexp-in-string
   "%[0-9A-Fa-f][0-9A-Fa-f]"
   (lambda (entity) (char-to-string (string-to-number (substring entity 1) 16)))
   str t t))

(defun my-image-dired-thumb-original-file-name (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルのファイル名(ディレクトリ
部分を含まない)を返します。"
  (my-image-dired--decode-thumb-name (file-name-sans-extension ;;jpgを取り除く
                                      (file-name-nondirectory thumb-path))))

(defun my-image-dired-thumb-original-dir (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルがあるディレクトリを返します。"
  (setq thumb-path (expand-file-name thumb-path))
  (unless (string-prefix-p (expand-file-name (image-dired-dir))
                           thumb-path)
    (error "THUMB-PATH is not in the `image-dired-dir'"))
  (my-image-dired--decode-thumb-name
   (file-name-nondirectory
    (directory-file-name
     (file-name-directory thumb-path)))))

(defun my-image-dired-thumb-original-path (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルへのパスを返します。
元画像ファイルは存在しているとは限りません。"
  (file-name-concat (my-image-dired-thumb-original-dir thumb-path)
                    (my-image-dired-thumb-original-file-name thumb-path)))

そしてこれらを使って元の画像が既に無くなっているサムネイルを削除するコマンドを作りました。

;;;; サムネイルの掃除
(defvar my-image-dired-clean-verbose nil)

(defun my-image-dired-clean-thumbs-dir (thumbs-dir &optional forced-delete-p)
  "サムネイルが格納されているディレクトリTHUMBS-DIR内を掃除します。

THUMBS-DIRは`my-image-dired--thumb-dir'で作成されたような、元のディ
レクトリが%エンコードされているパスです。その下にあるファイルは
`my-image-dired--thumb-file-name'でエンコードされている必要があり
ます。

FORCED-DELETE-Pがnon-nilの時は、元画像がまだあるサムネイルも含め
て全てのサムネイルが削除されます。"
  (let ((original-dir (my-image-dired-thumb-original-dir
                       (file-name-as-directory thumbs-dir)))
        (valid-count 0)
        (deleted-count 0)
        (error-count 0))
    (dolist (thumb-path (directory-files thumbs-dir t "\\.jpg\\'"))
      (let* ((original-filename (my-image-dired-thumb-original-file-name
                                 thumb-path))
             (original-path (file-name-concat original-dir original-filename)))
        (if (and (not forced-delete-p)
                 (file-exists-p original-path))
            ;; 有効なサムネイルが存在した。
            (cl-incf valid-count)
          ;; 無効なサムネイルを消す。
          (when my-image-dired-clean-verbose
            (message "Delete thumb %s (Original:%s)" thumb-path original-path))
          (condition-case err
              (progn
                (delete-file thumb-path)
                (cl-incf deleted-count))
            (error
             (when my-image-dired-clean-verbose
               (message "Failed to delete thumb %s (Original:%s) err=%s" thumb-path original-path err))
             (cl-incf error-count))))))

    ;; 有効なサムネイルが存在しないならディレクトリの削除を試みる。
    (when (= valid-count 0)
      (when my-image-dired-clean-verbose
        (message "Delete thumbs dir %s" thumbs-dir))
      (ignore-errors (delete-directory thumbs-dir)))

    ;; 結果を返す。
    (my-image-dired-clean-count-make deleted-count error-count valid-count)))

;; 処理結果の意味

(defun my-image-dired-clean-count-make (&optional deleted error valid)
  (list (or deleted 0) (or error 0) (or valid 0)))
(defun my-image-dired-clean-count-inc (dst src)
  (cl-loop for dst-cell on dst
           for src-value in src
           do (setcar dst-cell (+ (car dst-cell) src-value))))
(defun my-image-dired-clean-count-deleted (count) (nth 0 count))
(defun my-image-dired-clean-count-error (count) (nth 1 count))
(defun my-image-dired-clean-count-valid (count) (nth 2 count))

(defun my-image-dired-clean-report (count show-msg)
  (when show-msg
    (message "Deleted %s, Error %s, Valid %s"
             (my-image-dired-clean-count-deleted count)
             (my-image-dired-clean-count-error count)
             (my-image-dired-clean-count-valid count)))
  count)

(defun my-image-dired-clean-thumbs (&optional forced-delete-p)
  "既に存在しない画像に対するサムネイルを削除します。"
  (interactive)
  ;; `image-dired-dir'下の全てのディレクトリについて
  ;; `my-image-dired-clean-thumbs-dir'を呼び出す。
  (let ((thumbs-root-dir (image-dired-dir))
        (count (my-image-dired-clean-count-make)))
    (dolist (file (directory-files thumbs-root-dir))
      (when (not (member file '("." "..")))
        (let ((thumbs-dir (expand-file-name file thumbs-root-dir)))
          (when (file-directory-p thumbs-dir)
            (my-image-dired-clean-count-inc
             count
             (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p))))))
    (my-image-dired-clean-report count (called-interactively-p 'interactive))
    count))

ディレクトリを限定して、お掃除をするコマンドも追加します。

(defun my-image-dired-clean-thumbs-under (original-dir &optional subdirs-p
                                                       forced-delete-p)
  "ORIGINAL-DIR下の画像に対するサムネイルのうち不要になったものを削除します。

SUBDIRS-Pがnon-nilの場合、ORIGINAL-DIR以下にあった子孫ディレクト
リも処理に含めます。

インタラクティブに実行した場合は、現在のディレクトリが
original-dirになり、コマンドのプレフィックス引数がsubdirs-pになり
ます。"
  (interactive
   (list
    (if (derived-mode-p 'dired-mode 'wdired-mode)
        (dired-current-directory) ;;複数のディレクトリを表示している場合に備えて
      default-directory)
    current-prefix-arg))

  (let ((thumbs-dir (my-image-dired--thumb-dir
                     (file-name-as-directory original-dir)))
        (count (my-image-dired-clean-count-make)))
    (if subdirs-p
        ;; 子孫ディレクトリを含める。
        ;; エンコードしたディレクトリパスと先頭がマッチする
        ;; サムネイルディレクトリを全て対象にする。
        ;; 既に削除されているディレクトリも列挙できる。
        (dolist (thumbs-dir (directory-files
                             (image-dired-dir)
                             t
                             (concat "\\`"
                                     (regexp-quote
                                      (file-name-nondirectory thumbs-dir)))))
          (my-image-dired-clean-count-inc
           count
           (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p)))
      ;; 指定ディレクトリ下のみ。
      (when (file-directory-p thumbs-dir)
        (setq count
              (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p))))
    (my-image-dired-clean-report count (called-interactively-p 'interactive))
    count))

ついでに無条件でサムネイルを削除するコマンドも追加しておきます。

(defun my-image-dired-delete-thumbs ()
  "全てのサムネイルを削除します。"
  (interactive
   (unless (yes-or-no-p "Delete all thumbnails? ")
     (keyboard-quit)))
  (my-image-dired-clean-report
   (my-image-dired-clean-thumbs t)
   (called-interactively-p 'interactive)))


(defun my-image-dired-delete-thumbs-under (original-dir &optional subdirs-p)
  "ORIGINAL-DIR下の画像に対するサムネイルを全て削除します。

SUBDIRS-Pがnon-nilの場合、ORIGINAL-DIR以下にあった子孫ディレクト
リも処理に含めます。

インタラクティブに実行した場合は、現在のディレクトリが
original-dirになり、コマンドのプレフィックス引数がsubdirs-pになり
ます。"
  (interactive
   (list
    (if (derived-mode-p 'dired-mode 'wdired-mode)
        (dired-current-directory) ;;複数のディレクトリを表示している場合に備えて
      default-directory)
    current-prefix-arg))

  (my-image-dired-clean-report
   (my-image-dired-clean-thumbs-under original-dir subdirs-p t)
   (called-interactively-p 'interactive)))

これで次の4つのコマンドが出来ました。

  • my-image-dired-clean-thumbs : 元の画像が無くなったサムネイルを削除
  • my-image-dired-clean-thumbs-under : 同ディレクトリ指定バージョン
  • my-image-dired-delete-thumbs : 無条件でサムネイルを削除
  • my-image-dired-delete-thumbs-under : 同ディレクトリ指定バージョン

無条件削除はdiredで~/.emacs.d/image-diredを開いて手動で削除すればいい気もするんですけどね。一応。

cleanやclean-underをいつ呼び出すかは残された問題です。一括サムネイル表示コマンド(image-dired-show-all-from-dirや私の改造版だとmy-image-dired-dired-show-all-thumbsとか)に仕込んでおくくらいでしょうか? いや、ファイル数が多いディレクトリではあまり良くない? 最悪手動で呼び出すしかないですね。そのあたりは好き好きで。

後は許容容量に応じて古くなったサムネイルを削除する機能とか。「古くなった」と一口に言ってもそれはどういう意味なのか。最終アクセス日時情報が取れないと難しいところはあります。まぁ、その辺はもういいかなーという気もします。結局気が向いたときにdiredで~/.emacs.d/image-diredを開いて消せば良いのでしょう。MD5ハッシュだとそれすら簡単には出来ないので前回・今回の改造は意義があることでしょう。

それにしても本家はこの辺りどうするつもりなんでしょうね。タグやコメントでは~/.emacs.d/image-dired/.image-dired_dbというファイルを作って記録するので、結局その延長で対応表ファイルを作るくらいなのかなーと予想しますが。

2023-08-12

image-diredのサムネイルを元の画像の場所が分かる形式にする

image-diredのサムネイルはデフォルトでは ~/.emacs.d/image-dired に配置されます。(image-dired-dir変数やimage-dired-thumbnail-storage変数によって変更できます)

実際にそのディレクトリを見ると次のようになっています。

~/.emacs.d/image-diredの下
図1: ~/.emacs.d/image-diredの下

サムネイルのファイル名には、オリジナル画像のフルパス名をSHA-1で変換した文字列が用いられています。

これは見た目がスマートで格好よさげですが、実用上は元のファイル名に戻せないという問題があります。どこか別の場所に対応関係を保存しているのだろうと思いきや、そのようなものは見当たりません。となると、この沢山のサムネイルの中で元の画像がすでに削除されてしまったものを探すのは困難です。やるとすれば、ストレージの中にある全ての画像ファイルに対してSHA-1を適用して合致しなかったものを列挙するくらいでしょうか。過去に特定のディレクトリ下にあった(画像に対する)サムネイルを列挙するのもほぼ不可能でしょう。

サムネイルファイルに何かメタ情報を埋め込むという手もあります。例えば image-dired-thumbnail-storage が 'standard 等のときは、pngファイルの "Thumb::URI" 属性として "file://%f" が埋め込まれるようなので、これを元に元の画像を割り出すことも可能でしょう。とは言え特定のディレクトリ下にある画像のサムネイルを列挙するのに全てのサムネイルをスキャンしなければならないのは非効率です。

どこかに対応関係を示すファイルなりデータベースなりを作るというのも手ですが、(特にファイルであれば)サムネイル生成時のパフォーマンスや不整合の問題も出てくるでしょう。sqliteでも使う?

あくまでファイル名、ディレクトリ名レベルで何とかするのがお手軽ではないでしょうか。

フルパス名をURLエンコードのように%を付けてエスケープするのはどうでしょう。

例えば次のフルパスを

C:/home/hoge/project1/資料/room1.png

次のようにします。

C%3a%2fhome%2fhoge%2fproject1%2f資料%2froom1%2epng

これでもいいのですが、一応ディレクトリとファイル名を分けて最終的に次の場所にサムネイルを配置するようにしてみました。

~/.emacs.d/image-dired/C%3a%2fhome%2fhoge%2fproject1%2f資料%2f/room1%2epng.jpg

特定のディレクトリにある画像を列挙するのも簡単です(サブディレクトリを全て列挙するには多少ディレクトリを検索しなければなりませんが)。

長いファイル名問題はありますがそこは無視する方向で。

以下それを実現するコード。(すべてEmacs 29.1に対する追加です)

;;;; サムネイルファイル名のエンコード

(defun my-image-dired--encode-thumb-name (path)
  "PATHをファイルのベース名として使える文字列へエンコードします。"
  ;; url-hexify-stringは少し問題があるので使えない。
  (mapconcat (lambda (ch)
               ;; 変換元がファイル名なのでパス区切り文字以外は不要だと思
               ;; うが念のため色々エスケープしておく。
               ;; 拡張子を示す.もエスケープして完全にベース名として認識
               ;; されるようにする。
               (if (or (<= ch 32)
                       (memq ch '(?< ?> ?: ?\" ?/ ?\\ ?| ?? ?* ?. ?%))) ;;2023-08-13追記:?%が抜けていたのを修正
                   (format "%%%02x" ch)
                 (char-to-string ch)))
             path))

(defun my-image-dired--thumb-dir (original-file) ;;2023-08-17追記:追加
  "ORIGINAL-FILEに対するサムネイルを格納する場所を返します。"
  (expand-file-name
   ;; %エンコードする
   (my-image-dired--encode-thumb-name
    ;; 最後のスラッシュは含めてしまっていいかな。c:/とかあるし。
    (file-name-directory (expand-file-name original-file)))
   (image-dired-dir)))

(defun my-image-dired--thumb-file-name (original-file) ;;2023-08-17追記:追加
  "ORIGINAL-FILEに対応するサムネイルファイル名を返します。
ディレクトリは含まれません。"
  (concat
   ;; %エンコードする(ファイル名部分のみ・拡張子込み)
   (my-image-dired--encode-thumb-name
    (file-name-nondirectory original-file))
   ;; 拡張子を付ける
   ".jpg"))

(defun my-image-dired--thumb-file-path (original-file) ;;2023-08-17追記:追加
  "ORIGINAL-FILEに対応するサムネイルファイル名のフルパスを返します。"
  (file-name-concat (my-image-dired--thumb-dir original-file)
                    (my-image-dired--thumb-file-name original-file)))

;;;; サムネイルのファイル名を元の画像が分かるようなものにする

;; image-diredが生成するサムネイルのファイル名を、元画像の場所が分か
;; るような形式にする。

(defun my-image-dired-thumb-name (file)
  "`image-dired-thumb-name'を置き換えるための関数です。
画像ファイルFILEを格納するためのサムネイルファイルのパスを返します。
格納できるようにするために必要なディレクトリを作成する場合があります。"
  ;;2023-08-17追記:my-image-dired--thumb-dirとmy-image-dired--thumb-file-nameを使うようにしました。
  (let* ((thumb-dir (my-image-dired--thumb-dir file))
         (thumb-filename (my-image-dired--thumb-file-name file)))
    ;; ここでディレクトリを作ってしまうのはあまり良くないけど……
    (unless (file-directory-p thumb-dir)
      (with-file-modes #o700
        (make-directory thumb-dir t)))
    (file-name-concat thumb-dir thumb-filename)))

(defun my-image-dired-thumb-name-around (old-func file)
  "`image-dired-thumb-name'に対する:aroundアドバイスです。"
  (if (eq 'image-dired image-dired-thumbnail-storage)
      ;; image-dired-thumbnail-storageが'image-diredの時だけ
      ;; 独自のファイル名を生成する。
      (my-image-dired-thumb-name file)
    ;; その他は本来の関数を呼び出す。
    (funcall old-func file)))

(advice-add 'image-dired-thumb-name :around 'my-image-dired-thumb-name-around)

これを適用すると ~/.emacs.d/image-dired は次のようになります。

変更を適用した後の~/.emacs.d/image-diredの下
図2: 変更を適用した後の~/.emacs.d/image-diredの下

これならどこのディレクトリに対するサムネイルか一目瞭然ですし、diredで確認して不要だと思ったものだけを簡単に削除できます。別にこれで良くないですか?

もちろん自動的に指定したディレクトリのサムネイルを一括で削除するコマンドを作ったり、既に存在しない画像に対するサムネイルを掃除するコマンドを作るのも良さそうです。

この方法が嫌なのであれば、次善の策はサムイル画像へのメタ情報の埋め込み、sqlite等でサムネイルデータベースを構築、そこまでせず管理用のlispオブジェクトをファイルに読み書きする、といったくらいでしょうか。どれを取っても正直今ひとつといった感じはしますが。

なんかいじればいじるほどimage-diredは改善点が出てきてしまいますね。

2023-08-12

image-converterで動画ファイルを開いたときに固まるのを避ける

先日の設定でEmacsから動画系のファイルも画像として扱えるようになったのですが、実際にimage-modeで開こうとすると膨大な時間がかかることが分かりました。

気が付いたきっかけは@yoyaさんのこのツイート。

実際にimage-modeでビデオファイルを表示させようとしたところ確かにEmacsが固まりました。巨大なビデオファイルの場合はそもそもEmacsがファイルを読み込む段階で止まっているようでしたが、小さめなビデオファイルの場合は裏でffmpegが何やらテンポラリディレクトリにmagick-で始まるファイルを生成していてそれにかなり時間がかかっているようでした。小さめなものであればそのうち終わります。

しかしimage-diredでサムネイルを生成する場合にはそれほど時間がかかりません。巨大なビデオファイルでも大丈夫です。基本的にdiredからビデオファイルを開くときは外部のプレイヤーを起動するようになっていて、あくまでサムネイルを生成するために動画系のファイルに対応しただけなので気が付きませんでした。image-modeで表示しようとするとダメでサムネイル生成は大丈夫。両者は何が違うのでしょうか。

実際に実行されるコマンドを元に調査したところ、入力ファイル名の後に [0] があるかどうかで変わることが分かりました。

magick convert video.mp4 jpg:image.jpg
magick convert video.mp4[0] jpg:image.jpg

おそらく入力を指定する段階で最初の1枚目だけと限定されているので問題を回避できるのでしょう。ImageMagickは沢山の形式に対応していますから、おそらく入力は形式毎にモジュール化されていて、入力モジュールには必要なフレームに関する情報が引き渡されず、かといって読み込みを必要になるまで遅延するような仕組みも無いため全て読み込むしかない、といったところではないでしょうか。

image-diredの方はこの問題に気が付いたのかちゃんと対策をしてくれていますが、image-converterの方は対策されていません。まぁ、動画なんて開くなよ、ということなのかもしれません。

とは言え、一応次のようなコードで無理矢理入力ファイルの末尾に [0] を付けるようにしたところ問題は解消しました。

;; image-converterがImageMagickで動画ファイルを変換するときに長時間
;; 固まるのを避ける。
;; 全フレーム読み込もうとしてしまうのだとか!
(defun my-image-converter--convert-magick (old-fun type source image-format)
  ;; ファイル名の後に[0]をつける。data形式の場合は未対応。
  (unless image-format
    (setq source (concat source "[0]")))
  (funcall old-fun type source image-format))

(advice-add 'image-converter--convert-magick :around
            'my-image-converter--convert-magick)
2023-08-10

image-diredでmp3カバー画像を表示する

前回ImageMagickがサポートしている画像形式をできるだけ登録したので、いろんな画像ファイルを眺めては悦に入っていたのですが、音楽ディレクトリを見たときにmp3ファイルにサムネイルが表示されないことに気が付きました。mp3ファイルにはアルバムのカバー画像(アルバムアート?)が埋め込まれているケースが多いので、表示しようと思えば出来ないことはないはずです。少し調べてみたらffmpegを使って取り出せることが分かったので試してみました。

まずはimage-converter-add-handler関数を使ってmp3やm4aのときにffmpegを使うようにしてみました。

;; image-mode等で表示する方法

(defun my-image-convert-ffmpeg (source format)
  (image-converter--convert 'ffmpeg source format))

(image-converter-add-handler "mp3" #'my-image-convert-ffmpeg)
(image-converter-add-handler "m4a" #'my-image-convert-ffmpeg)

これだけで、mp3ファイルをEmacsで開いてからimage-modeを立ち上げるとカバー画像が表示されます。

しかしこれだけではimage-diredでサムネイルが表示されません。image-diredはimage-converterとはまた別の仕組みでサムネイルを生成していたのでした。こちらはImageMagickとGraphicsMagickのみ考慮されていてffmpegは対応していません。また、画像形式によってプログラムやオプションを変える仕組みもありません。なので、advice-addを使ってサムネイル生成関数の挙動を無理矢理変更することで実現しました。

;; image-diredでサムネイルを表示する方法

(defun my-image-dired-create-thumb-1-around (orig-func
                                             original-file thumbnail-file)
  (if (member (file-name-extension original-file) '("mp3" "m4a"))

      ;; ffmpegを使うように一時的に変数を書き替えてから実行
      (let ((image-dired-cmd-create-thumbnail-program "ffmpeg")
            (image-dired-cmd-create-thumbnail-options
             '("-i" "%f"
               "-vf" "scale=%w:%h:force_original_aspect_ratio=decrease"
               "%t")))
        (funcall orig-func original-file thumbnail-file))

    ;; 通常通りに実行
    (funcall orig-func original-file thumbnail-file)))

(advice-add 'image-dired-create-thumb-1 :around #'my-image-dired-create-thumb-1-around)

するとこんな感じでdired内にカバー画像が表示できました。

mp3やm4aファイルのカバー画像をDired内に表示した例
図1: mp3やm4aファイルのカバー画像をDired内に表示した例
2023-08-10

画像形式とimage-converterの設定

前回対応する画像形式を詳しく調べたことやimage-diredを色々いじっていたことを踏まえて、Emacsの画像形式に関する設定を更新しました。

;; (Emacs 29.1で確認)

;; 画像のコンバーターとしてImageMagickを使う。
;; GraphicsMagickは対応形式が少なくmp4やpsdに対応していない。
;; FFmpegは動画中心で色々足りない。
;; see: https://misohena.jp/blog/2023-08-09-imagemagick-vs-graphicsmagick-vs-ffmpeg-for-emacs.html
(setq image-converter 'imagemagick) ;; 注意: 変更時は下の拡張子を修正すること。

;; 変換対象の画像形式を登録する。
(let ((target-extensions
       '(;; ImageMagickが対応する形式のうち問題が無さそうなものを適当に残した。
         ;; セキュリティ的にはもっと絞った方が良いというのはある。
         ;; 他人が作ったファイルを不用意に開くべからず。
         ;; 一覧は (image-converter--probe 'imagemagick) で得られる。
         "3g2" "3gp" "ai" "apng" "art" "avi" "avif"
         "bmp" "cr2" "cr3" "cur" "dcm"
         "dcr" "dds" "dng" "dpx" "dxt1" "dxt5"
         "epdf" "epi" "eps" "epsf" "epsi" "ept" "ept2" "ept3" "erf"
         "fits" "fl32" "flif" "flv"
         "fts" "gif"
         "hdr" "heic" "heif" "hrz"
         "icb" "ico" "icon" "iiq" "ipl" "j2c" "j2k" "jbg" "jbig"
         "jng" "jnx" "jp2" "jpc" "jpe" "jpeg" "jpg" "jpm" "jps" "jpt" "k25"
         "kdc" "m2v" "m4v" "mef" "miff"
         "mkv" "mng" "mono" "mov" "mp4" "mpc" "mpeg" "mpg" "mpo" "mrw"
         "mtv" "mvg" "nef" "nrw" "orf" "otb" "otf"
         "pam" "pbm" "pcd" "pcds" "pcl"
         "pct" "pcx" "pdf" "pdfa" "pef" "pfa" "pfb" "pfm"
         "pgm" "pgx" "phm" "picon" "pict" "pix" "pjpeg" "png"
         "pnm"
         "ppm" "ps" "psb" "psd" "ptif" "pwp" "qoi" "raf" "ras"
         "rgf" "rla" "rle" "rmf" "rw2"
         "sfw"
         "sgi" "six" "sixel" "sr2" "srf"
         "sun"
         "svg" "svgz" "tga" "tiff" "tiff64" "tim"
         "tm2" "ttc" "ttf" "vda" "vicar" "viff" "vips"
         "vst" "wbmp" "webm" "webp" "wmv" "wpg" "x3f" "xbm"
         "xcf" "xpm" "xps" "xv")))

  ;; 対象をimage-file-name-extensionsに追加する。
  ;; おそらく本来はこれだけで良いはず。
  (setq image-file-name-extensions
        (seq-union image-file-name-extensions target-extensions))

  ;; いくつか問題があるので、image-converter.el内の変数を直接変更する。
  ;; (Emacs 29.1時点)
  ;;
  ;; 問題:
  ;;
  ;; - image-file-name-extensionsに指定していない形式もコンバーター
  ;;   を使ってimage-modeやcreate-imageで表示できてしまう。
  ;;
  ;; - コンバーターの初回起動に何秒もかかる。コンバーターの対応形式
  ;;   をリストアップするのに時間がかかるので。
  ;;
  ;; - 一度コンバーターが起動すると、image-file-name-extensionsに指
  ;;   定していない形式もimage-diredでサムネイル表示されるようになっ
  ;;   てしまう。
  ;;
  ;; 対応形式をリストアップする前に手動で設定してしまうことで問題を回避する。
  ;; ここはimage-converter.elの実装が変わると変更が必要になるかもしれない。
  (setq image-converter-file-name-extensions target-extensions)
  (setq image-converter-regexp
        (concat "\\." (regexp-opt target-extensions) "\\'"))

  ;; 変換対象の拡張子を持つファイルをimage-modeで開く。
  ;; auto-mode-alistの初期値には
  ;; 「Image file types probably supported by `image-convert'.」
  ;; として既に含まれているものも多いが、全てが登録されているわけではない。
  ;; psdとか。
  ;; auto-image-file-modeでもいいのかもしれない。
  (dolist (ext target-extensions)
    ;; すでにauto-mode-alistに登録されている拡張子は変更しない。
    (unless (assoc-default (format "a.%s" ext) auto-mode-alist 'string-match)
      (push (cons (format "\\.%s\\'" ext) 'image-mode)
            auto-mode-alist))))

;; create-imageでコンバーターを使う。
(setq image-use-external-converter t)

;; ImageMagickのconvertコマンドをmagick convertに置き換える。
;; convertはWindowsで困るので。
(with-eval-after-load "image-converter" ;;image-converter.elが読み込まれてから
  (setf (plist-get (alist-get 'imagemagick image-converter--converters)
                   :command)
        '("magick" "convert")))

;; 2023-08-12追記
;; MP3等対応
;; image-mode等でMP3等をを表示する方法
(defun my-image-convert-ffmpeg (source format)
  (image-converter--convert 'ffmpeg source format))
(image-converter-add-handler "mp3" #'my-image-convert-ffmpeg)
(image-converter-add-handler "m4a" #'my-image-convert-ffmpeg)
;; image-diredでMP3等をサムネイルを表示する方法
(defun my-image-dired-ffmpeg-options (file-ext)
  (pcase file-ext
    ("mp4" ;;動画のサムネイルもffmpegで生成する。
     '("-stream_loop" "-1" ;;短い動画に備えて無限ループさせる。
       "-ss" "30" ;;開始30秒時点のフレームを使う。
       "-i" "%f"
       "-vf" "scale=%w:%h:force_original_aspect_ratio=decrease"
       "-update" "true"
       "-vframes" "1"
       "%t"))
    ((or "mp3" "m4a")
     '("-i" "%f"
       "-vf" "scale=%w:%h:force_original_aspect_ratio=decrease"
       "-vframes" "1"
       "%t"))))
(defun my-image-dired-create-thumb-1-around (orig-func
                                             original-file thumbnail-file)
  (if-let ((ffmpeg-options (my-image-dired-ffmpeg-options
                            (file-name-extension original-file))))
      (let ((image-dired-cmd-create-thumbnail-program "ffmpeg")
            (image-dired-cmd-create-thumbnail-options ffmpeg-options))
        (funcall orig-func original-file thumbnail-file))
    (funcall orig-func original-file thumbnail-file)))
(advice-add 'image-dired-create-thumb-1 :around #'my-image-dired-create-thumb-1-around)

;; 2023-08-12追記
;; image-converterがImageMagickで動画ファイルを変換するときに長時間
;; 固まるのを避ける。
;; 全フレーム読み込もうとしてしまうのだとか!
;; image-diredの方は対策済み。
(defun my-image-converter--convert-magick (old-fun type source image-format)
  ;; ファイル名の後に[0]をつける。data形式の場合は未対応。
  (unless image-format
    (setq source (concat source "[0]")))
  (funcall old-fun type source image-format))
(advice-add 'image-converter--convert-magick :around
            'my-image-converter--convert-magick)

;; 2023-08-12追記
;; image-diredが生成するサムネイルのファイル名を、元画像の場所が分か
;; るような形式にする。
(defun my-image-dired--encode-thumb-name (path)
  "PATHをファイルのベース名として使える文字列へエンコードします。"
  ;; url-hexify-stringは少し問題があるので使えない。
  (mapconcat (lambda (ch)
               ;; 変換元がファイル名なのでパス区切り文字以外は不要だと思
               ;; うが念のため色々エスケープしておく。
               ;; 拡張子を示す.もエスケープして完全にベース名として認識
               ;; されるようにする。
               (if (or (<= ch 32)
                       (memq ch '(?< ?> ?: ?\" ?/ ?\\ ?| ?? ?* ?. ?%)))
                   (format "%%%02x" ch)
                 (char-to-string ch)))
             path))

(defun my-image-dired-thumb-name (old-func file)
  (if (eq 'image-dired image-dired-thumbnail-storage)
      ;; 独自のファイル名を生成する。
      (let* ((orig-path (expand-file-name file))
             (orig-filename (file-name-nondirectory orig-path))
             (orig-dir (file-name-directory orig-path)) ;;最後のスラッシュは含めてしまっていいかな。c:/とかあるし。
             (thumb-filename
              (concat (my-image-dired--encode-thumb-name orig-filename)
                      ".jpg"))
             (thumb-dir
              (expand-file-name (my-image-dired--encode-thumb-name orig-dir)
                                (image-dired-dir))))
        ;; ここでディレクトリを作ってしまうのはあまり良くないけど……
        (unless (file-directory-p thumb-dir)
          (with-file-modes #o700
            (make-directory thumb-dir t)))
        (file-name-concat thumb-dir thumb-filename))
    ;; 本来の関数を呼び出す。
    (funcall old-func file)))
(advice-add 'image-dired-thumb-name :around 'my-image-dired-thumb-name)

加えて以前書いたimage-dired用の設定を適用。

image-diredの改善 | Misohena Blog

特にWindowsでは、サムネイルを作成する際のconvertをmagick convertに置き換えておいた方が良いです。

というわけで、より多様な形式の画像ファイルをEmacsで扱えるようになりました。

image-diredでフォントファイルを表示した例
図1: image-diredでフォントファイルを表示した例

.ttfファイルってImageMagickで画像ファイルに変換できたんですね。

2023-08-09

ImageMagick vs GraphicsMagick vs FFmpeg (Emacsのimage-converter変数にはどれを指定すべきか)

Emacs 29からGraphicsMagick対応が入りました。image系の機能でImageMagick(convertコマンド)を使う箇所がGraphicsMagick(gmコマンド)にも対応した形です。

早速試してみたのですが、image-diredでmp4ファイルのサムネイルが表示できなくなってしまいました(image-diredで多様なファイル形式を扱うには確か色々設定が必要だったと思うのですが、それはまた別の機会に)。どうもImageMagickとGraphicsMagickでは対応している形式に差があるようです。

というわけで、具体的にどのような差があるのか調べてみました。Emacsにはimage-converterという仕組みがあって、Emacsが標準で対応していない形式の画像でも外部のコンバーターを使用して変換し、表示することが出来ます。image-converter.elの中で定義されている image-converter--probe 関数は指定されたコンバーターがサポートする形式をリストアップします。それを使ってみました。

ImageMagickが対応する形式:

(setq image-converter--converters
  '((graphicsmagick :command ("gm" "convert") :probe ("-list" "format"))
    (ffmpeg :command "ffmpeg" :probe "-decoders")
    (imagemagick :command ("magick" "convert") :probe ("-list" "format")))) ;; magickコマンドを使うように修正する(WindowsだとSystem32にconvert.exeがあるので)
(setq im-formats (image-converter--probe 'imagemagick))
("3fr" "3g2" "3gp" "aai" "ai" "apng" "art" "arw" "avi" "avif" "avs" "bayer"
 "bayera" "bgr" "bgra" "bgro" "bie" "bmp" "bmp2" "bmp3" "cal" "cals" "canvas"
 "caption" "cin" "clip" "clipboard" "cmyk" "cmyka" "cr2" "cr3" "crw" "cube"
 "cur" "cut" "data" "dcm" "dcr" "dcraw" "dcx" "dds" "dfont" "djvu" "dng"
 "dpx" "dxt1" "dxt5" "emf" "epdf" "epi" "eps" "epsf" "epsi" "ept" "ept2"
 "ept3" "erf" "farbfeld" "fax" "ff" "file" "fits" "fl32" "flif" "flv"
 "fractal" "ftp" "fts" "ftxt" "g3" "g4" "gif" "gif87" "gradient" "gray"
 "graya" "group4" "hald" "hdr" "heic" "heif" "hrz" "http" "https" "icb" "ico"
 "icon" "iiq" "inline" "ipl" "j2c" "j2k" "jbg" "jbig" "jng" "jnx" "jp2" "jpc"
 "jpe" "jpeg" "jpg" "jpm" "jps" "jpt" "k25" "kdc" "label" "m2v" "m4v" "mac"
 "map" "mask" "mat" "mef" "miff" "mkv" "mng" "mono" "mov" "mp4" "mpc" "mpeg"
 "mpg" "mpo" "mrw" "msl" "msvg" "mtv" "mvg" "nef" "nrw" "null" "orf" "otb"
 "otf" "pal" "palm" "pam" "pango" "pattern" "pbm" "pcd" "pcds" "pcl" "pct"
 "pcx" "pdb" "pdf" "pdfa" "pef" "pes" "pfa" "pfb" "pfm" "pgm" "pgx" "phm"
 "picon" "pict" "pix" "pjpeg" "plasma" "png" "png00" "png24" "png32" "png48"
 "png64" "png8" "pnm" "pocketmod" "ppm" "ps" "psb" "psd" "ptif" "pwp" "qoi"
 "raf" "ras" "raw" "rgb" "rgb565" "rgba" "rgbo" "rgf" "rla" "rle" "rmf"
 "rsvg" "rw2" "scr" "sct" "sfw" "sgi" "six" "sixel" "sr2" "srf" "stegano"
 "strimg" "sun" "svg" "svgz" "text" "tga" "tiff" "tiff64" "tile" "tim" "tm2"
 "ttc" "ttf" "txt" "uyvy" "vda" "vicar" "vid" "viff" "vips" "vst" "wbmp"
 "webm" "webp" "wmf" "wmv" "wpg" "x3f" "xbm" "xc" "xcf" "xpm" "xps" "xv"
 "ycbcr" "ycbcra" "yuv" "r")

ちなみに処理に9.3秒もかかりました。何にそんなにかかっているんだろう。

次いでGraphicsMagickが対応する形式:

(setq gm-formats (image-converter--probe 'graphicsmagick))
("3fr" "8bim" "8bimtext" "8bimwtext" "app1" "app1jpeg" "art" "arw" "avif"
 "avs" "b" "bie" "bigtiff" "bmp" "c" "cals" "caption" "cin" "clipboard"
 "cmyk" "cmyka" "cr2" "crw" "cur" "cut" "dcm" "dcr" "dcx" "dng" "dpx" "emf"
 "epdf" "epi" "eps" "epsf" "epsi" "ept" "ept2" "ept3" "erf" "exif" "fax"
 "file" "fits" "fractal" "g" "gif" "gif87" "gradient" "gray" "graya" "heic"
 "heif" "hrz" "http" "icb" "icc" "icm" "ico" "icon" "identity" "image" "iptc"
 "iptctext" "iptcwtext" "j2c" "jbg" "jbig" "jng" "jnx" "jp2" "jpc" "jpeg"
 "jpg" "k" "k25" "kdc" "label" "m" "mac" "map" "mat" "mef" "miff" "mng"
 "mono" "mpc" "mrw" "msl" "mtv" "mvg" "nef" "null" "o" "orf" "otb" "p7" "pal"
 "palm" "pam" "pbm" "pcd" "pcds" "pct" "pcx" "pdb" "pdf" "pef" "pfa" "pfb"
 "pgm" "pgx" "picon" "pict" "pix" "plasma" "png" "png00" "png24" "png32"
 "png48" "png64" "png8" "pnm" "ppm" "ps" "ptif" "pwp" "r" "raf" "ras" "rgb"
 "rgba" "rla" "rle" "sct" "sfw" "sgi" "sr2" "srf" "stegano" "sun" "svg"
 "svgz" "text" "tga" "tiff" "tile" "tim" "topol" "ttf" "txt" "uyvy" "vda"
 "vicar" "vid" "viff" "vst" "wbmp" "webp" "wmf" "wmfwin32" "wpg" "x3f" "xbm"
 "xc" "xcf" "xmp" "xpm" "xv" "y" "yuv")

こちらは3.3秒です。

対応フォーマット数の比較:

(list (list "ImageMagick" "GraphicsMagick")
      'hline
      (list (length im-formats) (length gm-formats)))
ImageMagick GraphicsMagick
238 172

どちらにもあるもの:

(seq-intersection im-formats gm-formats)
("3fr" "art" "arw" "avif" "avs" "bie" "bmp" "cals" "caption" "cin"
 "clipboard" "cmyk" "cmyka" "cr2" "crw" "cur" "cut" "dcm" "dcr" "dcx" "dng"
 "dpx" "emf" "epdf" "epi" "eps" "epsf" "epsi" "ept" "ept2" "ept3" "erf" "fax"
 "file" "fits" "fractal" "gif" "gif87" "gradient" "gray" "graya" "heic" "heif"
 "hrz" "http" "icb" "ico" "icon" "j2c" "jbg" "jbig" "jng" "jnx" "jp2" "jpc"
 "jpeg" "jpg" "k25" "kdc" "label" "mac" "map" "mat" "mef" "miff" "mng" "mono"
 "mpc" "mrw" "msl" "mtv" "mvg" "nef" "null" "orf" "otb" "pal" "palm" "pam"
 "pbm" "pcd" "pcds" "pct" "pcx" "pdb" "pdf" "pef" "pfa" "pfb" "pgm" "pgx"
 "picon" "pict" "pix" "plasma" "png" "png00" "png24" "png32" "png48" "png64"
 "png8" "pnm" "ppm" "ps" "ptif" "pwp" "raf" "ras" "rgb" "rgba" "rla" "rle"
 "sct" "sfw" "sgi" "sr2" "srf" "stegano" "sun" "svg" "svgz" "text" "tga"
 "tiff" "tile" "tim" "ttf" "txt" "uyvy" "vda" "vicar" "vid" "viff" "vst"
 "wbmp" "webp" "wmf" "wpg" "x3f" "xbm" "xc" "xcf" "xpm" "xv" "yuv" "r")

これらはどちらでもサポートされます。

ImageMagickにだけあるもの:

(seq-difference im-formats gm-formats)
("3g2" "3gp" "aai" "ai" "apng" "avi" "bayer" "bayera" "bgr" "bgra" "bgro"
 "bmp2" "bmp3" "cal" "canvas" "clip" "cr3" "cube" "data" "dcraw" "dds"
 "dfont" "djvu" "dxt1" "dxt5" "farbfeld" "ff" "fl32" "flif" "flv" "ftp" "fts"
 "ftxt" "g3" "g4" "group4" "hald" "hdr" "https" "iiq" "inline" "ipl" "j2k"
 "jpe" "jpm" "jps" "jpt" "m2v" "m4v" "mask" "mkv" "mov" "mp4" "mpeg" "mpg"
 "mpo" "msvg" "nrw" "otf" "pango" "pattern" "pcl" "pdfa" "pes" "pfm" "phm"
 "pjpeg" "pocketmod" "psb" "psd" "qoi" "raw" "rgb565" "rgbo" "rgf" "rmf"
 "rsvg" "rw2" "scr" "six" "sixel" "strimg" "tiff64" "tm2" "ttc" "vips" "webm"
 "wmv" "xps" "ycbcr" "ycbcra")

GraphicsMagickにだけあるもの:

(seq-difference gm-formats im-formats)
("8bim" "8bimtext" "8bimwtext" "app1" "app1jpeg" "b" "bigtiff" "c" "exif" "g"
 "icc" "icm" "identity" "image" "iptc" "iptctext" "iptcwtext" "k" "m" "o"
 "p7" "topol" "wmfwin32" "xmp" "y")

うーん、やはりmp4など動画系のファイルはImageMagickじゃないと対応していないみたいですね。GraphicsMagick Supported Formatsを見てもmp4はありません。地味にpsdがImageMagickのみというのは大きいです。

ということで私はImageMagickを使うように戻しました。Windowsだとinit.elに多少ごちゃごちゃ書かないといけませんが、その程度は我慢します。GraphicsMagickに対応する前にImageMagick7(magickコマンド)に対応してほしいと思うのは私だけでしょうか……。

ちなみにコンバーターとしてはffmpegも直接指定出来ます。そちらも軽く調べてみましょう。

FFmpegが対応する形式:

(image-converter--probe 'ffmpeg)
("012v" "4xm" "8bps" "aasc" "agm" "aic" "alias_pix" "amv" "anm" "ansi" "apng"
 "arbc" "argo" "asv1" "asv2" "aura" "aura2" "libdav1d" "libaom" "av1"
 "av1_cuvid" "av1_qsv" "avrn" "avrp" "avs" "avui" "ayuv" "bethsoftvid" "bfi"
 "binkvideo" "bintext" "bitpacked" "bmp" "bmv_video" "brender_pix" "c93"
 "cavs" "cdgraphics" "cdtoons" "cdxl" "cfhd" "cinepak" "clearvideo" "cljr"
 "cllc" "eacmv" "cpia" "cri" "camstudio" "cyuv" "dds" "dfa" "dirac" "dnxhd"
 "dpx" "dsicinvideo" "dvvideo" "dxa" "dxtory" "dxv" "escape124" "escape130"
 "exr" "ffv1" "ffvhuff" "fic" "fits" "flashsv" "flashsv2" "flic" "flv" "fmvc"
 "fraps" "frwu" "g2m" "gdv" "gem" "gif" "h261" "h263" "h263i" "h263p" "h264"
 "h264_qsv" "h264_cuvid" "hap" "hdr" "hevc" "hevc_qsv" "hevc_cuvid"
 "hnm4video" "hq_hqa" "hqx" "huffyuv" "hymt" "idcinvideo" "idf" "iff" "imm4"
 "imm5" "indeo2" "indeo3" "indeo4" "indeo5" "interplayvideo" "ipu" "jpeg2000"
 "libopenjpeg" "jpegls" "jv" "kgv1" "kmvc" "lagarith" "loco" "lscr" "m101"
 "eamad" "magicyuv" "mdec" "media100" "mimic" "mjpeg" "mjpeg_cuvid"
 "mjpeg_qsv" "mjpegb" "mmvideo" "mobiclip" "motionpixels" "mpeg1video"
 "mpeg1_cuvid" "mpeg2video" "mpegvideo" "mpeg2_qsv" "mpeg2_cuvid" "mpeg4"
 "mpeg4_cuvid" "msa1" "mscc" "msmpeg4v1" "msmpeg4v2" "msmpeg4" "msp2" "msrle"
 "mss1" "mss2" "msvideo1" "mszh" "mts2" "mv30" "mvc1" "mvc2" "mvdv" "mvha"
 "mwsc" "mxpeg" "notchlc" "nuv" "paf_video" "pam" "pbm" "pcx" "pfm" "pgm"
 "pgmyuv" "pgx" "phm" "photocd" "pictor" "pixlet" "png" "ppm" "prores"
 "prosumer" "psd" "ptx" "qdraw" "qoi" "qpeg" "qtrle" "r10k" "r210" "rasc"
 "rawvideo" "rl2" "roqvideo" "rpza" "rscc" "rv10" "rv20" "rv30" "rv40" "sanm"
 "scpr" "screenpresso" "sga" "sgi" "sgirle" "sheervideo" "simbiosis_imx"
 "smackvid" "smc" "smvjpeg" "snow" "sp5x" "speedhq" "srgc" "sunrast"
 "librsvg" "svq1" "svq3" "targa" "targa_y216" "tdsc" "eatgq" "eatgv" "theora"
 "thp" "tiertexseqvideo" "tiff" "tmv" "eatqi" "truemotion1" "truemotion2"
 "truemotion2rt" "camtasia" "tscc2" "txd" "ultimotion" "utvideo" "v210"
 "v210x" "v308" "v408" "v410" "vb" "vble" "vbn" "vc1" "vc1_qsv" "vc1_cuvid"
 "vc1image" "vcr1" "xl" "vmdvideo" "vmnc" "vnull" "vp3" "vp4" "vp5" "vp6"
 "vp6a" "vp6f" "vp7" "vp8" "libvpx" "vp8_cuvid" "vp8_qsv" "vp9" "libvpx"
 "vp9_cuvid" "vp9_qsv" "vqc" "wbmp" "wcmv" "webp" "wmv1" "wmv2" "wmv3"
 "wmv3image" "wnv1" "wrapped_avframe" "vqavideo" "xan_wc3" "xan_wc4" "xbin"
 "xbm" "xface" "xpm" "xwd" "y41p" "ylc" "yop" "yuv4" "zerocodec" "zlib"
 "zmbv")

数こそ多いもののやはり動画系が中心のようです。画像コンバーターとしてffmpegだけを指定するのはあまりおすすめ出来なそうです。

全部のコンバーターを切り替えて使えば良さそうにも思えますが、基本的にはimage-converter.elはそのようには出来ていないようです。image-converter-add-handler関数を使って逐一登録すればおそらく可能だとは思いますが。


余談ですが、とうとう長年使っていたCygwinを止めてMSYS2に統一しました。FFmpegやGraphicsMagickもパッケージとして登録されていますし。ノートPCの方ではMSYS2だけにしていて特に問題が無かったので。EmacsもMSYS2のを使えば良いかなと思っていたのですが、先日MSYS2版のEmacsにトラブルがあって以来、ノートPCの方でも公式ビルドを使っています。

image-diredでDiredの中に多様な形式の画像をサムネイル表示する方法についてですが、Emacs起動直後には標準で対応している形式しか表示できず、コンバーターを一回でも使った後は表示できるようになるようです(何か私がおかしな設定をしているのでなければ)。これは、image-diredが表示できる形式かどうかを (image-file-name-regexp) が返す正規表現でチェックしているからです。image-file-name-regexp関数は内部でimage-converter-file-name-extensionsという変数を参照しているのですが、その変数は何かimage-converter.el内の関数を呼ぶまでnilのままだからです。init.elで呼び出して初期化してやろうにも、上記の通り9秒もかかったりするのでおいそれとはできません。まぁ、その辺りがちゃんと初期化していない理由なのかもしれません。手動で必要な拡張子だけinit.elで設定するのが現実的かもしれません。

2023-08-06 ,

org-inline-image-fixのEmacs 29対応

先日も書いたように、Emacs 29に移行したらorg-modeで警告が繰り返し沢山出るようになった。

⛔ Warning (emacs): Redefining ‘file-exists-p’ might break native compilation of trampolines.
⛔ Warning (emacs): Redefining ‘expand-file-name’ might break native compilation of trampolines.

file-exists-pexpand-file-name を再定義? そんなことしてないだろう……と思ったが、ふと思い当たってorg-datauri-image.elorg-http-inline-image.elを無効化したら治まった。

これらはorg-inline-image-fixの中にあるEmacs Lispで、 [[data:[[http:[[https: で始まるリンクをインライン画像表示するためのものだ。それをorg-flyimage.elを使ってfont-lockのタイミングで自動的に即事画像化しているので、警告が繰り返し沢山出るというわけだ。

misohena/org-inline-image-fix: A collection of fixes related to the image display feature in org-mode

それらのEmacs Lispは、cl-letfを使ってインライン画像表示関数(org-display-inline-images)の中にいる間だけそこから呼び出される各種関数の挙動を変更し、無理矢理機能を実現している。その挙動を変更した関数の中にfile-exists-pやexpand-file-nameといったC言語で実装された関数があるため、何らかの理由でnative compilationと相性が悪いのだろう。

この方法はかなり強引だが、結果的にはうまく行った。過去何回かのorg-modeのバージョンアップに伴いorg-display-inline-images関数には度々変更が加えられたが、これらのEmacs Lispは何も変更せずに動作し続けた。もしorg-display-inline-imagesの一部をコピーした新しい関数を作成してそれに置き換えたりしていたら、org-modeのバージョンアップに伴い度々変更を取りこむ必要があったことだろう。もちろんこれはたまたま変更箇所が衝突しなかったということであり運が良かっただけとも言えるのだが、その賭けに私は勝ったわけだ。

しかし今、そのcl-letfを使う方法は封じられた。org-display-inline-images関数は一つの関数の中で多くのことをやり過ぎている。単純なadviceの追加ではどうにもならない。もはやorg-display-inline-images関数をコピーして、バラバラに切り刻み、よりカスタマイズしやすい形に再構成するしか道は無いように思える。

というわけで作成したのがorg-better-inline-images.elだ。これはorg-display-inline-images関数をよりカスタマイズしやすいものに置き換える。

そしてorg-datauri-image.elとorg-http-inline-image.elはそれを使うように書き替えた。

それによってEmacs 29でも警告が出ずにdata、http、httpsのリンクをインライン画像表示できるようになった。その代わり、org-modeのバージョンアップに伴うorg-display-inline-images関数の変化に注視し、必要な変更を取りこむ負担を負うことにもなったわけだ。

めでたしめでたし。

ちなみにorg-ytというパッケージがある。YouTubeリンクを実現するためのものだが、インライン画像表示にも対応している。ytリンクタイプのインライン画像表示は、org-display-inline-images関数に:after adviceを仕込むことで実現している。更新範囲の走査が二回になってしまうのが多少気になるところだ。また、結局はorg-display-inline-imagesの一部をコピーしたorg-image-update-overlayという関数を作成しているので、org-display-inline-imagesの変化に追従していく手間は避けられないだろう。一方で、ytリンクタイプに限らず任意のリンクタイプをサポートするための枠組みを提供しているのは興味深い点だ。org-modeが元々そのような仕組みを提供していたら皆ここまで悩まずに済んだことだろう(ただし、org-ytはdescription部分の画像リンクには対応していないように見える)。

2023-07-31

MS-Windows版 Emacs 29.1への移行作業

Emacs 29.1がリリースされたと聞いてファイル置き場を覗いてみたらまだWindows版が置いておらず、1日くらい待ってたまにはビルドしようかなーとソースコードを取りに行ったらすでにWindows版のバイナリが置いてありました。仕事が速いですね。

最近はIMEパッチも使っていないのでビルドする機会がほとんど無くなってしまいました。まぁ、自分でビルドしたら色々良いこともあるとは思いますが。細かい不具合を好きなだけ直せたりとかね!

それで一応移行作業をしたので以下その記録です。

1.ダウンロード

https://ftp.gnu.org/gnu/emacs/

  • emacs-29.1.zip
  • emacs-29.1.tar.xz (展開してfind-function-C-source-directory変数に指定し、describe-functionからソースコードを追えるようにするため)

2.zipを展開して適当な場所に置く

3.起動してみる

パッと見問題無し。

4.補う必要のあるファイルを確認する

  • 相変わらずlibgccjit関連のファイルは含まれていないのでネイティブコンパイルはそのままでは出来ない。
  • gdk_pixbufのloadersもないので、SVG内のimage要素も表示されない。

5.MSYS2で必要なファイルを取り寄せる

あ、MSYS2はucrt64環境に移行してしまったのでmingw64環境のファイルは無いんだった。パッケージアーカイブから直接ダウンロードすることも出来るかもしれないけど、面倒なのでMSYS2環境からインストールしてしまう。

pacman -S mingw-w64-x86_64-libgccjit
pacman -S mingw-w64-x86_64-gdk-pixbuf2

6.SVG内の画像要素を表示できるようにする

まずは簡単な方から。 msys64/mingw64/lib/gdk-pixbuf-2.0 ディレクトリを emacs-29.1/lib/ へコピー。これでSVG内のimage要素は表示できた。 loaders.cache については何もしなくて大丈夫だった。画像形式によっては追加の依存ファイルがあるかも? とりあえずjpgとpngは問題なし。

(以前も書いたが、SVGの描画はlibrsvgが行っており、librsvgはlibgdk_pixbufのローダーライブラリを使用して画像を読み込むので、これらのファイルが無いとSVG内に画像が表示されない。Emacsがjpgやpngを描画する仕組みとSVG内にjpgやpngを描画する仕組みは全然別物なのだ。用途としてはel-easydrawの画像ツール)

7.ネイティブコンパイルできるようにする

次のファイルをコピー。

  • emacs-29.1/binへ
    • msys64/mingw64/binから
      • libgccjit-0.dll
      • libisl-23.dll
      • libmpc-3.dll
      • libmpfr-6.dll
  • emacs-29.1/lib/gccへ
    • msys64/mingw64/binから
      • as.exe
      • ld.exe
    • msys64/mingw64/libから
      • crtbegin.o
      • crtend.o
      • dllcrt2.o
      • libadvapi32.a
      • libgcc_s.a
      • libkernel32.a
      • libmingw32.a
      • libmingwex.a
      • libmoldname.a
      • libmsvcrt.a
      • libpthread.a
      • libshell32.a
      • libuser32.a
    • msys64/mingw64/lib/gcc/x86_64-w64-mingw32/13.1.0/から
      • libgcc.a

.aや.oは全部必要なのか、また、不足するものが無いのかは確認していない。

./emacs.d/early-init.el には次のように設定してあるが、あまり覚えていないので正しいかは知らない。

(when (and (fboundp #'native-comp-available-p) ;;emacs-28以降
           (native-comp-available-p) ;;libgccjitが使える
           (eq system-type 'windows-nt)) ;;Windowsの場合 (他必要に応じて条件を追加すること)

  ;; コンパイル用にemacsを起動する関数をラップし、
  ;; カレントディレクトリを一時的に変更する
  (defun my-comp-set-env-and-call (orig-fun &rest args)
    ;; 一時的にカレントディレクトリを emacs-28.1/bin にする
    ;; でないと emacs-28.1/lib/gcc/as.exe を見つけてくれない
    ;; また、emacs-async-comp-*.elというファイルをあちこちに生成してしまう。
    (let ((default-directory invocation-directory))
      ;; 元の関数を呼び出す
      (apply orig-fun args)))

  (advice-add #'comp-final :around #'my-comp-set-env-and-call)
  (advice-add #'comp-run-async-workers :around #'my-comp-set-env-and-call)

  ;; ライブラリの位置を指定する
  (setq native-comp-driver-options (list "-B" (expand-file-name (file-name-concat invocation-directory "../lib/gcc")) )))

8.org-datauri-image.elとorg-http-inline-image.elを無効化する

次のような警告が沢山出て何かと思ったら自分で書いたクソコードが火を噴いただけだった。

⛔ Warning (emacs): Redefining ‘file-exists-p’ might break native compilation of trampolines.
⛔ Warning (emacs): Redefining ‘expand-file-name’ might break native compilation of trampolines.

cl-letfで一時的にsymbol-function書き替えたから。

そのうち書き直したい。

2023-04-17

新しいマウスを購入(Logicool M750)

一昨日、昨日と新しいマウスを購入した。

これまで使っていたMX Anywhere 2の左ボタンが連打されるようになってしまったからだ。ウィンドウ移動時に最大化されてしまったりあちこちで誤操作して困っていた。ちょっと前にクッキークリッカーで高橋名人バリの連射をしたのが寿命を縮めたのだろうか?

順当に行けば代わりは後継機のMX Anywhere 3なのだろうけど、このマウスはちょっと高い(Amazonで11000円くらい)。それにこれまで2を使ってきて不満も無いわけでは無い。一番はバッテリー。バッテリーがすぐに切れてしまうのでしょっちゅう有線マウス状態で使っていた。ちなみに私は無線マウスにそれほど価値を感じていない。机の上で使っている分には線が付いていようがいまいが操作性に差は無いからだ。ただ、接続が楽なこととPCを引き出すときにケーブルが引っかからないのは良い所だろう。充電だけならテーブルの上に出してあるテーブルタップに繋げれば良いが、机の下のPCに繋げるとなると多少配線に苦労する。自宅のデスクトップ専用のマウスなのでマルチペアリングや軽量性は必要ない。あまりUSBポートにドングルばかり挿したくないのでBluetoothが良い。専用ドングルのみだと困る。左右チルトは使っていない。そう考えると何も後継機にこだわる必要は無いだろう。

そうしてWeb上で新しいマウスを探して目を付けたのがM650。安いマウスでも十分だとは思ったが変なものに当たって何度も買い直すようだと困る(結果的には買い直したがw)。信頼の置ける同じメーカーということでロジクールの中から一番無難そうなM650にした。近所の量販店に行ったついでに購入。意外なことに通販とほとんど変わらない値段だった。

ロジクール Signature M650MOW ワイヤレスマウス

単三乾電池一本で長期間動くのでバッテリー劣化で悩む心配は無い。握りやすさも問題ない。LサイズもあったがMサイズにした。手は大きい方だが、小さめなマウスを指先でちょこちょこ動かしたいので。そういう意味ではこれ以上大きいと困るギリギリのサイズ。モバイル用途ならもう少し小さいものを選びたいところ。ボタンは静音仕様だがクリック感に問題は無い。ホイールを回したときのクリック感も柔らかいがしっかりとある。接続性も問題なし。

しかし実際に使ってすぐに気がついたのが専用の中ボタンが無いということだ。私は中ボタンをよく使うのでMX Anywhere 2ではホイール下のジェスチャーボタンを中ボタンにして使っていた。しかしこのM650にはホイール下に独立したボタンが無い。もちろんホイールはクリックできて中ボタンとして機能する。しかし硬いので押しづらい。ホイールの回転は柔らかいので押そうとすると先にホイールが回ったりもする。長いことMX Anywhere 2を使っている間にこういう問題があったことをすっかり忘れてしまっていた。

el-easydrawにスクロール機能を付けたとき、私は中ボタンドラッグをスクロールに割り当てた。この手のソフトではよく見る操作体系だが私はあまり好きでは無くPhotoshopと同じSPACE+ドラッグが好きだったりする。しかしEmacsではSPACEをmodifierとして使う方法が無いので仕方なく中ドラッグにしたのだった(代わりにSPACEでスクロール・ズームモードになる機能も追加したがモード切替はやはり少々使いづらい)。試しにel-easydrawでスクロールしてみたが、やはりボタンが硬くてスクロールしづらい。ホイールも微妙に回ってしまうので何だか指先が気持ち悪い。

実はM650の上位機種であるM750にはホイールの下に中央ボタンがついているのだった。デフォルトでは速度切り替えボタンになっているが中ボタンに割り当てることも出来る。全体的な形はM650と同じで機能が増えてわずかに重くなっている程度だ。

Logicool Signature M750MOW ワイヤレスマウス

というわけで、かなり勿体ないような気もしたがM750を追加で購入した。中央ボタンの位置がMX Anywhere 2と比べてやや手前でわずかに押しづらくはあるが、まぁ、それほど大きな問題では無い。M650よりは大幅に楽に中ボタンが押せるようになった。ホイールのボタンの方はタブを閉じる操作に割り当てた。こりゃ便利だ。

ホイール(M650、M750で違いは無い)の回転は柔らかいクリック感がありMX Anywhere 2のようなフリースピン切り替えは無いが、フリーモードとクリックモードの中間といったところ。SmartWheelという機能でゆっくり回したときと高速に回したときに挙動が変わるが、かなり自然な動きになっている。ホイールをびゃーっとはじいたときはちゃんとそれらしい動きをする。中間くらいの動きで時々アレ?とわずかに違和感を覚えることもあるが、今のところ実用上特に問題は無い。

全体的にこれまでのMX Anywhere 2と比べて大きな問題は無く、コストパフォーマンスの高いマウスだと感じた。

2023-04-16 , ,

PowerShellからWindows Searchで検索する

el-winsearchからadoquery.exeを起動するのが嫌なのでPowerShellからWindows Searchを実行する方法を調べた。次のようにすれば良いらしい。

$conn = New-Object -ComObject ADODB.Connection
$conn.Open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$query = "SELECT TOP 10 System.ItemUrl FROM SystemIndex WHERE System.Kind = 'picture'"
$rs = New-Object -ComObject ADODB.Recordset
$rs.Open($query, $conn)
While(-Not $rs.EOF){
  # 2023-04-18: 訂正
  Write-Output ($rs.Fields[0].Value -replace '^file:','');
  $rs.MoveNext()
}
$rs.Close()
$conn.Close()

System.ItemPathDisplayではなくSystem.ItemUrlを使うのは C:\ユーザー\ のようなローカライズされたパス名が出てきて都合が悪いから。しかしSystem.ItemUrlを使うと頭に file: が付いてしまうのでそれは出力前に削除している。

これをセミコロン区切りで1行にしてpowershellの-Commandオプションで実行することも出来る。スクリプトファイルにすると色々面倒なこともあるので。-Commandオプションで実行する場合は実行ポリシーなどは関係ないのだろうか。よく知らない。とりあえず手元では動いている。

というわけでel-winsearchはもはや専用のexeを必要としなくなった。 file: の部分も無くなったのでconsult-winsearchから使ったときにEmbarkやMarginaliaも正しく動くようになった。

consult-winsearchを使ったときにVerticoでtruncate-linesがtにならない問題に遭遇したが、検索オプションの書き方をpromptに無理矢理載せたことと、vertico–resize-windowが改行のあるpromptを考慮していないのが原因のようだ。次のように修正した。

;; truncate-linesにする条件が不完全なのを直す。
(with-eval-after-load 'vertico
  (cl-defgeneric vertico--resize-window (height)
    "Resize active minibuffer window to HEIGHT."
    (setq-local truncate-lines (<
                                ;; 旧:(point)
                                ;; ↑ここを修正した。
                                ;; 横に長いpromptで切り詰め表示すると入力がウィンドウ幅を超えたときに検索結果も水平スクロールされてしまうのを防止しているのだと思う。
                                ;; しかし(point)では改行や全角を考慮していない。
                                ;; 新:
                                (string-width
                                 (buffer-substring
                                  (let ((inhibit-field-text-motion t))
                                    (line-beginning-position))
                                  (point)))
                                ;;以下元のまま
                                (* 0.8 (vertico--window-width)))
                resize-mini-windows 'grow-only
                max-mini-window-height 1.0)
    (unless (frame-root-window-p (active-minibuffer-window))
      (unless vertico-resize
        (setq height (max height vertico-count)))
      (let* ((window-resize-pixelwise t)
             (dp (- (max (cdr (window-text-pixel-size))
                         (* (default-line-height) (1+ height)))
                    (window-pixel-height))))
        (when (or (and (> dp 0) (/= height 0))
                  (and (< dp 0) (eq vertico-resize t)))
          (window-resize nil dp nil nil 'pixelwise))))))