diff --git a/AUTHORS b/AUTHORS
index f30631c42c..e8051429f5 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -60,6 +60,7 @@ Other contributors, listed alphabetically, are:
* Sebastian Wiesner -- image handling, distutils support
* Michael Wilson -- Intersphinx HTTP basic auth support
* Joel Wurtz -- cellspanning support in LaTeX
+* Hong Xu -- svg support in imgmath extension and various bug fixes
Many thanks for all contributions!
diff --git a/doc/ext/math.rst b/doc/ext/math.rst
index 4c15461092..2635596ab5 100644
--- a/doc/ext/math.rst
+++ b/doc/ext/math.rst
@@ -4,7 +4,7 @@ Math support in Sphinx
======================
.. module:: sphinx.ext.mathbase
- :synopsis: Common math support for pngmath and mathjax / jsmath.
+ :synopsis: Common math support for imgmath and mathjax / jsmath.
.. versionadded:: 0.5
@@ -17,7 +17,7 @@ support extensions should, if possible, reuse that support too.
.. note::
:mod:`.mathbase` is not meant to be added to the :confval:`extensions` config
- value, instead, use either :mod:`sphinx.ext.pngmath` or
+ value, instead, use either :mod:`sphinx.ext.imgmath` or
:mod:`sphinx.ext.mathjax` as described below.
The input language for mathematics is LaTeX markup. This is the de-facto
@@ -96,20 +96,27 @@ or use Python raw strings (``r"raw"``).
beautiful mathematical formulas.
-:mod:`sphinx.ext.pngmath` -- Render math as PNG images
-------------------------------------------------------
+:mod:`sphinx.ext.imgmath` -- Render math as images
+--------------------------------------------------
-.. module:: sphinx.ext.pngmath
- :synopsis: Render math as PNG images.
+.. module:: sphinx.ext.imgmath
+ :synopsis: Render math as PNG or SVG images.
-This extension renders math via LaTeX and dvipng_ into PNG images. This of
-course means that the computer where the docs are built must have both programs
-available.
+.. versionadded:: 1.4
+
+This extension renders math via LaTeX and dvipng_ or dvisvgm_ into PNG or SVG
+images. This of course means that the computer where the docs are built must
+have both programs available.
There are various config values you can set to influence how the images are
built:
-.. confval:: pngmath_latex
+.. confval:: imgmath_image_format
+
+ The output image format. The default is ``'png'``. It should be either
+ ``'png'`` or ``'svg'``.
+
+.. confval:: imgmath_latex
The command name with which to invoke LaTeX. The default is ``'latex'``; you
may need to set this to a full path if ``latex`` is not in the executable
@@ -120,42 +127,51 @@ built:
:program:`sphinx-build` command line via the :option:`-D` option should be
preferable, like this::
- sphinx-build -b html -D pngmath_latex=C:\tex\latex.exe . _build/html
+ sphinx-build -b html -D imgmath_latex=C:\tex\latex.exe . _build/html
- .. versionchanged:: 0.5.1
- This value should only contain the path to the latex executable, not
- further arguments; use :confval:`pngmath_latex_args` for that purpose.
+ This value should only contain the path to the latex executable, not further
+ arguments; use :confval:`imgmath_latex_args` for that purpose.
-.. confval:: pngmath_dvipng
+.. confval:: imgmath_dvipng
The command name with which to invoke ``dvipng``. The default is
``'dvipng'``; you may need to set this to a full path if ``dvipng`` is not in
- the executable search path.
+ the executable search path. This option is only used when
+ ``imgmath_image_format`` is set to ``'png'``.
-.. confval:: pngmath_latex_args
+.. confval:: imgmath_dvisvgm
+
+ The command name with which to invoke ``dvisvgm``. The default is
+ ``'dvisvgm'``; you may need to set this to a full path if ``dvisvgm`` is not
+ in the executable search path. This option is only used when
+ ``imgmath_image_format`` is ``'svg'``.
+
+.. confval:: imgmath_latex_args
Additional arguments to give to latex, as a list. The default is an empty
list.
- .. versionadded:: 0.5.1
-
-.. confval:: pngmath_latex_preamble
+.. confval:: imgmath_latex_preamble
Additional LaTeX code to put into the preamble of the short LaTeX files that
are used to translate the math snippets. This is empty by default. Use it
e.g. to add more packages whose commands you want to use in the math.
-.. confval:: pngmath_dvipng_args
+.. confval:: imgmath_dvipng_args
Additional arguments to give to dvipng, as a list. The default value is
``['-gamma', '1.5', '-D', '110', '-bg', 'Transparent']`` which makes the
image a bit darker and larger then it is by default, and produces PNGs with a
- transparent background.
+ transparent background. This option is used only when
+ ``imgmath_image_format`` is ``'png'``.
- .. versionchanged:: 1.2
- Now includes ``-bg Transparent`` by default.
+.. confval:: imgmath_dvisvgm_args
-.. confval:: pngmath_use_preview
+ Additional arguments to give to dvisvgm, as a list. The default value is
+ ``['--no-fonts']``. This option is used only when ``imgmath_image_format``
+ is ``'svg'``.
+
+.. confval:: imgmath_use_preview
``dvipng`` has the ability to determine the "depth" of the rendered text: for
example, when typesetting a fraction inline, the baseline of surrounding text
@@ -165,14 +181,20 @@ built:
``vertical-align`` style that correctly aligns the baselines.
Unfortunately, this only works when the `preview-latex package`_ is
- installed. Therefore, the default for this option is ``False``.
+ installed. Therefore, the default for this option is ``False``.
-.. confval:: pngmath_add_tooltips
+ Currently this option is only used when ``imgmath_image_format`` is
+ ``'png'``.
+
+.. confval:: imgmath_add_tooltips
Default: ``True``. If false, do not add the LaTeX code as an "alt" attribute
for math images.
- .. versionadded:: 1.1
+.. confval:: imgmath_font_size
+
+ The font size (in ``pt``) of the displayed math. The default value is
+ ``12``. It must be a positive integer.
:mod:`sphinx.ext.mathjax` -- Render math via JavaScript
@@ -235,6 +257,7 @@ package jsMath_. It provides this config value:
.. _dvipng: http://savannah.nongnu.org/projects/dvipng/
+.. _dvisvgm: http://dvisvgm.bplaced.net/
.. _MathJax: http://www.mathjax.org/
.. _jsMath: http://www.math.union.edu/~dpvc/jsmath/
.. _preview-latex package: http://www.gnu.org/software/auctex/preview-latex.html
diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py
new file mode 100644
index 0000000000..c4b39f4c8e
--- /dev/null
+++ b/sphinx/ext/imgmath.py
@@ -0,0 +1,284 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.ext.imgmath
+ ~~~~~~~~~~~~~~~~~~
+
+ Render math in HTML via dvipng or dvisvgm.
+
+ :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+import codecs
+import shutil
+import tempfile
+import posixpath
+from os import path
+from subprocess import Popen, PIPE
+from hashlib import sha1
+
+from six import text_type
+from docutils import nodes
+
+import sphinx
+from sphinx.errors import SphinxError
+from sphinx.util.png import read_png_depth, write_png_depth
+from sphinx.util.osutil import ensuredir, ENOENT, cd
+from sphinx.util.pycompat import sys_encoding
+from sphinx.ext.mathbase import setup_math as mathbase_setup, wrap_displaymath
+
+
+class MathExtError(SphinxError):
+ category = 'Math extension error'
+
+ def __init__(self, msg, stderr=None, stdout=None):
+ if stderr:
+ msg += '\n[stderr]\n' + stderr.decode(sys_encoding, 'replace')
+ if stdout:
+ msg += '\n[stdout]\n' + stdout.decode(sys_encoding, 'replace')
+ SphinxError.__init__(self, msg)
+
+
+DOC_HEAD = r'''
+\documentclass[12pt]{article}
+\usepackage[utf8x]{inputenc}
+\usepackage{amsmath}
+\usepackage{amsthm}
+\usepackage{amssymb}
+\usepackage{amsfonts}
+\usepackage{anyfontsize}
+\usepackage{bm}
+\pagestyle{empty}
+'''
+
+DOC_BODY = r'''
+\begin{document}
+\fontsize{%d}{%d}\selectfont %s
+\end{document}
+'''
+
+DOC_BODY_PREVIEW = r'''
+\usepackage[active]{preview}
+\begin{document}
+\begin{preview}
+\fontsize{%s}{%s}\selectfont %s
+\end{preview}
+\end{document}
+'''
+
+depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]')
+
+
+def render_math(self, math):
+ """Render the LaTeX math expression *math* using latex and dvipng or
+ dvisvgm.
+
+ Return the filename relative to the built document and the "depth",
+ that is, the distance of image bottom and baseline in pixels, if the
+ option to use preview_latex is switched on.
+
+ Error handling may seem strange, but follows a pattern: if LaTeX or dvipng
+ (dvisvgm) aren't available, only a warning is generated (since that enables
+ people on machines without these programs to at least build the rest of the
+ docs successfully). If the programs are there, however, they may not fail
+ since that indicates a problem in the math source.
+ """
+ image_format = self.builder.config.imgmath_image_format
+ if image_format not in ('png', 'svg'):
+ raise MathExtError(
+ 'imgmath_image_format must be either "png" or "svg"')
+
+ font_size = self.builder.config.imgmath_font_size
+ use_preview = self.builder.config.imgmath_use_preview
+ latex = DOC_HEAD + self.builder.config.imgmath_latex_preamble
+ latex += (use_preview and DOC_BODY_PREVIEW or DOC_BODY) % (
+ font_size, int(round(font_size * 1.2)), math)
+
+ shasum = "%s.%s" % (sha1(latex.encode('utf-8')).hexdigest(), image_format)
+ relfn = posixpath.join(self.builder.imgpath, 'math', shasum)
+ outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', shasum)
+ if path.isfile(outfn):
+ depth = read_png_depth(outfn)
+ return relfn, depth
+
+ # if latex or dvipng (dvisvgm) has failed once, don't bother to try again
+ if hasattr(self.builder, '_imgmath_warned_latex') or \
+ hasattr(self.builder, '_imgmath_warned_image_translator'):
+ return None, None
+
+ # use only one tempdir per build -- the use of a directory is cleaner
+ # than using temporary files, since we can clean up everything at once
+ # just removing the whole directory (see cleanup_tempdir)
+ if not hasattr(self.builder, '_imgmath_tempdir'):
+ tempdir = self.builder._imgmath_tempdir = tempfile.mkdtemp()
+ else:
+ tempdir = self.builder._imgmath_tempdir
+
+ tf = codecs.open(path.join(tempdir, 'math.tex'), 'w', 'utf-8')
+ tf.write(latex)
+ tf.close()
+
+ # build latex command; old versions of latex don't have the
+ # --output-directory option, so we have to manually chdir to the
+ # temp dir to run it.
+ ltx_args = [self.builder.config.imgmath_latex, '--interaction=nonstopmode']
+ # add custom args from the config file
+ ltx_args.extend(self.builder.config.imgmath_latex_args)
+ ltx_args.append('math.tex')
+
+ with cd(tempdir):
+ try:
+ p = Popen(ltx_args, stdout=PIPE, stderr=PIPE)
+ except OSError as err:
+ if err.errno != ENOENT: # No such file or directory
+ raise
+ self.builder.warn('LaTeX command %r cannot be run (needed for math '
+ 'display), check the imgmath_latex setting' %
+ self.builder.config.imgmath_latex)
+ self.builder._imgmath_warned_latex = True
+ return None, None
+
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ raise MathExtError('latex exited with error', stderr, stdout)
+
+ ensuredir(path.dirname(outfn))
+ if image_format == 'png':
+ image_translator = 'dvipng'
+ image_translator_executable = self.builder.config.imgmath_dvipng
+ # use some standard dvipng arguments
+ image_translator_args = [self.builder.config.imgmath_dvipng]
+ image_translator_args += ['-o', outfn, '-T', 'tight', '-z9']
+ # add custom ones from config value
+ image_translator_args.extend(self.builder.config.imgmath_dvipng_args)
+ if use_preview:
+ image_translator_args.append('--depth')
+ elif image_format == 'svg':
+ image_translator = 'dvisvgm'
+ image_translator_executable = self.builder.config.imgmath_dvisvgm
+ # use some standard dvisvgm arguments
+ image_translator_args = [self.builder.config.imgmath_dvisvgm]
+ image_translator_args += ['-o', outfn]
+ # add custom ones from config value
+ image_translator_args.extend(self.builder.config.imgmath_dvisvgm_args)
+ # last, the input file name
+ image_translator_args.append(path.join(tempdir, 'math.dvi'))
+ else:
+ raise MathExtError(
+ 'imgmath_image_format must be either "png" or "svg"')
+
+ # last, the input file name
+ image_translator_args.append(path.join(tempdir, 'math.dvi'))
+
+ try:
+ p = Popen(image_translator_args, stdout=PIPE, stderr=PIPE)
+ except OSError as err:
+ if err.errno != ENOENT: # No such file or directory
+ raise
+ self.builder.warn('%s command %r cannot be run (needed for math '
+ 'display), check the imgmath_%s setting' %
+ image_translator, image_translator_executable,
+ image_translator)
+ self.builder._imgmath_warned_image_translator = True
+ return None, None
+
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ raise MathExtError('%s exited with error',
+ image_translator, stderr, stdout)
+ depth = None
+ if use_preview and image_format == 'png': # depth is only useful for png
+ for line in stdout.splitlines():
+ m = depth_re.match(line)
+ if m:
+ depth = int(m.group(1))
+ write_png_depth(outfn, depth)
+ break
+
+ return relfn, depth
+
+
+def cleanup_tempdir(app, exc):
+ if exc:
+ return
+ if not hasattr(app.builder, '_imgmath_tempdir'):
+ return
+ try:
+ shutil.rmtree(app.builder._mathpng_tempdir)
+ except Exception:
+ pass
+
+
+def get_tooltip(self, node):
+ if self.builder.config.imgmath_add_tooltips:
+ return ' alt="%s"' % self.encode(node['latex']).strip()
+ return ''
+
+
+def html_visit_math(self, node):
+ try:
+ fname, depth = render_math(self, '$'+node['latex']+'$')
+ except MathExtError as exc:
+ msg = text_type(exc)
+ sm = nodes.system_message(msg, type='WARNING', level=2,
+ backrefs=[], source=node['latex'])
+ sm.walkabout(self)
+ self.builder.warn('display latex %r: ' % node['latex'] + msg)
+ raise nodes.SkipNode
+ if fname is None:
+ # something failed -- use text-only as a bad substitute
+ self.body.append('%s' %
+ self.encode(node['latex']).strip())
+ else:
+ c = ('')
+ raise nodes.SkipNode
+
+
+def html_visit_displaymath(self, node):
+ if node['nowrap']:
+ latex = node['latex']
+ else:
+ latex = wrap_displaymath(node['latex'], None)
+ try:
+ fname, depth = render_math(self, latex)
+ except MathExtError as exc:
+ sm = nodes.system_message(str(exc), type='WARNING', level=2,
+ backrefs=[], source=node['latex'])
+ sm.walkabout(self)
+ self.builder.warn('inline latex %r: ' % node['latex'] + str(exc))
+ raise nodes.SkipNode
+ self.body.append(self.starttag(node, 'div', CLASS='math'))
+ self.body.append('
') + if node['number']: + self.body.append('(%s)' % node['number']) + if fname is None: + # something failed -- use text-only as a bad substitute + self.body.append('%s
\n' % + self.encode(node['latex']).strip()) + else: + self.body.append(('\s*
\s*
\s*\s*