mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
imgmath: Allow embedding images in HTML as base64 (#10816)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
1
CHANGES
1
CHANGES
@@ -29,6 +29,7 @@ Features added
|
|||||||
* #6316, #10804: Add domain objects to the table of contents. Patch by Adam Turner
|
* #6316, #10804: Add domain objects to the table of contents. Patch by Adam Turner
|
||||||
* #6692: HTML Search: Include explicit :rst:dir:`index` directive index entries
|
* #6692: HTML Search: Include explicit :rst:dir:`index` directive index entries
|
||||||
in the search index and search results. Patch by Adam Turner
|
in the search index and search results. Patch by Adam Turner
|
||||||
|
* #10816: imgmath: Allow embedding images in HTML as base64
|
||||||
|
|
||||||
Bugs fixed
|
Bugs fixed
|
||||||
----------
|
----------
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ are built:
|
|||||||
elements (cf the `dvisvgm FAQ`_). This option is used only when
|
elements (cf the `dvisvgm FAQ`_). This option is used only when
|
||||||
``imgmath_image_format`` is ``'svg'``.
|
``imgmath_image_format`` is ``'svg'``.
|
||||||
|
|
||||||
|
.. confval:: imgmath_embed
|
||||||
|
|
||||||
|
Default: ``False``. If true, encode LaTeX output images within HTML files
|
||||||
|
(base64 encoded) and do not save separate png/svg files to disk.
|
||||||
|
|
||||||
|
.. versionadded:: 5.2
|
||||||
|
|
||||||
:mod:`sphinx.ext.mathjax` -- Render math via JavaScript
|
:mod:`sphinx.ext.mathjax` -- Render math via JavaScript
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Render math in HTML via dvipng or dvisvgm."""
|
"""Render math in HTML via dvipng or dvisvgm."""
|
||||||
|
|
||||||
|
import base64
|
||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -30,6 +31,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
templates_path = path.join(package_dir, 'templates', 'imgmath')
|
templates_path = path.join(package_dir, 'templates', 'imgmath')
|
||||||
|
|
||||||
|
__all__ = ()
|
||||||
|
|
||||||
|
|
||||||
class MathExtError(SphinxError):
|
class MathExtError(SphinxError):
|
||||||
category = 'Math extension error'
|
category = 'Math extension error'
|
||||||
@@ -204,13 +207,17 @@ def convert_dvi_to_svg(dvipath: str, builder: Builder) -> Tuple[str, Optional[in
|
|||||||
return filename, depth
|
return filename, depth
|
||||||
|
|
||||||
|
|
||||||
def render_math(self: HTMLTranslator, math: str) -> Tuple[Optional[str], Optional[int]]:
|
def render_math(
|
||||||
|
self: HTMLTranslator,
|
||||||
|
math: str,
|
||||||
|
) -> Tuple[Optional[str], Optional[int], Optional[str], Optional[str]]:
|
||||||
"""Render the LaTeX math expression *math* using latex and dvipng or
|
"""Render the LaTeX math expression *math* using latex and dvipng or
|
||||||
dvisvgm.
|
dvisvgm.
|
||||||
|
|
||||||
Return the filename relative to the built document and the "depth",
|
Return the filename relative to the built document and the "depth",
|
||||||
that is, the distance of image bottom and baseline in pixels, if the
|
that is, the distance of image bottom and baseline in pixels, if the
|
||||||
option to use preview_latex is switched on.
|
option to use preview_latex is switched on.
|
||||||
|
Also return the temporary and destination files.
|
||||||
|
|
||||||
Error handling may seem strange, but follows a pattern: if LaTeX or dvipng
|
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
|
(dvisvgm) aren't available, only a warning is generated (since that enables
|
||||||
@@ -235,19 +242,19 @@ def render_math(self: HTMLTranslator, math: str) -> Tuple[Optional[str], Optiona
|
|||||||
depth = read_png_depth(outfn)
|
depth = read_png_depth(outfn)
|
||||||
elif image_format == 'svg':
|
elif image_format == 'svg':
|
||||||
depth = read_svg_depth(outfn)
|
depth = read_svg_depth(outfn)
|
||||||
return relfn, depth
|
return relfn, depth, None, outfn
|
||||||
|
|
||||||
# if latex or dvipng (dvisvgm) has failed once, don't bother to try again
|
# if latex or dvipng (dvisvgm) has failed once, don't bother to try again
|
||||||
if hasattr(self.builder, '_imgmath_warned_latex') or \
|
if hasattr(self.builder, '_imgmath_warned_latex') or \
|
||||||
hasattr(self.builder, '_imgmath_warned_image_translator'):
|
hasattr(self.builder, '_imgmath_warned_image_translator'):
|
||||||
return None, None
|
return None, None, None, None
|
||||||
|
|
||||||
# .tex -> .dvi
|
# .tex -> .dvi
|
||||||
try:
|
try:
|
||||||
dvipath = compile_math(latex, self.builder)
|
dvipath = compile_math(latex, self.builder)
|
||||||
except InvokeError:
|
except InvokeError:
|
||||||
self.builder._imgmath_warned_latex = True # type: ignore
|
self.builder._imgmath_warned_latex = True # type: ignore
|
||||||
return None, None
|
return None, None, None, None
|
||||||
|
|
||||||
# .dvi -> .png/.svg
|
# .dvi -> .png/.svg
|
||||||
try:
|
try:
|
||||||
@@ -257,13 +264,19 @@ def render_math(self: HTMLTranslator, math: str) -> Tuple[Optional[str], Optiona
|
|||||||
imgpath, depth = convert_dvi_to_svg(dvipath, self.builder)
|
imgpath, depth = convert_dvi_to_svg(dvipath, self.builder)
|
||||||
except InvokeError:
|
except InvokeError:
|
||||||
self.builder._imgmath_warned_image_translator = True # type: ignore
|
self.builder._imgmath_warned_image_translator = True # type: ignore
|
||||||
return None, None
|
return None, None, None, None
|
||||||
|
|
||||||
# Move generated image on tempdir to build dir
|
return relfn, depth, imgpath, outfn
|
||||||
ensuredir(path.dirname(outfn))
|
|
||||||
shutil.move(imgpath, outfn)
|
|
||||||
|
|
||||||
return relfn, depth
|
|
||||||
|
def render_maths_to_base64(image_format: str, outfn: Optional[str]) -> str:
|
||||||
|
with open(outfn, "rb") as f:
|
||||||
|
encoded = base64.b64encode(f.read()).decode(encoding='utf-8')
|
||||||
|
if image_format == 'png':
|
||||||
|
return f'data:image/png;base64,{encoded}'
|
||||||
|
if image_format == 'svg':
|
||||||
|
return f'data:image/svg+xml;base64,{encoded}'
|
||||||
|
raise MathExtError('imgmath_image_format must be either "png" or "svg"')
|
||||||
|
|
||||||
|
|
||||||
def cleanup_tempdir(app: Sphinx, exc: Exception) -> None:
|
def cleanup_tempdir(app: Sphinx, exc: Exception) -> None:
|
||||||
@@ -285,7 +298,7 @@ def get_tooltip(self: HTMLTranslator, node: Element) -> str:
|
|||||||
|
|
||||||
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
|
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
|
||||||
try:
|
try:
|
||||||
fname, depth = render_math(self, '$' + node.astext() + '$')
|
fname, depth, imgpath, outfn = render_math(self, '$' + node.astext() + '$')
|
||||||
except MathExtError as exc:
|
except MathExtError as exc:
|
||||||
msg = str(exc)
|
msg = str(exc)
|
||||||
sm = nodes.system_message(msg, type='WARNING', level=2,
|
sm = nodes.system_message(msg, type='WARNING', level=2,
|
||||||
@@ -293,14 +306,23 @@ def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
|
|||||||
sm.walkabout(self)
|
sm.walkabout(self)
|
||||||
logger.warning(__('display latex %r: %s'), node.astext(), msg)
|
logger.warning(__('display latex %r: %s'), node.astext(), msg)
|
||||||
raise nodes.SkipNode from exc
|
raise nodes.SkipNode from exc
|
||||||
if fname is None:
|
if self.builder.config.imgmath_embed:
|
||||||
|
image_format = self.builder.config.imgmath_image_format.lower()
|
||||||
|
img_src = render_maths_to_base64(image_format, outfn)
|
||||||
|
else:
|
||||||
|
# Move generated image on tempdir to build dir
|
||||||
|
if imgpath is not None:
|
||||||
|
ensuredir(path.dirname(outfn))
|
||||||
|
shutil.move(imgpath, outfn)
|
||||||
|
img_src = fname
|
||||||
|
if img_src is None:
|
||||||
# something failed -- use text-only as a bad substitute
|
# something failed -- use text-only as a bad substitute
|
||||||
self.body.append('<span class="math">%s</span>' %
|
self.body.append('<span class="math">%s</span>' %
|
||||||
self.encode(node.astext()).strip())
|
self.encode(node.astext()).strip())
|
||||||
else:
|
else:
|
||||||
c = ('<img class="math" src="%s"' % fname) + get_tooltip(self, node)
|
c = f'<img class="math" src="{img_src}"' + get_tooltip(self, node)
|
||||||
if depth is not None:
|
if depth is not None:
|
||||||
c += ' style="vertical-align: %dpx"' % (-depth)
|
c += f' style="vertical-align: {-depth:d}px"'
|
||||||
self.body.append(c + '/>')
|
self.body.append(c + '/>')
|
||||||
raise nodes.SkipNode
|
raise nodes.SkipNode
|
||||||
|
|
||||||
@@ -311,7 +333,7 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
|
|||||||
else:
|
else:
|
||||||
latex = wrap_displaymath(node.astext(), None, False)
|
latex = wrap_displaymath(node.astext(), None, False)
|
||||||
try:
|
try:
|
||||||
fname, depth = render_math(self, latex)
|
fname, depth, imgpath, outfn = render_math(self, latex)
|
||||||
except MathExtError as exc:
|
except MathExtError as exc:
|
||||||
msg = str(exc)
|
msg = str(exc)
|
||||||
sm = nodes.system_message(msg, type='WARNING', level=2,
|
sm = nodes.system_message(msg, type='WARNING', level=2,
|
||||||
@@ -326,12 +348,21 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
|
|||||||
self.body.append('<span class="eqno">(%s)' % number)
|
self.body.append('<span class="eqno">(%s)' % number)
|
||||||
self.add_permalink_ref(node, _('Permalink to this equation'))
|
self.add_permalink_ref(node, _('Permalink to this equation'))
|
||||||
self.body.append('</span>')
|
self.body.append('</span>')
|
||||||
if fname is None:
|
if self.builder.config.imgmath_embed:
|
||||||
|
image_format = self.builder.config.imgmath_image_format.lower()
|
||||||
|
img_src = render_maths_to_base64(image_format, outfn)
|
||||||
|
else:
|
||||||
|
# Move generated image on tempdir to build dir
|
||||||
|
if imgpath is not None:
|
||||||
|
ensuredir(path.dirname(outfn))
|
||||||
|
shutil.move(imgpath, outfn)
|
||||||
|
img_src = fname
|
||||||
|
if img_src is None:
|
||||||
# something failed -- use text-only as a bad substitute
|
# something failed -- use text-only as a bad substitute
|
||||||
self.body.append('<span class="math">%s</span></p>\n</div>' %
|
self.body.append('<span class="math">%s</span></p>\n</div>' %
|
||||||
self.encode(node.astext()).strip())
|
self.encode(node.astext()).strip())
|
||||||
else:
|
else:
|
||||||
self.body.append(('<img src="%s"' % fname) + get_tooltip(self, node) +
|
self.body.append(f'<img src="{img_src}"' + get_tooltip(self, node) +
|
||||||
'/></p>\n</div>')
|
'/></p>\n</div>')
|
||||||
raise nodes.SkipNode
|
raise nodes.SkipNode
|
||||||
|
|
||||||
@@ -354,5 +385,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
|||||||
app.add_config_value('imgmath_latex_preamble', '', 'html')
|
app.add_config_value('imgmath_latex_preamble', '', 'html')
|
||||||
app.add_config_value('imgmath_add_tooltips', True, 'html')
|
app.add_config_value('imgmath_add_tooltips', True, 'html')
|
||||||
app.add_config_value('imgmath_font_size', 12, 'html')
|
app.add_config_value('imgmath_font_size', 12, 'html')
|
||||||
|
app.add_config_value('imgmath_embed', False, 'html', [bool])
|
||||||
app.connect('build-finished', cleanup_tempdir)
|
app.connect('build-finished', cleanup_tempdir)
|
||||||
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
|
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
|
||||||
|
|||||||
@@ -56,6 +56,24 @@ def test_imgmath_svg(app, status, warning):
|
|||||||
assert re.search(html, content, re.S)
|
assert re.search(html, content, re.S)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not has_binary('dvisvgm'),
|
||||||
|
reason='Requires dvisvgm" binary')
|
||||||
|
@pytest.mark.sphinx('html', testroot='ext-math-simple',
|
||||||
|
confoverrides={'extensions': ['sphinx.ext.imgmath'],
|
||||||
|
'imgmath_image_format': 'svg',
|
||||||
|
'imgmath_embed': True})
|
||||||
|
def test_imgmath_svg_embed(app, status, warning):
|
||||||
|
app.builder.build_all()
|
||||||
|
if "LaTeX command 'latex' cannot be run" in warning.getvalue():
|
||||||
|
pytest.skip('LaTeX command "latex" is not available')
|
||||||
|
if "dvisvgm command 'dvisvgm' cannot be run" in warning.getvalue():
|
||||||
|
pytest.skip('dvisvgm command "dvisvgm" is not available')
|
||||||
|
|
||||||
|
content = (app.outdir / 'index.html').read_text(encoding='utf8')
|
||||||
|
html = r'<img src="data:image/svg\+xml;base64,[\w\+/=]+"'
|
||||||
|
assert re.search(html, content, re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.sphinx('html', testroot='ext-math',
|
@pytest.mark.sphinx('html', testroot='ext-math',
|
||||||
confoverrides={'extensions': ['sphinx.ext.mathjax'],
|
confoverrides={'extensions': ['sphinx.ext.mathjax'],
|
||||||
'mathjax_options': {'integrity': 'sha384-0123456789'}})
|
'mathjax_options': {'integrity': 'sha384-0123456789'}})
|
||||||
|
|||||||
Reference in New Issue
Block a user