mathmpl.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import hashlib
  2. from pathlib import Path
  3. from docutils import nodes
  4. from docutils.parsers.rst import Directive, directives
  5. import sphinx
  6. import matplotlib as mpl
  7. from matplotlib import cbook
  8. from matplotlib.mathtext import MathTextParser
  9. mathtext_parser = MathTextParser("Bitmap")
  10. # Define LaTeX math node:
  11. class latex_math(nodes.General, nodes.Element):
  12. pass
  13. def fontset_choice(arg):
  14. return directives.choice(arg, MathTextParser._font_type_mapping)
  15. def math_role(role, rawtext, text, lineno, inliner,
  16. options={}, content=[]):
  17. i = rawtext.find('`')
  18. latex = rawtext[i+1:-1]
  19. node = latex_math(rawtext)
  20. node['latex'] = latex
  21. node['fontset'] = options.get('fontset', 'cm')
  22. return [node], []
  23. math_role.options = {'fontset': fontset_choice}
  24. class MathDirective(Directive):
  25. has_content = True
  26. required_arguments = 0
  27. optional_arguments = 0
  28. final_argument_whitespace = False
  29. option_spec = {'fontset': fontset_choice}
  30. def run(self):
  31. latex = ''.join(self.content)
  32. node = latex_math(self.block_text)
  33. node['latex'] = latex
  34. node['fontset'] = self.options.get('fontset', 'cm')
  35. return [node]
  36. # This uses mathtext to render the expression
  37. def latex2png(latex, filename, fontset='cm'):
  38. latex = "$%s$" % latex
  39. with mpl.rc_context({'mathtext.fontset': fontset}):
  40. if Path(filename).exists():
  41. depth = mathtext_parser.get_depth(latex, dpi=100)
  42. else:
  43. try:
  44. depth = mathtext_parser.to_png(filename, latex, dpi=100)
  45. except Exception:
  46. cbook._warn_external(
  47. f"Could not render math expression {latex}")
  48. depth = 0
  49. return depth
  50. # LaTeX to HTML translation stuff:
  51. def latex2html(node, source):
  52. inline = isinstance(node.parent, nodes.TextElement)
  53. latex = node['latex']
  54. fontset = node['fontset']
  55. name = 'math-{}'.format(
  56. hashlib.md5((latex + fontset).encode()).hexdigest()[-10:])
  57. destdir = Path(setup.app.builder.outdir, '_images', 'mathmpl')
  58. destdir.mkdir(parents=True, exist_ok=True)
  59. dest = destdir / f'{name}.png'
  60. depth = latex2png(latex, dest, fontset)
  61. if inline:
  62. cls = ''
  63. else:
  64. cls = 'class="center" '
  65. if inline and depth != 0:
  66. style = 'style="position: relative; bottom: -%dpx"' % (depth + 1)
  67. else:
  68. style = ''
  69. return (f'<img src="{setup.app.builder.imgpath}/mathmpl/{name}.png"'
  70. f' {cls}{style}/>')
  71. def setup(app):
  72. setup.app = app
  73. # Add visit/depart methods to HTML-Translator:
  74. def visit_latex_math_html(self, node):
  75. source = self.document.attributes['source']
  76. self.body.append(latex2html(node, source))
  77. def depart_latex_math_html(self, node):
  78. pass
  79. # Add visit/depart methods to LaTeX-Translator:
  80. def visit_latex_math_latex(self, node):
  81. inline = isinstance(node.parent, nodes.TextElement)
  82. if inline:
  83. self.body.append('$%s$' % node['latex'])
  84. else:
  85. self.body.extend(['\\begin{equation}',
  86. node['latex'],
  87. '\\end{equation}'])
  88. def depart_latex_math_latex(self, node):
  89. pass
  90. app.add_node(latex_math,
  91. html=(visit_latex_math_html, depart_latex_math_html),
  92. latex=(visit_latex_math_latex, depart_latex_math_latex))
  93. app.add_role('mathmpl', math_role)
  94. app.add_directive('mathmpl', MathDirective)
  95. if sphinx.version_info < (1, 8):
  96. app.add_role('math', math_role)
  97. app.add_directive('math', MathDirective)
  98. metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
  99. return metadata