latextools.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. """Tools for handling LaTeX."""
  3. # Copyright (c) IPython Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from io import BytesIO, open
  6. import os
  7. import tempfile
  8. import shutil
  9. import subprocess
  10. from IPython.utils.process import find_cmd, FindCmdError
  11. from traitlets.config import get_config
  12. from traitlets.config.configurable import SingletonConfigurable
  13. from traitlets import List, Bool, Unicode
  14. from IPython.utils.py3compat import cast_unicode, cast_unicode_py2 as u, PY3
  15. try: # Py3
  16. from base64 import encodebytes
  17. except ImportError: # Py2
  18. from base64 import encodestring as encodebytes
  19. class LaTeXTool(SingletonConfigurable):
  20. """An object to store configuration of the LaTeX tool."""
  21. def _config_default(self):
  22. return get_config()
  23. backends = List(
  24. Unicode(), ["matplotlib", "dvipng"],
  25. help="Preferred backend to draw LaTeX math equations. "
  26. "Backends in the list are checked one by one and the first "
  27. "usable one is used. Note that `matplotlib` backend "
  28. "is usable only for inline style equations. To draw "
  29. "display style equations, `dvipng` backend must be specified. ",
  30. # It is a List instead of Enum, to make configuration more
  31. # flexible. For example, to use matplotlib mainly but dvipng
  32. # for display style, the default ["matplotlib", "dvipng"] can
  33. # be used. To NOT use dvipng so that other repr such as
  34. # unicode pretty printing is used, you can use ["matplotlib"].
  35. ).tag(config=True)
  36. use_breqn = Bool(
  37. True,
  38. help="Use breqn.sty to automatically break long equations. "
  39. "This configuration takes effect only for dvipng backend.",
  40. ).tag(config=True)
  41. packages = List(
  42. ['amsmath', 'amsthm', 'amssymb', 'bm'],
  43. help="A list of packages to use for dvipng backend. "
  44. "'breqn' will be automatically appended when use_breqn=True.",
  45. ).tag(config=True)
  46. preamble = Unicode(
  47. help="Additional preamble to use when generating LaTeX source "
  48. "for dvipng backend.",
  49. ).tag(config=True)
  50. def latex_to_png(s, encode=False, backend=None, wrap=False):
  51. """Render a LaTeX string to PNG.
  52. Parameters
  53. ----------
  54. s : str
  55. The raw string containing valid inline LaTeX.
  56. encode : bool, optional
  57. Should the PNG data base64 encoded to make it JSON'able.
  58. backend : {matplotlib, dvipng}
  59. Backend for producing PNG data.
  60. wrap : bool
  61. If true, Automatically wrap `s` as a LaTeX equation.
  62. None is returned when the backend cannot be used.
  63. """
  64. s = cast_unicode(s)
  65. allowed_backends = LaTeXTool.instance().backends
  66. if backend is None:
  67. backend = allowed_backends[0]
  68. if backend not in allowed_backends:
  69. return None
  70. if backend == 'matplotlib':
  71. f = latex_to_png_mpl
  72. elif backend == 'dvipng':
  73. f = latex_to_png_dvipng
  74. else:
  75. raise ValueError('No such backend {0}'.format(backend))
  76. bin_data = f(s, wrap)
  77. if encode and bin_data:
  78. bin_data = encodebytes(bin_data)
  79. return bin_data
  80. def latex_to_png_mpl(s, wrap):
  81. try:
  82. from matplotlib import mathtext
  83. from pyparsing import ParseFatalException
  84. except ImportError:
  85. return None
  86. # mpl mathtext doesn't support display math, force inline
  87. s = s.replace('$$', '$')
  88. if wrap:
  89. s = u'${0}$'.format(s)
  90. try:
  91. mt = mathtext.MathTextParser('bitmap')
  92. f = BytesIO()
  93. mt.to_png(f, s, fontsize=12)
  94. return f.getvalue()
  95. except (ValueError, RuntimeError, ParseFatalException):
  96. return None
  97. def latex_to_png_dvipng(s, wrap):
  98. try:
  99. find_cmd('latex')
  100. find_cmd('dvipng')
  101. except FindCmdError:
  102. return None
  103. try:
  104. workdir = tempfile.mkdtemp()
  105. tmpfile = os.path.join(workdir, "tmp.tex")
  106. dvifile = os.path.join(workdir, "tmp.dvi")
  107. outfile = os.path.join(workdir, "tmp.png")
  108. with open(tmpfile, "w", encoding='utf8') as f:
  109. f.writelines(genelatex(s, wrap))
  110. with open(os.devnull, 'wb') as devnull:
  111. subprocess.check_call(
  112. ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
  113. cwd=workdir, stdout=devnull, stderr=devnull)
  114. subprocess.check_call(
  115. ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
  116. "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
  117. stdout=devnull, stderr=devnull)
  118. with open(outfile, "rb") as f:
  119. return f.read()
  120. except subprocess.CalledProcessError:
  121. return None
  122. finally:
  123. shutil.rmtree(workdir)
  124. def kpsewhich(filename):
  125. """Invoke kpsewhich command with an argument `filename`."""
  126. try:
  127. find_cmd("kpsewhich")
  128. proc = subprocess.Popen(
  129. ["kpsewhich", filename],
  130. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  131. (stdout, stderr) = proc.communicate()
  132. return stdout.strip().decode('utf8', 'replace')
  133. except FindCmdError:
  134. pass
  135. def genelatex(body, wrap):
  136. """Generate LaTeX document for dvipng backend."""
  137. lt = LaTeXTool.instance()
  138. breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
  139. yield u(r'\documentclass{article}')
  140. packages = lt.packages
  141. if breqn:
  142. packages = packages + ['breqn']
  143. for pack in packages:
  144. yield u(r'\usepackage{{{0}}}'.format(pack))
  145. yield u(r'\pagestyle{empty}')
  146. if lt.preamble:
  147. yield lt.preamble
  148. yield u(r'\begin{document}')
  149. if breqn:
  150. yield u(r'\begin{dmath*}')
  151. yield body
  152. yield u(r'\end{dmath*}')
  153. elif wrap:
  154. yield u'$${0}$$'.format(body)
  155. else:
  156. yield body
  157. yield u'\end{document}'
  158. _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
  159. def latex_to_html(s, alt='image'):
  160. """Render LaTeX to HTML with embedded PNG data using data URIs.
  161. Parameters
  162. ----------
  163. s : str
  164. The raw string containing valid inline LateX.
  165. alt : str
  166. The alt text to use for the HTML.
  167. """
  168. base64_data = latex_to_png(s, encode=True).decode('ascii')
  169. if base64_data:
  170. return _data_uri_template_png % (base64_data, alt)