Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Switch Tfm metrics to TrueType-compatible API. #29838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/next_api_changes/deprecations/29817-AL.rst
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 49 additions & 28 deletions lib/matplotlib/dviread.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

from collections import namedtuple
import dataclasses
import enum
from functools import cache, lru_cache, partial, wraps
import logging
Expand Down Expand Up @@ -604,32 +605,30 @@

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

Check warning on line 611 in lib/matplotlib/dviread.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/dviread.py#L610-L611

Added lines #L610 - L611 were not covered by tests
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]

Check warning on line 619 in lib/matplotlib/dviread.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/dviread.py#L618-L619

Added lines #L618 - L619 were not covered by tests
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):
Expand Down Expand Up @@ -761,6 +760,22 @@
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.
Expand All @@ -778,12 +793,7 @@
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)
Expand All @@ -799,15 +809,26 @@
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')
Expand Down
19 changes: 16 additions & 3 deletions lib/matplotlib/dviread.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from pathlib import Path
import io
import os
Expand Down Expand Up @@ -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
Expand Down
Loading