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(('

\n') + raise nodes.SkipNode + + +def setup(app): + mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) + app.add_config_value('imgmath_image_format', 'png', 'html') + app.add_config_value('imgmath_dvipng', 'dvipng', 'html') + app.add_config_value('imgmath_dvisvgm', 'dvisvgm', 'html') + app.add_config_value('imgmath_latex', 'latex', 'html') + app.add_config_value('imgmath_use_preview', False, 'html') + app.add_config_value('imgmath_dvipng_args', + ['-gamma', '1.5', '-D', '110', '-bg', 'Transparent'], + 'html') + app.add_config_value('imgmath_dvisvgm_args', ['--no-fonts'], 'html') + app.add_config_value('imgmath_latex_args', [], 'html') + app.add_config_value('imgmath_latex_preamble', '', 'html') + app.add_config_value('imgmath_add_tooltips', True, 'html') + app.add_config_value('imgmath_font_size', 12, 'html') + app.connect('build-finished', cleanup_tempdir) + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index 8ef860f802..8f9014acd1 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -3,7 +3,8 @@ sphinx.ext.pngmath ~~~~~~~~~~~~~~~~~~ - Render math in HTML via dvipng. + Render math in HTML via dvipng. This extension has been deprecated; please + use sphinx.ext.imgmath instead. :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. @@ -236,6 +237,7 @@ def html_visit_displaymath(self, node): def setup(app): + app.warn('sphinx.ext.pngmath has been deprecated. Please use sphinx.ext.imgmath instead.') mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) app.add_config_value('pngmath_dvipng', 'dvipng', 'html') app.add_config_value('pngmath_latex', 'latex', 'html') diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index ffc4961dcd..f4514a2b20 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -60,7 +60,7 @@ DEFAULT_VALUE = { } EXTENSIONS = ('autodoc', 'doctest', 'intersphinx', 'todo', 'coverage', - 'pngmath', 'mathjax', 'ifconfig', 'viewcode') + 'imgmath', 'mathjax', 'ifconfig', 'viewcode') PROMPT_PREFIX = '> ' @@ -1255,16 +1255,16 @@ Please indicate if you want to use one of the following Sphinx extensions:''') if 'ext_coverage' not in d: do_prompt(d, 'ext_coverage', 'coverage: checks for documentation ' 'coverage (y/n)', 'n', boolean) - if 'ext_pngmath' not in d: - do_prompt(d, 'ext_pngmath', 'pngmath: include math, rendered ' - 'as PNG images (y/n)', 'n', boolean) + if 'ext_imgmath' not in d: + do_prompt(d, 'ext_imgmath', 'imgmath: include math, rendered ' + 'as PNG or SVG images (y/n)', 'n', boolean) if 'ext_mathjax' not in d: do_prompt(d, 'ext_mathjax', 'mathjax: include math, rendered in the ' 'browser by MathJax (y/n)', 'n', boolean) - if d['ext_pngmath'] and d['ext_mathjax']: - print('''Note: pngmath and mathjax cannot be enabled at the same time. -pngmath has been deselected.''') - d['ext_pngmath'] = False + if d['ext_imgmath'] and d['ext_mathjax']: + print('''Note: imgmath and mathjax cannot be enabled at the same time. +imgmath has been deselected.''') + d['ext_imgmath'] = False if 'ext_ifconfig' not in d: do_prompt(d, 'ext_ifconfig', 'ifconfig: conditional inclusion of ' 'content based on config values (y/n)', 'n', boolean) diff --git a/tests/roots/test-ext-imgmath/conf.py b/tests/roots/test-ext-imgmath/conf.py new file mode 100644 index 0000000000..3f3e2a7834 --- /dev/null +++ b/tests/roots/test-ext-imgmath/conf.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +extensions = ['sphinx.ext.imgmath'] +master_doc = 'index' diff --git a/tests/roots/test-ext-imgmath/index.rst b/tests/roots/test-ext-imgmath/index.rst new file mode 100644 index 0000000000..f7780a4228 --- /dev/null +++ b/tests/roots/test-ext-imgmath/index.rst @@ -0,0 +1,6 @@ +Test imgmath +============ + +.. math:: a^2+b^2=c^2 + +Inline :math:`E=mc^2` diff --git a/tests/test_ext_imgmath.py b/tests/test_ext_imgmath.py new file mode 100644 index 0000000000..bf0f016db8 --- /dev/null +++ b/tests/test_ext_imgmath.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" + test_ext_imgmath + ~~~~~~~~~~~~~~~~ + + Test sphinx.ext.imgmath extension. + + :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re + +from util import with_app, SkipTest + + +@with_app('html', testroot='ext-imgmath') +def test_imgmath_png(app, status, warning): + app.builder.build_all() + if "LaTeX command 'latex' cannot be run" in warning.getvalue(): + raise SkipTest('LaTeX command "latex" is not available') + if "dvipng command 'dvipng' cannot be run" in warning.getvalue(): + raise SkipTest('dvipng command "dvipng" is not available') + + content = (app.outdir / 'index.html').text() + html = ('
\s*

\s*\s*

\s*
') + assert re.search(html, content, re.S) + +@with_app('html', testroot='ext-imgmath', + confoverrides={'imgmath_image_format': 'svg'}) +def test_imgmath_svg(app, status, warning): + app.builder.build_all() + if "LaTeX command 'latex' cannot be run" in warning.getvalue(): + raise SkipTest('LaTeX command "latex" is not available') + if "dvisvgm command 'dvisvgm' cannot be run" in warning.getvalue(): + raise SkipTest('dvisvgm command "dvisvgm" is not available') + + content = (app.outdir / 'index.html').text() + html = ('
\s*

\s*\s*

\s*
') + assert re.search(html, content, re.S) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 0137004177..45ff8671d1 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -181,7 +181,7 @@ def test_quickstart_all_answers(tempdir): 'intersphinx': 'no', 'todo': 'y', 'coverage': 'no', - 'pngmath': 'N', + 'imgmath': 'N', 'mathjax': 'no', 'ifconfig': 'no', 'viewcode': 'no',