2020-05-26 ,

org-modeのインライン画像のサイズを制限する(最大サイズを指定する)

前回の続きでorg-modeのインライン画像の気に入らないところその2。画像の最大サイズを指定できないところ。

Org文書中からEmacsのフレームよりも大きい画像へリンクを張ることがあります(特にOrg2Blogを使っていると画像はサーバ側で様々なバリエーションを自動生成するので元画像は大きめになります)。そんなときに org-toggle-inline-images でインライン画像を表示するとフレームから画像がはみ出して目も当てられません。

フレームからはみ出す画像
図1: フレームからはみ出す画像

org-modeには org-image-actual-width という変数があり、ImageMagickが有効なときは指定したサイズへスケーリングできます。しかし全ての画像が指定したサイズで表示されてしまうため、小さな画像も大きく拡大されてしまいます。 #+ATTR_HTML: :width 300 のような属性指定を元にサイズを決める機能もありますが、Emacs内での表示のためだけにこの指定をあちこちに入れるのは面倒です。それに用途によってはエクスポートしたときの属性値はEmacs内での表示サイズとは別のものが必要になるでしょう(追記:そういう用途には #+ATTR_ORG: :width を併用するようです> How do I re-scale inline org-mode images to specific widths? - Stack Overflow)。

あくまで欲しいのは可能な限り元のサイズで表示し、設定した最大サイズを超えた場合のみ縮小して表示する機能です。

org-modeにこの機能が無いのはおそらくEmacsのcreate-image関数にそれをサポートする機能が無いためではないでしょうか。create-image関数は幅を指定して読み込む機能(:widthプロパティ)はありますが指定された幅より大きい場合にのみ縮小するような機能(例えば:max-widthとか)はありません(max-image-sizeという変数はありますが、これは読み込む最大のサイズを指定します。それを超えた画像は読み込まれません!)。

幸い画像のサイズを取得するimage-size関数はあるので自分でサイズを計算して読み込むことは可能です。ただしimage-size関数がどのように実装されているかは確認していません。ヘッダーだけ読み込んでサイズを返すならばそれなりに速い動作が期待できますが、画像全体を読み込んでからサイズを返すようだと二度手間になるので速度は低下することでしょう。

それは覚悟の上で最大サイズを指定できるようにするのが次のコードです。

訂正: ImageMagickを使った読み込みでは :max-width、:max-heightが指定できました!(ImageMagick Images - GNU Emacs Lisp Reference Manual) それらを使用して最大サイズを指定できるようにするのが次のコードです。

(defcustom org-limit-image-size '(0.99 . 0.5) "Maximum image size") ;; integer or float or (width-int-or-float . height-int-or-float)

(defun org-limit-image-size--get-limit-size (width-p)
  (let ((limit-size (if (numberp org-limit-image-size)
                        org-limit-image-size
                      (if width-p (car org-limit-image-size)
                        (cdr org-limit-image-size)))))
    (if (floatp limit-size)
        (ceiling (* limit-size (if width-p (frame-text-width) (frame-text-height))))
      limit-size)))

(defvar org-limit-image-size--in-org-display-inline-images nil)

(defun org-limit-image-size--create-image
    (old-func file-or-data &optional type data-p &rest props)

  (if (and org-limit-image-size--in-org-display-inline-images
           org-limit-image-size
           (null type)
           ;;(image-type-available-p 'imagemagick) ;;Emacs27 support scaling by default?
           (null (plist-get props :width)))
      ;; limit to maximum size
      (apply
       old-func
       file-or-data
       (if (image-type-available-p 'imagemagick) 'imagemagick)
       data-p
       (plist-put
        (plist-put
         (org-plist-delete props :width) ;;remove (:width nil)
         :max-width (org-limit-image-size--get-limit-size t))
        :max-height (org-limit-image-size--get-limit-size nil)))

    ;; default
    (apply old-func file-or-data type data-p props)))

(defun org-limit-image-size--org-display-inline-images (old-func &rest args)
  (let ((org-limit-image-size--in-org-display-inline-images t))
    (apply old-func args)))

(defun org-limit-image-size-activate ()
  (interactive)
  (advice-add #'create-image :around #'org-limit-image-size--create-image)
  (advice-add #'org-display-inline-images :around #'org-limit-image-size--org-display-inline-images))

(defun org-limit-image-size-deactivate ()
  (interactive)
  (advice-remove #'create-image #'org-limit-image-size--create-image)
  (advice-remove #'org-display-inline-images #'org-limit-image-size--org-display-inline-images))

org-limit-image-size-activate で有効化、 org-limit-image-size-deactivate で無効化します。adviceを使って既存の関数をフックしているのでorgのバージョンが上がると動かなくなるかもしれません(9.3.6で確認)。

変数 org-limit-image-size には最大サイズを指定します。一つの数値で指定した場合は幅と高さの最大値は同じになります。ドット対 (width . height) の形で二つの数値を指定した場合は幅と高さの最大値は別々になります。数値は整数の場合はピクセル数となります。浮動小数点数の場合はフレームのサイズに対する比率となります。デフォルトは (0.99 . 0.5) で、幅の最大値はほぼフレーム一杯、高さの最大値はフレームの半分くらいまでとしています。

実装にあたっては org-display-inline-images 関数の途中をいじりたかったのですがうまく分割できないので create-image をフックして org-display-inline-images 経由で呼び出されたときだけ動作を変えています。

正直このくらいのことはorg-mode標準で対応して欲しいなぁという気もします。やってることは結局create-imageに:max-widthと:max-heightを付加することだけなので。フックするためのコードが馬鹿らしいですよね。ハマリどころとしては :width nil はダメってことです。Emacs27は分かりませんが、ImageMagickでは :width nil で :max-widthを指定すると画像が出ません。不要な:widthプロパティは削除する必要があります。

追記: Emacs27対応について。Emacs27ではImageMagick対応がデフォルト無効になるそうです。その代わりスケーリングは標準対応になるとNEWS.27に書いてあります(Windowsでも対応するのか不安……)。image.cやdisplay.texiを見る限り:max-widthや:max-heightも標準対応していそう。なのでimagemagickの有無を判定して処理を変えている部分を少し修正して、無くても:max-width、:max-heightプロパティを付加するようにしてみました。ちゃんと対応するなら新しく追加される関数image-transform-pを使用して判別した方が良いかもしれません。org-mode側ではImageMagickの有無で:widthプロパティを設定するかどうかを判別しているので、org-image-actual-widthや#+ATTR_*でのサイズ指定は今のところImageMagick必須です。

Pingback / Trackback