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