latextools.py
251 lines
| 7.6 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r3279 | # -*- coding: utf-8 -*- | ||
"""Tools for handling LaTeX. | ||||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
Takafumi Arakaki
|
r7311 | # Copyright (C) 2010 IPython Development Team. | ||
Brian Granger
|
r3279 | # | ||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Thomas Kluyver
|
r8013 | from io import BytesIO | ||
Brian Granger
|
r3279 | from base64 import encodestring | ||
Takafumi Arakaki
|
r7306 | import os | ||
import tempfile | ||||
import shutil | ||||
import subprocess | ||||
Brian Granger
|
r3279 | |||
Takafumi Arakaki
|
r7330 | from IPython.utils.process import find_cmd, FindCmdError | ||
Takafumi Arakaki
|
r7861 | from IPython.config.configurable import SingletonConfigurable | ||
from IPython.utils.traitlets import Instance, List, CBool, CUnicode | ||||
Thomas Kluyver
|
r8013 | from IPython.utils.py3compat import bytes_to_str | ||
Takafumi Arakaki
|
r7330 | |||
Brian Granger
|
r3279 | #----------------------------------------------------------------------------- | ||
# Tools | ||||
#----------------------------------------------------------------------------- | ||||
Takafumi Arakaki
|
r7861 | class LaTeXTool(SingletonConfigurable): | ||
"""An object to store configuration of the LaTeX tool.""" | ||||
backends = List( | ||||
CUnicode, ["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"]. | ||||
config=True) | ||||
use_breqn = CBool( | ||||
True, | ||||
help="Use breqn.sty to automatically break long equations. " | ||||
"This configuration takes effect only for dvipng backend.", | ||||
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.", | ||||
config=True) | ||||
preamble = CUnicode( | ||||
help="Additional preamble to use when generating LaTeX source " | ||||
"for dvipng backend.", | ||||
config=True) | ||||
def latex_to_png(s, encode=False, backend=None, wrap=False): | ||||
Takafumi Arakaki
|
r7306 | """Render a LaTeX string to PNG. | ||
Brian Granger
|
r3279 | |||
Parameters | ||||
---------- | ||||
s : str | ||||
The raw string containing valid inline LaTeX. | ||||
encode : bool, optional | ||||
Should the PNG data bebase64 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. | ||||
Takafumi Arakaki
|
r7306 | |||
Takafumi Arakaki
|
r7309 | None is returned when the backend cannot be used. | ||
Brian Granger
|
r3279 | """ | ||
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 | ||||
else: | ||||
raise ValueError('No such backend {0}'.format(backend)) | ||||
Takafumi Arakaki
|
r7852 | bin_data = f(s, wrap) | ||
Takafumi Arakaki
|
r7309 | if encode and bin_data: | ||
Takafumi Arakaki
|
r7306 | bin_data = encodestring(bin_data) | ||
return bin_data | ||||
Takafumi Arakaki
|
r7852 | def latex_to_png_mpl(s, wrap): | ||
Takafumi Arakaki
|
r7309 | try: | ||
from matplotlib import mathtext | ||||
except ImportError: | ||||
return None | ||||
Takafumi Arakaki
|
r7852 | |||
if wrap: | ||||
s = '${0}$'.format(s) | ||||
Brian Granger
|
r3279 | mt = mathtext.MathTextParser('bitmap') | ||
Thomas Kluyver
|
r8013 | f = BytesIO() | ||
Brian Granger
|
r3279 | mt.to_png(f, s, fontsize=12) | ||
Takafumi Arakaki
|
r7306 | return f.getvalue() | ||
Takafumi Arakaki
|
r7852 | def latex_to_png_dvipng(s, wrap): | ||
Takafumi Arakaki
|
r7306 | try: | ||
Takafumi Arakaki
|
r7330 | find_cmd('latex') | ||
find_cmd('dvipng') | ||||
except FindCmdError: | ||||
return None | ||||
try: | ||||
Takafumi Arakaki
|
r7306 | workdir = tempfile.mkdtemp() | ||
tmpfile = os.path.join(workdir, "tmp.tex") | ||||
dvifile = os.path.join(workdir, "tmp.dvi") | ||||
outfile = os.path.join(workdir, "tmp.png") | ||||
with open(tmpfile, "w") as f: | ||||
Takafumi Arakaki
|
r7852 | f.writelines(genelatex(s, wrap)) | ||
Takafumi Arakaki
|
r7306 | |||
subprocess.check_call( | ||||
Takafumi Arakaki
|
r7329 | ["latex", "-halt-on-errror", tmpfile], cwd=workdir, | ||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||||
Takafumi Arakaki
|
r7306 | |||
subprocess.check_call( | ||||
["dvipng", "-T", "tight", "-x", "1500", "-z", "9", | ||||
Takafumi Arakaki
|
r7329 | "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir, | ||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||||
Takafumi Arakaki
|
r7306 | |||
Thomas Kluyver
|
r8013 | with open(outfile, "rb") as f: | ||
Takafumi Arakaki
|
r7306 | bin_data = f.read() | ||
finally: | ||||
shutil.rmtree(workdir) | ||||
Brian Granger
|
r3279 | return bin_data | ||
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() | ||||
return stdout.strip() | ||||
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") | ||||
Takafumi Arakaki
|
r7852 | 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: | ||
yield r'\usepackage{{{0}}}'.format(pack) | ||||
yield r'\pagestyle{empty}' | ||||
Takafumi Arakaki
|
r7861 | if lt.preamble: | ||
yield lt.preamble | ||||
Takafumi Arakaki
|
r7852 | yield r'\begin{document}' | ||
if breqn: | ||||
yield r'\begin{dmath*}' | ||||
yield body | ||||
yield r'\end{dmath*}' | ||||
elif wrap: | ||||
yield '$${0}$$'.format(body) | ||||
else: | ||||
yield body | ||||
yield r'\end{document}' | ||||
Takafumi Arakaki
|
r7306 | |||
Brian Granger
|
r3279 | _data_uri_template_png = """<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. | ||||
""" | ||||
Thomas Kluyver
|
r8013 | base64_data = bytes_to_str(latex_to_png(s, encode=True), 'ascii') | ||
Takafumi Arakaki
|
r7309 | if base64_data: | ||
return _data_uri_template_png % (base64_data, alt) | ||||
Brian Granger
|
r3279 | |||
Brian Granger
|
r3881 | |||
# From matplotlib, thanks to mdboom. Once this is in matplotlib releases, we | ||||
# will remove. | ||||
def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None): | ||||
""" | ||||
Given a math expression, renders it in a closely-clipped bounding | ||||
box to an image file. | ||||
*s* | ||||
A math expression. The math portion should be enclosed in | ||||
dollar signs. | ||||
*filename_or_obj* | ||||
A filepath or writable file-like object to write the image data | ||||
to. | ||||
*prop* | ||||
If provided, a FontProperties() object describing the size and | ||||
style of the text. | ||||
*dpi* | ||||
Override the output dpi, otherwise use the default associated | ||||
with the output format. | ||||
*format* | ||||
The output format, eg. 'svg', 'pdf', 'ps' or 'png'. If not | ||||
provided, will be deduced from the filename. | ||||
""" | ||||
from matplotlib import figure | ||||
# backend_agg supports all of the core output formats | ||||
from matplotlib.backends import backend_agg | ||||
from matplotlib.font_manager import FontProperties | ||||
from matplotlib.mathtext import MathTextParser | ||||
if prop is None: | ||||
prop = FontProperties() | ||||
parser = MathTextParser('path') | ||||
width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) | ||||
fig = figure.Figure(figsize=(width / 72.0, height / 72.0)) | ||||
fig.text(0, depth/height, s, fontproperties=prop) | ||||
backend_agg.FigureCanvasAgg(fig) | ||||
fig.savefig(filename_or_obj, dpi=dpi, format=format) | ||||
return depth | ||||