1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 sphinx.ext.pngmath |
|
4 ~~~~~~~~~~~~~~~~~~ |
|
5 |
|
6 Render math in HTML via dvipng. |
|
7 |
|
8 :copyright: 2008 by Georg Brandl. |
|
9 :license: BSD. |
|
10 """ |
|
11 |
|
12 import re |
|
13 import shutil |
|
14 import tempfile |
|
15 import posixpath |
|
16 from os import path, getcwd, chdir |
|
17 from subprocess import Popen, PIPE |
|
18 try: |
|
19 from hashlib import sha1 as sha |
|
20 except ImportError: |
|
21 from sha import sha |
|
22 |
|
23 from docutils import nodes |
|
24 |
|
25 from sphinx.util import ensuredir |
|
26 from sphinx.util.png import read_png_depth, write_png_depth |
|
27 from sphinx.application import SphinxError |
|
28 from sphinx.ext.mathbase import setup as mathbase_setup, wrap_displaymath |
|
29 |
|
30 class MathExtError(SphinxError): |
|
31 category = 'Math extension error' |
|
32 |
|
33 |
|
34 DOC_HEAD = r''' |
|
35 \documentclass[12pt]{article} |
|
36 \usepackage[utf8]{inputenc} |
|
37 \usepackage{amsmath} |
|
38 \usepackage{amsthm} |
|
39 \usepackage{amssymb} |
|
40 \usepackage{amsfonts} |
|
41 \usepackage{bm} |
|
42 \pagestyle{empty} |
|
43 ''' |
|
44 |
|
45 DOC_BODY = r''' |
|
46 \begin{document} |
|
47 %s |
|
48 \end{document} |
|
49 ''' |
|
50 |
|
51 DOC_BODY_PREVIEW = r''' |
|
52 \usepackage[active]{preview} |
|
53 \begin{document} |
|
54 \begin{preview} |
|
55 %s |
|
56 \end{preview} |
|
57 \end{document} |
|
58 ''' |
|
59 |
|
60 depth_re = re.compile(r'\[\d+ depth=(-?\d+)\]') |
|
61 |
|
62 def render_math(self, math): |
|
63 """ |
|
64 Render the LaTeX math expression *math* using latex and dvipng. |
|
65 |
|
66 Return the filename relative to the built document and the "depth", |
|
67 that is, the distance of image bottom and baseline in pixels, if the |
|
68 option to use preview_latex is switched on. |
|
69 |
|
70 Error handling may seem strange, but follows a pattern: if LaTeX or |
|
71 dvipng aren't available, only a warning is generated (since that enables |
|
72 people on machines without these programs to at least build the rest |
|
73 of the docs successfully). If the programs are there, however, they |
|
74 may not fail since that indicates a problem in the math source. |
|
75 """ |
|
76 use_preview = self.builder.config.pngmath_use_preview |
|
77 |
|
78 shasum = "%s.png" % sha(math.encode('utf-8')).hexdigest() |
|
79 relfn = posixpath.join(self.builder.imgpath, 'math', shasum) |
|
80 outfn = path.join(self.builder.outdir, '_images', 'math', shasum) |
|
81 if path.isfile(outfn): |
|
82 depth = read_png_depth(outfn) |
|
83 return relfn, depth |
|
84 |
|
85 latex = DOC_HEAD + self.builder.config.pngmath_latex_preamble |
|
86 latex += (use_preview and DOC_BODY_PREVIEW or DOC_BODY) % math |
|
87 if isinstance(latex, unicode): |
|
88 latex = latex.encode('utf-8') |
|
89 |
|
90 # use only one tempdir per build -- the use of a directory is cleaner |
|
91 # than using temporary files, since we can clean up everything at once |
|
92 # just removing the whole directory (see cleanup_tempdir) |
|
93 if not hasattr(self.builder, '_mathpng_tempdir'): |
|
94 tempdir = self.builder._mathpng_tempdir = tempfile.mkdtemp() |
|
95 else: |
|
96 tempdir = self.builder._mathpng_tempdir |
|
97 |
|
98 tf = open(path.join(tempdir, 'math.tex'), 'w') |
|
99 tf.write(latex) |
|
100 tf.close() |
|
101 |
|
102 # build latex command; old versions of latex don't have the |
|
103 # --output-directory option, so we have to manually chdir to the |
|
104 # temp dir to run it. |
|
105 ltx_args = [self.builder.config.pngmath_latex, '--interaction=nonstopmode'] |
|
106 # add custom args from the config file |
|
107 ltx_args.extend(self.builder.config.pngmath_latex_args) |
|
108 ltx_args.append('math.tex') |
|
109 |
|
110 curdir = getcwd() |
|
111 chdir(tempdir) |
|
112 |
|
113 try: |
|
114 try: |
|
115 p = Popen(ltx_args, stdout=PIPE, stderr=PIPE) |
|
116 except OSError, err: |
|
117 if err.errno != 2: # No such file or directory |
|
118 raise |
|
119 if not hasattr(self.builder, '_mathpng_warned_latex'): |
|
120 self.builder.warn('LaTeX command %r cannot be run (needed for math ' |
|
121 'display), check the pngmath_latex setting' % |
|
122 self.builder.config.pngmath_latex) |
|
123 self.builder._mathpng_warned_latex = True |
|
124 return relfn, None |
|
125 finally: |
|
126 chdir(curdir) |
|
127 |
|
128 stdout, stderr = p.communicate() |
|
129 if p.returncode != 0: |
|
130 raise MathExtError('latex exited with error:\n[stderr]\n%s\n[stdout]\n%s' |
|
131 % (stderr, stdout)) |
|
132 |
|
133 ensuredir(path.dirname(outfn)) |
|
134 # use some standard dvipng arguments |
|
135 dvipng_args = [self.builder.config.pngmath_dvipng] |
|
136 dvipng_args += ['-o', outfn, '-T', 'tight', '-z9'] |
|
137 # add custom ones from config value |
|
138 dvipng_args.extend(self.builder.config.pngmath_dvipng_args) |
|
139 if use_preview: |
|
140 dvipng_args.append('--depth') |
|
141 # last, the input file name |
|
142 dvipng_args.append(path.join(tempdir, 'math.dvi')) |
|
143 try: |
|
144 p = Popen(dvipng_args, stdout=PIPE, stderr=PIPE) |
|
145 except OSError, err: |
|
146 if err.errno != 2: # No such file or directory |
|
147 raise |
|
148 if not hasattr(self.builder, '_mathpng_warned_dvipng'): |
|
149 self.builder.warn('dvipng command %r cannot be run (needed for math ' |
|
150 'display), check the pngmath_dvipng setting' % |
|
151 self.builder.config.pngmath_dvipng) |
|
152 self.builder._mathpng_warned_dvipng = True |
|
153 return relfn, None |
|
154 stdout, stderr = p.communicate() |
|
155 if p.returncode != 0: |
|
156 raise MathExtError('dvipng exited with error:\n[stderr]\n%s\n[stdout]\n%s' |
|
157 % (stderr, stdout)) |
|
158 depth = None |
|
159 if use_preview: |
|
160 for line in stdout.splitlines(): |
|
161 m = depth_re.match(line) |
|
162 if m: |
|
163 depth = int(m.group(1)) |
|
164 write_png_depth(outfn, depth) |
|
165 break |
|
166 |
|
167 return relfn, depth |
|
168 |
|
169 def cleanup_tempdir(app, exc): |
|
170 if exc: |
|
171 return |
|
172 if not hasattr(app.builder, '_mathpng_tempdir'): |
|
173 return |
|
174 try: |
|
175 shutil.rmtree(app.builder._mathpng_tempdir) |
|
176 except Exception: |
|
177 pass |
|
178 |
|
179 def html_visit_math(self, node): |
|
180 try: |
|
181 fname, depth = render_math(self, '$'+node['latex']+'$') |
|
182 except MathExtError, exc: |
|
183 sm = nodes.system_message(str(exc), type='WARNING', level=2, |
|
184 backrefs=[], source=node['latex']) |
|
185 sm.walkabout(self) |
|
186 self.builder.warn('display latex %r: ' % node['latex'] + str(exc)) |
|
187 raise nodes.SkipNode |
|
188 self.body.append('<img class="math" src="%s" alt="%s" %s/>' % |
|
189 (fname, self.encode(node['latex']).strip(), |
|
190 depth and 'style="vertical-align: %dpx" ' % (-depth) or '')) |
|
191 raise nodes.SkipNode |
|
192 |
|
193 def html_visit_displaymath(self, node): |
|
194 if node['nowrap']: |
|
195 latex = node['latex'] |
|
196 else: |
|
197 latex = wrap_displaymath(node['latex'], None) |
|
198 try: |
|
199 fname, depth = render_math(self, latex) |
|
200 except MathExtError, exc: |
|
201 sm = nodes.system_message(str(exc), type='WARNING', level=2, |
|
202 backrefs=[], source=node['latex']) |
|
203 sm.walkabout(self) |
|
204 self.builder.warn('inline latex %r: ' % node['latex'] + str(exc)) |
|
205 raise nodes.SkipNode |
|
206 self.body.append(self.starttag(node, 'div', CLASS='math')) |
|
207 self.body.append('<p>') |
|
208 if node['number']: |
|
209 self.body.append('<span class="eqno">(%s)</span>' % node['number']) |
|
210 self.body.append('<img src="%s" alt="%s" />\n</div>' % |
|
211 (fname, self.encode(node['latex']).strip())) |
|
212 self.body.append('</p>') |
|
213 raise nodes.SkipNode |
|
214 |
|
215 |
|
216 def setup(app): |
|
217 mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) |
|
218 app.add_config_value('pngmath_dvipng', 'dvipng', False) |
|
219 app.add_config_value('pngmath_latex', 'latex', False) |
|
220 app.add_config_value('pngmath_use_preview', False, False) |
|
221 app.add_config_value('pngmath_dvipng_args', ['-gamma 1.5', '-D 110'], False) |
|
222 app.add_config_value('pngmath_latex_args', [], False) |
|
223 app.add_config_value('pngmath_latex_preamble', '', False) |
|
224 app.connect('build-finished', cleanup_tempdir) |
|