Definitive solution for emacs's org-mode html exporting latex tikz

1. Introduction

TikZ is an latex package that lets you draw basic math shapes including lines, dots, curves, circles, rectangles etc. I found the need for it right on my first attempt of writing a math article here: the wave model pt1.

TikZ example:

\begin{tikzpicture}

\draw (-2,0) -- (2,0);
\filldraw [gray] (0,0) circle (2pt);
\draw (-2,-2) .. controls (0,0) .. (2,-2);
\draw (-2,2) .. controls (-1,0) and (1,0) .. (2,2);

\end{tikzpicture}

RESULT:

After hours of googling and trying gigantic blocks of elisp, exporting things to svg or png images and trying to automatically generate <img> tags for them on the exported html I couldn't find something that made me happy.

I actually didn't want to have to host those images, this is a full static website, the latex formulas are rendered with MathJax and that gave me the idea that something similar should exist for tikz. It is just a latex library after all. The less file I have to host the better it is.

So I found this: https://github.com/kisonecat/tikzjax. Basically you just wrap latex code within <script type="text/tikz"></script> and it gets rendered into a svg! This is done by using a build of tex into webassembly. This means this svg is generated on the fly in your browser, costing your cpu time instead of my storage ;).

With the code bellow I was able to automatically convert the latex source blocks into the html export to use tikzjax automatically using org-publish hooks.

2. Code

2.1. Org snippet (Yas)

# -*- mode: snippet -*-
# name: tikz image for html export
# key: tikz
# --
#+name: tikz Image
#+header: :results file drawer
#+header: :file /tmp/org_latex_image.png
#+header: :imagemagick yes
#+header: :headers '("\\\\usepackage{tikz}")
#+HEADER: :fit yes :imoutoptions -geometry 400
#+begin_src latex
\begin{tikzpicture}
\fill[yellow] (0,0) circle (3cm);
\end{tikzpicture}
#+end_src

2.2. init.el

This is still good to have so that you can C-c C-c on the block and see the image on the org result. The result will be removed automatically by the next code.

(add-to-list 'org-latex-packages-alist
             '("" "tikz" t))
(add-to-list 'org-latex-packages-alist
             '("" "tikz-cd" t))

2.3. org-publish elisp hook

Loop through source code blocks and if it is a latex src with the \usepackage{tikz} header then it must be converted into an image. This is done by changing it from a src block to a html_export one with the tikzjax script tags around the original latex code.

Finally this is passed to org-export-before-parsing-hook which will run for every org file org-publish converts.

(defun tikzjax-convert (backend)
  "Convert a latex org src block with tikz headers into a tizkjax html export block."
  (setq is-tikz nil)
  ;; Loop over src blocks and stops when there is no left
  (while (condition-case nil (org-babel-next-src-block)
           (error
            nil))

    ;; Extract relevant info from the org src block
    (setq src-info (org-babel-get-src-block-info))
    (let ((type (nth 0 src-info))
          (code (nth 1 src-info))
          (headers (seq-filter
                    (lambda (str)
                      (not (string= str ":headers")))
                    (assoc :headers (nth 2 src-info)))))

     ;; Check if it is a latex block with tikz on the headers
     (if (and headers (string= type "latex")
            (string-match-p (regexp-quote "\\usepackage{tikz}") (car headers)))
          ;; Clean it up, change it to a html export and wrap it with the script tags
          (progn
            (setq is-tikz t)
            (org-babel-remove-result)
            (beginning-of-line)
            (kill-line)
            (insert "#+begin_export html")
            (end-of-line)
            (newline-and-indent)
            (insert "<div class=\"tikzjax\"><script type=\"text/tikz\">")
            (search-forward-regexp "^\s*#\\+end_src\s*$")
            (beginning-of-line)
            (kill-line)
            (insert "#+end_export")
            (forward-line -1)
            (end-of-line)
            (newline-and-indent)
            (insert "</script></div>")))))
  ;; In the end, if it is a tikz src block add the proper html headers for tikzjax on this org file
  (if is-tikz (progn
                ;; Here im just trying to add it after all #+ on the beginning of the file
                (goto-char (point-min))
                (end-of-line)
                (newline-and-indent)
                ;; Insert tikzjax headers
                (insert "#+HTML_HEAD: <link rel=\"stylesheet\" type=\"text/css\" href=\"https://tikzjax.com/v1/fonts.css\">")
                (end-of-line)
                (newline-and-indent)
                (insert "#+HTML_HEAD: <script src=\"https://tikzjax.com/v1/tikzjax.js\"></script>"))))


(add-hook 'org-export-before-parsing-hook #'tikzjax-convert)

2.4. CSS

Centralize horizontally, some bad color choices as usual.

.tikzjax {
    background-color: beige;
    border-style: solid;
    border-width: 3px;
    border-color: #3c81ba;
    display: flex;
    justify-content: center;
}

3. Import latex libraries

3.1. A nice tikzjax fork

One problem you will face with this approach though is that you cannot import tex libraries with it as described on this issue. Thankfully 3geek14 pointed out the nice fork of that has this feature: https://github.com/drgrice1/tikzjax. Problem is, no nice cnd to use so we will have to build it ourselves if we want to use latex libs.

I took the freedom to host my build with cloudflare at: https://tikzjax.pages.dev/tikzjax.js

I adapted my build elisp script to place the data-tikz-libraries attribute and you can check all of it at https://github.com/matheusfillipe/myblog/blob/master/build-site.el. The functions that matter are match-tikz-package and tickzjax-convert. This now gives me the power to do something like this:

CODE:

#+name: mass spring system
#+header: :results file drawer
#+header: :file /tmp/org_latex_image.png
#+header: :imagemagick yes
#+header: :headers '("\\usepackage{tikz} \\usetikzlibrary{decorations.pathmorphing,patterns}")
#+HEADER: :fit yes :imoutoptions -geometry 500
#+begin_src latex
\begin{tikzpicture}
\node[circle,fill=blue,inner sep=2.5mm] (a) at (0,0) {};
\node[circle,fill=blue,inner sep=2.5mm] (b) at (2,2) {};
\draw[decoration={aspect=0.3, segment length=3mm, amplitude=3mm,coil},decorate] (0,5) -- (a);
\draw[decoration={aspect=0.3, segment length=1.5mm, amplitude=3mm,coil},decorate] (2,5) -- (b);
\fill [pattern = north east lines] (-1,5) rectangle (3,5.2);
\draw[thick] (-1,5) -- (3,5);
\end{tikzpicture}
#+end_src

RESULT:

It would be also possible to import tex libraries using this approach but I didn't implement that yet.

3.2. Javascript workaround

For some reason with this tikzjax for the group wouldn't be centered inside the svg, so I created a very dirty javascript script as a workaround:

// Post process tikzjax (centralize workaround)
document.addEventListener('tikzjax-load-finished', function (e) {
    let svg = e.target;
    let g = svg.childNodes[0]

    g.transform.baseVal.getItem(0).setTranslate(0, 0)
    let gbox = g.getBoundingClientRect()
    let sbox = svg.getBoundingClientRect()

    g.transform.baseVal.getItem(0).setTranslate(1, 1)
    let gbox_by_1 = g.getBoundingClientRect()
    let scale = { x:  gbox.x - gbox_by_1.x, y: gbox.y - gbox_by_1.y }

    g.transform.baseVal.getItem(0).setTranslate((gbox.x - sbox.x) / scale.x, (gbox.y - sbox.y) / scale.y)
});

And now it works!

4. Conclusion

This works and seems to be a lot of potential. Webassembly is just crazy and opens a new world of possibilities for the web frontend, but it seems to be still a work in progress. Safari is a problematic browser and tikzjax wont ever work on it.

In general I will keep try using this for this blog and take this as a reason to learn tikz since I don't really know nothing about it yet :P



terminal

Comments


Be the first to comment!

    ﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏

    Definitive solution for emacs's org-mode html exporting latex tikz

    Author: Matheus Fillipe

    Creation Date: 2022-04-15 Fri 00:00

    Modified: 2022-07-15 Fri 02:40

    View this page on github

    Emacs 28.1 (Org mode 9.5.2)