如何对齐 org-latex-preview 预览图的水平基准线?

如图所示, 预览的 LaTeX 公式由于公式内存在上标导致了整个预览图片的下沉

Screenshot 2022-09-19 at 15.19.03

希望达到的效果是让公式的主要元素与文本处于同一个水平基准线 (预览的图片上移到红线处) 请问是否有办法实现?


下面是个人的 org-preview-latex-process-alist 设置:

(setq org-preview-latex-process-alist
      '((dvisvgm
			   :programs ("xelatex" "dvisvgm")
			   :description "xdv > svg"
			   :image-input-type "xdv"
			   :image-output-type "svg"
			   :image-size-adjust (1.7 . 1.5)
			   :latex-compiler ;; Default `xelatex' as the process previewing LaTeX fragments
			   ("xelatex -no-pdf -interaction nonstopmode -output-directory %o %f")
			   :image-converter ;; Set `dvisvgm' with --exact option
			   ("dvisvgm %f -e -n -b min -c %S -o %O"))))

org--make-preview-overlay 里面位置固定了 center,想改的话可以覆盖或用 advice :

(defun my-org-latex-preview-advice (beg end &rest _args)
  (let* ((ov (car (overlays-in beg end)))
         (img (cdr (overlay-get ov 'display)))
         (new-img (plist-put img :ascent 90)))
    (overlay-put ov 'display (cons 'image new-img))))
(advice-add 'org--make-preview-overlay
            :after #'my-org-latex-preview-advice)
4 个赞

谢谢帮助. 我 Copy 了代码, 但是注意到我这边把 :ascent 的值无论调到多少都不见效果. 改动这段代码中的这段变量 (:ascent) 在我这边似乎并没有改变 org-latex-preview 的行为.

你在 emacs -Q 下试过了吗?

2022-09-19_19-35 2022-09-19_19-33

上两图分别是 emacs -Q 下 :ascent 10 和没有 advice

:ascent 的范围是 0-100

1 个赞

抱歉, 我是个傻瓜… 发现原来是 ltximg 的缓存忘记清理了…

Screenshot 2022-09-19 at 20.07.54

非常感谢! 问题已经解决.

【分享】在 org-mode 和 latex-mode 中美化公式预览 - #31,来自 Ilya.w 这里的讨论

参考了 auctex(主要是 preview.el)中计算 ascent 的方法,考虑使用 preview 宏包来实现基线对齐。但中间遇到了点小问题, preview 宏包使用 “fake error" 来传递目标数据:

这在 xenops 中被认为是编译错误而报错无法预览,所以必须修改 xenops-aio-subprocess

这样就能精准地对齐基准线了,最终效果: Peek 2022-10-18 16-34

代码如下:

  (setq xenops-math-latex-process-alist
        '((dvisvgm :programs
                   ("latex" "dvisvgm")
                   :description "dvi > svg" :message "you need to install the programs: latex and dvisvgm." :image-input-type "dvi" :image-output-type "svg" :image-size-adjust
                   (1.7 . 1.5)
                   :latex-compiler
                   ("latex -interaction nonstopmode -shell-escape -output-format dvi -output-directory %o \"\\nonstopmode\\nofiles\\PassOptionsToPackage{active,tightpage,auctex}{preview}\\AtBeginDocument{\\ifx\\ifPreview\\undefined\\RequirePackage[displaymath,floats,graphics,textmath,footnotes]{preview}[2004/11/05]\\fi}\\input\\detokenize{\"%f\"}\" %f")
                   :image-converter
                   ("dvisvgm %f -n -b %B -c %S -o %O"))))

  (defun xenops-aio-subprocess (command &optional _ __)
    "Start asynchronous subprocess; return a promise.

COMMAND is the command to run as an asynchronous subprocess.

Resolve the promise when the process exits. The value function
does nothing if the exit is successful, but if the process exits
with an error status, then the value function signals the error."
    (let* ((promise (aio-promise))
           (name (format "xenops-aio-subprocess-%s"
                         (sha1 (prin1-to-string command))))
           (output-buffer (generate-new-buffer name))
           (sentinel
            (lambda (process event)
              (unless (process-live-p process)
                (aio-resolve
                 promise
                 (lambda ()
                   (if (or (eq 0 (process-exit-status process))
                           (and (eq 1 (process-exit-status process))
                                (not (string-match-p
                                      "^! [^P]"
                                      (with-current-buffer output-buffer
                                        (buffer-string))))))
                       (kill-buffer output-buffer)
                     (signal 'error
                             (prog1 (list :xenops-aio-subprocess-error-data
                                          (list (s-join " " command)
                                                event
                                                (with-current-buffer output-buffer
                                                  (buffer-string))))
                               (kill-buffer output-buffer))))))))))
      (prog1 promise
        (make-process
         :name name
         :buffer output-buffer
         :command command
         :sentinel sentinel))))
  
  (defun eli/xenops-preview-align-baseline (element &rest _args)
    "Redisplay SVG image resulting from successful LaTeX compilation of ELEMENT.

Use the data in log file (e.g. \"! Preview: Snippet 1 ended.(368640+1505299x1347810).\")
to calculate the decent value of `:ascent'. "
    (let* ((inline-p (eq 'inline-math (plist-get element :type)))
           (ov-beg (plist-get element :begin))
           (ov-end (plist-get element :end))
           (colors (xenops-math-latex-get-colors))
           (latex (buffer-substring-no-properties ov-beg
                                                  ov-end))
           (cache-svg (xenops-math-compute-file-name latex colors))
           (cache-log (file-name-with-extension cache-svg "log"))
           (cache-log-exist-p (file-exists-p cache-log))
           (tmp-log (f-join temporary-file-directory "xenops"
                            (concat (f-base cache-svg) ".log")))
           (ov (car (overlays-at (/ (+ ov-beg ov-end) 2) t)))
           (regexp-string "^! Preview:.*\(\\([0-9]*?\\)\\+\\([0-9]*?\\)x\\([0-9]*\\))")
           img new-img ascent bbox log-text log)
      (when (and ov inline-p)
        (if cache-log-exist-p
            (let ((text (f-read-text cache-log)))
              (string-match regexp-string text)
              (setq log (match-string 0 text))
              (setq bbox (mapcar #'(lambda (x)
                                     (* (preview-get-magnification)
                                        (string-to-number x)))
                                 (list
                                  (match-string 1 text)
                                  (match-string 2 text)
                                  (match-string 3 text)))))
          (with-temp-file cache-log
            (insert-file-contents-literally tmp-log)
            (goto-char (point-max))
            (if (re-search-backward regexp-string nil t)
                (progn
                  (setq log (match-string 0))
                  (setq bbox (mapcar #'(lambda (x)
                                         (* (preview-get-magnification)
                                            (string-to-number x)))
                                     (list
                                      (match-string 1)
                                      (match-string 2)
                                      (match-string 3))))))
            (erase-buffer)
            (insert log)))
        (setq ascent (preview-ascent-from-bb (preview-TeX-bb bbox)))
        (setq img (cdr (overlay-get ov 'display)))
        (setq new-img (plist-put img :ascent ascent))
        (overlay-put ov 'display (cons 'image new-img)))))
  (advice-add 'xenops-math-display-image :after
              #'eli/xenops-preview-align-baseline)
2 个赞

代码实现的有点 tricky ,也还有不完善的地方,需要进一步测试,不过基本满足了需求。

请问 log file 默认生成的位置是…?

(我试着查阅了 AUCTeX 的文档但并没有找到其描述相应 log file 的生成及存放位置.)

另外, 我试着使用了 xenops 与你提供的代码, 但是这边提示找不到 log 文件:

Error running timer ‘funcall’: (file-missing "Opening input file" "No such file or directory" "/tmp/xenops/59880c8330599c97ad2ef72d08ad25523e5e6f28.log")

我怀疑是 xenops 路径错误的原因, 于是改成了 xenops 默认的缓存路径 ~/.emacs.d/xenops/, 但依然没有发现有 log 文件输出:

Error running timer ‘funcall’: (file-missing "Opening input file" "No such file or directory" "~/.emacs.d/xenops/a396251f620c647256949f65854d98c6a0e8dfce.log")

感谢帮忙测试,我好像忘了跨平台的事情,已更新代码。

这是图片的缓存路径,编译过程中的文件在 (f-join temporary-file-directory "xenops") 下,linux 中是 /tmp/xenops/

1 个赞

抱歉,确实应该放在这里,突然想起 /tmp/ 里的文件重启就没了 :sweat_smile:

我在 Patch to align baseline of latex fragments and surrounding text - Matt Huszagh 找到了一种使用 svg 图片来计算 ascent 的方法. 这样就不用担心缓存图片被清除的问题了.

这样做另一个可能的好处是, 如果使用原生的 org-latex-preview, 其缓存图片的生成函数放在了 org-create-formula-image 中的一个 let* 定义的局部变量里, 很难获取到这个值 (很难读取到生成的 .log 的文件名):

(defun org-create-formula-image
    (string tofile options buffer &optional processing-type)
  "Create an image from LaTeX source using external processes.
..."
  (let* (;;...
	 (tmpdir temporary-file-directory)
	 (texfilebase (make-temp-name
		       (expand-file-name "orgtex" tmpdir)))
	 (texfile (concat texfilebase ".tex"))
	;;...
	(when (file-exists-p (concat texfilebase e))
	  (delete-file (concat texfilebase e))))
      image-output-file)))

下面是效果与解决方案:

效果:

代码:

;; (setq org-preview-latex-default-process 'dvisvgm)

(defun my/org--latex-header-preview (orig &rest args)
  "Setup dedicated `org-format-latex-header' to `my/org--match-text-baseline-ascent'."
  (let ((org-format-latex-header
         "\\documentclass[preview]{standalone}
\\usepackage[usenames]{color}
[PACKAGES]
[DEFAULT-PACKAGES]"))
    (apply orig args)))

(defun my/org--match-text-baseline-ascent (imagefile)
  "Set `:ascent' to match the text baseline of an image to the surrounding text.
Compute `ascent' with the data collected in IMAGEFILE."
  (advice-add 'org-create-formula-image :around #'my/org--latex-header-preview)
  (let* ((viewbox (split-string
                   (xml-get-attribute (car (xml-parse-file imagefile)) 'viewBox)))
         (min-y (string-to-number (nth 1 viewbox)))
         (height (string-to-number (nth 3 viewbox)))
         (ascent (round (* -100 (/ min-y height)))))
    (if (or (< ascent 0) (> ascent 100))
        'center
      ascent)))

(defun org--make-preview-overlay (beg end image &optional imagetype)
  "Build an overlay between BEG and END using IMAGE file.
Argument IMAGETYPE is the extension of the displayed image,
as a string.  It defaults to \"png\"."
  (let ((ov (make-overlay beg end))
	(imagetype (or (intern imagetype) 'png)))
    (let ((ascent (my/org--match-text-baseline-ascent image)))
      (overlay-put ov 'org-overlay-type 'org-latex-overlay)
      (overlay-put ov 'evaporate t)
      (overlay-put ov
		   'modification-hooks
		   (list (lambda (o _flag _beg _end &optional _l)
			   (delete-overlay o))))
      (overlay-put ov
		   'display
		   (list 'image :type imagetype :file image :ascent ascent)))))

其中建议

(setq org-preview-latex-process-alist
      '((dvisvgm ;; ...
         :image-converter ("dvisvgm %f -e -n -b 1 -c %S -o %O")))) ;; -b 1

dvisvgm:image-converter 中加入 -e (–exact-bbox) 并修改默认 -b min-b 1 来保证公式显示的完整性 (见 Options).


不足之处

这个方案只支持 dvisvgmpdflatex, 见: dvisvgm: Generating scalable vector graphics from DVI and EPS files

当前的 preview package 似乎仅支持在 dvi 文件中写入 baseline 数据. 而不支持 xelatex 所需要的 xdv 文件.


另外一些可能的发展方向:

  • 结合 org-latex-previewpreview-latex (AUCTeX).

这个方案很早就被人提出了, 但直到现在也没有完美的解决方案. 见 How to use Auctex + preview-latex + orgmode in emacs 这里的讨论.

org-auctex 目前实现了二者的结合, 可以见 blog FAST LATEX PREVIEWS IN ORG-MODE. 可惜当前这个包不能够支持 dvisvgm. 同时我尝试了结合 preview-dvisvgm 这个包一起使用, 但并没有解决问题 (org-auctex 编译无法通过).

2 个赞

看来我又重复造了轮子 :joy:

org-format-latex-header 也会被 org latex babel 用到,后者会受影响,只在生成 preview 的时候更改可能会更好。

1 个赞

感谢提醒, 已修改原贴子代码.

texlive升级到2023版,dvisvgm升级到3.0.4后,latex预览在dvisvgm转换后无法实现水平基准。imagemagick则可以。

第一个是imagemagick,第二个是dvisvgm

我也有同样的问题 (macOS), 现在只能先用回 2022

@ltmsyvag @eita 试试看这个项目: 项目分享: Org Mode 中 LaTeX 预览与导出的优化 (Improved LaTeX Preview).

已经确认了是 latex 版本的问题了, 和 org 没关系, 没必要试. 要试还不如等到 9.7 正式版出来了再试试