|
|
# -*- coding: utf-8 -*-
|
|
|
"""Tools for handling LaTeX."""
|
|
|
|
|
|
# Copyright (c) IPython Development Team.
|
|
|
# Distributed under the terms of the Modified BSD License.
|
|
|
|
|
|
from io import BytesIO, open
|
|
|
import os
|
|
|
import tempfile
|
|
|
import shutil
|
|
|
import subprocess
|
|
|
from base64 import encodebytes
|
|
|
import textwrap
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from IPython.utils.process import find_cmd, FindCmdError
|
|
|
from traitlets.config import get_config
|
|
|
from traitlets.config.configurable import SingletonConfigurable
|
|
|
from traitlets import List, Bool, Unicode
|
|
|
|
|
|
|
|
|
class LaTeXTool(SingletonConfigurable):
|
|
|
"""An object to store configuration of the LaTeX tool."""
|
|
|
def _config_default(self):
|
|
|
return get_config()
|
|
|
|
|
|
backends = List(
|
|
|
Unicode(), ["matplotlib", "dvipng"],
|
|
|
help="Preferred backend to draw LaTeX math equations. "
|
|
|
"Backends in the list are checked one by one and the first "
|
|
|
"usable one is used. Note that `matplotlib` backend "
|
|
|
"is usable only for inline style equations. To draw "
|
|
|
"display style equations, `dvipng` backend must be specified. ",
|
|
|
# It is a List instead of Enum, to make configuration more
|
|
|
# flexible. For example, to use matplotlib mainly but dvipng
|
|
|
# for display style, the default ["matplotlib", "dvipng"] can
|
|
|
# be used. To NOT use dvipng so that other repr such as
|
|
|
# unicode pretty printing is used, you can use ["matplotlib"].
|
|
|
).tag(config=True)
|
|
|
|
|
|
use_breqn = Bool(
|
|
|
True,
|
|
|
help="Use breqn.sty to automatically break long equations. "
|
|
|
"This configuration takes effect only for dvipng backend.",
|
|
|
).tag(config=True)
|
|
|
|
|
|
packages = List(
|
|
|
['amsmath', 'amsthm', 'amssymb', 'bm'],
|
|
|
help="A list of packages to use for dvipng backend. "
|
|
|
"'breqn' will be automatically appended when use_breqn=True.",
|
|
|
).tag(config=True)
|
|
|
|
|
|
preamble = Unicode(
|
|
|
help="Additional preamble to use when generating LaTeX source "
|
|
|
"for dvipng backend.",
|
|
|
).tag(config=True)
|
|
|
|
|
|
|
|
|
def latex_to_png(
|
|
|
s: str, encode=False, backend=None, wrap=False, color="Black", scale=1.0
|
|
|
):
|
|
|
"""Render a LaTeX string to PNG.
|
|
|
|
|
|
Parameters
|
|
|
----------
|
|
|
s : str
|
|
|
The raw string containing valid inline LaTeX.
|
|
|
encode : bool, optional
|
|
|
Should the PNG data base64 encoded to make it JSON'able.
|
|
|
backend : {matplotlib, dvipng}
|
|
|
Backend for producing PNG data.
|
|
|
wrap : bool
|
|
|
If true, Automatically wrap `s` as a LaTeX equation.
|
|
|
color : string
|
|
|
Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
|
|
|
format, e.g. '#AA20FA'.
|
|
|
scale : float
|
|
|
Scale factor for the resulting PNG.
|
|
|
None is returned when the backend cannot be used.
|
|
|
|
|
|
"""
|
|
|
assert isinstance(s, str)
|
|
|
allowed_backends = LaTeXTool.instance().backends
|
|
|
if backend is None:
|
|
|
backend = allowed_backends[0]
|
|
|
if backend not in allowed_backends:
|
|
|
return None
|
|
|
if backend == 'matplotlib':
|
|
|
f = latex_to_png_mpl
|
|
|
elif backend == 'dvipng':
|
|
|
f = latex_to_png_dvipng
|
|
|
if color.startswith('#'):
|
|
|
# Convert hex RGB color to LaTeX RGB color.
|
|
|
if len(color) == 7:
|
|
|
try:
|
|
|
color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
|
|
|
textwrap.wrap(color[1:], 2)]))
|
|
|
except ValueError as e:
|
|
|
raise ValueError('Invalid color specification {}.'.format(color)) from e
|
|
|
else:
|
|
|
raise ValueError('Invalid color specification {}.'.format(color))
|
|
|
else:
|
|
|
raise ValueError('No such backend {0}'.format(backend))
|
|
|
bin_data = f(s, wrap, color, scale)
|
|
|
if encode and bin_data:
|
|
|
bin_data = encodebytes(bin_data)
|
|
|
return bin_data
|
|
|
|
|
|
|
|
|
def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
|
|
|
try:
|
|
|
from matplotlib import figure, font_manager, mathtext
|
|
|
from matplotlib.backends import backend_agg
|
|
|
from pyparsing import ParseFatalException
|
|
|
except ImportError:
|
|
|
return None
|
|
|
|
|
|
# mpl mathtext doesn't support display math, force inline
|
|
|
s = s.replace('$$', '$')
|
|
|
if wrap:
|
|
|
s = u'${0}$'.format(s)
|
|
|
|
|
|
try:
|
|
|
prop = font_manager.FontProperties(size=12)
|
|
|
dpi = 120 * scale
|
|
|
buffer = BytesIO()
|
|
|
|
|
|
# Adapted from mathtext.math_to_image
|
|
|
parser = mathtext.MathTextParser("path")
|
|
|
width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
|
|
|
fig = figure.Figure(figsize=(width / 72, height / 72))
|
|
|
fig.text(0, depth / height, s, fontproperties=prop, color=color)
|
|
|
backend_agg.FigureCanvasAgg(fig)
|
|
|
fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
|
|
|
return buffer.getvalue()
|
|
|
except (ValueError, RuntimeError, ParseFatalException):
|
|
|
return None
|
|
|
|
|
|
|
|
|
def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
|
|
|
try:
|
|
|
find_cmd('latex')
|
|
|
find_cmd('dvipng')
|
|
|
except FindCmdError:
|
|
|
return None
|
|
|
|
|
|
startupinfo = None
|
|
|
if os.name == "nt":
|
|
|
# prevent popup-windows
|
|
|
startupinfo = subprocess.STARTUPINFO()
|
|
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
|
|
|
|
try:
|
|
|
workdir = Path(tempfile.mkdtemp())
|
|
|
tmpfile = "tmp.tex"
|
|
|
dvifile = "tmp.dvi"
|
|
|
outfile = "tmp.png"
|
|
|
|
|
|
with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f:
|
|
|
f.writelines(genelatex(s, wrap))
|
|
|
|
|
|
subprocess.check_call(
|
|
|
["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
|
|
|
cwd=workdir,
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
startupinfo=startupinfo,
|
|
|
)
|
|
|
|
|
|
resolution = round(150 * scale)
|
|
|
subprocess.check_call(
|
|
|
[
|
|
|
"dvipng",
|
|
|
"-T",
|
|
|
"tight",
|
|
|
"-D",
|
|
|
str(resolution),
|
|
|
"-z",
|
|
|
"9",
|
|
|
"-bg",
|
|
|
"Transparent",
|
|
|
"-o",
|
|
|
outfile,
|
|
|
dvifile,
|
|
|
"-fg",
|
|
|
color,
|
|
|
],
|
|
|
cwd=workdir,
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
startupinfo=startupinfo,
|
|
|
)
|
|
|
|
|
|
with workdir.joinpath(outfile).open("rb") as f:
|
|
|
return f.read()
|
|
|
except subprocess.CalledProcessError:
|
|
|
return None
|
|
|
finally:
|
|
|
shutil.rmtree(workdir)
|
|
|
|
|
|
|
|
|
def kpsewhich(filename):
|
|
|
"""Invoke kpsewhich command with an argument `filename`."""
|
|
|
try:
|
|
|
find_cmd("kpsewhich")
|
|
|
proc = subprocess.Popen(
|
|
|
["kpsewhich", filename],
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
(stdout, stderr) = proc.communicate()
|
|
|
return stdout.strip().decode('utf8', 'replace')
|
|
|
except FindCmdError:
|
|
|
pass
|
|
|
|
|
|
|
|
|
def genelatex(body, wrap):
|
|
|
"""Generate LaTeX document for dvipng backend."""
|
|
|
lt = LaTeXTool.instance()
|
|
|
breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
|
|
|
yield r'\documentclass{article}'
|
|
|
packages = lt.packages
|
|
|
if breqn:
|
|
|
packages = packages + ['breqn']
|
|
|
for pack in packages:
|
|
|
yield r'\usepackage{{{0}}}'.format(pack)
|
|
|
yield r'\pagestyle{empty}'
|
|
|
if lt.preamble:
|
|
|
yield lt.preamble
|
|
|
yield r'\begin{document}'
|
|
|
if breqn:
|
|
|
yield r'\begin{dmath*}'
|
|
|
yield body
|
|
|
yield r'\end{dmath*}'
|
|
|
elif wrap:
|
|
|
yield u'$${0}$$'.format(body)
|
|
|
else:
|
|
|
yield body
|
|
|
yield u'\\end{document}'
|
|
|
|
|
|
|
|
|
_data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
|
|
|
|
|
|
def latex_to_html(s, alt='image'):
|
|
|
"""Render LaTeX to HTML with embedded PNG data using data URIs.
|
|
|
|
|
|
Parameters
|
|
|
----------
|
|
|
s : str
|
|
|
The raw string containing valid inline LateX.
|
|
|
alt : str
|
|
|
The alt text to use for the HTML.
|
|
|
"""
|
|
|
base64_data = latex_to_png(s, encode=True).decode('ascii')
|
|
|
if base64_data:
|
|
|
return _data_uri_template_png % (base64_data, alt)
|
|
|
|
|
|
|
|
|
|