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