Category Archives: 未分類

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内に表示した例