Source code elsie/ext/latex.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import functools
import hashlib
import os
import subprocess
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING

import lxml.etree as et

from ..boxtree.boxitem import SimpleBoxItem
from ..boxtree.boxmixin import BoxMixin
from ..render.backends.svg.utils import rename_ids, svg_size_to_pixels

if TYPE_CHECKING:
    from . import boxitem


def latex(
    parent: BoxMixin, text: str, scale=1.0, header: str = None, tail: str = None
) -> "boxitem.BoxItem":
    """
    Renders LaTeX.

    Parameters
    ----------
    parent: BoxMixin

    text: str
        Source code of the LaTeX snippet.
    scale: float
        Scale of the rendered output.
    header: str
        Prelude of the LaTeX source (for example package imports).
        Will be included at the beginning of the source code.
    tail: str
        End of the LaTeX source (for example end of the document).
        Will be included at the end of the source code.
    """

    if header is None:
        header = """
\\documentclass[varwidth,border=1pt]{standalone}
\\usepackage[utf8x]{inputenc}
\\usepackage{ucs}
\\usepackage{amsmath}
\\usepackage{amsfonts}
\\usepackage{amssymb}
\\usepackage{graphicx}
\\begin{document}"""

    if tail is None:
        tail = "\\end{document}"

    tex_text = "{}\n{}\n{}".format(header, text, tail)

    box = parent._get_box()
    svg = box.slide.fs_cache.get(tex_text, "svg", _render_latex)
    root = et.fromstring(svg)
    svg_width = svg_size_to_pixels(root.get("width")) * scale
    svg_height = svg_size_to_pixels(root.get("height")) * scale

    box.layout.ensure_width(svg_width)
    box.layout.ensure_height(svg_height)

    draw = functools.partial(_draw, parent, svg, svg_width, svg_height, scale)
    item = SimpleBoxItem(box, draw)
    box.add_child(item)
    return item


def _draw(parent, svg, svg_width, svg_height, scale, ctx):
    box = parent._get_box()
    rect = box.layout.rect
    x = rect.x + (rect.width - svg_width) / 2
    y = rect.y + (rect.height - svg_height) / 2
    ctx.draw_svg(
        svg=svg,
        x=x,
        y=y,
        width=rect.width,
        height=rect.height,
        scale=scale,
    )


def _render_latex(text):
    args = ("/usr/bin/pdflatex", "-interaction=batchmode", "content.tex")

    with TemporaryDirectory(prefix="elsie-") as wdir:
        with open(os.path.join(wdir, "content.tex"), "w") as f:
            f.write(text)

        p = subprocess.Popen(
            args,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=wdir,
        )

        _stdout, _stderr = p.communicate()
        if p.returncode != 0:
            with open(os.path.join(wdir, "content.log")) as f:
                error = f.read()
            raise Exception("pdflatex failed:\n" + error)

        svg_name = os.path.join(wdir, "content.svg")

        args = ("/usr/bin/pdf2svg", os.path.join(wdir, "content.pdf"), svg_name)

        p = subprocess.Popen(
            args,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=wdir,
        )

        _stdout, stderr = p.communicate()
        if p.returncode != 0:
            raise Exception("pdf2svg failed:\n" + stderr.decode())

        root = et.parse(svg_name).getroot()

    h = hashlib.md5()
    h.update(text.encode())
    suffix = "-" + h.hexdigest()
    rename_ids(root, suffix)
    return et.tostring(root).decode()