##// END OF EJS Templates
latex_to_png: avoid deprecated matplotlib functions
Jake VanderPlas -
Show More
@@ -1,222 +1,230 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tools for handling LaTeX."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from io import BytesIO, open
8 8 import os
9 9 import tempfile
10 10 import shutil
11 11 import subprocess
12 12 from base64 import encodebytes
13 13 import textwrap
14 14
15 15 from pathlib import Path, PurePath
16 16
17 17 from IPython.utils.process import find_cmd, FindCmdError
18 18 from traitlets.config import get_config
19 19 from traitlets.config.configurable import SingletonConfigurable
20 20 from traitlets import List, Bool, Unicode
21 21 from IPython.utils.py3compat import cast_unicode
22 22
23 23
24 24 class LaTeXTool(SingletonConfigurable):
25 25 """An object to store configuration of the LaTeX tool."""
26 26 def _config_default(self):
27 27 return get_config()
28 28
29 29 backends = List(
30 30 Unicode(), ["matplotlib", "dvipng"],
31 31 help="Preferred backend to draw LaTeX math equations. "
32 32 "Backends in the list are checked one by one and the first "
33 33 "usable one is used. Note that `matplotlib` backend "
34 34 "is usable only for inline style equations. To draw "
35 35 "display style equations, `dvipng` backend must be specified. ",
36 36 # It is a List instead of Enum, to make configuration more
37 37 # flexible. For example, to use matplotlib mainly but dvipng
38 38 # for display style, the default ["matplotlib", "dvipng"] can
39 39 # be used. To NOT use dvipng so that other repr such as
40 40 # unicode pretty printing is used, you can use ["matplotlib"].
41 41 ).tag(config=True)
42 42
43 43 use_breqn = Bool(
44 44 True,
45 45 help="Use breqn.sty to automatically break long equations. "
46 46 "This configuration takes effect only for dvipng backend.",
47 47 ).tag(config=True)
48 48
49 49 packages = List(
50 50 ['amsmath', 'amsthm', 'amssymb', 'bm'],
51 51 help="A list of packages to use for dvipng backend. "
52 52 "'breqn' will be automatically appended when use_breqn=True.",
53 53 ).tag(config=True)
54 54
55 55 preamble = Unicode(
56 56 help="Additional preamble to use when generating LaTeX source "
57 57 "for dvipng backend.",
58 58 ).tag(config=True)
59 59
60 60
61 61 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
62 62 scale=1.0):
63 63 """Render a LaTeX string to PNG.
64 64
65 65 Parameters
66 66 ----------
67 67 s : str
68 68 The raw string containing valid inline LaTeX.
69 69 encode : bool, optional
70 70 Should the PNG data base64 encoded to make it JSON'able.
71 71 backend : {matplotlib, dvipng}
72 72 Backend for producing PNG data.
73 73 wrap : bool
74 74 If true, Automatically wrap `s` as a LaTeX equation.
75 75 color : string
76 76 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
77 77 format, e.g. '#AA20FA'.
78 78 scale : float
79 79 Scale factor for the resulting PNG.
80 80
81 81 None is returned when the backend cannot be used.
82 82
83 83 """
84 84 s = cast_unicode(s)
85 85 allowed_backends = LaTeXTool.instance().backends
86 86 if backend is None:
87 87 backend = allowed_backends[0]
88 88 if backend not in allowed_backends:
89 89 return None
90 90 if backend == 'matplotlib':
91 91 f = latex_to_png_mpl
92 92 elif backend == 'dvipng':
93 93 f = latex_to_png_dvipng
94 94 if color.startswith('#'):
95 95 # Convert hex RGB color to LaTeX RGB color.
96 96 if len(color) == 7:
97 97 try:
98 98 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
99 99 textwrap.wrap(color[1:], 2)]))
100 100 except ValueError as e:
101 101 raise ValueError('Invalid color specification {}.'.format(color)) from e
102 102 else:
103 103 raise ValueError('Invalid color specification {}.'.format(color))
104 104 else:
105 105 raise ValueError('No such backend {0}'.format(backend))
106 106 bin_data = f(s, wrap, color, scale)
107 107 if encode and bin_data:
108 108 bin_data = encodebytes(bin_data)
109 109 return bin_data
110 110
111 111
112 112 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
113 113 try:
114 from matplotlib import mathtext
114 from matplotlib import figure, font_manager, mathtext
115 from matplotlib.backends import backend_agg
115 116 from pyparsing import ParseFatalException
116 117 except ImportError:
117 118 return None
118 119
119 120 # mpl mathtext doesn't support display math, force inline
120 121 s = s.replace('$$', '$')
121 122 if wrap:
122 123 s = u'${0}$'.format(s)
123 124
124 125 try:
125 mt = mathtext.MathTextParser('bitmap')
126 f = BytesIO()
127 dpi = 120*scale
128 mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
129 return f.getvalue()
126 prop = font_manager.FontProperties(size=12)
127 dpi = 120 * scale
128 buffer = BytesIO()
129
130 # Adapted from mathtext.math_to_image
131 parser = mathtext.MathTextParser("path")
132 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
133 fig = figure.Figure(figsize=(width / 72, height / 72))
134 fig.text(0, depth / height, s, fontproperties=prop, color=color)
135 backend_agg.FigureCanvasAgg(fig)
136 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
137 return buffer.getvalue()
130 138 except (ValueError, RuntimeError, ParseFatalException):
131 139 return None
132 140
133 141
134 142 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
135 143 try:
136 144 find_cmd('latex')
137 145 find_cmd('dvipng')
138 146 except FindCmdError:
139 147 return None
140 148 try:
141 149 workdir = Path(tempfile.mkdtemp())
142 150 tmpfile = workdir.joinpath("tmp.tex")
143 151 dvifile = workdir.joinpath("tmp.dvi")
144 152 outfile = workdir.joinpath("tmp.png")
145 153
146 154 with tmpfile.open("w", encoding="utf8") as f:
147 155 f.writelines(genelatex(s, wrap))
148 156
149 157 with open(os.devnull, 'wb') as devnull:
150 158 subprocess.check_call(
151 159 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
152 160 cwd=workdir, stdout=devnull, stderr=devnull)
153 161
154 162 resolution = round(150*scale)
155 163 subprocess.check_call(
156 164 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
157 165 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
158 166 cwd=workdir, stdout=devnull, stderr=devnull)
159 167
160 168 with outfile.open("rb") as f:
161 169 return f.read()
162 170 except subprocess.CalledProcessError:
163 171 return None
164 172 finally:
165 173 shutil.rmtree(workdir)
166 174
167 175
168 176 def kpsewhich(filename):
169 177 """Invoke kpsewhich command with an argument `filename`."""
170 178 try:
171 179 find_cmd("kpsewhich")
172 180 proc = subprocess.Popen(
173 181 ["kpsewhich", filename],
174 182 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
175 183 (stdout, stderr) = proc.communicate()
176 184 return stdout.strip().decode('utf8', 'replace')
177 185 except FindCmdError:
178 186 pass
179 187
180 188
181 189 def genelatex(body, wrap):
182 190 """Generate LaTeX document for dvipng backend."""
183 191 lt = LaTeXTool.instance()
184 192 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
185 193 yield r'\documentclass{article}'
186 194 packages = lt.packages
187 195 if breqn:
188 196 packages = packages + ['breqn']
189 197 for pack in packages:
190 198 yield r'\usepackage{{{0}}}'.format(pack)
191 199 yield r'\pagestyle{empty}'
192 200 if lt.preamble:
193 201 yield lt.preamble
194 202 yield r'\begin{document}'
195 203 if breqn:
196 204 yield r'\begin{dmath*}'
197 205 yield body
198 206 yield r'\end{dmath*}'
199 207 elif wrap:
200 208 yield u'$${0}$$'.format(body)
201 209 else:
202 210 yield body
203 211 yield u'\\end{document}'
204 212
205 213
206 214 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
207 215
208 216 def latex_to_html(s, alt='image'):
209 217 """Render LaTeX to HTML with embedded PNG data using data URIs.
210 218
211 219 Parameters
212 220 ----------
213 221 s : str
214 222 The raw string containing valid inline LateX.
215 223 alt : str
216 224 The alt text to use for the HTML.
217 225 """
218 226 base64_data = latex_to_png(s, encode=True).decode('ascii')
219 227 if base64_data:
220 228 return _data_uri_template_png % (base64_data, alt)
221 229
222 230
General Comments 0
You need to be logged in to leave comments. Login now