Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/texmanager.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

147 statements  

1r""" 

2Support for embedded TeX expressions in Matplotlib. 

3 

4Requirements: 

5 

6* LaTeX. 

7* \*Agg backends: dvipng>=1.6. 

8* PS backend: PSfrag, dvips, and Ghostscript>=9.0. 

9* PDF and SVG backends: if LuaTeX is present, it will be used to speed up some 

10 post-processing steps, but note that it is not used to parse the TeX string 

11 itself (only LaTeX is supported). 

12 

13To enable TeX rendering of all text in your Matplotlib figure, set 

14:rc:`text.usetex` to True. 

15 

16TeX and dvipng/dvips processing results are cached 

17in ~/.matplotlib/tex.cache for reuse between sessions. 

18 

19`TexManager.get_rgba` can also be used to directly obtain raster output as RGBA 

20NumPy arrays. 

21""" 

22 

23import functools 

24import hashlib 

25import logging 

26import os 

27from pathlib import Path 

28import subprocess 

29from tempfile import TemporaryDirectory 

30 

31import numpy as np 

32 

33import matplotlib as mpl 

34from matplotlib import _api, cbook, dviread 

35 

36_log = logging.getLogger(__name__) 

37 

38 

39def _usepackage_if_not_loaded(package, *, option=None): 

40 """ 

41 Output LaTeX code that loads a package (possibly with an option) if it 

42 hasn't been loaded yet. 

43 

44 LaTeX cannot load twice a package with different options, so this helper 

45 can be used to protect against users loading arbitrary packages/options in 

46 their custom preamble. 

47 """ 

48 option = f"[{option}]" if option is not None else "" 

49 return ( 

50 r"\makeatletter" 

51 r"\@ifpackageloaded{%(package)s}{}{\usepackage%(option)s{%(package)s}}" 

52 r"\makeatother" 

53 ) % {"package": package, "option": option} 

54 

55 

56class TexManager: 

57 """ 

58 Convert strings to dvi files using TeX, caching the results to a directory. 

59 

60 The cache directory is called ``tex.cache`` and is located in the directory 

61 returned by `.get_cachedir`. 

62 

63 Repeated calls to this constructor always return the same instance. 

64 """ 

65 

66 texcache = _api.deprecate_privatize_attribute("3.8") 

67 _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') 

68 _grey_arrayd = {} 

69 

70 _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') 

71 _font_preambles = { 

72 'new century schoolbook': r'\renewcommand{\rmdefault}{pnc}', 

73 'bookman': r'\renewcommand{\rmdefault}{pbk}', 

74 'times': r'\usepackage{mathptmx}', 

75 'palatino': r'\usepackage{mathpazo}', 

76 'zapf chancery': r'\usepackage{chancery}', 

77 'cursive': r'\usepackage{chancery}', 

78 'charter': r'\usepackage{charter}', 

79 'serif': '', 

80 'sans-serif': '', 

81 'helvetica': r'\usepackage{helvet}', 

82 'avant garde': r'\usepackage{avant}', 

83 'courier': r'\usepackage{courier}', 

84 # Loading the type1ec package ensures that cm-super is installed, which 

85 # is necessary for Unicode computer modern. (It also allows the use of 

86 # computer modern at arbitrary sizes, but that's just a side effect.) 

87 'monospace': r'\usepackage{type1ec}', 

88 'computer modern roman': r'\usepackage{type1ec}', 

89 'computer modern sans serif': r'\usepackage{type1ec}', 

90 'computer modern typewriter': r'\usepackage{type1ec}', 

91 } 

92 _font_types = { 

93 'new century schoolbook': 'serif', 

94 'bookman': 'serif', 

95 'times': 'serif', 

96 'palatino': 'serif', 

97 'zapf chancery': 'cursive', 

98 'charter': 'serif', 

99 'helvetica': 'sans-serif', 

100 'avant garde': 'sans-serif', 

101 'courier': 'monospace', 

102 'computer modern roman': 'serif', 

103 'computer modern sans serif': 'sans-serif', 

104 'computer modern typewriter': 'monospace', 

105 } 

106 

107 @functools.lru_cache # Always return the same instance. 

108 def __new__(cls): 

109 Path(cls._texcache).mkdir(parents=True, exist_ok=True) 

110 return object.__new__(cls) 

111 

112 @classmethod 

113 def _get_font_family_and_reduced(cls): 

114 """Return the font family name and whether the font is reduced.""" 

115 ff = mpl.rcParams['font.family'] 

116 ff_val = ff[0].lower() if len(ff) == 1 else None 

117 if len(ff) == 1 and ff_val in cls._font_families: 

118 return ff_val, False 

119 elif len(ff) == 1 and ff_val in cls._font_preambles: 

120 return cls._font_types[ff_val], True 

121 else: 

122 _log.info('font.family must be one of (%s) when text.usetex is ' 

123 'True. serif will be used by default.', 

124 ', '.join(cls._font_families)) 

125 return 'serif', False 

126 

127 @classmethod 

128 def _get_font_preamble_and_command(cls): 

129 requested_family, is_reduced_font = cls._get_font_family_and_reduced() 

130 

131 preambles = {} 

132 for font_family in cls._font_families: 

133 if is_reduced_font and font_family == requested_family: 

134 preambles[font_family] = cls._font_preambles[ 

135 mpl.rcParams['font.family'][0].lower()] 

136 else: 

137 for font in mpl.rcParams['font.' + font_family]: 

138 if font.lower() in cls._font_preambles: 

139 preambles[font_family] = \ 

140 cls._font_preambles[font.lower()] 

141 _log.debug( 

142 'family: %s, font: %s, info: %s', 

143 font_family, font, 

144 cls._font_preambles[font.lower()]) 

145 break 

146 else: 

147 _log.debug('%s font is not compatible with usetex.', 

148 font) 

149 else: 

150 _log.info('No LaTeX-compatible font found for the %s font' 

151 'family in rcParams. Using default.', 

152 font_family) 

153 preambles[font_family] = cls._font_preambles[font_family] 

154 

155 # The following packages and commands need to be included in the latex 

156 # file's preamble: 

157 cmd = {preambles[family] 

158 for family in ['serif', 'sans-serif', 'monospace']} 

159 if requested_family == 'cursive': 

160 cmd.add(preambles['cursive']) 

161 cmd.add(r'\usepackage{type1cm}') 

162 preamble = '\n'.join(sorted(cmd)) 

163 fontcmd = (r'\sffamily' if requested_family == 'sans-serif' else 

164 r'\ttfamily' if requested_family == 'monospace' else 

165 r'\rmfamily') 

166 return preamble, fontcmd 

167 

168 @classmethod 

169 def get_basefile(cls, tex, fontsize, dpi=None): 

170 """ 

171 Return a filename based on a hash of the string, fontsize, and dpi. 

172 """ 

173 src = cls._get_tex_source(tex, fontsize) + str(dpi) 

174 filehash = hashlib.md5(src.encode('utf-8')).hexdigest() 

175 filepath = Path(cls._texcache) 

176 

177 num_letters, num_levels = 2, 2 

178 for i in range(0, num_letters*num_levels, num_letters): 

179 filepath = filepath / Path(filehash[i:i+2]) 

180 

181 filepath.mkdir(parents=True, exist_ok=True) 

182 return os.path.join(filepath, filehash) 

183 

184 @classmethod 

185 def get_font_preamble(cls): 

186 """ 

187 Return a string containing font configuration for the tex preamble. 

188 """ 

189 font_preamble, command = cls._get_font_preamble_and_command() 

190 return font_preamble 

191 

192 @classmethod 

193 def get_custom_preamble(cls): 

194 """Return a string containing user additions to the tex preamble.""" 

195 return mpl.rcParams['text.latex.preamble'] 

196 

197 @classmethod 

198 def _get_tex_source(cls, tex, fontsize): 

199 """Return the complete TeX source for processing a TeX string.""" 

200 font_preamble, fontcmd = cls._get_font_preamble_and_command() 

201 baselineskip = 1.25 * fontsize 

202 return "\n".join([ 

203 r"\documentclass{article}", 

204 r"% Pass-through \mathdefault, which is used in non-usetex mode", 

205 r"% to use the default text font but was historically suppressed", 

206 r"% in usetex mode.", 

207 r"\newcommand{\mathdefault}[1]{#1}", 

208 font_preamble, 

209 r"\usepackage[utf8]{inputenc}", 

210 r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", 

211 r"% geometry is loaded before the custom preamble as ", 

212 r"% convert_psfrags relies on a custom preamble to change the ", 

213 r"% geometry.", 

214 r"\usepackage[papersize=72in, margin=1in]{geometry}", 

215 cls.get_custom_preamble(), 

216 r"% Use `underscore` package to take care of underscores in text.", 

217 r"% The [strings] option allows to use underscores in file names.", 

218 _usepackage_if_not_loaded("underscore", option="strings"), 

219 r"% Custom packages (e.g. newtxtext) may already have loaded ", 

220 r"% textcomp with different options.", 

221 _usepackage_if_not_loaded("textcomp"), 

222 r"\pagestyle{empty}", 

223 r"\begin{document}", 

224 r"% The empty hbox ensures that a page is printed even for empty", 

225 r"% inputs, except when using psfrag which gets confused by it.", 

226 r"% matplotlibbaselinemarker is used by dviread to detect the", 

227 r"% last line's baseline.", 

228 rf"\fontsize{{{fontsize}}}{{{baselineskip}}}%", 

229 r"\ifdefined\psfrag\else\hbox{}\fi%", 

230 rf"{{{fontcmd} {tex}}}%", 

231 r"\end{document}", 

232 ]) 

233 

234 @classmethod 

235 def make_tex(cls, tex, fontsize): 

236 """ 

237 Generate a tex file to render the tex string at a specific font size. 

238 

239 Return the file name. 

240 """ 

241 texfile = cls.get_basefile(tex, fontsize) + ".tex" 

242 Path(texfile).write_text(cls._get_tex_source(tex, fontsize), 

243 encoding='utf-8') 

244 return texfile 

245 

246 @classmethod 

247 def _run_checked_subprocess(cls, command, tex, *, cwd=None): 

248 _log.debug(cbook._pformat_subprocess(command)) 

249 try: 

250 report = subprocess.check_output( 

251 command, cwd=cwd if cwd is not None else cls._texcache, 

252 stderr=subprocess.STDOUT) 

253 except FileNotFoundError as exc: 

254 raise RuntimeError( 

255 f'Failed to process string with tex because {command[0]} ' 

256 'could not be found') from exc 

257 except subprocess.CalledProcessError as exc: 

258 raise RuntimeError( 

259 '{prog} was not able to process the following string:\n' 

260 '{tex!r}\n\n' 

261 'Here is the full command invocation and its output:\n\n' 

262 '{format_command}\n\n' 

263 '{exc}\n\n'.format( 

264 prog=command[0], 

265 format_command=cbook._pformat_subprocess(command), 

266 tex=tex.encode('unicode_escape'), 

267 exc=exc.output.decode('utf-8', 'backslashreplace')) 

268 ) from None 

269 _log.debug(report) 

270 return report 

271 

272 @classmethod 

273 def make_dvi(cls, tex, fontsize): 

274 """ 

275 Generate a dvi file containing latex's layout of tex string. 

276 

277 Return the file name. 

278 """ 

279 basefile = cls.get_basefile(tex, fontsize) 

280 dvifile = '%s.dvi' % basefile 

281 if not os.path.exists(dvifile): 

282 texfile = Path(cls.make_tex(tex, fontsize)) 

283 # Generate the dvi in a temporary directory to avoid race 

284 # conditions e.g. if multiple processes try to process the same tex 

285 # string at the same time. Having tmpdir be a subdirectory of the 

286 # final output dir ensures that they are on the same filesystem, 

287 # and thus replace() works atomically. It also allows referring to 

288 # the texfile with a relative path (for pathological MPLCONFIGDIRs, 

289 # the absolute path may contain characters (e.g. ~) that TeX does 

290 # not support; n.b. relative paths cannot traverse parents, or it 

291 # will be blocked when `openin_any = p` in texmf.cnf). 

292 cwd = Path(dvifile).parent 

293 with TemporaryDirectory(dir=cwd) as tmpdir: 

294 tmppath = Path(tmpdir) 

295 cls._run_checked_subprocess( 

296 ["latex", "-interaction=nonstopmode", "--halt-on-error", 

297 f"--output-directory={tmppath.name}", 

298 f"{texfile.name}"], tex, cwd=cwd) 

299 (tmppath / Path(dvifile).name).replace(dvifile) 

300 return dvifile 

301 

302 @classmethod 

303 def make_png(cls, tex, fontsize, dpi): 

304 """ 

305 Generate a png file containing latex's rendering of tex string. 

306 

307 Return the file name. 

308 """ 

309 basefile = cls.get_basefile(tex, fontsize, dpi) 

310 pngfile = '%s.png' % basefile 

311 # see get_rgba for a discussion of the background 

312 if not os.path.exists(pngfile): 

313 dvifile = cls.make_dvi(tex, fontsize) 

314 cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), 

315 "-T", "tight", "-o", pngfile, dvifile] 

316 # When testing, disable FreeType rendering for reproducibility; but 

317 # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 

318 # mode, so for it we keep FreeType enabled; the image will be 

319 # slightly off. 

320 if (getattr(mpl, "_called_from_pytest", False) and 

321 mpl._get_executable_info("dvipng").raw_version != "1.16"): 

322 cmd.insert(1, "--freetype0") 

323 cls._run_checked_subprocess(cmd, tex) 

324 return pngfile 

325 

326 @classmethod 

327 def get_grey(cls, tex, fontsize=None, dpi=None): 

328 """Return the alpha channel.""" 

329 if not fontsize: 

330 fontsize = mpl.rcParams['font.size'] 

331 if not dpi: 

332 dpi = mpl.rcParams['savefig.dpi'] 

333 key = cls._get_tex_source(tex, fontsize), dpi 

334 alpha = cls._grey_arrayd.get(key) 

335 if alpha is None: 

336 pngfile = cls.make_png(tex, fontsize, dpi) 

337 rgba = mpl.image.imread(os.path.join(cls._texcache, pngfile)) 

338 cls._grey_arrayd[key] = alpha = rgba[:, :, -1] 

339 return alpha 

340 

341 @classmethod 

342 def get_rgba(cls, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): 

343 r""" 

344 Return latex's rendering of the tex string as an RGBA array. 

345 

346 Examples 

347 -------- 

348 >>> texmanager = TexManager() 

349 >>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!" 

350 >>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0)) 

351 """ 

352 alpha = cls.get_grey(tex, fontsize, dpi) 

353 rgba = np.empty((*alpha.shape, 4)) 

354 rgba[..., :3] = mpl.colors.to_rgb(rgb) 

355 rgba[..., -1] = alpha 

356 return rgba 

357 

358 @classmethod 

359 def get_text_width_height_descent(cls, tex, fontsize, renderer=None): 

360 """Return width, height and descent of the text.""" 

361 if tex.strip() == '': 

362 return 0, 0, 0 

363 dvifile = cls.make_dvi(tex, fontsize) 

364 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 

365 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: 

366 page, = dvi 

367 # A total height (including the descent) needs to be returned. 

368 return page.width, page.height + page.descent, page.descent