From a18e072a13f40fe7fafc163eb0cf2d00800ae5d1 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 31 Mar 2025 10:16:28 +0200 Subject: [PATCH 1/2] Switch Tfm metrics to TrueType-compatible API. Instead of directly exposing widths, heights, depths dicts, provide a get_metrics method to access a glyph's (tex) metrics. This change is in preparation for {xe,lua}tex support, which would use an alternative metrics-loading class (`TtfMetrics`) where it would be excessive to load the metrics of all glyphs at once. --- .../deprecations/29817-AL.rst | 4 + lib/matplotlib/backends/backend_pdf.py | 5 +- lib/matplotlib/dviread.py | 77 ++++++++++++------- lib/matplotlib/dviread.pyi | 17 +++- 4 files changed, 70 insertions(+), 33 deletions(-) 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..2b0edd1d54f0 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,23 @@ 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 + 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 From 5d0adf185d0e5ec74d709bdb23b29663aa3eec41 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 31 Mar 2025 17:13:09 -0400 Subject: [PATCH 2/2] TYP: work around mypy limitation --- lib/matplotlib/dviread.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 2b0edd1d54f0..f8d8f979fd8c 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -74,6 +74,8 @@ 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy