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