Monthly Archives: 8月 2023

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で画像ファイルに変換できたんですね。