Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/texmanager.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1r"""
2This module supports embedded TeX expressions in matplotlib via dvipng
3and dvips for the raster and postscript backends. The tex and
4dvipng/dvips information is cached in ~/.matplotlib/tex.cache for reuse between
5sessions
7Requirements:
9* latex
10* \*Agg backends: dvipng>=1.6
11* PS backend: psfrag, dvips, and Ghostscript>=8.60
13Backends:
15* \*Agg
16* PS
17* PDF
19For raster output, you can get RGBA numpy arrays from TeX expressions
20as follows::
22 texmanager = TexManager()
23 s = ('\TeX\ is Number '
24 '$\displaystyle\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}$!')
25 Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0))
27To enable tex rendering of all text in your matplotlib figure, set
28:rc:`text.usetex` to True.
29"""
31import copy
32import functools
33import glob
34import hashlib
35import logging
36import os
37from pathlib import Path
38import re
39import subprocess
41import numpy as np
43import matplotlib as mpl
44from matplotlib import cbook, dviread, rcParams
46_log = logging.getLogger(__name__)
49class TexManager:
50 """
51 Convert strings to dvi files using TeX, caching the results to a directory.
53 Repeated calls to this constructor always return the same instance.
54 """
56 cachedir = mpl.get_cachedir()
57 if cachedir is not None:
58 texcache = os.path.join(cachedir, 'tex.cache')
59 Path(texcache).mkdir(parents=True, exist_ok=True)
60 else:
61 # Should only happen in a restricted environment (such as Google App
62 # Engine). Deal with this gracefully by not creating a cache directory.
63 texcache = None
65 # Caches.
66 rgba_arrayd = {}
67 grey_arrayd = {}
69 serif = ('cmr', '')
70 sans_serif = ('cmss', '')
71 monospace = ('cmtt', '')
72 cursive = ('pzc', r'\usepackage{chancery}')
73 font_family = 'serif'
74 font_families = ('serif', 'sans-serif', 'cursive', 'monospace')
76 font_info = {
77 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'),
78 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'),
79 'times': ('ptm', r'\usepackage{mathptmx}'),
80 'palatino': ('ppl', r'\usepackage{mathpazo}'),
81 'zapf chancery': ('pzc', r'\usepackage{chancery}'),
82 'cursive': ('pzc', r'\usepackage{chancery}'),
83 'charter': ('pch', r'\usepackage{charter}'),
84 'serif': ('cmr', ''),
85 'sans-serif': ('cmss', ''),
86 'helvetica': ('phv', r'\usepackage{helvet}'),
87 'avant garde': ('pag', r'\usepackage{avant}'),
88 'courier': ('pcr', r'\usepackage{courier}'),
89 # Loading the type1ec package ensures that cm-super is installed, which
90 # is necessary for unicode computer modern. (It also allows the use of
91 # computer modern at arbitrary sizes, but that's just a side effect.)
92 'monospace': ('cmtt', r'\usepackage{type1ec}'),
93 'computer modern roman': ('cmr', r'\usepackage{type1ec}'),
94 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'),
95 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')}
97 _rc_cache = None
98 _rc_cache_keys = (
99 ('text.latex.preamble', 'text.latex.unicode', 'text.latex.preview',
100 'font.family') + tuple('font.' + n for n in font_families))
102 @functools.lru_cache() # Always return the same instance.
103 def __new__(cls):
104 self = object.__new__(cls)
105 self._reinit()
106 return self
108 def _reinit(self):
109 if self.texcache is None:
110 raise RuntimeError('Cannot create TexManager, as there is no '
111 'cache directory available')
113 Path(self.texcache).mkdir(parents=True, exist_ok=True)
114 ff = rcParams['font.family']
115 if len(ff) == 1 and ff[0].lower() in self.font_families:
116 self.font_family = ff[0].lower()
117 elif isinstance(ff, str) and ff.lower() in self.font_families:
118 self.font_family = ff.lower()
119 else:
120 _log.info('font.family must be one of (%s) when text.usetex is '
121 'True. serif will be used by default.',
122 ', '.join(self.font_families))
123 self.font_family = 'serif'
125 fontconfig = [self.font_family]
126 for font_family in self.font_families:
127 font_family_attr = font_family.replace('-', '_')
128 for font in rcParams['font.' + font_family]:
129 if font.lower() in self.font_info:
130 setattr(self, font_family_attr,
131 self.font_info[font.lower()])
132 _log.debug('family: %s, font: %s, info: %s',
133 font_family, font, self.font_info[font.lower()])
134 break
135 else:
136 _log.debug('%s font is not compatible with usetex.',
137 font_family)
138 else:
139 _log.info('No LaTeX-compatible font found for the %s font '
140 'family in rcParams. Using default.', font_family)
141 setattr(self, font_family_attr, self.font_info[font_family])
142 fontconfig.append(getattr(self, font_family_attr)[0])
143 # Add a hash of the latex preamble to self._fontconfig so that the
144 # correct png is selected for strings rendered with same font and dpi
145 # even if the latex preamble changes within the session
146 preamble_bytes = self.get_custom_preamble().encode('utf-8')
147 fontconfig.append(hashlib.md5(preamble_bytes).hexdigest())
148 self._fontconfig = ''.join(fontconfig)
150 # The following packages and commands need to be included in the latex
151 # file's preamble:
152 cmd = [self.serif[1], self.sans_serif[1], self.monospace[1]]
153 if self.font_family == 'cursive':
154 cmd.append(self.cursive[1])
155 self._font_preamble = '\n'.join(
156 [r'\usepackage{type1cm}'] + cmd + [r'\usepackage{textcomp}'])
158 def get_basefile(self, tex, fontsize, dpi=None):
159 """
160 Return a filename based on a hash of the string, fontsize, and dpi.
161 """
162 s = ''.join([tex, self.get_font_config(), '%f' % fontsize,
163 self.get_custom_preamble(), str(dpi or '')])
164 return os.path.join(
165 self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest())
167 def get_font_config(self):
168 """Reinitializes self if relevant rcParams on have changed."""
169 if self._rc_cache is None:
170 self._rc_cache = dict.fromkeys(self._rc_cache_keys)
171 changed = [par for par in self._rc_cache_keys
172 if rcParams[par] != self._rc_cache[par]]
173 if changed:
174 _log.debug('following keys changed: %s', changed)
175 for k in changed:
176 _log.debug('%-20s: %-10s -> %-10s',
177 k, self._rc_cache[k], rcParams[k])
178 # deepcopy may not be necessary, but feels more future-proof
179 self._rc_cache[k] = copy.deepcopy(rcParams[k])
180 _log.debug('RE-INIT\nold fontconfig: %s', self._fontconfig)
181 self._reinit()
182 _log.debug('fontconfig: %s', self._fontconfig)
183 return self._fontconfig
185 def get_font_preamble(self):
186 """
187 Return a string containing font configuration for the tex preamble.
188 """
189 return self._font_preamble
191 def get_custom_preamble(self):
192 """Return a string containing user additions to the tex preamble."""
193 return rcParams['text.latex.preamble']
195 def _get_preamble(self):
196 unicode_preamble = "\n".join([
197 r"\usepackage[utf8]{inputenc}",
198 r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}",
199 ]) if rcParams["text.latex.unicode"] else ""
200 return "\n".join([
201 r"\documentclass{article}",
202 # Pass-through \mathdefault, which is used in non-usetex mode to
203 # use the default text font but was historically suppressed in
204 # usetex mode.
205 r"\newcommand{\mathdefault}[1]{#1}",
206 self._font_preamble,
207 unicode_preamble,
208 # Needs to come early so that the custom preamble can change the
209 # geometry, e.g. in convert_psfrags.
210 r"\usepackage[papersize=72in,body=70in,margin=1in]{geometry}",
211 self.get_custom_preamble(),
212 ])
214 def make_tex(self, tex, fontsize):
215 """
216 Generate a tex file to render the tex string at a specific font size.
218 Return the file name.
219 """
220 basefile = self.get_basefile(tex, fontsize)
221 texfile = '%s.tex' % basefile
222 fontcmd = {'sans-serif': r'{\sffamily %s}',
223 'monospace': r'{\ttfamily %s}'}.get(self.font_family,
224 r'{\rmfamily %s}')
225 tex = fontcmd % tex
227 s = r"""
228%s
229\pagestyle{empty}
230\begin{document}
231%% The empty hbox ensures that a page is printed even for empty inputs, except
232%% when using psfrag which gets confused by it.
233\fontsize{%f}{%f}%%
234\ifdefined\psfrag\else\hbox{}\fi%%
235%s
236\end{document}
237""" % (self._get_preamble(), fontsize, fontsize * 1.25, tex)
238 with open(texfile, 'wb') as fh:
239 if rcParams['text.latex.unicode']:
240 fh.write(s.encode('utf8'))
241 else:
242 try:
243 fh.write(s.encode('ascii'))
244 except UnicodeEncodeError:
245 _log.info("You are using unicode and latex, but have not "
246 "enabled the 'text.latex.unicode' rcParam.")
247 raise
249 return texfile
251 _re_vbox = re.compile(
252 r"MatplotlibBox:\(([\d.]+)pt\+([\d.]+)pt\)x([\d.]+)pt")
254 def make_tex_preview(self, tex, fontsize):
255 """
256 Generate a tex file to render the tex string at a specific font size.
258 It uses the preview.sty to determine the dimension (width, height,
259 descent) of the output.
261 Return the file name.
262 """
263 basefile = self.get_basefile(tex, fontsize)
264 texfile = '%s.tex' % basefile
265 fontcmd = {'sans-serif': r'{\sffamily %s}',
266 'monospace': r'{\ttfamily %s}'}.get(self.font_family,
267 r'{\rmfamily %s}')
268 tex = fontcmd % tex
270 # newbox, setbox, immediate, etc. are used to find the box
271 # extent of the rendered text.
273 s = r"""
274%s
275\usepackage[active,showbox,tightpage]{preview}
277%% we override the default showbox as it is treated as an error and makes
278%% the exit status not zero
279\def\showbox#1%%
280{\immediate\write16{MatplotlibBox:(\the\ht#1+\the\dp#1)x\the\wd#1}}
282\begin{document}
283\begin{preview}
284{\fontsize{%f}{%f}%s}
285\end{preview}
286\end{document}
287""" % (self._get_preamble(), fontsize, fontsize * 1.25, tex)
288 with open(texfile, 'wb') as fh:
289 if rcParams['text.latex.unicode']:
290 fh.write(s.encode('utf8'))
291 else:
292 try:
293 fh.write(s.encode('ascii'))
294 except UnicodeEncodeError:
295 _log.info("You are using unicode and latex, but have not "
296 "enabled the 'text.latex.unicode' rcParam.")
297 raise
299 return texfile
301 def _run_checked_subprocess(self, command, tex):
302 _log.debug(cbook._pformat_subprocess(command))
303 try:
304 report = subprocess.check_output(command,
305 cwd=self.texcache,
306 stderr=subprocess.STDOUT)
307 except FileNotFoundError as exc:
308 raise RuntimeError(
309 'Failed to process string with tex because {} could not be '
310 'found'.format(command[0])) from exc
311 except subprocess.CalledProcessError as exc:
312 raise RuntimeError(
313 '{prog} was not able to process the following string:\n'
314 '{tex!r}\n\n'
315 'Here is the full report generated by {prog}:\n'
316 '{exc}\n\n'.format(
317 prog=command[0],
318 tex=tex.encode('unicode_escape'),
319 exc=exc.output.decode('utf-8'))) from exc
320 _log.debug(report)
321 return report
323 def make_dvi(self, tex, fontsize):
324 """
325 Generate a dvi file containing latex's layout of tex string.
327 Return the file name.
328 """
330 if rcParams['text.latex.preview']:
331 return self.make_dvi_preview(tex, fontsize)
333 basefile = self.get_basefile(tex, fontsize)
334 dvifile = '%s.dvi' % basefile
335 if not os.path.exists(dvifile):
336 texfile = self.make_tex(tex, fontsize)
337 with cbook._lock_path(texfile):
338 self._run_checked_subprocess(
339 ["latex", "-interaction=nonstopmode", "--halt-on-error",
340 texfile], tex)
341 for fname in glob.glob(basefile + '*'):
342 if not fname.endswith(('dvi', 'tex')):
343 try:
344 os.remove(fname)
345 except OSError:
346 pass
348 return dvifile
350 def make_dvi_preview(self, tex, fontsize):
351 """
352 Generate a dvi file containing latex's layout of tex string.
354 It calls make_tex_preview() method and store the size information
355 (width, height, descent) in a separate file.
357 Return the file name.
358 """
359 basefile = self.get_basefile(tex, fontsize)
360 dvifile = '%s.dvi' % basefile
361 baselinefile = '%s.baseline' % basefile
363 if not os.path.exists(dvifile) or not os.path.exists(baselinefile):
364 texfile = self.make_tex_preview(tex, fontsize)
365 report = self._run_checked_subprocess(
366 ["latex", "-interaction=nonstopmode", "--halt-on-error",
367 texfile], tex)
369 # find the box extent information in the latex output
370 # file and store them in ".baseline" file
371 m = TexManager._re_vbox.search(report.decode("utf-8"))
372 with open(basefile + '.baseline', "w") as fh:
373 fh.write(" ".join(m.groups()))
375 for fname in glob.glob(basefile + '*'):
376 if not fname.endswith(('dvi', 'tex', 'baseline')):
377 try:
378 os.remove(fname)
379 except OSError:
380 pass
382 return dvifile
384 def make_png(self, tex, fontsize, dpi):
385 """
386 Generate a png file containing latex's rendering of tex string.
388 Return the file name.
389 """
390 basefile = self.get_basefile(tex, fontsize, dpi)
391 pngfile = '%s.png' % basefile
392 # see get_rgba for a discussion of the background
393 if not os.path.exists(pngfile):
394 dvifile = self.make_dvi(tex, fontsize)
395 cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
396 "-T", "tight", "-o", pngfile, dvifile]
397 # When testing, disable FreeType rendering for reproducibility; but
398 # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0
399 # mode, so for it we keep FreeType enabled; the image will be
400 # slightly off.
401 if (getattr(mpl, "_called_from_pytest", False)
402 and mpl._get_executable_info("dvipng").version != "1.16"):
403 cmd.insert(1, "--freetype0")
404 self._run_checked_subprocess(cmd, tex)
405 return pngfile
407 def get_grey(self, tex, fontsize=None, dpi=None):
408 """Return the alpha channel."""
409 from matplotlib import _png
410 key = tex, self.get_font_config(), fontsize, dpi
411 alpha = self.grey_arrayd.get(key)
412 if alpha is None:
413 pngfile = self.make_png(tex, fontsize, dpi)
414 with open(os.path.join(self.texcache, pngfile), "rb") as file:
415 X = _png.read_png(file)
416 self.grey_arrayd[key] = alpha = X[:, :, -1]
417 return alpha
419 def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
420 """Return latex's rendering of the tex string as an rgba array."""
421 if not fontsize:
422 fontsize = rcParams['font.size']
423 if not dpi:
424 dpi = rcParams['savefig.dpi']
425 r, g, b = rgb
426 key = tex, self.get_font_config(), fontsize, dpi, tuple(rgb)
427 Z = self.rgba_arrayd.get(key)
429 if Z is None:
430 alpha = self.get_grey(tex, fontsize, dpi)
431 Z = np.dstack([r, g, b, alpha])
432 self.rgba_arrayd[key] = Z
434 return Z
436 def get_text_width_height_descent(self, tex, fontsize, renderer=None):
437 """Return width, height and descent of the text."""
438 if tex.strip() == '':
439 return 0, 0, 0
441 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
443 if rcParams['text.latex.preview']:
444 # use preview.sty
445 basefile = self.get_basefile(tex, fontsize)
446 baselinefile = '%s.baseline' % basefile
448 if not os.path.exists(baselinefile):
449 dvifile = self.make_dvi_preview(tex, fontsize)
451 with open(baselinefile) as fh:
452 l = fh.read().split()
453 height, depth, width = [float(l1) * dpi_fraction for l1 in l]
454 return width, height + depth, depth
456 else:
457 # use dviread.
458 dvifile = self.make_dvi(tex, fontsize)
459 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
460 page, = dvi
461 # A total height (including the descent) needs to be returned.
462 return page.width, page.height + page.descent, page.descent