;; latex-live-preview.el by Jan Böcker ;; Feel free to improve this. ;; License: Pick one of GNU GPL v3, BSD, MIT ;; This is a quick and dirty hack to show an automatically updated preview image ;; of the LaTeX formula at point in an org-mode buffer. ;; Usage: ;; * Eval the elisp code in this file in some way (e.g. M-x load-file or M-x eval-buffer) ;; * In your org buffer: M-x jb/toggle-latex-live-preview ;; * Split the window (e.g. C-x 2) ;; * open the exported image file (default: /tmp/livepreview.png) in ;; the second buffer ;; * edit your Org file ;; To speed things up slightly, you might want to move the tempdir to a ramdisk. ;; the tempdir will contain the exported image and might be cluttered ;; with temporary files from running LaTeX if something goes wrong (defconst jb/latex-live-preview-tempdir "/tmp/") ; must include final / (defconst jb/latex-live-preview-filename "livepreview.png") (defvar jb/current-texfilebase "") (defvar jb/current-tmpdir nil) (defvar jb/current-tofile "") (defvar jb/latex-preview-job nil) (defvar jb/enable-live-preview nil) (defun jb/ocfi (string tofile options buffer) "Modified version of org-create-formula-image" (require 'org-latex) (let* ((tmpdir (if (featurep 'xemacs) (temp-directory) temporary-file-directory)) (texfilebase (make-temp-name (expand-file-name "orgtex" tmpdir))) (texfile (concat texfilebase ".tex")) (dvifile (concat texfilebase ".dvi")) (pngfile (concat texfilebase ".png")) (fnh (if (featurep 'xemacs) (font-height (get-face-font 'default)) (face-attribute 'default :height nil))) (scale (or (plist-get options (if buffer :scale :html-scale)) 1.0)) (dpi (number-to-string (* scale (floor (* 0.9 (if buffer fnh 140.)))))) (fg (or (plist-get options (if buffer :foreground :html-foreground)) "Black")) (bg (or (plist-get options (if buffer :background :html-background)) "Transparent"))) (if (eq fg 'default) (setq fg (org-dvipng-color :foreground))) (if (eq bg 'default) (setq bg (org-dvipng-color :background))) (with-temp-file texfile (insert (org-splice-latex-header org-format-latex-header org-export-latex-default-packages-alist org-export-latex-packages-alist t org-format-latex-header-extra)) (insert "\n\\begin{document}\n" string "\n\\end{document}\n") (require 'org-latex) (org-export-latex-fix-inputenc)) (setq jb/current-texfilebase texfilebase) (setq jb/current-tmpdir tmpdir) (setq jb/current-tofile tofile) (jb/call-latex) )) (defun jb/call-latex () (cd tmpdir) (set-process-sentinel (start-process "live-latex-preview-latex" ; process name nil ; no buffer "latex" "--halt-on-error" (concat jb/current-texfilebase ".tex")) 'jb/call-latex-sentinel) ;(message "latex-live-preview: started latex") ) (defun jb/call-latex-sentinel (process event) ;(message "latex-sentinel: %s" event) (when (string= event "finished\n") (jb/call-dvipng)) (when (string= event "exited abnormally with code 1\n") (setq jb/current-tmpdir nil) (jb/update-latex-preview))) (defun jb/call-dvipng () (let* ((tmpdir jb/current-tmpdir) (buffer nil) (options org-format-latex-options) (texfilebase jb/current-texfilebase) (texfile (concat texfilebase ".tex")) (dvifile (concat texfilebase ".dvi")) (pngfile (concat texfilebase ".png")) (fnh (if (featurep 'xemacs) (font-height (get-face-font 'default)) (face-attribute 'default :height nil))) (scale (or (plist-get options (if buffer :scale :html-scale)) 1.0)) (dpi (number-to-string (* scale (floor (* 0.9 (if buffer fnh 140.)))))) (fg (or (plist-get options (if buffer :foreground :html-foreground)) "Black")) (bg (or (plist-get options (if buffer :background :html-background)) "Transparent"))) (if (eq fg 'default) (setq fg (org-dvipng-color :foreground))) (if (eq bg 'default) (setq bg (org-dvipng-color :background))) (cd tmpdir) (set-process-sentinel (start-process "live-latex-preview-dvipng" nil "dvipng" "-fg" fg "-bg" bg "-D" dpi ;;"-x" scale "-y" scale "-T" "tight" "-o" pngfile dvifile) 'jb/call-dvipng-sentinel) )) (defun jb/call-dvipng-sentinel (process event) (when (string= event "finished\n") (copy-file (concat jb/current-texfilebase ".png") jb/current-tofile 'replace) (loop for e in '(".dvi" ".tex" ".aux" ".log" ".png") do (delete-file (concat jb/current-texfilebase e))) (when (get-buffer jb/latex-live-preview-filename) (set-buffer jb/latex-live-preview-filename) (jb/refresh-current-image-buffer)) (setq jb/current-tmpdir nil) (jb/update-latex-preview) )) (defun jb/refresh-current-image-buffer () (flet ((message (msg &rest args) nil)) (clear-image-cache) (revert-buffer nil t t) (image-mode) )) (defun jb/update-latex-preview () (when (and jb/latex-preview-job (not jb/current-tmpdir)) (jb/preview-fragment jb/latex-preview-job) (setq jb/latex-preview-job nil))) (defun jb/preview-fragment (latex) (save-excursion (let ((temporary-file-directory jb/latex-live-preview-tempdir)) (jb/ocfi latex (concat jb/latex-live-preview-tempdir jb/latex-live-preview-filename) org-format-latex-options nil)) )) (defun jb/plfp () "Preview the latex fragment at point" (interactive) (catch 'exit (save-excursion (save-restriction (let ((at (org-inside-LaTeX-fragment-p)) beg end msg start-delimiter end-delimiter (add-end-delimiter nil)) (if (not at) (progn (setq jb/latex-preview-job "$ $") (jb/update-latex-preview)) (progn (goto-char (cdr at)) (setq beg (point)) (setq start-delimiter (car at)) ;; (message "start-delimiter is %s " start-delimiter) ;; Determine what the end of this block must look like (setq end-delimiter (cdr (assoc start-delimiter '(("$$" . "$$"), ("\\[" . "\\]"), ("\\(" . "\\)"))))) ;; Set end-delimiter to \end{region} if block starts ;; with \begin{region} (when (not end-delimiter) (save-match-data (if (not (string-match "\\\\begin{\\([^}]*\\)}" start-delimiter)) (throw 'exit nil)) (setq end-delimiter (concat "\\end{" (match-string 1 start-delimiter) "}")))) ;; (message "end-delimiter is %s " end-delimiter) ;; set end of our region (goto-char (1+ beg)) (setq end (re-search-forward (regexp-quote end-delimiter) nil t)) ;; If there is no end delimiter, make a note that we ;; need to add it later before creating the image. ;; This will make the preview work on unfinished LaTeX blocks. (when (not end) (setq end (point-max)) (setq add-end-delimiter t)) (narrow-to-region beg end) ;; (message "-%s-" (buffer-string)) (goto-char beg) (setq jb/latex-preview-job (concat (buffer-string) (if add-end-delimiter end-delimiter "") )) (jb/update-latex-preview)))))))) (defun pseudo-message (format-string &rest args) nil) (defun jb/ach (a b c) "hook for after-change-functions" (when jb/enable-live-preview (let ((message (lambda (s &rest args) nil))) (jb/plfp) (setq deactivate-mark nil)))) (defun jb/pch () "hook for post-command-hook" (when jb/enable-live-preview (let ((message (lambda (s &rest args) nil))) (jb/plfp) (setq deactivate-mark nil)))) (defun jb/toggle-latex-live-preview () "toggles the buffer-local variable jb/enable-live-preview" (interactive) (setq jb/current-tmpdir nil) (make-variable-buffer-local 'jb/enable-live-preview) (setq jb/enable-live-preview (not jb/enable-live-preview))) (defun jb/clear-current-tmpdir () (interactive) (setq jb/current-tmpdir nil)) (remove-hook 'after-change-functions 'jb/ach) (remove-hook 'post-command-hook 'jb/pch) (add-hook 'post-command-hook 'jb/pch) (add-hook 'after-change-functions 'jb/ach)