texmanager.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. r"""
  2. Support for embedded TeX expressions in Matplotlib via dvipng and dvips for the
  3. raster and PostScript backends. The tex and dvipng/dvips information is cached
  4. in ~/.matplotlib/tex.cache for reuse between sessions.
  5. Requirements:
  6. * latex
  7. * \*Agg backends: dvipng>=1.6
  8. * PS backend: psfrag, dvips, and Ghostscript>=8.60
  9. Backends:
  10. * \*Agg
  11. * PS
  12. * PDF
  13. For raster output, you can get RGBA numpy arrays from TeX expressions
  14. as follows::
  15. texmanager = TexManager()
  16. s = ('\TeX\ is Number '
  17. '$\displaystyle\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}$!')
  18. Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0))
  19. To enable tex rendering of all text in your matplotlib figure, set
  20. :rc:`text.usetex` to True.
  21. """
  22. import functools
  23. import glob
  24. import hashlib
  25. import logging
  26. import os
  27. from pathlib import Path
  28. import re
  29. import subprocess
  30. import numpy as np
  31. import matplotlib as mpl
  32. from matplotlib import cbook, dviread, rcParams
  33. _log = logging.getLogger(__name__)
  34. class TexManager:
  35. """
  36. Convert strings to dvi files using TeX, caching the results to a directory.
  37. Repeated calls to this constructor always return the same instance.
  38. """
  39. # Caches.
  40. texcache = os.path.join(mpl.get_cachedir(), 'tex.cache')
  41. grey_arrayd = {}
  42. font_family = 'serif'
  43. font_families = ('serif', 'sans-serif', 'cursive', 'monospace')
  44. font_info = {
  45. 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'),
  46. 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'),
  47. 'times': ('ptm', r'\usepackage{mathptmx}'),
  48. 'palatino': ('ppl', r'\usepackage{mathpazo}'),
  49. 'zapf chancery': ('pzc', r'\usepackage{chancery}'),
  50. 'cursive': ('pzc', r'\usepackage{chancery}'),
  51. 'charter': ('pch', r'\usepackage{charter}'),
  52. 'serif': ('cmr', ''),
  53. 'sans-serif': ('cmss', ''),
  54. 'helvetica': ('phv', r'\usepackage{helvet}'),
  55. 'avant garde': ('pag', r'\usepackage{avant}'),
  56. 'courier': ('pcr', r'\usepackage{courier}'),
  57. # Loading the type1ec package ensures that cm-super is installed, which
  58. # is necessary for unicode computer modern. (It also allows the use of
  59. # computer modern at arbitrary sizes, but that's just a side effect.)
  60. 'monospace': ('cmtt', r'\usepackage{type1ec}'),
  61. 'computer modern roman': ('cmr', r'\usepackage{type1ec}'),
  62. 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'),
  63. 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')}
  64. @cbook.deprecated("3.3", alternative="matplotlib.get_cachedir()")
  65. @property
  66. def cachedir(self):
  67. return mpl.get_cachedir()
  68. @cbook.deprecated("3.3")
  69. @property
  70. def rgba_arrayd(self):
  71. return {}
  72. @functools.lru_cache() # Always return the same instance.
  73. def __new__(cls):
  74. Path(cls.texcache).mkdir(parents=True, exist_ok=True)
  75. return object.__new__(cls)
  76. _fonts = {} # Only for deprecation period.
  77. @cbook.deprecated("3.3")
  78. @property
  79. def serif(self):
  80. return self._fonts.get("serif", ('cmr', ''))
  81. @cbook.deprecated("3.3")
  82. @property
  83. def sans_serif(self):
  84. return self._fonts.get("sans-serif", ('cmss', ''))
  85. @cbook.deprecated("3.3")
  86. @property
  87. def cursive(self):
  88. return self._fonts.get("cursive", ('pzc', r'\usepackage{chancery}'))
  89. @cbook.deprecated("3.3")
  90. @property
  91. def monospace(self):
  92. return self._fonts.get("monospace", ('cmtt', ''))
  93. def get_font_config(self):
  94. ff = rcParams['font.family']
  95. if len(ff) == 1 and ff[0].lower() in self.font_families:
  96. self.font_family = ff[0].lower()
  97. else:
  98. _log.info('font.family must be one of (%s) when text.usetex is '
  99. 'True. serif will be used by default.',
  100. ', '.join(self.font_families))
  101. self.font_family = 'serif'
  102. fontconfig = [self.font_family]
  103. for font_family in self.font_families:
  104. for font in rcParams['font.' + font_family]:
  105. if font.lower() in self.font_info:
  106. self._fonts[font_family] = self.font_info[font.lower()]
  107. _log.debug('family: %s, font: %s, info: %s',
  108. font_family, font, self.font_info[font.lower()])
  109. break
  110. else:
  111. _log.debug('%s font is not compatible with usetex.', font)
  112. else:
  113. _log.info('No LaTeX-compatible font found for the %s font '
  114. 'family in rcParams. Using default.', font_family)
  115. self._fonts[font_family] = self.font_info[font_family]
  116. fontconfig.append(self._fonts[font_family][0])
  117. # Add a hash of the latex preamble to fontconfig so that the
  118. # correct png is selected for strings rendered with same font and dpi
  119. # even if the latex preamble changes within the session
  120. preamble_bytes = self.get_custom_preamble().encode('utf-8')
  121. fontconfig.append(hashlib.md5(preamble_bytes).hexdigest())
  122. # The following packages and commands need to be included in the latex
  123. # file's preamble:
  124. cmd = [self._fonts['serif'][1],
  125. self._fonts['sans-serif'][1],
  126. self._fonts['monospace'][1]]
  127. if self.font_family == 'cursive':
  128. cmd.append(self._fonts['cursive'][1])
  129. self._font_preamble = '\n'.join([r'\usepackage{type1cm}', *cmd])
  130. return ''.join(fontconfig)
  131. def get_basefile(self, tex, fontsize, dpi=None):
  132. """
  133. Return a filename based on a hash of the string, fontsize, and dpi.
  134. """
  135. s = ''.join([tex, self.get_font_config(), '%f' % fontsize,
  136. self.get_custom_preamble(), str(dpi or '')])
  137. return os.path.join(
  138. self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest())
  139. def get_font_preamble(self):
  140. """
  141. Return a string containing font configuration for the tex preamble.
  142. """
  143. return self._font_preamble
  144. def get_custom_preamble(self):
  145. """Return a string containing user additions to the tex preamble."""
  146. return rcParams['text.latex.preamble']
  147. def _get_preamble(self):
  148. return "\n".join([
  149. r"\documentclass{article}",
  150. # Pass-through \mathdefault, which is used in non-usetex mode to
  151. # use the default text font but was historically suppressed in
  152. # usetex mode.
  153. r"\newcommand{\mathdefault}[1]{#1}",
  154. self._font_preamble,
  155. r"\usepackage[utf8]{inputenc}",
  156. r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}",
  157. # geometry is loaded before the custom preamble as convert_psfrags
  158. # relies on a custom preamble to change the geometry.
  159. r"\usepackage[papersize=72in,body=70in,margin=1in]{geometry}",
  160. self.get_custom_preamble(),
  161. # textcomp is loaded last (if not already loaded by the custom
  162. # preamble) in order not to clash with custom packages (e.g.
  163. # newtxtext) which load it with different options.
  164. r"\makeatletter"
  165. r"\@ifpackageloaded{textcomp}{}{\usepackage{textcomp}}"
  166. r"\makeatother",
  167. ])
  168. def make_tex(self, tex, fontsize):
  169. """
  170. Generate a tex file to render the tex string at a specific font size.
  171. Return the file name.
  172. """
  173. basefile = self.get_basefile(tex, fontsize)
  174. texfile = '%s.tex' % basefile
  175. fontcmd = {'sans-serif': r'{\sffamily %s}',
  176. 'monospace': r'{\ttfamily %s}'}.get(self.font_family,
  177. r'{\rmfamily %s}')
  178. Path(texfile).write_text(
  179. r"""
  180. %s
  181. \pagestyle{empty}
  182. \begin{document}
  183. %% The empty hbox ensures that a page is printed even for empty inputs, except
  184. %% when using psfrag which gets confused by it.
  185. \fontsize{%f}{%f}%%
  186. \ifdefined\psfrag\else\hbox{}\fi%%
  187. %s
  188. \end{document}
  189. """ % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex),
  190. encoding='utf-8')
  191. return texfile
  192. _re_vbox = re.compile(
  193. r"MatplotlibBox:\(([\d.]+)pt\+([\d.]+)pt\)x([\d.]+)pt")
  194. @cbook.deprecated("3.3")
  195. def make_tex_preview(self, tex, fontsize):
  196. """
  197. Generate a tex file to render the tex string at a specific font size.
  198. It uses the preview.sty to determine the dimension (width, height,
  199. descent) of the output.
  200. Return the file name.
  201. """
  202. basefile = self.get_basefile(tex, fontsize)
  203. texfile = '%s.tex' % basefile
  204. fontcmd = {'sans-serif': r'{\sffamily %s}',
  205. 'monospace': r'{\ttfamily %s}'}.get(self.font_family,
  206. r'{\rmfamily %s}')
  207. # newbox, setbox, immediate, etc. are used to find the box
  208. # extent of the rendered text.
  209. Path(texfile).write_text(
  210. r"""
  211. %s
  212. \usepackage[active,showbox,tightpage]{preview}
  213. %% we override the default showbox as it is treated as an error and makes
  214. %% the exit status not zero
  215. \def\showbox#1%%
  216. {\immediate\write16{MatplotlibBox:(\the\ht#1+\the\dp#1)x\the\wd#1}}
  217. \begin{document}
  218. \begin{preview}
  219. {\fontsize{%f}{%f}%s}
  220. \end{preview}
  221. \end{document}
  222. """ % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex),
  223. encoding='utf-8')
  224. return texfile
  225. def _run_checked_subprocess(self, command, tex):
  226. _log.debug(cbook._pformat_subprocess(command))
  227. try:
  228. report = subprocess.check_output(command,
  229. cwd=self.texcache,
  230. stderr=subprocess.STDOUT)
  231. except FileNotFoundError as exc:
  232. raise RuntimeError(
  233. 'Failed to process string with tex because {} could not be '
  234. 'found'.format(command[0])) from exc
  235. except subprocess.CalledProcessError as exc:
  236. raise RuntimeError(
  237. '{prog} was not able to process the following string:\n'
  238. '{tex!r}\n\n'
  239. 'Here is the full report generated by {prog}:\n'
  240. '{exc}\n\n'.format(
  241. prog=command[0],
  242. tex=tex.encode('unicode_escape'),
  243. exc=exc.output.decode('utf-8'))) from exc
  244. _log.debug(report)
  245. return report
  246. def make_dvi(self, tex, fontsize):
  247. """
  248. Generate a dvi file containing latex's layout of tex string.
  249. Return the file name.
  250. """
  251. if dict.__getitem__(rcParams, 'text.latex.preview'):
  252. return self.make_dvi_preview(tex, fontsize)
  253. basefile = self.get_basefile(tex, fontsize)
  254. dvifile = '%s.dvi' % basefile
  255. if not os.path.exists(dvifile):
  256. texfile = self.make_tex(tex, fontsize)
  257. with cbook._lock_path(texfile):
  258. self._run_checked_subprocess(
  259. ["latex", "-interaction=nonstopmode", "--halt-on-error",
  260. texfile], tex)
  261. for fname in glob.glob(basefile + '*'):
  262. if not fname.endswith(('dvi', 'tex')):
  263. try:
  264. os.remove(fname)
  265. except OSError:
  266. pass
  267. return dvifile
  268. @cbook.deprecated("3.3")
  269. def make_dvi_preview(self, tex, fontsize):
  270. """
  271. Generate a dvi file containing latex's layout of tex string.
  272. It calls make_tex_preview() method and store the size information
  273. (width, height, descent) in a separate file.
  274. Return the file name.
  275. """
  276. basefile = self.get_basefile(tex, fontsize)
  277. dvifile = '%s.dvi' % basefile
  278. baselinefile = '%s.baseline' % basefile
  279. if not os.path.exists(dvifile) or not os.path.exists(baselinefile):
  280. texfile = self.make_tex_preview(tex, fontsize)
  281. report = self._run_checked_subprocess(
  282. ["latex", "-interaction=nonstopmode", "--halt-on-error",
  283. texfile], tex)
  284. # find the box extent information in the latex output
  285. # file and store them in ".baseline" file
  286. m = TexManager._re_vbox.search(report.decode("utf-8"))
  287. with open(basefile + '.baseline', "w") as fh:
  288. fh.write(" ".join(m.groups()))
  289. for fname in glob.glob(basefile + '*'):
  290. if not fname.endswith(('dvi', 'tex', 'baseline')):
  291. try:
  292. os.remove(fname)
  293. except OSError:
  294. pass
  295. return dvifile
  296. def make_png(self, tex, fontsize, dpi):
  297. """
  298. Generate a png file containing latex's rendering of tex string.
  299. Return the file name.
  300. """
  301. basefile = self.get_basefile(tex, fontsize, dpi)
  302. pngfile = '%s.png' % basefile
  303. # see get_rgba for a discussion of the background
  304. if not os.path.exists(pngfile):
  305. dvifile = self.make_dvi(tex, fontsize)
  306. cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
  307. "-T", "tight", "-o", pngfile, dvifile]
  308. # When testing, disable FreeType rendering for reproducibility; but
  309. # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0
  310. # mode, so for it we keep FreeType enabled; the image will be
  311. # slightly off.
  312. if (getattr(mpl, "_called_from_pytest", False)
  313. and mpl._get_executable_info("dvipng").version != "1.16"):
  314. cmd.insert(1, "--freetype0")
  315. self._run_checked_subprocess(cmd, tex)
  316. return pngfile
  317. def get_grey(self, tex, fontsize=None, dpi=None):
  318. """Return the alpha channel."""
  319. if not fontsize:
  320. fontsize = rcParams['font.size']
  321. if not dpi:
  322. dpi = rcParams['savefig.dpi']
  323. key = tex, self.get_font_config(), fontsize, dpi
  324. alpha = self.grey_arrayd.get(key)
  325. if alpha is None:
  326. pngfile = self.make_png(tex, fontsize, dpi)
  327. rgba = mpl.image.imread(os.path.join(self.texcache, pngfile))
  328. self.grey_arrayd[key] = alpha = rgba[:, :, -1]
  329. return alpha
  330. def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
  331. """Return latex's rendering of the tex string as an rgba array."""
  332. alpha = self.get_grey(tex, fontsize, dpi)
  333. rgba = np.empty((*alpha.shape, 4))
  334. rgba[..., :3] = mpl.colors.to_rgb(rgb)
  335. rgba[..., -1] = alpha
  336. return rgba
  337. def get_text_width_height_descent(self, tex, fontsize, renderer=None):
  338. """Return width, height and descent of the text."""
  339. if tex.strip() == '':
  340. return 0, 0, 0
  341. dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
  342. if dict.__getitem__(rcParams, 'text.latex.preview'):
  343. # use preview.sty
  344. basefile = self.get_basefile(tex, fontsize)
  345. baselinefile = '%s.baseline' % basefile
  346. if not os.path.exists(baselinefile):
  347. dvifile = self.make_dvi_preview(tex, fontsize)
  348. with open(baselinefile) as fh:
  349. l = fh.read().split()
  350. height, depth, width = [float(l1) * dpi_fraction for l1 in l]
  351. return width, height + depth, depth
  352. else:
  353. # use dviread.
  354. dvifile = self.make_dvi(tex, fontsize)
  355. with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
  356. page, = dvi
  357. # A total height (including the descent) needs to be returned.
  358. return page.width, page.height + page.descent, page.descent