From d04f64f91d21aec5ae589258b3e892359eed30e8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 19 Jun 2021 13:39:42 +0200 Subject: [PATCH] Various cleanups to pgf backend. - Replace _font_properties_str by _escape_and_apply_props, which handles all the preprocessing needed before sending a str to tex. (This means that LatexManager.get_text_width_height_descent now additionally handles unescaped strings as well, too.) - Replace str_cache by a (per-instance) lru_cache. - When querying tex for a string's metrics, send both `\sbox` and `\typeout` in a single go, which saves one cycle of waiting for the interactive prompt. --- .../deprecations/20466-AL.rst | 3 + lib/matplotlib/backends/backend_pgf.py | 76 ++++++++----------- 2 files changed, 35 insertions(+), 44 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/20466-AL.rst diff --git a/doc/api/next_api_changes/deprecations/20466-AL.rst b/doc/api/next_api_changes/deprecations/20466-AL.rst new file mode 100644 index 000000000000..d986ab87273f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20466-AL.rst @@ -0,0 +1,3 @@ +``backend_pgf.LatexManager.str_cache`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated. diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 10dd817b1764..575d2d263496 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -122,8 +122,11 @@ def writeln(fh, line): fh.write("%\n") -def _font_properties_str(prop): - # translate font properties to latex commands, return as string +def _escape_and_apply_props(s, prop): + """ + Generate a TeX string that renders string *s* with font properties *prop*, + also applying any required escapes to *s*. + """ commands = [] families = {"serif": r"\rmfamily", "sans": r"\sffamily", @@ -149,7 +152,7 @@ def _font_properties_str(prop): commands.append(r"\bfseries") commands.append(r"\selectfont") - return "".join(commands) + return "".join(commands) + " " + common_texification(s) def _metadata_to_str(key, value): @@ -296,7 +299,10 @@ def __init__(self): "or error in preamble.", stdout) self.latex = None # Will be set up on first use. - self.str_cache = {} # cache for strings already processed + # Per-instance cache. + self._get_box_metrics = functools.lru_cache()(self._get_box_metrics) + + str_cache = _api.deprecated("3.5")(property(lambda self: {})) def _setup_latex_process(self): # Open LaTeX process for real work; register it for deletion. On @@ -322,46 +328,34 @@ def finalize_latex(latex): def get_width_height_descent(self, text, prop): """ - Get the width, total height and descent for a text typeset by the - current LaTeX environment. + Get the width, total height, and descent (in TeX points) for a text + typeset by the current LaTeX environment. """ + return self._get_box_metrics(_escape_and_apply_props(text, prop)) - # apply font properties and define textbox - prop_cmds = _font_properties_str(prop) - textbox = "\\sbox0{%s %s}" % (prop_cmds, text) - - # check cache - if textbox in self.str_cache: - return self.str_cache[textbox] - - # send textbox to LaTeX and wait for prompt - self._stdin_writeln(textbox) - try: - self._expect_prompt() - except LatexError as e: - raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" - .format(text, e.latex_output)) from e - - # typeout width, height and text offset of the last textbox - self._stdin_writeln(r"\typeout{\the\wd0,\the\ht0,\the\dp0}") - # read answer from latex and advance to the next prompt + def _get_box_metrics(self, tex): + """ + Get the width, total height and descent (in TeX points) for a TeX + command's output in the current LaTeX environment. + """ + # This method gets wrapped in __init__ for per-instance caching. + self._stdin_writeln( # Send textbox to TeX & request metrics typeout. + r"\sbox0{%s}\typeout{\the\wd0,\the\ht0,\the\dp0}" % tex) try: answer = self._expect_prompt() - except LatexError as e: - raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" - .format(text, e.latex_output)) from e - - # parse metrics from the answer string + except LatexError as err: + raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}" + .format(tex, err.latex_output)) from err try: - width, height, offset = answer.splitlines()[0].split(",") + # Parse metrics from the answer string. Last line is prompt, and + # next-to-last-line is blank line from \typeout. + width, height, offset = answer.splitlines()[-3].split(",") except Exception as err: - raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" - .format(text, answer)) from err + raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}" + .format(tex, answer)) from err w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2]) - - # the height returned from LaTeX goes from base to top. - # the height matplotlib expects goes from bottom to top. - self.str_cache[textbox] = (w, h + o, o) + # The height returned from LaTeX goes from base to top; + # the height Matplotlib expects goes from bottom to top. return w, h + o, o @@ -671,9 +665,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited # prepare string for tex - s = common_texification(s) - prop_cmds = _font_properties_str(prop) - s = r"%s %s" % (prop_cmds, s) + s = _escape_and_apply_props(s, prop) writeln(self.fh, r"\begin{pgfscope}") @@ -718,10 +710,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited - - # check if the math is supposed to be displaystyled - s = common_texification(s) - # get text metrics in units of latex pt, convert to display units w, h, d = (LatexManager._get_cached_or_new() .get_width_height_descent(s, prop))