diff --git a/doc/api/next_api_changes/deprecations/29817-AL.rst b/doc/api/next_api_changes/deprecations/29817-AL.rst index 204751d1ea85..f3b339ed7c10 100644 --- a/doc/api/next_api_changes/deprecations/29817-AL.rst +++ b/doc/api/next_api_changes/deprecations/29817-AL.rst @@ -1,3 +1,7 @@ ``DviFont.widths`` ~~~~~~~~~~~~~~~~~~ ... is deprecated with no replacement. + +Direct access to ``Tfm``'s ``widths``, ``heights``, ``depths`` dicts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated; access a glyph's metrics with `.Tfm.get_metrics` instead. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 559a684050b8..8bb1736f3d27 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -993,8 +993,9 @@ def _embedTeXFont(self, fontinfo): widthsObject = self.reserveObject('font widths') tfm = fontinfo.dvifont._tfm # convert from TeX's 12.20 representation to 1/1000 text space units. - widths = [(1000 * tfm.width.get(char, 0)) >> 20 - for char in range(max(tfm.width, default=-1) + 1)] + widths = [(1000 * metrics.tex_width) >> 20 + if (metrics := tfm.get_metrics(char)) else 0 + for char in range(max(tfm._glyph_metrics, default=-1) + 1)] self.writeObject(widthsObject, widths) # Font dictionary diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 1e6dcfdc4409..3f05e1cf0c80 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -18,6 +18,7 @@ """ from collections import namedtuple +import dataclasses import enum from functools import cache, lru_cache, partial, wraps import logging @@ -604,32 +605,30 @@ def __repr__(self): def _width_of(self, char): """Width of char in dvi units.""" - width = self._tfm.width.get(char, None) - if width is not None: - return _mul1220(width, self._scale) - _log.debug('No width for char %d in font %s.', char, self.texname) - return 0 + metrics = self._tfm.get_metrics(char) + if metrics is None: + _log.debug('No width for char %d in font %s.', char, self.texname) + return 0 + return _mul1220(metrics.tex_width, self._scale) def _height_depth_of(self, char): """Height and depth of char in dvi units.""" - result = [] - for metric, name in ((self._tfm.height, "height"), - (self._tfm.depth, "depth")): - value = metric.get(char, None) - if value is None: - _log.debug('No %s for char %d in font %s', - name, char, self.texname) - result.append(0) - else: - result.append(_mul1220(value, self._scale)) + metrics = self._tfm.get_metrics(char) + if metrics is None: + _log.debug('No metrics for char %d in font %s', char, self.texname) + return [0, 0] + hd = [ + _mul1220(metrics.tex_height, self._scale), + _mul1220(metrics.tex_depth, self._scale), + ] # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent # so that TeX aligns equations properly # (https://tex.stackexchange.com/q/526103/) # but we actually care about the rasterization depth to align # the dvipng-generated images. if re.match(br'^cmsy\d+$', self.texname) and char == 0: - result[-1] = 0 - return result + hd[-1] = 0 + return hd class Vf(Dvi): @@ -761,6 +760,22 @@ def _mul1220(num1, num2): return (num1*num2) >> 20 +@dataclasses.dataclass(frozen=True, kw_only=True) +class TexMetrics: + """ + Metrics of a glyph, with TeX semantics. + + TeX metrics have different semantics from FreeType metrics: tex_width + corresponds to FreeType's ``advance`` (i.e., including whitespace padding); + tex_height to ``bearingY`` (how much the glyph extends over the baseline); + tex_depth to ``height - bearingY`` (how much the glyph extends under the + baseline, as a positive number). + """ + tex_width: int + tex_height: int + tex_depth: int + + class Tfm: """ A TeX Font Metric file. @@ -778,12 +793,7 @@ class Tfm: design_size : int Design size of the font (in 12.20 TeX points); unused because it is overridden by the scale factor specified in the dvi file. - width, height, depth : dict - Dimensions of each character, need to be scaled by the factor - specified in the dvi file. These are dicts because indexing may - not start from 0. """ - __slots__ = ('checksum', 'design_size', 'width', 'height', 'depth') def __init__(self, filename): _log.debug('opening tfm file %s', filename) @@ -799,15 +809,26 @@ def __init__(self, filename): widths = struct.unpack(f'!{nw}i', file.read(4*nw)) heights = struct.unpack(f'!{nh}i', file.read(4*nh)) depths = struct.unpack(f'!{nd}i', file.read(4*nd)) - self.width = {} - self.height = {} - self.depth = {} + self._glyph_metrics = {} for idx, char in enumerate(range(bc, ec+1)): byte0 = char_info[4*idx] byte1 = char_info[4*idx+1] - self.width[char] = widths[byte0] - self.height[char] = heights[byte1 >> 4] - self.depth[char] = depths[byte1 & 0xf] + self._glyph_metrics[char] = TexMetrics( + tex_width=widths[byte0], + tex_height=heights[byte1 >> 4], + tex_depth=depths[byte1 & 0xf], + ) + + def get_metrics(self, idx): + """Return a glyph's TexMetrics, or None if unavailable.""" + return self._glyph_metrics.get(idx) + + width = _api.deprecated("3.11", alternative="get_metrics")( + property(lambda self: {c: m.tex_width for c, m in self._glyph_metrics})) + height = _api.deprecated("3.11", alternative="get_metrics")( + property(lambda self: {c: m.tex_height for c, m in self._glyph_metrics})) + depth = _api.deprecated("3.11", alternative="get_metrics")( + property(lambda self: {c: m.tex_depth for c, m in self._glyph_metrics})) PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 29d9288c047f..f8d8f979fd8c 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -1,3 +1,4 @@ +import dataclasses from pathlib import Path import io import os @@ -68,13 +69,25 @@ class Vf(Dvi): def __init__(self, filename: str | os.PathLike) -> None: ... def __getitem__(self, code: int) -> Page: ... +@dataclasses.dataclass(frozen=True, kw_only=True) +class TexMetrics: + tex_width: int + tex_height: int + tex_depth: int + # work around mypy not respecting kw_only=True in stub files + __match_args__ = () + class Tfm: checksum: int design_size: int - width: dict[int, int] - height: dict[int, int] - depth: dict[int, int] def __init__(self, filename: str | os.PathLike) -> None: ... + def get_metrics(self, idx: int) -> TexMetrics | None: ... + @property + def width(self) -> dict[int, int]: ... + @property + def height(self) -> dict[int, int]: ... + @property + def depth(self) -> dict[int, int]: ... class PsFont(NamedTuple): texname: bytes