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