##// END OF EJS Templates
Enable changing the font color for LaTeX rendering
Oscar Gustafsson -
Show More
@@ -1,201 +1,203 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
13
14 from IPython.utils.process import find_cmd, FindCmdError
14 from IPython.utils.process import find_cmd, FindCmdError
15 from traitlets.config import get_config
15 from traitlets.config import get_config
16 from traitlets.config.configurable import SingletonConfigurable
16 from traitlets.config.configurable import SingletonConfigurable
17 from traitlets import List, Bool, Unicode
17 from traitlets import List, Bool, Unicode
18 from IPython.utils.py3compat import cast_unicode
18 from IPython.utils.py3compat import cast_unicode
19
19
20
20
21 class LaTeXTool(SingletonConfigurable):
21 class LaTeXTool(SingletonConfigurable):
22 """An object to store configuration of the LaTeX tool."""
22 """An object to store configuration of the LaTeX tool."""
23 def _config_default(self):
23 def _config_default(self):
24 return get_config()
24 return get_config()
25
25
26 backends = List(
26 backends = List(
27 Unicode(), ["matplotlib", "dvipng"],
27 Unicode(), ["matplotlib", "dvipng"],
28 help="Preferred backend to draw LaTeX math equations. "
28 help="Preferred backend to draw LaTeX math equations. "
29 "Backends in the list are checked one by one and the first "
29 "Backends in the list are checked one by one and the first "
30 "usable one is used. Note that `matplotlib` backend "
30 "usable one is used. Note that `matplotlib` backend "
31 "is usable only for inline style equations. To draw "
31 "is usable only for inline style equations. To draw "
32 "display style equations, `dvipng` backend must be specified. ",
32 "display style equations, `dvipng` backend must be specified. ",
33 # It is a List instead of Enum, to make configuration more
33 # It is a List instead of Enum, to make configuration more
34 # flexible. For example, to use matplotlib mainly but dvipng
34 # flexible. For example, to use matplotlib mainly but dvipng
35 # for display style, the default ["matplotlib", "dvipng"] can
35 # for display style, the default ["matplotlib", "dvipng"] can
36 # be used. To NOT use dvipng so that other repr such as
36 # be used. To NOT use dvipng so that other repr such as
37 # unicode pretty printing is used, you can use ["matplotlib"].
37 # unicode pretty printing is used, you can use ["matplotlib"].
38 ).tag(config=True)
38 ).tag(config=True)
39
39
40 use_breqn = Bool(
40 use_breqn = Bool(
41 True,
41 True,
42 help="Use breqn.sty to automatically break long equations. "
42 help="Use breqn.sty to automatically break long equations. "
43 "This configuration takes effect only for dvipng backend.",
43 "This configuration takes effect only for dvipng backend.",
44 ).tag(config=True)
44 ).tag(config=True)
45
45
46 packages = List(
46 packages = List(
47 ['amsmath', 'amsthm', 'amssymb', 'bm'],
47 ['amsmath', 'amsthm', 'amssymb', 'bm'],
48 help="A list of packages to use for dvipng backend. "
48 help="A list of packages to use for dvipng backend. "
49 "'breqn' will be automatically appended when use_breqn=True.",
49 "'breqn' will be automatically appended when use_breqn=True.",
50 ).tag(config=True)
50 ).tag(config=True)
51
51
52 preamble = Unicode(
52 preamble = Unicode(
53 help="Additional preamble to use when generating LaTeX source "
53 help="Additional preamble to use when generating LaTeX source "
54 "for dvipng backend.",
54 "for dvipng backend.",
55 ).tag(config=True)
55 ).tag(config=True)
56
56
57
57
58 def latex_to_png(s, encode=False, backend=None, wrap=False):
58 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black'):
59 """Render a LaTeX string to PNG.
59 """Render a LaTeX string to PNG.
60
60
61 Parameters
61 Parameters
62 ----------
62 ----------
63 s : str
63 s : str
64 The raw string containing valid inline LaTeX.
64 The raw string containing valid inline LaTeX.
65 encode : bool, optional
65 encode : bool, optional
66 Should the PNG data base64 encoded to make it JSON'able.
66 Should the PNG data base64 encoded to make it JSON'able.
67 backend : {matplotlib, dvipng}
67 backend : {matplotlib, dvipng}
68 Backend for producing PNG data.
68 Backend for producing PNG data.
69 wrap : bool
69 wrap : bool
70 If true, Automatically wrap `s` as a LaTeX equation.
70 If true, Automatically wrap `s` as a LaTeX equation.
71 color : string
72 Foreground color name among dvipsnames.
71
73
72 None is returned when the backend cannot be used.
74 None is returned when the backend cannot be used.
73
75
74 """
76 """
75 s = cast_unicode(s)
77 s = cast_unicode(s)
76 allowed_backends = LaTeXTool.instance().backends
78 allowed_backends = LaTeXTool.instance().backends
77 if backend is None:
79 if backend is None:
78 backend = allowed_backends[0]
80 backend = allowed_backends[0]
79 if backend not in allowed_backends:
81 if backend not in allowed_backends:
80 return None
82 return None
81 if backend == 'matplotlib':
83 if backend == 'matplotlib':
82 f = latex_to_png_mpl
84 f = latex_to_png_mpl
83 elif backend == 'dvipng':
85 elif backend == 'dvipng':
84 f = latex_to_png_dvipng
86 f = latex_to_png_dvipng
85 else:
87 else:
86 raise ValueError('No such backend {0}'.format(backend))
88 raise ValueError('No such backend {0}'.format(backend))
87 bin_data = f(s, wrap)
89 bin_data = f(s, wrap, color)
88 if encode and bin_data:
90 if encode and bin_data:
89 bin_data = encodebytes(bin_data)
91 bin_data = encodebytes(bin_data)
90 return bin_data
92 return bin_data
91
93
92
94
93 def latex_to_png_mpl(s, wrap):
95 def latex_to_png_mpl(s, wrap, color='Black'):
94 try:
96 try:
95 from matplotlib import mathtext
97 from matplotlib import mathtext
96 from pyparsing import ParseFatalException
98 from pyparsing import ParseFatalException
97 except ImportError:
99 except ImportError:
98 return None
100 return None
99
101
100 # mpl mathtext doesn't support display math, force inline
102 # mpl mathtext doesn't support display math, force inline
101 s = s.replace('$$', '$')
103 s = s.replace('$$', '$')
102 if wrap:
104 if wrap:
103 s = u'${0}$'.format(s)
105 s = u'${0}$'.format(s)
104
106
105 try:
107 try:
106 mt = mathtext.MathTextParser('bitmap')
108 mt = mathtext.MathTextParser('bitmap')
107 f = BytesIO()
109 f = BytesIO()
108 mt.to_png(f, s, fontsize=12)
110 mt.to_png(f, s, fontsize=12, color=color)
109 return f.getvalue()
111 return f.getvalue()
110 except (ValueError, RuntimeError, ParseFatalException):
112 except (ValueError, RuntimeError, ParseFatalException):
111 return None
113 return None
112
114
113
115
114 def latex_to_png_dvipng(s, wrap):
116 def latex_to_png_dvipng(s, wrap, color='Black'):
115 try:
117 try:
116 find_cmd('latex')
118 find_cmd('latex')
117 find_cmd('dvipng')
119 find_cmd('dvipng')
118 except FindCmdError:
120 except FindCmdError:
119 return None
121 return None
120 try:
122 try:
121 workdir = tempfile.mkdtemp()
123 workdir = tempfile.mkdtemp()
122 tmpfile = os.path.join(workdir, "tmp.tex")
124 tmpfile = os.path.join(workdir, "tmp.tex")
123 dvifile = os.path.join(workdir, "tmp.dvi")
125 dvifile = os.path.join(workdir, "tmp.dvi")
124 outfile = os.path.join(workdir, "tmp.png")
126 outfile = os.path.join(workdir, "tmp.png")
125
127
126 with open(tmpfile, "w", encoding='utf8') as f:
128 with open(tmpfile, "w", encoding='utf8') as f:
127 f.writelines(genelatex(s, wrap))
129 f.writelines(genelatex(s, wrap))
128
130
129 with open(os.devnull, 'wb') as devnull:
131 with open(os.devnull, 'wb') as devnull:
130 subprocess.check_call(
132 subprocess.check_call(
131 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
133 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
132 cwd=workdir, stdout=devnull, stderr=devnull)
134 cwd=workdir, stdout=devnull, stderr=devnull)
133
135
134 subprocess.check_call(
136 subprocess.check_call(
135 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
137 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
136 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
138 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
137 stdout=devnull, stderr=devnull)
139 cwd=workdir, stdout=devnull, stderr=devnull)
138
140
139 with open(outfile, "rb") as f:
141 with open(outfile, "rb") as f:
140 return f.read()
142 return f.read()
141 except subprocess.CalledProcessError:
143 except subprocess.CalledProcessError:
142 return None
144 return None
143 finally:
145 finally:
144 shutil.rmtree(workdir)
146 shutil.rmtree(workdir)
145
147
146
148
147 def kpsewhich(filename):
149 def kpsewhich(filename):
148 """Invoke kpsewhich command with an argument `filename`."""
150 """Invoke kpsewhich command with an argument `filename`."""
149 try:
151 try:
150 find_cmd("kpsewhich")
152 find_cmd("kpsewhich")
151 proc = subprocess.Popen(
153 proc = subprocess.Popen(
152 ["kpsewhich", filename],
154 ["kpsewhich", filename],
153 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
155 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
154 (stdout, stderr) = proc.communicate()
156 (stdout, stderr) = proc.communicate()
155 return stdout.strip().decode('utf8', 'replace')
157 return stdout.strip().decode('utf8', 'replace')
156 except FindCmdError:
158 except FindCmdError:
157 pass
159 pass
158
160
159
161
160 def genelatex(body, wrap):
162 def genelatex(body, wrap):
161 """Generate LaTeX document for dvipng backend."""
163 """Generate LaTeX document for dvipng backend."""
162 lt = LaTeXTool.instance()
164 lt = LaTeXTool.instance()
163 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
165 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
164 yield r'\documentclass{article}'
166 yield r'\documentclass{article}'
165 packages = lt.packages
167 packages = lt.packages
166 if breqn:
168 if breqn:
167 packages = packages + ['breqn']
169 packages = packages + ['breqn']
168 for pack in packages:
170 for pack in packages:
169 yield r'\usepackage{{{0}}}'.format(pack)
171 yield r'\usepackage{{{0}}}'.format(pack)
170 yield r'\pagestyle{empty}'
172 yield r'\pagestyle{empty}'
171 if lt.preamble:
173 if lt.preamble:
172 yield lt.preamble
174 yield lt.preamble
173 yield r'\begin{document}'
175 yield r'\begin{document}'
174 if breqn:
176 if breqn:
175 yield r'\begin{dmath*}'
177 yield r'\begin{dmath*}'
176 yield body
178 yield body
177 yield r'\end{dmath*}'
179 yield r'\end{dmath*}'
178 elif wrap:
180 elif wrap:
179 yield u'$${0}$$'.format(body)
181 yield u'$${0}$$'.format(body)
180 else:
182 else:
181 yield body
183 yield body
182 yield u'\\end{document}'
184 yield u'\\end{document}'
183
185
184
186
185 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
187 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
186
188
187 def latex_to_html(s, alt='image'):
189 def latex_to_html(s, alt='image'):
188 """Render LaTeX to HTML with embedded PNG data using data URIs.
190 """Render LaTeX to HTML with embedded PNG data using data URIs.
189
191
190 Parameters
192 Parameters
191 ----------
193 ----------
192 s : str
194 s : str
193 The raw string containing valid inline LateX.
195 The raw string containing valid inline LateX.
194 alt : str
196 alt : str
195 The alt text to use for the HTML.
197 The alt text to use for the HTML.
196 """
198 """
197 base64_data = latex_to_png(s, encode=True).decode('ascii')
199 base64_data = latex_to_png(s, encode=True).decode('ascii')
198 if base64_data:
200 if base64_data:
199 return _data_uri_template_png % (base64_data, alt)
201 return _data_uri_template_png % (base64_data, alt)
200
202
201
203
@@ -1,134 +1,157 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.utils.path.py"""
2 """Tests for IPython.utils.path.py"""
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 from unittest.mock import patch
6 from unittest.mock import patch
7 import nose.tools as nt
7 import nose.tools as nt
8
8
9 from IPython.lib import latextools
9 from IPython.lib import latextools
10 from IPython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib
10 from IPython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib
11 from IPython.utils.process import FindCmdError
11 from IPython.utils.process import FindCmdError
12
12
13
13
14 def test_latex_to_png_dvipng_fails_when_no_cmd():
14 def test_latex_to_png_dvipng_fails_when_no_cmd():
15 """
15 """
16 `latex_to_png_dvipng` should return None when there is no required command
16 `latex_to_png_dvipng` should return None when there is no required command
17 """
17 """
18 for command in ['latex', 'dvipng']:
18 for command in ['latex', 'dvipng']:
19 yield (check_latex_to_png_dvipng_fails_when_no_cmd, command)
19 yield (check_latex_to_png_dvipng_fails_when_no_cmd, command)
20
20
21
21
22 def check_latex_to_png_dvipng_fails_when_no_cmd(command):
22 def check_latex_to_png_dvipng_fails_when_no_cmd(command):
23 def mock_find_cmd(arg):
23 def mock_find_cmd(arg):
24 if arg == command:
24 if arg == command:
25 raise FindCmdError
25 raise FindCmdError
26
26
27 with patch.object(latextools, "find_cmd", mock_find_cmd):
27 with patch.object(latextools, "find_cmd", mock_find_cmd):
28 nt.assert_equal(latextools.latex_to_png_dvipng("whatever", True),
28 nt.assert_equal(latextools.latex_to_png_dvipng("whatever", True),
29 None)
29 None)
30
30
31
31
32 @onlyif_cmds_exist('latex', 'dvipng')
32 @onlyif_cmds_exist('latex', 'dvipng')
33 def test_latex_to_png_dvipng_runs():
33 def test_latex_to_png_dvipng_runs():
34 """
34 """
35 Test that latex_to_png_dvipng just runs without error.
35 Test that latex_to_png_dvipng just runs without error.
36 """
36 """
37 def mock_kpsewhich(filename):
37 def mock_kpsewhich(filename):
38 nt.assert_equal(filename, "breqn.sty")
38 nt.assert_equal(filename, "breqn.sty")
39 return None
39 return None
40
40
41 for (s, wrap) in [(u"$$x^2$$", False), (u"x^2", True)]:
41 for (s, wrap) in [(u"$$x^2$$", False), (u"x^2", True)]:
42 yield (latextools.latex_to_png_dvipng, s, wrap)
42 yield (latextools.latex_to_png_dvipng, s, wrap)
43
43
44 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
44 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
45 yield (latextools.latex_to_png_dvipng, s, wrap)
45 yield (latextools.latex_to_png_dvipng, s, wrap)
46
46
47 @skipif_not_matplotlib
47 @skipif_not_matplotlib
48 def test_latex_to_png_mpl_runs():
48 def test_latex_to_png_mpl_runs():
49 """
49 """
50 Test that latex_to_png_mpl just runs without error.
50 Test that latex_to_png_mpl just runs without error.
51 """
51 """
52 def mock_kpsewhich(filename):
52 def mock_kpsewhich(filename):
53 nt.assert_equal(filename, "breqn.sty")
53 nt.assert_equal(filename, "breqn.sty")
54 return None
54 return None
55
55
56 for (s, wrap) in [("$x^2$", False), ("x^2", True)]:
56 for (s, wrap) in [("$x^2$", False), ("x^2", True)]:
57 yield (latextools.latex_to_png_mpl, s, wrap)
57 yield (latextools.latex_to_png_mpl, s, wrap)
58
58
59 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
59 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
60 yield (latextools.latex_to_png_mpl, s, wrap)
60 yield (latextools.latex_to_png_mpl, s, wrap)
61
61
62 @skipif_not_matplotlib
62 @skipif_not_matplotlib
63 def test_latex_to_html():
63 def test_latex_to_html():
64 img = latextools.latex_to_html("$x^2$")
64 img = latextools.latex_to_html("$x^2$")
65 nt.assert_in("data:image/png;base64,iVBOR", img)
65 nt.assert_in("data:image/png;base64,iVBOR", img)
66
66
67
67
68 def test_genelatex_no_wrap():
68 def test_genelatex_no_wrap():
69 """
69 """
70 Test genelatex with wrap=False.
70 Test genelatex with wrap=False.
71 """
71 """
72 def mock_kpsewhich(filename):
72 def mock_kpsewhich(filename):
73 assert False, ("kpsewhich should not be called "
73 assert False, ("kpsewhich should not be called "
74 "(called with {0})".format(filename))
74 "(called with {0})".format(filename))
75
75
76 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
76 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
77 nt.assert_equal(
77 nt.assert_equal(
78 '\n'.join(latextools.genelatex("body text", False)),
78 '\n'.join(latextools.genelatex("body text", False)),
79 r'''\documentclass{article}
79 r'''\documentclass{article}
80 \usepackage{amsmath}
80 \usepackage{amsmath}
81 \usepackage{amsthm}
81 \usepackage{amsthm}
82 \usepackage{amssymb}
82 \usepackage{amssymb}
83 \usepackage{bm}
83 \usepackage{bm}
84 \pagestyle{empty}
84 \pagestyle{empty}
85 \begin{document}
85 \begin{document}
86 body text
86 body text
87 \end{document}''')
87 \end{document}''')
88
88
89
89
90 def test_genelatex_wrap_with_breqn():
90 def test_genelatex_wrap_with_breqn():
91 """
91 """
92 Test genelatex with wrap=True for the case breqn.sty is installed.
92 Test genelatex with wrap=True for the case breqn.sty is installed.
93 """
93 """
94 def mock_kpsewhich(filename):
94 def mock_kpsewhich(filename):
95 nt.assert_equal(filename, "breqn.sty")
95 nt.assert_equal(filename, "breqn.sty")
96 return "path/to/breqn.sty"
96 return "path/to/breqn.sty"
97
97
98 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
98 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
99 nt.assert_equal(
99 nt.assert_equal(
100 '\n'.join(latextools.genelatex("x^2", True)),
100 '\n'.join(latextools.genelatex("x^2", True)),
101 r'''\documentclass{article}
101 r'''\documentclass{article}
102 \usepackage{amsmath}
102 \usepackage{amsmath}
103 \usepackage{amsthm}
103 \usepackage{amsthm}
104 \usepackage{amssymb}
104 \usepackage{amssymb}
105 \usepackage{bm}
105 \usepackage{bm}
106 \usepackage{breqn}
106 \usepackage{breqn}
107 \pagestyle{empty}
107 \pagestyle{empty}
108 \begin{document}
108 \begin{document}
109 \begin{dmath*}
109 \begin{dmath*}
110 x^2
110 x^2
111 \end{dmath*}
111 \end{dmath*}
112 \end{document}''')
112 \end{document}''')
113
113
114
114
115 def test_genelatex_wrap_without_breqn():
115 def test_genelatex_wrap_without_breqn():
116 """
116 """
117 Test genelatex with wrap=True for the case breqn.sty is not installed.
117 Test genelatex with wrap=True for the case breqn.sty is not installed.
118 """
118 """
119 def mock_kpsewhich(filename):
119 def mock_kpsewhich(filename):
120 nt.assert_equal(filename, "breqn.sty")
120 nt.assert_equal(filename, "breqn.sty")
121 return None
121 return None
122
122
123 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
123 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
124 nt.assert_equal(
124 nt.assert_equal(
125 '\n'.join(latextools.genelatex("x^2", True)),
125 '\n'.join(latextools.genelatex("x^2", True)),
126 r'''\documentclass{article}
126 r'''\documentclass{article}
127 \usepackage{amsmath}
127 \usepackage{amsmath}
128 \usepackage{amsthm}
128 \usepackage{amsthm}
129 \usepackage{amssymb}
129 \usepackage{amssymb}
130 \usepackage{bm}
130 \usepackage{bm}
131 \pagestyle{empty}
131 \pagestyle{empty}
132 \begin{document}
132 \begin{document}
133 $$x^2$$
133 $$x^2$$
134 \end{document}''')
134 \end{document}''')
135
136
137 def test_latex_to_png_color():
138 """
139 Test color settings for latex_to_png.
140 """
141 latex_string = "$x^2$"
142 default_value = latextools.latex_to_png(latex_string, wrap=False)
143 dvipng_default = latextools.latex_to_png_dvipng(latex_string, False)
144 dvipng_black = latextools.latex_to_png_dvipng(latex_string, False, 'Black')
145 nt.assert_equal(dvipng_default, dvipng_black)
146 mpl_default = latextools.latex_to_png_mpl(latex_string, False)
147 mpl_black = latextools.latex_to_png_mpl(latex_string, False, 'Black')
148 nt.assert_equal(mpl_default, mpl_black)
149 nt.assert_in(default_value, [dvipng_black, mpl_black])
150
151 # Test that dvips name colors can be used without error
152 dvipng_maroon = latextools.latex_to_png_dvipng(latex_string, False, 'Maroon')
153 # And that it doesn't return the black one
154 nt.assert_not_equal(dvipng_black, dvipng_maroon)
155
156 mpl_maroon = latextools.latex_to_png_mpl(latex_string, False, 'Maroon')
157 nt.assert_not_equal(mpl_black, mpl_maroon)
General Comments 0
You need to be logged in to leave comments. Login now