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
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: