##// END OF EJS Templates
Merge pull request #13372 from bnavigator/latex-png-bg-transparency...
Matthias Bussonnier -
r27240:74f70ce7 merge
parent child Browse files
Show More
@@ -1,230 +1,247 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 114 from matplotlib import figure, font_manager, mathtext
115 115 from matplotlib.backends import backend_agg
116 116 from pyparsing import ParseFatalException
117 117 except ImportError:
118 118 return None
119 119
120 120 # mpl mathtext doesn't support display math, force inline
121 121 s = s.replace('$$', '$')
122 122 if wrap:
123 123 s = u'${0}$'.format(s)
124 124
125 125 try:
126 126 prop = font_manager.FontProperties(size=12)
127 127 dpi = 120 * scale
128 128 buffer = BytesIO()
129 129
130 130 # Adapted from mathtext.math_to_image
131 131 parser = mathtext.MathTextParser("path")
132 132 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
133 133 fig = figure.Figure(figsize=(width / 72, height / 72))
134 134 fig.text(0, depth / height, s, fontproperties=prop, color=color)
135 135 backend_agg.FigureCanvasAgg(fig)
136 136 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
137 137 return buffer.getvalue()
138 138 except (ValueError, RuntimeError, ParseFatalException):
139 139 return None
140 140
141 141
142 142 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
143 143 try:
144 144 find_cmd('latex')
145 145 find_cmd('dvipng')
146 146 except FindCmdError:
147 147 return None
148 148 try:
149 149 workdir = Path(tempfile.mkdtemp())
150 150 tmpfile = workdir.joinpath("tmp.tex")
151 151 dvifile = workdir.joinpath("tmp.dvi")
152 152 outfile = workdir.joinpath("tmp.png")
153 153
154 154 with tmpfile.open("w", encoding="utf8") as f:
155 155 f.writelines(genelatex(s, wrap))
156 156
157 157 with open(os.devnull, 'wb') as devnull:
158 158 subprocess.check_call(
159 159 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
160 160 cwd=workdir, stdout=devnull, stderr=devnull)
161 161
162 162 resolution = round(150*scale)
163 163 subprocess.check_call(
164 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
165 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
166 cwd=workdir, stdout=devnull, stderr=devnull)
164 [
165 "dvipng",
166 "-T",
167 "tight",
168 "-D",
169 str(resolution),
170 "-z",
171 "9",
172 "-bg",
173 "Transparent",
174 "-o",
175 outfile,
176 dvifile,
177 "-fg",
178 color,
179 ],
180 cwd=workdir,
181 stdout=devnull,
182 stderr=devnull,
183 )
167 184
168 185 with outfile.open("rb") as f:
169 186 return f.read()
170 187 except subprocess.CalledProcessError:
171 188 return None
172 189 finally:
173 190 shutil.rmtree(workdir)
174 191
175 192
176 193 def kpsewhich(filename):
177 194 """Invoke kpsewhich command with an argument `filename`."""
178 195 try:
179 196 find_cmd("kpsewhich")
180 197 proc = subprocess.Popen(
181 198 ["kpsewhich", filename],
182 199 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
183 200 (stdout, stderr) = proc.communicate()
184 201 return stdout.strip().decode('utf8', 'replace')
185 202 except FindCmdError:
186 203 pass
187 204
188 205
189 206 def genelatex(body, wrap):
190 207 """Generate LaTeX document for dvipng backend."""
191 208 lt = LaTeXTool.instance()
192 209 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
193 210 yield r'\documentclass{article}'
194 211 packages = lt.packages
195 212 if breqn:
196 213 packages = packages + ['breqn']
197 214 for pack in packages:
198 215 yield r'\usepackage{{{0}}}'.format(pack)
199 216 yield r'\pagestyle{empty}'
200 217 if lt.preamble:
201 218 yield lt.preamble
202 219 yield r'\begin{document}'
203 220 if breqn:
204 221 yield r'\begin{dmath*}'
205 222 yield body
206 223 yield r'\end{dmath*}'
207 224 elif wrap:
208 225 yield u'$${0}$$'.format(body)
209 226 else:
210 227 yield body
211 228 yield u'\\end{document}'
212 229
213 230
214 231 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
215 232
216 233 def latex_to_html(s, alt='image'):
217 234 """Render LaTeX to HTML with embedded PNG data using data URIs.
218 235
219 236 Parameters
220 237 ----------
221 238 s : str
222 239 The raw string containing valid inline LateX.
223 240 alt : str
224 241 The alt text to use for the HTML.
225 242 """
226 243 base64_data = latex_to_png(s, encode=True).decode('ascii')
227 244 if base64_data:
228 245 return _data_uri_template_png % (base64_data, alt)
229 246
230 247
@@ -1,123 +1,122 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for timing code execution.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import time
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Code
21 21 #-----------------------------------------------------------------------------
22 22
23 23 # If possible (Unix), use the resource module instead of time.clock()
24 24 try:
25 25 import resource
26 26 except ImportError:
27 27 resource = None
28 28
29 29 # Some implementations (like jyputerlite) don't have getrusage
30 30 if resource is not None and hasattr(resource, "getrusage"):
31 31 def clocku():
32 32 """clocku() -> floating point number
33 33
34 34 Return the *USER* CPU time in seconds since the start of the process.
35 35 This is done via a call to resource.getrusage, so it avoids the
36 36 wraparound problems in time.clock()."""
37 37
38 38 return resource.getrusage(resource.RUSAGE_SELF)[0]
39 39
40 40 def clocks():
41 41 """clocks() -> floating point number
42 42
43 43 Return the *SYSTEM* CPU time in seconds since the start of the process.
44 44 This is done via a call to resource.getrusage, so it avoids the
45 45 wraparound problems in time.clock()."""
46 46
47 47 return resource.getrusage(resource.RUSAGE_SELF)[1]
48 48
49 49 def clock():
50 50 """clock() -> floating point number
51 51
52 52 Return the *TOTAL USER+SYSTEM* CPU time in seconds since the start of
53 53 the process. This is done via a call to resource.getrusage, so it
54 54 avoids the wraparound problems in time.clock()."""
55 55
56 56 u,s = resource.getrusage(resource.RUSAGE_SELF)[:2]
57 57 return u+s
58 58
59 59 def clock2():
60 60 """clock2() -> (t_user,t_system)
61 61
62 62 Similar to clock(), but return a tuple of user/system times."""
63 63 return resource.getrusage(resource.RUSAGE_SELF)[:2]
64 64
65
66 65 else:
67 66 # There is no distinction of user/system time under windows, so we just use
68 67 # time.perff_counter() for everything...
69 68 clocku = clocks = clock = time.perf_counter
70 69 def clock2():
71 70 """Under windows, system CPU time can't be measured.
72 71
73 72 This just returns perf_counter() and zero."""
74 73 return time.perf_counter(),0.0
75 74
76 75
77 76 def timings_out(reps,func,*args,**kw):
78 77 """timings_out(reps,func,*args,**kw) -> (t_total,t_per_call,output)
79 78
80 79 Execute a function reps times, return a tuple with the elapsed total
81 80 CPU time in seconds, the time per call and the function's output.
82 81
83 82 Under Unix, the return value is the sum of user+system time consumed by
84 83 the process, computed via the resource module. This prevents problems
85 84 related to the wraparound effect which the time.clock() function has.
86 85
87 86 Under Windows the return value is in wall clock seconds. See the
88 87 documentation for the time module for more details."""
89 88
90 89 reps = int(reps)
91 90 assert reps >=1, 'reps must be >= 1'
92 91 if reps==1:
93 92 start = clock()
94 93 out = func(*args,**kw)
95 94 tot_time = clock()-start
96 95 else:
97 96 rng = range(reps-1) # the last time is executed separately to store output
98 97 start = clock()
99 98 for dummy in rng: func(*args,**kw)
100 99 out = func(*args,**kw) # one last time
101 100 tot_time = clock()-start
102 101 av_time = tot_time / reps
103 102 return tot_time,av_time,out
104 103
105 104
106 105 def timings(reps,func,*args,**kw):
107 106 """timings(reps,func,*args,**kw) -> (t_total,t_per_call)
108 107
109 108 Execute a function reps times, return a tuple with the elapsed total CPU
110 109 time in seconds and the time per call. These are just the first two values
111 110 in timings_out()."""
112 111
113 112 return timings_out(reps,func,*args,**kw)[0:2]
114 113
115 114
116 115 def timing(func,*args,**kw):
117 116 """timing(func,*args,**kw) -> t_total
118 117
119 118 Execute a function once, return the elapsed total CPU time in
120 119 seconds. This is just the first value in timings_out()."""
121 120
122 121 return timings_out(1,func,*args,**kw)[0]
123 122
General Comments 0
You need to be logged in to leave comments. Login now