【分享】在 org-mode 和 latex-mode 中美化公式预览

先上效果图

预览用的是 xenops ,优点是异步渲染图片,不会卡; 而且两个模式都能用。

代码主要借鉴自 John Kitchin 的网站:The Kitchin Research Group

  1. 如何对齐 org-latex-preview 预览图的水平基准线?
(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)
  1. 图片居中或右对齐
(plist-put org-format-latex-options :justify 'right)
(defun eli/xenops-justify-fragment-overlay (element &rest _args)
    (let* ((ov-beg (plist-get element :begin))
           (ov-end (plist-get element :end))
           (ov (car (overlays-at (/ (+ ov-beg ov-end) 2) t)))
           (position (plist-get org-format-latex-options :justify))
           (inline-p (eq 'inline-math (plist-get element :type)))
           width offset)
      (when (and ov
                 (imagep (overlay-get ov 'display)))
        (setq width (car (image-display-size (overlay-get ov 'display))))
        (cond
         ((and (eq 'center position) 
               (not inline-p))
          (setq offset (floor (- (/ fill-column 2)
                                 (/ width 2))))
          (if (< offset 0)
              (setq offset 0))
          (overlay-put ov 'before-string (make-string offset ? )))
         ((and (eq 'right position) 
               (not inline-p))
          (setq offset (floor (- fill-column
                                 width)))
          (if (< offset 0)
              (setq offset 0))
          (overlay-put ov 'before-string (make-string offset ? )))))))
  (advice-add 'xenops-math-display-image :after
              #'eli/xenops-justify-fragment-overlay)
  1. 公式编号
  (defun eli/xenops-renumber-environment (orig-func element latex colors
                                                    cache-file display-image)
    (let ((results '()) 
          (counter -1)
          (numberp))
      (setq results (cl-loop for (begin .  env) in 
                             (org-element-map (org-element-parse-buffer)
                                 'latex-environment
                               (lambda (env)
                                 (cons
                                  (org-element-property :begin env)
                                  (org-element-property :value env))))
                             collect
                             (cond
                              ((and (string-match "\\\\begin{equation}" env)
                                    (not (string-match "\\\\tag{" env)))
                               (cl-incf counter)
                               (cons begin counter))
                              ((and (string-match "\\\\begin{align}" env)
                                    (string-match "\\\\notag" env))
                               (cl-incf counter)
                               (cons begin counter))
                              ((string-match "\\\\begin{align}" env)
                               (prog2
                                   (cl-incf counter)
                                   (cons begin counter)                          
                                 (with-temp-buffer
                                   (insert env)
                                   (goto-char (point-min))
                                   ;; \\ is used for a new line. Each one leads
                                   ;; to a number
                                   (cl-incf counter (count-matches "\\\\$"))
                                   ;; unless there are nonumbers.
                                   (goto-char (point-min))
                                   (cl-decf counter
                                            (count-matches "\\nonumber")))))
                              (t
                               (cons begin nil)))))
      (when (setq numberp (cdr (assoc (plist-get element :begin) results)))
        (setq latex
              (concat
               (format "\\setcounter{equation}{%s}\n" numberp)
               latex))))
    (funcall orig-func element latex colors cache-file display-image))
  (advice-add 'xenops-math-latex-create-image :around #'eli/xenops-renumber-environment)
  1. 预览时显示标签
  ;; show `#+name:' while in latex preview.
  (defun eli/org-preview-show-label (orig-fun beg end &rest _args)
  (let* ((beg (save-excursion
                (goto-char beg)
                (if (re-search-forward "#\\+name:" end t)
                    (progn
                      (next-line)
                      (line-beginning-position))
                  beg))))
    (apply orig-fun beg end _args)))
  (advice-add 'org--make-preview-overlay :around #'eli/org-preview-show-label)

效果如下:

Peek 2022-10-13 21-14

这样视觉效果基本就和 pdf 的结果差不多了。

如果不用 xenops 而是用 org-fragtop 或 org 自带的 org-latex-preview ,可以用下面的代码:

代码
  ;; Vertically align LaTeX preview in org mode
  (defun my-org-latex-preview-advice (beg end &rest _args)
    (let* ((ov (car (overlays-at (/ (+ beg end) 2) t)))
           (img (cdr (overlay-get ov 'display)))
           (new-img (plist-put img :ascent 95)))
      (overlay-put ov 'display (cons 'image new-img))))
  (advice-add 'org--make-preview-overlay
              :after #'my-org-latex-preview-advice)
  
  ;; from: https://kitchingroup.cheme.cmu.edu/blog/2016/11/06/
  ;; Justifying-LaTeX-preview-fragments-in-org-mode/
  ;; specify the justification you want
  (plist-put org-format-latex-options :justify 'right)

  (defun eli/org-justify-fragment-overlay (beg end image imagetype)
    (let* ((position (plist-get org-format-latex-options :justify))
           (img (create-image image 'svg t))
           (ov (car (overlays-at (/ (+ beg end) 2) t)))
           (width (car (image-display-size (overlay-get ov 'display))))
           offset)
      (cond
       ((and (eq 'center position) 
             (= beg (line-beginning-position)))
        (setq offset (floor (- (/ fill-column 2)
                               (/ width 2))))
        (if (< offset 0)
            (setq offset 0))
        (overlay-put ov 'before-string (make-string offset ? )))
       ((and (eq 'right position) 
             (= beg (line-beginning-position)))
        (setq offset (floor (- fill-column
                               width)))
        (if (< offset 0)
            (setq offset 0))
        (overlay-put ov 'before-string (make-string offset ? ))))))
  (advice-add 'org--make-preview-overlay
              :after 'eli/org-justify-fragment-overlay)
  
  ;; from: https://kitchingroup.cheme.cmu.edu/blog/2016/11/07/
  ;; Better-equation-numbering-in-LaTeX-fragments-in-org-mode/
  (defun org-renumber-environment (orig-func &rest args)
    (let ((results '()) 
          (counter -1)
          (numberp))
      (setq results (cl-loop for (begin .  env) in 
                             (org-element-map (org-element-parse-buffer)
                                 'latex-environment
                               (lambda (env)
                                 (cons
                                  (org-element-property :begin env)
                                  (org-element-property :value env))))
                             collect
                             (cond
                              ((and (string-match "\\\\begin{equation}" env)
                                    (not (string-match "\\\\tag{" env)))
                               (cl-incf counter)
                               (cons begin counter))
                              ((and (string-match "\\\\begin{align}" env)
                                    (string-match "\\\\notag" env))
                               (cl-incf counter)
                               (cons begin counter))
                              ((string-match "\\\\begin{align}" env)
                               (prog2
                                   (cl-incf counter)
                                   (cons begin counter)                          
                                 (with-temp-buffer
                                   (insert env)
                                   (goto-char (point-min))
                                   ;; \\ is used for a new line. Each one leads
                                   ;; to a number
                                   (cl-incf counter (count-matches "\\\\$"))
                                   ;; unless there are nonumbers.
                                   (goto-char (point-min))
                                   (cl-decf counter
                                            (count-matches "\\nonumber")))))
                              (t
                               (cons begin nil)))))
      (when (setq numberp (cdr (assoc (point) results)))
        (setf (car args)
              (concat
               (format "\\setcounter{equation}{%s}\n" numberp)
               (car args)))))
    (apply orig-func args))
  (advice-add 'org-create-formula-image :around #'org-renumber-environment)
19 个赞

我国庆的时候,跑到你的配置下,把latex那边抄了个大半 :rofl:

哈哈,我也是抄别人的。

方便分享下配置链接么,有空想去参考下

1 个赞

您好,可以把字体分享一下吗?

2 个赞

啥是mixed-pitch

是这个包,简单说就是不同的结构用不同的字体。

比如在 org-mode 里面正文用变宽字体方便阅读,而代码块、表格等部分用等宽字体方便编辑。

1 个赞

awesome

xenops 只支持 documentclass 为 article 的,导致很大一部分情况用不了

可以修改 org-format-latex-header ,我改成了 standalone ,目前体验下来还行。

不,怎么能改那个模板呢…其它体验不要了嘛?

实际上应该是 org-latex-make-preamble 的 template 不应该在 xenops-math-latex-make-latex-document 里面指定。

另外这个问题就算是直接使用 xenops 编辑 latex 文件也不行。

能具体说说吗?目前我就了解到它和 latex preview 有关。

那是否可以用 advice 或 override 来支持自定义的模板呢?

能具体说说吗?目前我就了解到它和 latex preview 有关。

org latex babel

那是否可以用 advice 或 override 来支持自定义的模板呢?

那我猜你要 override 那一整个函数,而且依然不可靠。

xenops 本质也是使用 org 那一套,和修改 org-preview-latex-process-alist 后设置 org-preview-latex-default-process 没多少区别,反正现在 emacs 支持 svg(

而直接写 latex 大概又不需要在文档里面预览,有 auctex 的字体基本也够了…

1 个赞

这个是 org-mode 自带的 latex preview 效果,当然需要的话也可以 center

在这一部分中, 修改 :ascent 值在范围 90 ~ 100 中只能保证预览图片的底部与行内文本对齐: (:ascent 100, 效果).

这样对行内公式有上标的情况是友好的, 但倘若公式内有下标或者其他一些非文本高度的符号, 依然是不能保证文本与公式内文本对齐的, 见下图 (:ascent 100)

Screenshot 2022-10-16 at 10.18.54

因此我在思考能否有一种动态修改 :ascent 值的方法? 比方说公式内有上标时将其改为 90 ~ 100, 下标时 改为 0-10, 而其他情况则默认 :ascent center 这样. 但本人目前没有实现这个功能的思路.

发现有一个包 texfrag 基于 mathjax + dvipng 很好的实现了行内公式的对齐. 我尝试读了代码… 但本人水平很有限, 没有弄明白是如何实现的.

自带的不也是用的 org-format-latex-header 嘛,限制和 xenops 应该是一样的吧?

确实,反正都要自己写那一堆东西。其实最主要的吐槽是 xenops 不能在非 article 的 latex-mode 下用x

latex-mode 里面确实挺烦的,动不动就因为有些包没有而不能预览。

我的想法是这样的:

(defun eli/change-xenops-latex-header (orig &rest args)
  (let ( foo
          (org-format-latex-header "xxx"))
    (apply orig args)))
(advice-add 'xenops-math-latex-make-latex-document :around #'eli/change-xenops-latex-header)
(advice-add 'xenops-math-file-name-static-hash-data :around #'eli/change-xenops-latex-header)

自动根据当前项目设置 latex-header ,不知道会有什么问题。