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