latextools.py
257 lines
| 7.9 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r3279 | # -*- coding: utf-8 -*- | ||
Min RK
|
r19562 | """Tools for handling LaTeX.""" | ||
Brian Granger
|
r3279 | |||
Min RK
|
r19562 | # Copyright (c) IPython Development Team. | ||
Brian Granger
|
r3279 | # Distributed under the terms of the Modified BSD License. | ||
Min RK
|
r19562 | from io import BytesIO, open | ||
Takafumi Arakaki
|
r7306 | import os | ||
import tempfile | ||||
import shutil | ||||
import subprocess | ||||
Srinivas Reddy Thatiparthy
|
r23063 | from base64 import encodebytes | ||
Matthias Bussonnier
|
r25162 | import textwrap | ||
Brian Granger
|
r3279 | |||
Matthias Bussonnier
|
r27764 | from pathlib import Path | ||
Romulo Filho
|
r26059 | |||
Takafumi Arakaki
|
r7330 | from IPython.utils.process import find_cmd, FindCmdError | ||
Min RK
|
r21253 | from traitlets.config import get_config | ||
from traitlets.config.configurable import SingletonConfigurable | ||||
from traitlets import List, Bool, Unicode | ||||
Matthias Bussonnier
|
r21869 | |||
Brian Granger
|
r3279 | |||
Takafumi Arakaki
|
r7861 | class LaTeXTool(SingletonConfigurable): | ||
"""An object to store configuration of the LaTeX tool.""" | ||||
Min RK
|
r19565 | def _config_default(self): | ||
return get_config() | ||||
Oscar Gustafsson
|
r25160 | |||
Takafumi Arakaki
|
r7861 | backends = List( | ||
Jason Grout
|
r21538 | Unicode(), ["matplotlib", "dvipng"], | ||
Takafumi Arakaki
|
r7861 | 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"]. | ||||
Matthias Bussonnier
|
r22343 | ).tag(config=True) | ||
Takafumi Arakaki
|
r7861 | |||
Min RK
|
r19562 | use_breqn = Bool( | ||
Takafumi Arakaki
|
r7861 | True, | ||
help="Use breqn.sty to automatically break long equations. " | ||||
"This configuration takes effect only for dvipng backend.", | ||||
Matthias Bussonnier
|
r22343 | ).tag(config=True) | ||
Takafumi Arakaki
|
r7861 | |||
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.", | ||||
Matthias Bussonnier
|
r22343 | ).tag(config=True) | ||
Takafumi Arakaki
|
r7861 | |||
Min RK
|
r19562 | preamble = Unicode( | ||
Takafumi Arakaki
|
r7861 | help="Additional preamble to use when generating LaTeX source " | ||
"for dvipng backend.", | ||||
Matthias Bussonnier
|
r22343 | ).tag(config=True) | ||
Takafumi Arakaki
|
r7861 | |||
M Bussonnier
|
r28938 | def latex_to_png( | ||
s: str, encode=False, backend=None, wrap=False, color="Black", scale=1.0 | ||||
): | ||||
Takafumi Arakaki
|
r7306 | """Render a LaTeX string to PNG. | ||
Brian Granger
|
r3279 | |||
Parameters | ||||
---------- | ||||
klonuo
|
r22484 | s : str | ||
Brian Granger
|
r3279 | The raw string containing valid inline LaTeX. | ||
encode : bool, optional | ||||
Min RK
|
r19562 | Should the PNG data base64 encoded to make it JSON'able. | ||
Takafumi Arakaki
|
r7861 | backend : {matplotlib, dvipng} | ||
Takafumi Arakaki
|
r7306 | Backend for producing PNG data. | ||
Takafumi Arakaki
|
r7852 | wrap : bool | ||
If true, Automatically wrap `s` as a LaTeX equation. | ||||
Oscar Gustafsson
|
r25160 | color : string | ||
Oscar Gustafsson
|
r25161 | Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB | ||
format, e.g. '#AA20FA'. | ||||
scale : float | ||||
Scale factor for the resulting PNG. | ||||
Takafumi Arakaki
|
r7309 | None is returned when the backend cannot be used. | ||
Brian Granger
|
r3279 | """ | ||
M Bussonnier
|
r28938 | assert isinstance(s, str) | ||
Takafumi Arakaki
|
r7861 | allowed_backends = LaTeXTool.instance().backends | ||
if backend is None: | ||||
backend = allowed_backends[0] | ||||
if backend not in allowed_backends: | ||||
return None | ||||
if backend == 'matplotlib': | ||||
Takafumi Arakaki
|
r7306 | f = latex_to_png_mpl | ||
elif backend == 'dvipng': | ||||
f = latex_to_png_dvipng | ||||
Oscar Gustafsson
|
r25161 | 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 | ||||
Matthias Bussonnier
|
r25162 | textwrap.wrap(color[1:], 2)])) | ||
Ram Rachum
|
r25833 | except ValueError as e: | ||
raise ValueError('Invalid color specification {}.'.format(color)) from e | ||||
Oscar Gustafsson
|
r25161 | else: | ||
raise ValueError('Invalid color specification {}.'.format(color)) | ||||
Takafumi Arakaki
|
r7306 | else: | ||
raise ValueError('No such backend {0}'.format(backend)) | ||||
Oscar Gustafsson
|
r25161 | bin_data = f(s, wrap, color, scale) | ||
Takafumi Arakaki
|
r7309 | if encode and bin_data: | ||
Matthias Bussonnier
|
r21869 | bin_data = encodebytes(bin_data) | ||
Takafumi Arakaki
|
r7306 | return bin_data | ||
Oscar Gustafsson
|
r25161 | def latex_to_png_mpl(s, wrap, color='Black', scale=1.0): | ||
Takafumi Arakaki
|
r7309 | try: | ||
Jake VanderPlas
|
r26423 | from matplotlib import figure, font_manager, mathtext | ||
from matplotlib.backends import backend_agg | ||||
Carlos Cordoba
|
r22075 | from pyparsing import ParseFatalException | ||
Takafumi Arakaki
|
r7309 | except ImportError: | ||
return None | ||||
Carlos Cordoba
|
r22075 | |||
Min RK
|
r19564 | # mpl mathtext doesn't support display math, force inline | ||
s = s.replace('$$', '$') | ||||
Takafumi Arakaki
|
r7852 | if wrap: | ||
Min RK
|
r19562 | s = u'${0}$'.format(s) | ||
Carlos Cordoba
|
r22050 | |||
try: | ||||
Jake VanderPlas
|
r26423 | 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() | ||||
Carlos Cordoba
|
r22075 | except (ValueError, RuntimeError, ParseFatalException): | ||
Carlos Cordoba
|
r22050 | return None | ||
Takafumi Arakaki
|
r7306 | |||
Oscar Gustafsson
|
r25161 | def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): | ||
Takafumi Arakaki
|
r7306 | try: | ||
Takafumi Arakaki
|
r7330 | find_cmd('latex') | ||
find_cmd('dvipng') | ||||
except FindCmdError: | ||||
return None | ||||
Pieter Eendebak
|
r27699 | |||
startupinfo = None | ||||
Pieter Eendebak
|
r27700 | if os.name == "nt": | ||
Pieter Eendebak
|
r27699 | # prevent popup-windows | ||
startupinfo = subprocess.STARTUPINFO() | ||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | ||||
Takafumi Arakaki
|
r7330 | try: | ||
Matthias Bussonnier
|
r26183 | workdir = Path(tempfile.mkdtemp()) | ||
Pieter Eendebak
|
r27702 | tmpfile = "tmp.tex" | ||
dvifile = "tmp.dvi" | ||||
outfile = "tmp.png" | ||||
Takafumi Arakaki
|
r7306 | |||
Pieter Eendebak
|
r27702 | with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f: | ||
Takafumi Arakaki
|
r7852 | f.writelines(genelatex(s, wrap)) | ||
Takafumi Arakaki
|
r7306 | |||
Yann Pellegrini
|
r28090 | 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, | ||||
) | ||||
Takafumi Arakaki
|
r7306 | |||
Pieter Eendebak
|
r27702 | with workdir.joinpath(outfile).open("rb") as f: | ||
Abhinav Upadhyay
|
r12883 | return f.read() | ||
Carlos Cordoba
|
r22075 | except subprocess.CalledProcessError: | ||
Carlos Cordoba
|
r22050 | return None | ||
Takafumi Arakaki
|
r7306 | finally: | ||
shutil.rmtree(workdir) | ||||
Brian Granger
|
r3279 | |||
Brian Granger
|
r3881 | |||
Takafumi Arakaki
|
r7852 | 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() | ||||
Min RK
|
r19562 | return stdout.strip().decode('utf8', 'replace') | ||
Takafumi Arakaki
|
r7852 | except FindCmdError: | ||
pass | ||||
def genelatex(body, wrap): | ||||
"""Generate LaTeX document for dvipng backend.""" | ||||
Takafumi Arakaki
|
r7861 | lt = LaTeXTool.instance() | ||
breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty") | ||||
Srinivas Reddy Thatiparthy
|
r23669 | yield r'\documentclass{article}' | ||
Takafumi Arakaki
|
r7861 | packages = lt.packages | ||
Takafumi Arakaki
|
r7852 | if breqn: | ||
Takafumi Arakaki
|
r7861 | packages = packages + ['breqn'] | ||
Takafumi Arakaki
|
r7852 | for pack in packages: | ||
Srinivas Reddy Thatiparthy
|
r23669 | yield r'\usepackage{{{0}}}'.format(pack) | ||
yield r'\pagestyle{empty}' | ||||
Takafumi Arakaki
|
r7861 | if lt.preamble: | ||
yield lt.preamble | ||||
Srinivas Reddy Thatiparthy
|
r23669 | yield r'\begin{document}' | ||
Takafumi Arakaki
|
r7852 | if breqn: | ||
Srinivas Reddy Thatiparthy
|
r23669 | yield r'\begin{dmath*}' | ||
Takafumi Arakaki
|
r7852 | yield body | ||
Srinivas Reddy Thatiparthy
|
r23669 | yield r'\end{dmath*}' | ||
Takafumi Arakaki
|
r7852 | elif wrap: | ||
Min RK
|
r19562 | yield u'$${0}$$'.format(body) | ||
Takafumi Arakaki
|
r7852 | else: | ||
yield body | ||||
Matthias Bussonnier
|
r24780 | yield u'\\end{document}' | ||
Takafumi Arakaki
|
r7306 | |||
Min RK
|
r19562 | _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />""" | ||
Brian Granger
|
r3279 | |||
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. | ||||
""" | ||||
Min RK
|
r19562 | base64_data = latex_to_png(s, encode=True).decode('ascii') | ||
Takafumi Arakaki
|
r7309 | if base64_data: | ||
return _data_uri_template_png % (base64_data, alt) | ||||
Brian Granger
|
r3279 | |||
Brian Granger
|
r3881 | |||