8000 Merge pull request #22725 from anntzer/tmfc · matplotlib/matplotlib@a5fec21 · GitHub
[go: up one dir, main page]

Skip to content

Commit a5fec21

Browse files
authored
Merge pull request #22725 from anntzer/tmfc
Move towards making texmanager stateless.
2 parents ee5d9c4 + 1314799 commit a5fec21

File tree

3 files changed

+125
-97
lines changed

3 files changed

+125
-97
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``TexManager.get_font_config``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
... is deprecated with no replacement. (It previously returned an internal
4+
hashed key for used for caching purposes.)

lib/matplotlib/tests/test_texmanager.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,14 @@
77

88

99
def test_fontconfig_preamble():
10-
"""Test that the preamble is included in _fontconfig."""
10+
"""Test that the preamble is included in the source."""
1111
plt.rcParams['text.usetex'] = True
1212

13-
tm1 = TexManager()
14-
font_config1 = tm1.get_font_config()
15-
13+
src1 = TexManager()._get_tex_source("", fontsize=12)
1614
plt.rcParams['text.latex.preamble'] = '\\usepackage{txfonts}'
17-
tm2 = TexManager()
18-
font_config2 = tm2.get_font_config()
15+
src2 = TexManager()._get_tex_source("", fontsize=12)
1916

20-
assert font_config1 != font_config2
17+
assert src1 != src2
2118

2219

2320
@pytest.mark.parametrize(

lib/matplotlib/texmanager.py

Lines changed: 117 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,44 @@ class TexManager:
6161
"""
6262

6363
texcache = os.path.join(mpl.get_cachedir(), 'tex.cache')
64-
6564
_grey_arrayd = {}
66-
_font_family = 'serif'
65+
6766
_font_families = ('serif', 'sans-serif', 'cursive', 'monospace')
68-
_font_info = {
69-
'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'),
70-
'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'),
71-
'times': ('ptm', r'\usepackage{mathptmx}'),
72-
'palatino': ('ppl', r'\usepackage{mathpazo}'),
73-
'zapf chancery': ('pzc', r'\usepackage{chancery}'),
74-
'cursive': ('pzc', r'\usepackage{chancery}'),
75-
'charter': ('pch', r'\usepackage{charter}'),
76-
'serif': ('cmr', ''),
77-
'sans-serif': ('cmss', ''),
78-
'helvetica': ('phv', r'\usepackage{helvet}'),
79-
'avant garde': ('pag', r'\usepackage{avant}'),
80-
'courier': ('pcr', r'\usepackage{courier}'),
67+
_font_preambles = {
68+
'new century schoolbook': r'\renewcommand{\rmdefault}{pnc}',
69+
'bookman': r'\renewcommand{\rmdefault}{pbk}',
70+
'times': r'\usepackage{mathptmx}',
71+
'palatino': r'\usepackage{mathpazo}',
72+
'zapf chancery': r'\usepackage{chancery}',
73+
'cursive': r'\usepackage{chancery}',
74+
'charter': r'\usepackage{charter}',
75+
'serif': '',
76+
'sans-serif': '',
77+
'helvetica': r'\usepackage{helvet}',
78+
'avant garde': r'\usepackage{avant}',
79+
'courier': r'\usepackage{courier}',
8180
# Loading the type1ec package ensures that cm-super is installed, which
8281
# is necessary for Unicode computer modern. (It also allows the use of
8382
# computer modern at arbitrary sizes, but that's just a side effect.)
84-
'monospace': ('cmtt', r'\usepackage{type1ec}'),
85-
'computer modern roman': ('cmr', r'\usepackage{type1ec}'),
86-
'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'),
87-
'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')}
83+
'monospace': r'\usepackage{type1ec}',
84+
'computer modern roman': r'\usepackage{type1ec}',
85+
'computer modern sans serif': r'\usepackage{type1ec}',
86+
'computer modern typewriter': r'\usepackage{type1ec}',
87+
}
8888
_font_types = {
89-
'new century schoolbook': 'serif', 'bookman': 'serif',
90-
'times': 'serif', 'palatino': 'serif', 'charter': 'serif',
91-
'computer modern roman': 'serif', 'zapf chancery': 'cursive',
92-
'helvetica': 'sans-serif', 'avant garde': 'sans-serif',
89+
'new century schoolbook': 'serif',
90+
'bookman': 'serif',
91+
'times': 'serif',
92+
'palatino': 'serif',
93+
'zapf chancery': 'cursive',
94+
'charter': 'serif',
95+
'helvetica': 'sans-serif',
96+
'avant garde': 'sans-serif',
97+
'courier': 'monospace',
98+
'computer modern roman': 'serif',
9399
'computer modern sans serif': 'sans-serif',
94-
'courier': 'monospace', 'computer modern typewriter': 'monospace'}
100+
'computer modern typewriter': 'monospace',
101+
}
95102

96103
grey_arrayd = _api.deprecate_privatize_attribute("3.5")
97104
font_family = _api.deprecate_privatize_attribute("3.5")
@@ -103,33 +110,48 @@ def __new__(cls):
103110
Path(cls.texcache).mkdir(parents=True, exist_ok=True)
104111
return object.__new__(cls)
105112

113+
@_api.deprecated("3.6")
106114
def get_font_config(self):
115+
preamble, font_cmd = self._get_font_preamble_and_command()
116+
# Add a hash of the latex preamble to fontconfig so that the
117+
# correct png is selected for strings rendered with same font and dpi
118+
# even if the latex preamble changes within the session
119+
preambles = preamble + font_cmd + self.get_custom_preamble()
120+
return hashlib.md5(preambles.encode('utf-8')).hexdigest()
121+
122+
@classmethod
123+
def _get_font_family_and_reduced(cls):
124+
"""Return the font family name and whether the font is reduced."""
107125
ff = rcParams['font.family']
108126
ff_val = ff[0].lower() if len(ff) == 1 else None
109-
reduced_notation = False
110-
if len(ff) == 1 and ff_val in self._font_families:
111-
self._font_family = ff_val
112-
elif len(ff) == 1 and ff_val in self._font_info:
113-
reduced_notation = True
114-
self._font_family = self._font_types[ff_val]
127+
if len(ff) == 1 and ff_val in cls._font_families:
128+
return ff_val, False
129+
elif len(ff) == 1 and ff_val in cls._font_preambles:
130+
return cls._font_types[ff_val], True
115131
else:
116132
_log.info('font.family must be one of (%s) when text.usetex is '
117133
'True. serif will be used by default.',
118-
', '.join(self._font_families))
119-
self._font_family = 'serif'
120-
121-
fontconfig = [self._font_family]
122-
fonts = {}
123-
for font_family in self._font_families:
124-
if reduced_notation and self._font_family == font_family:
125-
fonts[font_family] = self._font_info[ff_val]
134+
', '.join(cls._font_families))
135+
return 'serif', False
136+
137+
@classmethod
138+
def _get_font_preamble_and_command(cls):
139+
requested_family, is_reduced_font = cls._get_font_family_and_reduced()
140+
141+
preambles = {}
142+
for font_family in cls._font_families:
143+
if is_reduced_font and font_family == requested_family:
144+
preambles[font_family] = cls._font_preambles[
145+
rcParams['font.family'][0].lower()]
126146
else:
127147
for font in rcParams['font.' + font_family]:
128-
if font.lower() in self._font_info:
129-
fonts[font_family] = self._font_info[font.lower()]
148+
if font.lower() in cls._font_preambles:
149+
preambles[font_family] = \
150+
cls._font_preambles[font.lower()]
130151
_log.debug(
131152
'family: %s, font: %s, info: %s',
132-
font_family, font, self._font_info[font.lower()])
153+
font_family, font,
154+
cls._font_preambles[font.lower()])
133155
break
134156
else:
135157
_log.debug('%s font is not compatible with usetex.',
@@ -138,64 +160,62 @@ def get_font_config(self):
138160
_log.info('No LaTeX-compatible font found for the %s font'
139161
'family in rcParams. Using default.',
140162
font_family)
141-
fonts[font_family] = self._font_info[font_family]
142-
fontconfig.append(fonts[font_family][0])
143-
# Add a hash of the latex preamble to 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())
163+
preambles[font_family] = cls._font_preambles[font_family]
148164

149165
# The following packages and commands need to be included in the latex
150166
# file's preamble:
151-
cmd = {fonts[family][1]
167+
cmd = {preambles[family]
152168
for family in ['serif', 'sans-serif', 'monospace']}
153-
if self._font_family == 'cursive':
154-
cmd.add(fonts['cursive'][1])
169+
if requested_family == 'cursive':
170+
cmd.add(preambles['cursive'])
155171
cmd.add(r'\usepackage{type1cm}')
156-
self._font_preamble = '\n'.join(sorted(cmd))
157-
158-
return ''.join(fontconfig)
172+
preamble = '\n'.join(sorted(cmd))
173+
fontcmd = (r'\sffamily' if requested_family == 'sans-serif' else
174+
r'\ttfamily' if requested_family == 'monospace' else
175+
r'\rmfamily')
176+
return preamble, fontcmd
159177

160-
def get_basefile(self, tex, fontsize, dpi=None):
178+
@classmethod
179+
def get_basefile(cls, tex, fontsize, dpi=None):
161180
"""
162181
Return a filename based on a hash of the string, fontsize, and dpi.
163182
"""
164-
src = self._get_tex_source(tex, fontsize) + str(dpi)
183+
src = cls._get_tex_source(tex, fontsize) + str(dpi)
165184
return os.path.join(
166-
self.texcache, hashlib.md5(src.encode('utf-8')).hexdigest())
185+
cls.texcache, hashlib.md5(src.encode('utf-8')).hexdigest())
167186

168-
def get_font_preamble(self):
187+
@classmethod
188+
def get_font_preamble(cls):
169189
"""
170190
Return a string containing font configuration for the tex preamble.
171191
"""
172-
return self._font_preamble
192+
font_preamble, command = cls._get_font_preamble_and_command()
193+
return font_preamble
173194

174-
def get_custom_preamble(self):
195+
@classmethod
196+
def get_custom_preamble(cls):
175197
"""Return a string containing user additions to the tex preamble."""
176198
return rcParams['text.latex.preamble']
177199

178-
def _get_tex_source(self, tex, fontsize):
200+
@classmethod
201+
def _get_tex_source(cls, tex, fontsize):
179202
"""Return the complete TeX source for processing a TeX string."""
180-
self.get_font_config() # Updates self._font_preamble.
203+
font_preamble, fontcmd = cls._get_font_preamble_and_command()
181204
baselineskip = 1.25 * fontsize
182-
fontcmd = (r'\sffamily' if self._font_family == 'sans-serif' else
183-
r'\ttfamily' if self._font_family == 'monospace' else
184-
r'\rmfamily')
185205
return "\n".join([
186206
r"\documentclass{article}",
187207
r"% Pass-through \mathdefault, which is used in non-usetex mode",
188208
r"% to use the default text font but was historically suppressed",
189209
r"% in usetex mode.",
190210
r"\newcommand{\mathdefault}[1]{#1}",
191-
self._font_preamble,
211+
font_preamble,
192212
r"\usepackage[utf8]{inputenc}",
193213
r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}",
194214
r"% geometry is loaded before the custom preamble as ",
195215
r"% convert_psfrags relies on a custom preamble to change the ",
196216
r"% geometry.",
197217
r"\usepackage[papersize=72in, margin=1in]{geometry}",
198-
self.get_custom_preamble(),
218+
cls.get_custom_preamble(),
199219
r"% Use `underscore` package to take care of underscores in text.",
200220
r"% The [strings] option allows to use underscores in file names.",
201221
_usepackage_if_not_loaded("underscore", option="strings"),
@@ -215,21 +235,23 @@ def _get_tex_source(self, tex, fontsize):
215235
r"\end{document}",
216236
])
217237

218-
def make_tex(self, tex, fontsize):
238+
@classmethod
239+
def make_tex(cls, tex, fontsize):
219240
"""
220241
Generate a tex file to render the tex string at a specific font size.
221242
222243
Return the file name.
223244
"""
224-
texfile = self.get_basefile(tex, fontsize) + ".tex"
225-
Path(texfile).write_text(self._get_tex_source(tex, fontsize))
245+
texfile = cls.get_basefile(tex, fontsize) + ".tex"
246+
Path(texfile).write_text(cls._get_tex_source(tex, fontsize))
226247
return texfile
227248

228-
def _run_checked_subprocess(self, command, tex, *, cwd=None):
249+
@classmethod
250+
def _run_checked_subprocess(cls, command, tex, *, cwd=None):
229251
_log.debug(cbook._pformat_subprocess(command))
230252
try:
231253
report = subprocess.check_output(
232-
command, cwd=cwd if cwd is not None else self.texcache,
254+
command, cwd=cwd if cwd is not None else cls.texcache,
233255
stderr=subprocess.STDOUT)
234256
except FileNotFoundError as exc:
235257
raise RuntimeError(
@@ -247,16 +269,17 @@ def _run_checked_subprocess(self, command, tex, *, cwd=None):
247269
_log.debug(report)
248270
return report
249271

250-
def make_dvi(self, tex, fontsize):
272+
@classmethod
273+
def make_dvi(cls, tex, fontsize):
251274
"""
252275
Generate a dvi file containing latex's layout of tex string.
253276
254277
Return the file name.
255278
"""
256-
basefile = self.get_basefile(tex, fontsize)
279+
basefile = cls.get_basefile(tex, fontsize)
257280
dvifile = '%s.dvi' % basefile
258281
if not os.path.exists(dvifile):
259-
texfile = Path(self.make_tex(tex, fontsize))
282+
texfile = Path(cls.make_tex(tex, fontsize))
260283
# Generate the dvi in a temporary directory to avoid race
261284
# conditions e.g. if multiple processes try to process the same tex
262285
# string at the same time. Having tmpdir be a subdirectory of the
@@ -266,23 +289,24 @@ def make_dvi(self, tex, fontsize):
266289
# the absolute path may contain characters (e.g. ~) that TeX does
267290
# not support.)
268291
with TemporaryDirectory(dir=Path(dvifile).parent) as tmpdir:
269-
self._run_checked_subprocess(
292+
cls._run_checked_subprocess(
270293
["latex", "-interaction=nonstopmode", "--halt-on-error",
271294
f"../{texfile.name}"], tex, cwd=tmpdir)
272295
(Path(tmpdir) / Path(dvifile).name).replace(dvifile)
273296
return dvifile
274297

275-
def make_png(self, tex, fontsize, dpi):
298+
@classmethod
299+
def make_png(cls, tex, fontsize, dpi):
276300
"""
277301
Generate a png file containing latex's rendering of tex string.
278302
279303
Return the file name.
280304
"""
281-
basefile = self.get_basefile(tex, fontsize, dpi)
305+
basefile = cls.get_basefile(tex, fontsize, dpi)
282306
pngfile = '%s.png' % basefile
283307
# see get_rgba for a discussion of the background
284308
if not os.path.exists(pngfile):
285-
dvifile = self.make_dvi(tex, fontsize)
309+
dvifile = cls.make_dvi(tex, fontsize)
286310
cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
287311
"-T", "tight", "-o", pngfile, dvifile]
288312
# When testing, disable FreeType rendering for reproducibility; but
@@ -292,24 +316,26 @@ def make_png(self, tex, fontsize, dpi):
292316
if (getattr(mpl, "_called_from_pytest", False) and
293317
mpl._get_executable_info("dvipng").raw_version != "1.16"):
294318
cmd.insert(1, "--freetype0")
295-
self._run_checked_subprocess(cmd, tex)
319+
cls._run_checked_subprocess(cmd, tex)
296320
return pngfile
297321

298-
def get_grey(self, tex, fontsize=None, dpi=None):
322+
@classmethod
323+
def get_grey(cls, tex, fontsize=None, dpi=None):
299324
"""Return the alpha channel."""
300325
if not fontsize:
301326
fontsize = rcParams['font.size']
302327
if not dpi:
303328
dpi = rcParams['savefig.dpi']
304-
key = tex, self.get_font_config(), fontsize, dpi
305-
alpha = self._grey_arrayd.get(key)
329+
key = cls._get_tex_source(tex, fontsize), dpi
330+
alpha = cls._grey_arrayd.get(key)
306331
if alpha is None:
307-
pngfile = self.make_png(tex, fontsize, dpi)
308-
rgba = mpl.image.imread(os.path.join(self.texcache, pngfile))
309-
self._grey_arrayd[key] = alpha = rgba[:, :, -1]
332+
pngfile = cls.make_png(tex, fontsize, dpi)
333+
rgba = mpl.image.imread(os.path.join(cls.texcache, pngfile))
334+
cls._grey_arrayd[key] = alpha = rgba[:, :, -1]
310335
return alpha
311336

312-
def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
337+
@classmethod
338+
def get_rgba(cls, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
313339
r"""
314340
Return latex's rendering of the tex string as an rgba array.
315341
@@ -319,17 +345,18 @@ def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
319345
>>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!"
320346
>>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0))
321347
"""
322-
alpha = self.get_grey(tex, fontsize, dpi)
348+
alpha = cls.get_grey(tex, fontsize, dpi)
323349
rgba = np.empty((*alpha.shape, 4))
324350
rgba[..., :3] = mpl.colors.to_rgb(rgb)
325351
rgba[..., -1] = alpha
326352
return rgba
327353

328-
def get_text_width_height_descent(self, tex, fontsize, renderer=None):
354+
@classmethod
355+
def get_text_width_height_descent(cls, tex, fontsize, renderer=None):
329356
"""Return width, height and descent of the text."""
330357
if tex.strip() == '':
331358
return 0, 0, 0
332-
dvifile = self.make_dvi(tex, fontsize)
359+
dvifile = cls.make_dvi(tex, fontsize)
333360
dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
334361
with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
335362
page, = dvi

0 commit comments

Comments
 (0)
0