179
|
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)
|