Skip to content

Commit 412cabc

Browse files
committed
Rework/fix Text layout cache.
Instead of caching the text layout based on a bunch of properties, only cache the computation of the text's metrics, which 1) should be the most expensive part (everything else in _get_layout is relatively simple iteration and arithmetic) and 2) depends on fewer implicit parameters. In fact, the old cache key was insufficient in that it would conflate usetex and non-usetex strings together, even though they have different metrics; e.g. with the (extremely artificial) example ```python figtext(.1, .5, "foo\nbar", size=32) # (0) figtext(.1, .5, "foo\nbar", usetex=True, size=32, c="r", alpha=.5) # (1) figtext(.3, .5, "foo\nbar", usetex=True, size=32, c="r", alpha=.5) # (2) ``` the linespacing of the first usetex string (1) would be "wrong": it is bigger that the one of the second usetex string (2), because it instead reuses the layout computed for the non-usetex string (0). The motivation is also to in the future let the renderer have better control on cache invalidation (with a yet-to-be-added renderer method), e.g. multiple instances of the same renderer cache could share the same layout info.
1 parent 76012ae commit 412cabc

File tree

2 files changed

+43
-23
lines changed

2 files changed

+43
-23
lines changed

lib/matplotlib/tests/test_text.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,3 +764,26 @@ def test_pdf_chars_beyond_bmp():
764764
plt.rcParams['mathtext.fontset'] = 'stixsans'
765765
plt.figure()
766766
plt.figtext(0.1, 0.5, "Mass $m$ \U00010308", size=30)
767+
768+
769+
def test_metrics_cache():
770+
fig = plt.figure()
771+
fig.text(.3, .5, "foo\nbar")
772+
fig.text(.3, .5, "foo\nbar", usetex=True)
773+
fig.text(.5, .5, "foo\nbar", usetex=True)
774+
fig.canvas.draw()
775+
renderer = fig._cachedRenderer
776+
ys = {} # mapping of strings to where they were drawn in y with draw_tex.
777+
778+
def call(*args, **kwargs):
779+
renderer, x, y, s, *_ = args
780+
ys.setdefault(s, set()).add(y)
781+
782+
renderer.draw_tex = call
783+
fig.canvas.draw()
784+
assert [*ys] == ["foo", "bar"]
785+
# Check that both TeX strings were drawn with the same y-position for both
786+
# single-line substrings. Previously, there used to be an incorrect cache
787+
# collision with the non-TeX string (drawn first here) whose metrics would
788+
# get incorrectly reused by the first TeX string.
789+
assert len(ys["foo"]) == len(ys["bar"]) == 1

lib/matplotlib/text.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -273,30 +273,29 @@ def update_from(self, other):
273273
self._linespacing = other._linespacing
274274
self.stale = True
275275

276-
def _get_layout_cache_key(self, renderer):
277-
"""
278-
Return a hashable tuple of properties that lets `_get_layout` know
279-
whether a previously computed layout can be reused.
280-
"""
281-
return (
282-
self.get_unitless_position(), self.get_text(),
283-
hash(self._fontproperties),
284-
self._verticalalignment, self._horizontalalignment,
285-
self._linespacing,
286-
self._rotation, self._rotation_mode, self._transform_rotates_text,
287-
self.figure.dpi, weakref.ref(renderer),
276+
def _get_text_metrics_with_cache(
277+
self, renderer, text, fontproperties, ismath):
278+
"""
279+
Call ``renderer.get_text_width_height_descent``, caching the results.
280+
"""
281+
cache_key = (
282+
weakref.ref(renderer),
283+
text,
284+
hash(fontproperties),
285+
ismath,
286+
self.figure.dpi,
288287
)
288+
if cache_key not in self._cached:
289+
self._cached[cache_key] = renderer.get_text_width_height_descent(
290+
text, fontproperties, ismath)
291+
return self._cached[cache_key]
289292

290293
def _get_layout(self, renderer):
291294
"""
292295
Return the extent (bbox) of the text together with
293296
multiple-alignment information. Note that it returns an extent
294297
of a rotated text when necessary.
295298
"""
296-
key = self._get_layout_cache_key(renderer=renderer)
297-
if key in self._cached:
298-
return self._cached[key]
299-
300299
thisx, thisy = 0.0, 0.0
301300
lines = self.get_text().split("\n") # Ensures lines is not empty.
302301

@@ -306,16 +305,16 @@ def _get_layout(self, renderer):
306305
ys = []
307306

308307
# Full vertical extent of font, including ascenders and descenders:
309-
_, lp_h, lp_d = renderer.get_text_width_height_descent(
310-
"lp", self._fontproperties,
308+
_, lp_h, lp_d = self._get_text_metrics_with_cache(
309+
renderer, "lp", self._fontproperties,
311310
ismath="TeX" if self.get_usetex() else False)
312311
min_dy = (lp_h - lp_d) * self._linespacing
313312

314313
for i, line in enumerate(lines):
315314
clean_line, ismath = self._preprocess_math(line)
316315
if clean_line:
317-
w, h, d = renderer.get_text_width_height_descent(
318-
clean_line, self._fontproperties, ismath=ismath)
316+
w, h, d = self._get_text_metrics_with_cache(
317+
renderer, clean_line, self._fontproperties, ismath)
319318
else:
320319
w = h = d = 0
321320

@@ -439,9 +438,7 @@ def _get_layout(self, renderer):
439438
# now rotate the positions around the first (x, y) position
440439
xys = M.transform(offset_layout) - (offsetx, offsety)
441440

442-
ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
443-
self._cached[key] = ret
444-
return ret
441+
return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
445442

446443
def set_bbox(self, rectprops):
447444
"""

0 commit comments

Comments
 (0)