##// END OF EJS Templates
Merge pull request #13680 from eendebakpt/latex_rendering_tempdir...
Matthias Bussonnier -
r27735:2f93f505 merge
parent child Browse files
Show More
@@ -0,0 +1,7 b''
1 Relative filenames in Latex rendering
2 =====================================
3
4 The `latex_to_png_dvipng` command internally generates input and output file arguments to `latex` and `dvipis`. These arguments are now generated as relative files to the current working directory instead of absolute file paths.
5 This solves a problem where the current working directory contains characters that are not handled properly by `latex` and `dvips`.
6 There are no changes to the user API.
7
@@ -1,246 +1,246 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 None is returned when the backend cannot be used.
81 81
82 82 """
83 83 s = cast_unicode(s)
84 84 allowed_backends = LaTeXTool.instance().backends
85 85 if backend is None:
86 86 backend = allowed_backends[0]
87 87 if backend not in allowed_backends:
88 88 return None
89 89 if backend == 'matplotlib':
90 90 f = latex_to_png_mpl
91 91 elif backend == 'dvipng':
92 92 f = latex_to_png_dvipng
93 93 if color.startswith('#'):
94 94 # Convert hex RGB color to LaTeX RGB color.
95 95 if len(color) == 7:
96 96 try:
97 97 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
98 98 textwrap.wrap(color[1:], 2)]))
99 99 except ValueError as e:
100 100 raise ValueError('Invalid color specification {}.'.format(color)) from e
101 101 else:
102 102 raise ValueError('Invalid color specification {}.'.format(color))
103 103 else:
104 104 raise ValueError('No such backend {0}'.format(backend))
105 105 bin_data = f(s, wrap, color, scale)
106 106 if encode and bin_data:
107 107 bin_data = encodebytes(bin_data)
108 108 return bin_data
109 109
110 110
111 111 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
112 112 try:
113 113 from matplotlib import figure, font_manager, mathtext
114 114 from matplotlib.backends import backend_agg
115 115 from pyparsing import ParseFatalException
116 116 except ImportError:
117 117 return None
118 118
119 119 # mpl mathtext doesn't support display math, force inline
120 120 s = s.replace('$$', '$')
121 121 if wrap:
122 122 s = u'${0}$'.format(s)
123 123
124 124 try:
125 125 prop = font_manager.FontProperties(size=12)
126 126 dpi = 120 * scale
127 127 buffer = BytesIO()
128 128
129 129 # Adapted from mathtext.math_to_image
130 130 parser = mathtext.MathTextParser("path")
131 131 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
132 132 fig = figure.Figure(figsize=(width / 72, height / 72))
133 133 fig.text(0, depth / height, s, fontproperties=prop, color=color)
134 134 backend_agg.FigureCanvasAgg(fig)
135 135 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
136 136 return buffer.getvalue()
137 137 except (ValueError, RuntimeError, ParseFatalException):
138 138 return None
139 139
140 140
141 141 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
142 142 try:
143 143 find_cmd('latex')
144 144 find_cmd('dvipng')
145 145 except FindCmdError:
146 146 return None
147 147 try:
148 148 workdir = Path(tempfile.mkdtemp())
149 tmpfile = workdir.joinpath("tmp.tex")
150 dvifile = workdir.joinpath("tmp.dvi")
151 outfile = workdir.joinpath("tmp.png")
149 tmpfile = "tmp.tex"
150 dvifile = "tmp.dvi"
151 outfile = "tmp.png"
152 152
153 with tmpfile.open("w", encoding="utf8") as f:
153 with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f:
154 154 f.writelines(genelatex(s, wrap))
155 155
156 156 with open(os.devnull, 'wb') as devnull:
157 157 subprocess.check_call(
158 158 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
159 159 cwd=workdir, stdout=devnull, stderr=devnull)
160 160
161 161 resolution = round(150*scale)
162 162 subprocess.check_call(
163 163 [
164 164 "dvipng",
165 165 "-T",
166 166 "tight",
167 167 "-D",
168 168 str(resolution),
169 169 "-z",
170 170 "9",
171 171 "-bg",
172 172 "Transparent",
173 173 "-o",
174 174 outfile,
175 175 dvifile,
176 176 "-fg",
177 177 color,
178 178 ],
179 179 cwd=workdir,
180 180 stdout=devnull,
181 181 stderr=devnull,
182 182 )
183 183
184 with outfile.open("rb") as f:
184 with workdir.joinpath(outfile).open("rb") as f:
185 185 return f.read()
186 186 except subprocess.CalledProcessError:
187 187 return None
188 188 finally:
189 189 shutil.rmtree(workdir)
190 190
191 191
192 192 def kpsewhich(filename):
193 193 """Invoke kpsewhich command with an argument `filename`."""
194 194 try:
195 195 find_cmd("kpsewhich")
196 196 proc = subprocess.Popen(
197 197 ["kpsewhich", filename],
198 198 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
199 199 (stdout, stderr) = proc.communicate()
200 200 return stdout.strip().decode('utf8', 'replace')
201 201 except FindCmdError:
202 202 pass
203 203
204 204
205 205 def genelatex(body, wrap):
206 206 """Generate LaTeX document for dvipng backend."""
207 207 lt = LaTeXTool.instance()
208 208 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
209 209 yield r'\documentclass{article}'
210 210 packages = lt.packages
211 211 if breqn:
212 212 packages = packages + ['breqn']
213 213 for pack in packages:
214 214 yield r'\usepackage{{{0}}}'.format(pack)
215 215 yield r'\pagestyle{empty}'
216 216 if lt.preamble:
217 217 yield lt.preamble
218 218 yield r'\begin{document}'
219 219 if breqn:
220 220 yield r'\begin{dmath*}'
221 221 yield body
222 222 yield r'\end{dmath*}'
223 223 elif wrap:
224 224 yield u'$${0}$$'.format(body)
225 225 else:
226 226 yield body
227 227 yield u'\\end{document}'
228 228
229 229
230 230 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
231 231
232 232 def latex_to_html(s, alt='image'):
233 233 """Render LaTeX to HTML with embedded PNG data using data URIs.
234 234
235 235 Parameters
236 236 ----------
237 237 s : str
238 238 The raw string containing valid inline LateX.
239 239 alt : str
240 240 The alt text to use for the HTML.
241 241 """
242 242 base64_data = latex_to_png(s, encode=True).decode('ascii')
243 243 if base64_data:
244 244 return _data_uri_template_png % (base64_data, alt)
245 245
246 246
General Comments 0
You need to be logged in to leave comments. Login now