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は改善点が出てきてしまいますね。