From 8ad5931a68bceb47019590a4cbf501237c805825 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 3 Jun 2025 18:59:00 -0400 Subject: [PATCH 1/3] TYP: Make glyph indices distinct from character codes Previously, these were both typed as `int`, which means you could mix and match them erroneously. While the character code can't be made a distinct type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means these can't be fully swapped. Unfortunately, you can still go back to the base type, so glyph indices still work as character codes. But this is still sufficient to catch errors such as the wrong call to `FT2Font.get_kerning` in `_mathtext.py`. --- .../next_api_changes/development/30143-ES.rst | 7 +++++ lib/matplotlib/_afm.py | 19 +++++++------ lib/matplotlib/_mathtext.py | 28 +++++++++---------- lib/matplotlib/_mathtext_data.py | 18 +++++++----- lib/matplotlib/_text_helpers.py | 4 +-- lib/matplotlib/dviread.pyi | 7 +++-- lib/matplotlib/ft2font.pyi | 22 +++++++++------ lib/matplotlib/tests/test_ft2font.py | 5 ++-- src/ft2font_wrapper.cpp | 3 ++ 9 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 doc/api/next_api_changes/development/30143-ES.rst diff --git a/doc/api/next_api_changes/development/30143-ES.rst b/doc/api/next_api_changes/development/30143-ES.rst new file mode 100644 index 000000000000..2d79ad6bbe9d --- /dev/null +++ b/doc/api/next_api_changes/development/30143-ES.rst @@ -0,0 +1,7 @@ +Glyph indices now typed distinctly from character codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, character codes and glyph indices were both typed as `int`, which means you +could mix and match them erroneously. While the character code can't be made a distinct +type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means +these can't be fully swapped. diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py index 352d3c42247e..3d7f7a44baca 100644 --- a/lib/matplotlib/_afm.py +++ b/lib/matplotlib/_afm.py @@ -30,9 +30,10 @@ import inspect import logging import re -from typing import BinaryIO, NamedTuple, TypedDict +from typing import BinaryIO, NamedTuple, TypedDict, cast from ._mathtext_data import uni2type1 +from .ft2font import CharacterCodeType, GlyphIndexType _log = logging.getLogger(__name__) @@ -197,7 +198,7 @@ class CharMetrics(NamedTuple): The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" -def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics], +def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[CharacterCodeType, CharMetrics], dict[str, CharMetrics]]: """ Parse the given filehandle for character metrics information. @@ -218,7 +219,7 @@ def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics], """ required_keys = {'C', 'WX', 'N', 'B'} - ascii_d: dict[int, CharMetrics] = {} + ascii_d: dict[CharacterCodeType, CharMetrics] = {} name_d: dict[str, CharMetrics] = {} for bline in fh: # We are defensively letting values be utf8. The spec requires @@ -409,19 +410,21 @@ def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]: return left, miny, total_width, maxy - miny, -miny - def get_glyph_name(self, glyph_ind: int) -> str: # For consistency with FT2Font. + def get_glyph_name(self, # For consistency with FT2Font. + glyph_ind: GlyphIndexType) -> str: """Get the name of the glyph, i.e., ord(';') is 'semicolon'.""" - return self._metrics[glyph_ind].name + return self._metrics[cast(CharacterCodeType, glyph_ind)].name - def get_char_index(self, c: int) -> int: # For consistency with FT2Font. + def get_char_index(self, # For consistency with FT2Font. + c: CharacterCodeType) -> GlyphIndexType: """ Return the glyph index corresponding to a character code point. Note, for AFM fonts, we treat the glyph index the same as the codepoint. """ - return c + return cast(GlyphIndexType, c) - def get_width_char(self, c: int) -> float: + def get_width_char(self, c: CharacterCodeType) -> float: """Get the width of the character code from the character metric WX field.""" return self._metrics[c].width diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 19ddbb6d0883..afaa9ade6018 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -37,7 +37,8 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import Glyph + from .ft2font import CharacterCodeType, Glyph + ParserElement.enable_packrat() _log = logging.getLogger("matplotlib.mathtext") @@ -47,7 +48,7 @@ # FONTS -def get_unicode_index(symbol: str) -> int: # Publicly exported. +def get_unicode_index(symbol: str) -> CharacterCodeType: # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. @@ -85,7 +86,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, int, float, float]] + glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -212,7 +213,7 @@ class FontInfo(NamedTuple): fontsize: float postscript_name: str metrics: FontMetrics - num: int + num: CharacterCodeType glyph: Glyph offset: float @@ -365,7 +366,7 @@ def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float, return 0. def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: raise NotImplementedError # The return value of _get_info is cached per-instance. @@ -459,7 +460,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -551,7 +552,7 @@ class UnicodeFonts(TruetypeFonts): # Some glyphs are not present in the `cmr10` font, and must be brought in # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at # which they are found in `cmsy10`. - _cmr10_substitutions = { + _cmr10_substitutions: dict[CharacterCodeType, CharacterCodeType] = { 0x00D7: 0x00A3, # Multiplication sign. 0x2212: 0x00A1, # Minus sign. } @@ -594,11 +595,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: return fontname, uniindex def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: try: uniindex = get_unicode_index(sym) found_symbol = True @@ -607,8 +608,7 @@ def _get_glyph(self, fontname: str, font_class: str, found_symbol = False _log.warning("No TeX to Unicode mapping for %a.", sym) - fontname, uniindex = self._map_virtual_font( - fontname, font_class, uniindex) + fontname, uniindex = self._map_virtual_font(fontname, font_class, uniindex) new_fontname = fontname @@ -693,7 +693,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: # Override prime symbol to use Bakoma. if sym == r'\prime': return self.bakoma._get_glyph(fontname, font_class, sym) @@ -783,7 +783,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: # Handle these "fonts" that are actually embedded in # other fonts. font_mapping = stix_virtual_fonts.get(fontname) @@ -1170,7 +1170,7 @@ def __init__(self, elements: T.Sequence[Node]): self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching self.glue_order = 0 # The order of infinity (0 - 3) for the glue - def __repr__(self): + def __repr__(self) -> str: return "{}[{}]".format( super().__repr__(), self.width, self.height, diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 5819ee743044..0451791e9f26 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -3,9 +3,12 @@ """ from __future__ import annotations -from typing import overload +from typing import TypeAlias, overload -latex_to_bakoma = { +from .ft2font import CharacterCodeType + + +latex_to_bakoma: dict[str, tuple[str, CharacterCodeType]] = { '\\__sqrt__' : ('cmex10', 0x70), '\\bigcap' : ('cmex10', 0x5c), '\\bigcup' : ('cmex10', 0x5b), @@ -241,7 +244,7 @@ # Automatically generated. -type12uni = { +type12uni: dict[str, CharacterCodeType] = { 'aring' : 229, 'quotedblright' : 8221, 'V' : 86, @@ -475,7 +478,7 @@ # for key in sd: # print("{0:24} : {1: dict[str, float]: ... @property - def index(self) -> int: ... # type: ignore[override] + def index(self) -> GlyphIndexType: ... # type: ignore[override] @property - def glyph_name_or_index(self) -> int | str: ... + def glyph_name_or_index(self) -> GlyphIndexType | str: ... class Dvi: file: io.BufferedReader diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index a413cd3c1a76..de04dcd9aadd 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,6 +1,6 @@ from enum import Enum, Flag import sys -from typing import BinaryIO, Literal, TypedDict, final, overload, cast +from typing import BinaryIO, Literal, NewType, TypeAlias, TypedDict, cast, final, overload from typing_extensions import Buffer # < Py 3.12 import numpy as np @@ -9,6 +9,12 @@ from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str +# We can't change the type hints for standard library chr/ord, so character codes are a +# simple type alias. +CharacterCodeType: TypeAlias = int +# But glyph indices are internal, so use a distinct type hint. +GlyphIndexType = NewType('GlyphIndexType', int) + class FaceFlags(Flag): SCALABLE = cast(int, ...) FIXED_SIZES = cast(int, ...) @@ -202,13 +208,13 @@ class FT2Font(Buffer): ) -> None: ... def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ... def get_bitmap_offset(self) -> tuple[int, int]: ... - def get_char_index(self, codepoint: int) -> int: ... - def get_charmap(self) -> dict[int, int]: ... + def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ... + def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ... def get_descent(self) -> int: ... - def get_glyph_name(self, index: int) -> str: ... + def get_glyph_name(self, index: GlyphIndexType) -> str: ... def get_image(self) -> NDArray[np.uint8]: ... - def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ... - def get_name_index(self, name: str) -> int: ... + def get_kerning(self, left: GlyphIndexType, right: GlyphIndexType, mode: Kerning) -> int: ... + def get_name_index(self, name: str) -> GlyphIndexType: ... def get_num_glyphs(self) -> int: ... def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ... def get_ps_font_info( @@ -230,8 +236,8 @@ class FT2Font(Buffer): @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ... - def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ... + def load_char(self, charcode: CharacterCodeType, flags: LoadFlags = ...) -> Glyph: ... + def load_glyph(self, glyphindex: GlyphIndexType, flags: LoadFlags = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index b39df1f52996..9ef1f107f1d7 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,6 +1,7 @@ import itertools import io from pathlib import Path +from typing import cast import numpy as np import pytest @@ -235,7 +236,7 @@ def enc(name): assert unic == after # This is just a random sample from FontForge. - glyph_names = { + glyph_names = cast(dict[str, ft2font.GlyphIndexType], { 'non-existent-glyph-name': 0, 'plusminus': 115, 'Racute': 278, @@ -247,7 +248,7 @@ def enc(name): 'uni2A02': 4464, 'u1D305': 5410, 'u1F0A1': 5784, - } + }) for name, index in glyph_names.items(): assert font.get_name_index(name) == index if name == 'non-existent-glyph-name': diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index cb816efff9a9..f4f9dd6fe009 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1772,5 +1772,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + auto py_int = py::module_::import("builtins").attr("int"); + m.attr("CharacterCodeType") = py_int; + m.attr("GlyphIndexType") = py_int; m.def("__getattr__", ft2font__getattr__); } From 1cbf39eaaae571873468f3d8a2515dedc0b5effa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Jun 2025 23:18:52 -0400 Subject: [PATCH 2/3] Fix kerning of mathtext The `FontInfo.num` value returned by `TruetypeFonts._get_info` is a character code, but `FT2Font.get_kerning` takes *glyph indices*, meaning that kerning was likely off in most cases. --- lib/matplotlib/_mathtext.py | 4 +- .../test_mathtext/mathtext_cm_21.svg | 1476 +++++++++-------- .../test_mathtext/mathtext_cm_23.png | Bin 3144 -> 1320 bytes .../test_mathtext/mathtext_cm_23.svg | 599 +++---- .../test_mathtext/mathtext_dejavusans_21.svg | 907 +++++----- .../test_mathtext/mathtext_dejavusans_23.png | Bin 3122 -> 1312 bytes .../test_mathtext/mathtext_dejavusans_23.svg | 537 +++--- .../test_mathtext/mathtext_dejavusans_27.svg | 383 +++-- .../test_mathtext/mathtext_dejavusans_46.svg | 229 +-- .../test_mathtext/mathtext_dejavusans_49.svg | 211 +-- .../test_mathtext/mathtext_dejavusans_60.svg | 418 ++--- .../test_mathtext/mathtext_dejavuserif_21.svg | 1020 ++++++------ .../test_mathtext/mathtext_dejavuserif_23.png | Bin 3125 -> 1313 bytes .../test_mathtext/mathtext_dejavuserif_23.svg | 559 ++++--- .../test_mathtext/mathtext_dejavuserif_60.svg | 444 ++--- .../test_mathtext/mathtext_stix_21.svg | 1096 ++++++------ .../test_mathtext/mathtext_stix_23.png | Bin 3135 -> 1314 bytes .../test_mathtext/mathtext_stix_23.svg | 573 ++++--- .../test_mathtext/mathtext_stixsans_21.svg | 904 +++++----- .../test_mathtext/mathtext_stixsans_23.png | Bin 3099 -> 1307 bytes .../test_mathtext/mathtext_stixsans_23.svg | 539 +++--- 21 files changed, 5183 insertions(+), 4716 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index afaa9ade6018..78f8913cd65a 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -426,7 +426,9 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64 + return font.get_kerning(font.get_char_index(info1.num), + font.get_char_index(info2.num), + Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg index 6967f80a1186..cd1fb82317ce 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:03.134386 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,721 +26,752 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png index 0317cb99e1c00d2d126a11b341ffe67100702976..43eda4b5b5310aea7cf8dce25434cde4e160e6d1 100644 GIT binary patch literal 1320 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 diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg index 9d57faac5f18..b3ce894c5022 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:21:16.989160 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35, https://matplotlib.org/ + + + + + - + @@ -15,297 +26,317 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg index 90f9b2cec969..a6a52bea53d0 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:05.581308 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,437 +26,467 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png index d6802f84bfda1b1eb62f0b43e1655f44b1b690b2..fc21215db60e9e1ed50a46c106332ba8025eb602 100644 GIT binary patch literal 1312 zcmeAS@N?(olHy`uVBq!ia0y~yVB!U`y*Ypc!?m}s1%Q-qiEBhjaDG}zd16s2gKuI< zK~8>2PG*uqS!z*nW`3Tro}rg9K9&!45iOUsC3sAo}i-H^UmJBY5D$9xtZKD5w$Csk3au<{{4f^pMSsh@lLdA>&Ozgs<6s2 zq zT~^j+!e0N5O>}iF-nYwR?T=jv7gruWYI9GLQGfYo z?zN@a&n&uy2c{{#YZ@QhPZqgh6d|`!qc}%vO{S69hmWPv zEZj<~6rZ-4+cUGC)mkytl94Go{`;DJDjTjIxW3Jz^2KcK4VS(N-pZAKWU6v$9_#f~ zg#!%TmljtuNlXmmwfpvh{ervcl9-LUinnT14y>xq-@Wp(DZgWa`m1?Yr?Y(H7RdhO zQqf`h_WP$VFE1avZRPYnQZkPDu(I>BX+f{&_%dm1dC_{pqT%N@UU_{NH71+2QbBsx z6z4AuWAfSFwEcekWLp`g?yI(|^*PsYEx4-uaN(t0cSLXbWH@CX$hNRoUf;?4Yiht! zQAUx-Etk}o*K9lcBO)=Fk?HD$Dq&^!PsZ5|X9O8?UtNFT&1B6H5}jDlvAmz@&VfHw zrVPu)%(NAzWEip~^te7g>&Cn%%l=Wa@8*vT{j1xq+)hqbZe`G&$FgtBFVi2B4x}-i zelz*QfeYKz3p>O7W22aP-Z@{|U^X@2=B-6Mr!2U?WXiM4f8Lz^AZc2b!q=j+-PVypSPX~d)%5= z_S^dXw_Qh9-H_Os{(a%`H@pXq3svvf^5x*`5ayNU*FQ1*Sh+j$w5sLT><_AqQ&VPq zwVGpdz}zxzTAlpDt=ri5v~|tth`bz_zH+Okxo-8b&l4?l&KvADV>oc!{PNj0pFrOK z>Za^v^Zx#R+WP)`qxH702Q2?N{ah!nwetPH*^EiMw}nrB^7v)0d*%f1ZypKYfEv_o~nRWyd#r$*q ZA4B}N2&c?XyY)atpQo#z%Q~loCID{|V(I_@ literal 3122 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 H&N2T6^r6IS diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg index 77ded780c3f1..a2d79631d97f 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:21:24.919009 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35, https://matplotlib.org/ + + + + + - + @@ -15,268 +26,288 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - + - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg index 7a7b7ec42c25..626c87d7e049 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:05.146521 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,190 +26,202 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - + - - - + - - - + - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg index 0846b552246a..dec92d277878 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:05.935609 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,115 +26,121 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - - + - - - - - - +M 1381 2969 +Q 1594 3256 1914 3420 +Q 2234 3584 2584 3584 +Q 3122 3584 3439 3221 +Q 3756 2859 3756 2241 +Q 3756 1734 3570 1259 +Q 3384 784 3041 416 +Q 2816 172 2522 40 +Q 2228 -91 1906 -91 +Q 1566 -91 1316 65 +Q 1066 222 909 531 +L 806 0 +L 231 0 +L 1178 4863 +L 1753 4863 +L 1381 2969 +z +" transform="scale(0.015625)"/> + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg index 24db824fd37c..af3a9f70fd2e 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:06.101194 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,103 +26,109 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - + + - + - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg index 189491319c10..2b075b3e4a11 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:05.096717 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,209 +26,220 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + + - + - - - + - - + - - - - - - - - - - - - - +M 1959 2075 +Q 2384 2075 2632 2365 +Q 2881 2656 2881 3163 +Q 2881 3666 2632 3958 +Q 2384 4250 1959 4250 +Q 1534 4250 1286 3958 +Q 1038 3666 1038 3163 +Q 1038 2656 1286 2365 +Q 1534 2075 1959 2075 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg index e0721c9e47a4..fc980d7431e4 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:05.783136 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,493 +26,524 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png index b405dd438309c2f44abc7e9ee3bbf115dd84c202..c95639ff929f971ab68116529525ac14771507d9 100644 GIT binary patch literal 1313 zcmeAS@N?(olHy`uVBq!ia0y~yVB!U`y*Ypc!?m}s1%Q-qiEBhjaDG}zd16s2gKuI< zK~8>2PG*uqS!z*nW`3Tro}roH{j8 zU}46r)Mb9YXBAY-m!;n;(|iBOQhI-}Oe<&Utsi^-Rqm_VZ&UyGUYm!qvx{j*mcUho zRgNLeE7$^g7fFExCa_#p=*tptH|q~pgY%Mmnx)(gx-moKC_H16Q2C13t?Zqa?#@r{*Y z%PO^Quu^!)sk#H7b-gD3LZ@#b~)Y*`m;?H|PZvP^3=&yO?H4(yW^ zn4LYpDSaC)2iS9L$;*nXI*>o3DLg<;Ls zy(&-arp7qiytXXZ#mgv>lUVh&hdFuEIW>P~#$T)3ws7C#I<)pU)0*Aym%Z+avSpb! zH=`zW`;pD@4IalP|5Kb~`Qho!&FQ||ylUKYU5j{o_G zm>I#d5)aoM4bK*35EWau^(DjW-0s;=U;ylgqfc*TP_V0tMs~W#VXDns-bLH}yw_T6G z?pgKe%r6-)nFp_vj8E^Bk3Vti?#vkH6&4B8EZ@oSnoUm;Jy&Tb(0sKrExdAq&EA>G zW=U;XKkPf--LHK2e!6)1Th`|{w*7tnQ`>a+{?)2iSG;Ca+Fd@U^~%dH-xizH>#l06 zzifVhy|F0Dv}D>)62kl6*Kd?;m@d8v-SV2_6%ZM W!uHfWh`tOe`#fF!T-G@yGywqUt!8Zi literal 3125 zcmd5;`9D-`8$ZNEwkSP}HL?`tiLs1jh_UyWvJ<04sE3KjHZt~oPgEFDqQpG1jxfoV zD5jAiGb4sELS$)VeNXS_{R7_L-uM2TbMA9K*E#pO?(4d~-|L%t!Ol|X5cCiL07BMQ zNP7U_76R{W`5@qREvaD;oOpswtsVKm5yj`70zUHxTDb)SfXJ2KCs&nmhi9Y(mwk5LzANFQ@JB3JXJM&E@6X7cX86Bzo+RNnZJyuPt<4!Ktc z?|p^akM?J@NqR;7==r^>>Bdef##d12XoHUb7yyj5W1A)JNEU#RdYPqxjc~>+SO83@?@q6{)yzZqIN%(nwQNGw0ExZ=Z*5Hcrp<-*auo$hcp+6vQ4r zE|F>B|D}PSw6~@9Wn!W(a^ogDq&23u{R{btWzgoz74}Bt42yO3=i;~xw#mIlFYqg{ zwY{C2lM_Ruc_btz{xzndt}aL<5|4~#Hj-*<&%ktbnQs#X_(Vkdb~%&T>+2!sA8KBw z{h(oV5xeUS(#Fx!TD~6>#{GLMYzv3Ux$=oLR@M(TRM@2;iw~39dt@#tOmB0T>b#$9663K{MIL#%mQ?%6DcNhF+ z&*eYgfj%p#tDhHy#E(#^*l#-@jw{&P+bbMDeq2ma@}%-TYg02br88&jqW6ByogM0% zz1?4Kp5E=PY3=U*C1Gy<*7tAUZh{+48!U!96rAxq6+HJ*yvAAWSo(EZU9AUELBx3H zA0Q_of#;z?SZ#NA_q*OqDc%^5j;Qha-nck!E_u-B+e1biKA?YaFh4eyi_l&*0xz-a z@9!4{G<`oxKh*Ng8hoTZ;EOK5+&ye^u7*HB{q{*iW2m~FNOS|l zoxw}DiQWsNHQXkM{heUcHoIs$j#r@jW@B2)yOouHQZ{i8LLoE|2reH>3{(^pc6WF4 z^70fw5PzDSG-gT5;dbhP92c*CD)>8nC3Bm9cvV-Fe4)) zHH)%LtE;P99lMqt!4emRhkY*w%g={aXTiDTj*pMWH?_4H zk{31*`|cbSWn~L|1WR&ggI(llW)_cHaxLZN(ZmD<)VO%s+NJ|JB?e(x@V-1H+Y@m3 zxNp{@M^G+#85tR7t&*muz0NI$;nX8S$;48qt)E|2+varok)Q3(rlxU*>%(SY`6*Qg<)U)RV&i0QB|LrHe5DJbCoMek z`7;-=x3h*?oM;vS@qo}>|2#UXprUdxH8u5t5mslK$qf22ZSK($*y&6A@!h(`S)Tc< zvJ&g5aj8krz2+EL;L&}ul9HWu*ZLk)<}aYpS+8Dc=HI_Rm8r{kldEV60F`zn(BJ^y zA-p*bXMF6m`_s14i_lC691d62(CBNuvv9%9jlcS{{uPp%OYP(OddjjSnyzDLm~lkK zPF_*5Ypx?fT+R7%c(?=@3~(#hULYqox2!dweB#)@w*tZ+EHMEmCnr^Pb)=`KjI?kv zRVW9z;Ov|I}5h4g&zl%VSLeQXsR$pT5-eW-0Vq zPnvjDZEde-7=5;R$P2g23$!q^45H9l&`i9sh>*}Zx!{b4vP?!#L zgiO`3l?8r39FH2in0kDCz%g!B$>Fhk$-P;@4C}S9Y7^I^lmT(AdYb%nl z(2nJBuk3!gn(tiQ^TMfmcw{7j!C=HtsiUKid;PE}X6`8kMa8-s6lHaV9`eYB{!~6R6 zG9pouw7+LySBl7NZPkD2R_{aGXl!b_I`uY@+StrspdWnjfoW@(y?!mFQzV4teD49* zO`jbo%%0%fcR|O}F_sn<5X8=k+2WT*0T8DtSy>QZsGYy3r-!|;5Dzw$AIv`T_;_k1 zKJw5cZr>;^J>7^zDsy)MGgt3BRQ~ue;9M;Z9=yD~Em6N#@04(n1FqZO>N?Sy=s(%v z30T?Kq)k#$#d^m<*m9cyDP+HV&)1>W_V#(8b0|97LD;U6;wy+l>G0@d931W;&CMge z$%TLAnqOR$4igs5C6M;qK_)fa1yVXM=UB={cn@mTjr(((yzS~zQd9f;tyWtors#vR zwY4?APn^rj$|_}MZZ5rR>Oo*z{6P_$+qYlM=_)az*8Yl^uP8)5A*T$?u8z2=A0ucz z)W7p0zo1|%qtc!8(O?-|e`Tb`^R=^$jm_pd53f-sAI_@8>i)p2sjsifVyFR_=IrMV zm3Hr2o`h{LY($<=R))Y_!oqZ;x0iH5g(iRIj4&jA%A%mQ+o%3_)D_}3H^UQ>lf5{u z*@!LG=$#*yJF5d4=p_*1Y&JUvERw5N6+lX6HIr`AzkZdsE74D6v;DezdOSIisTdVd z_=OB?v5Icx6cn(CjOJT%gFag5@F`w@dg~oA7mK6Cpj>1ZR8ZyvK}DvBK*$;y8R^~K zf`KhFjm2U~brdF~aQA`AviW6c^cKI@-3{?bEVhg?pHp0%vADQc^plh&cJcCmL?=`K h|4(%OSDJW7 - - + + + + + + 2025-06-05T23:21:28.038159 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35, https://matplotlib.org/ + + + + + - + @@ -15,278 +26,298 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - + - - - + - - + - - - - - - - - - - - - - - - - - - - - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg index a4fb4be582a4..1e68578cc6b0 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:06.736945 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,223 +26,234 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - + - - - + - - + - + - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg index 4623754e2963..8262dfef1daa 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:38:04.739929 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,531 +26,562 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png index 1923648b80a3d4e1b4098614faa2b8b4fd08c3ad..de12ed2891716b5b03eff61c2d61f3f6837651a1 100644 GIT binary patch literal 1314 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 he?&|Fi#1P2Z4VcyJD7HQfj - - + + + + + + 2025-06-05T23:21:19.380527 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35, https://matplotlib.org/ + + + + + - + @@ -15,284 +26,304 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg index d61317816ad6..68d2d5c9cd14 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:40:19.297908 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35.d20250606, https://matplotlib.org/ + + + + + - + @@ -15,435 +26,466 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png index a86119004e62ecf23497cd90cf563911bc3679d9..eca593e6c547fbaff0ec06d4127d423e40ba66e5 100644 GIT binary patch literal 1307 zcmeAS@N?(olHy`uVBq!ia0y~yVB!U`y*Ypc!?m}s1%Q-qiEBhjaDG}zd16s2gKuI< zK~8>2PG*uqS!z*nW`3Tro}rVvEg;fu$qXabgC?8UJ?nQ;D^z^$EoCS~j9mdo9_{r1sydkgz} zdyc+cm3eDc4X^&qx=CC`!ZF$VLYugC%H}p(+orCU&%bBWeQAs7PpNOwg?G!P{s%}u zPRokOY=8YPh}SnH@i@zY5NnR(Vdv&-XI`*p%eVGQZ_6;L Z*Wb@D2KQo=8uN4{1 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 B_&ERo diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg index 4e129aa6c87d..23698c1bc60e 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-06-05T23:21:22.111532 + image/svg+xml + + + Matplotlib v3.11.0.dev907+g64fb606e35, https://matplotlib.org/ + + + + + - + @@ -15,269 +26,289 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - + - - + - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + From 33418b60d5d68f667c2cee63a18e302aebdb953a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Jun 2025 15:49:39 -0400 Subject: [PATCH 3/3] Use glyph indices for font tracking in vector formats With libraqm, string layout produces glyph indices, not character codes, and font features may even produce different glyphs for the same character code (e.g., by picking a different Stylistic Set). Thus we cannot rely on character codes as unique items within a font, and must move toward glyph indices everywhere. --- lib/matplotlib/_mathtext.py | 16 ++--- lib/matplotlib/_text_helpers.py | 2 +- lib/matplotlib/backends/_backend_pdf_ps.py | 27 ++++---- lib/matplotlib/backends/backend_cairo.py | 10 +-- lib/matplotlib/backends/backend_pdf.py | 76 +++++++++++----------- lib/matplotlib/backends/backend_ps.py | 56 ++++++++-------- lib/matplotlib/backends/backend_svg.py | 32 +++++---- lib/matplotlib/tests/test_backend_pdf.py | 6 +- lib/matplotlib/textpath.py | 37 +++++------ 9 files changed, 129 insertions(+), 133 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 78f8913cd65a..4ebe09cbc311 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -37,7 +37,7 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import CharacterCodeType, Glyph + from .ft2font import CharacterCodeType, Glyph, GlyphIndexType ParserElement.enable_packrat() @@ -86,7 +86,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]] + glyphs: list[tuple[FT2Font, float, GlyphIndexType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -131,7 +131,7 @@ def __init__(self, box: Box): def to_vector(self) -> VectorParse: w, h, d = map( np.ceil, [self.box.width, self.box.height, self.box.depth]) - gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + gs = [(info.font, info.fontsize, info.glyph_id, ox, h - oy + info.offset) for ox, oy, info in self.glyphs] rs = [(x1, h - y2, x2 - x1, y2 - y1) for x1, y1, x2, y2 in self.rects] @@ -213,7 +213,7 @@ class FontInfo(NamedTuple): fontsize: float postscript_name: str metrics: FontMetrics - num: CharacterCodeType + glyph_id: GlyphIndexType glyph: Glyph offset: float @@ -374,7 +374,8 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, dpi: float) -> FontInfo: font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char(num, flags=self.load_glyph_flags) + glyph_id = font.get_char_index(num) + glyph = font.load_glyph(glyph_id, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) @@ -396,7 +397,7 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, fontsize=fontsize, postscript_name=font.postscript_name, metrics=metrics, - num=num, + glyph_id=glyph_id, glyph=glyph, offset=offset ) @@ -426,8 +427,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(font.get_char_index(info1.num), - font.get_char_index(info2.num), + return font.get_kerning(info1.glyph_id, info2.glyph_id, Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index 1a9b4e4c989c..22b52f943af6 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -14,7 +14,7 @@ class LayoutItem: ft_object: FT2Font char: str - glyph_idx: GlyphIndexType + glyph_index: GlyphIndexType x: float prev_kern: float diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a2a878d54156..21f1e82f5956 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -20,18 +20,18 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) -def get_glyphs_subset(fontfile, characters): +def get_glyphs_subset(fontfile, glyphs): """ - Subset a TTF font + Subset a TTF font. - Reads the named fontfile and restricts the font to the characters. + Reads the named fontfile and restricts the font to the glyphs. Parameters ---------- fontfile : str Path to the font file - characters : str - Continuous set of characters to include in subset + glyphs : set[int] + Set of glyph IDs to include in subset. Returns ------- @@ -39,8 +39,8 @@ def get_glyphs_subset(fontfile, characters): An open font object representing the subset, which needs to be closed by the caller. """ - - options = subset.Options(glyph_names=True, recommended_glyphs=True) + options = subset.Options(glyph_names=True, recommended_glyphs=True, + retain_gids=True) # Prevent subsetting extra tables. options.drop_tables += [ @@ -71,7 +71,7 @@ def get_glyphs_subset(fontfile, characters): font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) - subsetter.populate(text=characters) + subsetter.populate(gids=glyphs) subsetter.subset(font) return font @@ -97,10 +97,10 @@ def font_as_file(font): class CharacterTracker: """ - Helper for font subsetting by the pdf and ps backends. + Helper for font subsetting by the PDF and PS backends. - Maintains a mapping of font paths to the set of character codepoints that - are being used from that font. + Maintains a mapping of font paths to the set of glyphs that are being used from that + font. """ def __init__(self): @@ -110,10 +110,11 @@ def track(self, font, s): """Record that string *s* is being typeset using font *font*.""" char_to_font = font._get_fontmap(s) for _c, _f in char_to_font.items(): - self.used.setdefault(_f.fname, set()).add(ord(_c)) + glyph_index = _f.get_char_index(ord(_c)) + self.used.setdefault(_f.fname, set()).add(glyph_index) def track_glyph(self, font, glyph): - """Record that codepoint *glyph* is being typeset using font *font*.""" + """Record that glyph index *glyph* is being typeset using font *font*.""" self.used.setdefault(font.fname, set()).add(glyph) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 7409cd35b394..b10431800f1f 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -8,6 +8,7 @@ import functools import gzip +import itertools import math import numpy as np @@ -248,13 +249,12 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if angle: ctx.rotate(np.deg2rad(-angle)) - for font, fontsize, idx, ox, oy in glyphs: + for (font, fontsize), font_glyphs in itertools.groupby( + glyphs, key=lambda x: (x[0], x[1])): ctx.new_path() - ctx.move_to(ox, -oy) - ctx.select_font_face( - *_cairo_font_args_from_font_prop(ttfFontProperty(font))) + ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_text(chr(idx)) + ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) for ox, oy, w, h in rects: ctx.new_path() diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index ff351e301176..187094949f5c 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -960,9 +960,9 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - chars = self._character_tracker.used.get(filename) - if chars: - fonts[Fx] = self.embedTTF(filename, chars) + glyphs = self._character_tracker.used.get(filename) + if glyphs: + fonts[Fx] = self.embedTTF(filename, glyphs) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1136,9 +1136,8 @@ def _get_xobject_glyph_name(self, filename, glyph_name): end end""" - def embedTTF(self, filename, characters): + def embedTTF(self, filename, glyphs): """Embed the TTF font from the named file into the document.""" - font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1153,7 +1152,7 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, characters, descriptor): + def embedTTFType3(font, glyphs, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') @@ -1200,15 +1199,13 @@ def get_char_width(charcode): # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids # that we need from this font. - glyph_ids = [] differences = [] multi_byte_chars = set() - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph_ids.append(gind) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: glyph_name = font.get_glyph_name(gind) - if ccode <= 255: + ccode = charmap.get(gind) + if ccode is not None and ccode <= 255: differences.append((ccode, glyph_name)) else: multi_byte_chars.add(glyph_name) @@ -1222,7 +1219,7 @@ def get_char_width(charcode): last_c = c # Make the charprocs array. - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + rawcharprocs = _get_pdf_charprocs(filename, glyphs) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] @@ -1259,7 +1256,7 @@ def get_char_width(charcode): return fontdictObject - def embedTTFType42(font, characters, descriptor): + def embedTTFType42(font, glyphs, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1269,9 +1266,8 @@ def embedTTFType42(font, characters, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - subset_str = "".join(chr(c) for c in characters) - _log.debug("SUBSET %s characters: %s", filename, subset_str) - with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: + _log.debug("SUBSET %s characters: %s", filename, glyphs) + with _backend_pdf_ps.get_glyphs_subset(filename, glyphs) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( "SUBSET %s %d -> %d", filename, @@ -1319,11 +1315,11 @@ def embedTTFType42(font, characters, descriptor): cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph = font.load_char(ccode, - flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: + glyph = font.load_glyph(gind, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + ccode = charmap[gind] widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1361,11 +1357,10 @@ def embedTTFType42(font, characters, descriptor): (len(unicode_groups), b"\n".join(unicode_bfrange))) # Add XObjects for unsupported chars - glyph_ids = [] - for ccode in characters: - if not _font_supports_glyph(fonttype, ccode): - gind = full_font.get_char_index(ccode) - glyph_ids.append(gind) + glyph_ids = [ + gind for gind in glyphs + if not _font_supports_glyph(fonttype, charmap[gind]) + ] bbox = [cvt(x, nearest=False) for x in full_font.bbox] rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) @@ -1450,9 +1445,9 @@ def embedTTFType42(font, characters, descriptor): } if fonttype == 3: - return embedTTFType3(font, characters, descriptor) + return embedTTFType3(font, glyphs, descriptor) elif fonttype == 42: - return embedTTFType42(font, characters, descriptor) + return embedTTFType42(font, glyphs, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" @@ -2215,14 +2210,19 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): oldx, oldy = 0, 0 unsupported_chars = [] + font_charmaps = {} self.file.output(Op.begin_text) - for font, fontsize, num, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, num) + for font, fontsize, glyph_index, ox, oy in glyphs: + self.file._character_tracker.track_glyph(font, glyph_index) fontname = font.fname - if not _font_supports_glyph(fonttype, num): + if font not in font_charmaps: + font_charmaps[font] = {gind: ccode + for ccode, gind in font.get_charmap().items()} + ccode = font_charmaps[font].get(glyph_index) + if ccode is None or not _font_supports_glyph(fonttype, ccode): # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in # Type 42) must be emitted separately (below). - unsupported_chars.append((font, fontsize, ox, oy, num)) + unsupported_chars.append((font, fontsize, ox, oy, glyph_index)) else: self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy @@ -2230,13 +2230,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.file.output(self.file.fontName(fontname), fontsize, Op.selectfont) prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), + self.file.output(self.encode_string(chr(ccode), fonttype), Op.show) self.file.output(Op.end_text) - for font, fontsize, ox, oy, num in unsupported_chars: - self._draw_xobject_glyph( - font, fontsize, font.get_char_index(num), ox, oy) + for font, fontsize, ox, oy, glyph_index in unsupported_chars: + self._draw_xobject_glyph(font, fontsize, glyph_index, ox, oy) # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: @@ -2399,7 +2398,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): singlebyte_chunks[-1][2].append(item.char) prev_was_multibyte = False else: - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_idx)) + multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) prev_was_multibyte = True # Do the rotation and global translation as a single matrix # concatenation up front @@ -2409,7 +2408,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): -math.sin(a), math.cos(a), x, y, Op.concat_matrix) # Emit all the 1-byte characters in a BT/ET group. - self.file.output(Op.begin_text) prev_start_x = 0 for ft_object, start_x, kerns_or_chars in singlebyte_chunks: diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 368564a1518d..f55d093a67bf 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -88,16 +88,16 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, chars): +def _font_to_ps_type3(font_path, glyph_indices): """ - Subset *chars* from the font at *font_path* into a Type 3 font. + Subset *glyphs_indices* from the font at *font_path* into a Type 3 font. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. Returns ------- @@ -106,7 +106,6 @@ def _font_to_ps_type3(font_path, chars): verbatim into a PostScript file. """ font = get_font(font_path, hinting_factor=1) - glyph_ids = [font.get_char_index(c) for c in chars] preamble = """\ %!PS-Adobe-3.0 Resource-Font @@ -123,9 +122,9 @@ def _font_to_ps_type3(font_path, chars): """.format(font_name=font.postscript_name, inv_units_per_em=1 / font.units_per_EM, bbox=" ".join(map(str, font.bbox)), - encoding=" ".join(f"/{font.get_glyph_name(glyph_id)}" - for glyph_id in glyph_ids), - num_glyphs=len(glyph_ids) + 1) + encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}" + for glyph_index in glyph_indices), + num_glyphs=len(glyph_indices) + 1) postamble = """ end readonly def @@ -146,12 +145,12 @@ def _font_to_ps_type3(font_path, chars): """ entries = [] - for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { - "name": font.get_glyph_name(glyph_id), + "name": font.get_glyph_name(glyph_index), "bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])), } + _path.convert_to_string( @@ -169,21 +168,20 @@ def _font_to_ps_type3(font_path, chars): return preamble + "\n".join(entries) + postamble -def _font_to_ps_type42(font_path, chars, fh): +def _font_to_ps_type42(font_path, glyph_indices, fh): """ - Subset *chars* from the font at *font_path* into a Type 42 font at *fh*. + Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. fh : file-like Where to write the font. """ - subset_str = ''.join(chr(c) for c in chars) - _log.debug("SUBSET %s characters: %s", font_path, subset_str) + _log.debug("SUBSET %s characters: %s", font_path, glyph_indices) try: kw = {} # fix this once we support loading more fonts from a collection @@ -191,7 +189,7 @@ def _font_to_ps_type42(font_path, chars, fh): if font_path.endswith('.ttc'): kw['fontNumber'] = 0 with (fontTools.ttLib.TTFont(font_path, **kw) as font, - _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset): + _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, @@ -775,8 +773,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) - ps_name = (font.postscript_name.encode("ascii", "replace") - .decode("ascii")) + ps_name = font.postscript_name.encode("ascii", "replace").decode("ascii") scale = 0.001 * prop.get_size_in_points() thisx = 0 last_name = '' # kerns returns 0 for ''. @@ -799,7 +796,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): for item in _text_helpers.layout(s, font): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) - glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) + glyph_name = item.ft_object.get_glyph_name(item.glyph_index) stream.append((ps_name, item.x, glyph_name)) self.set_color(*gc.get_rgb()) @@ -828,13 +825,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{x:g} {y:g} translate\n" f"{angle:g} rotate\n") lastfont = None - for font, fontsize, num, ox, oy in glyphs: - self._character_tracker.track_glyph(font, num) + for font, fontsize, glyph_index, ox, oy in glyphs: + self._character_tracker.track_glyph(font, glyph_index) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - glyph_name = font.get_glyph_name(font.get_char_index(num)) + glyph_name = font.get_glyph_name(glyph_index) self._pswriter.write( f"{ox:g} {oy:g} moveto\n" f"/{glyph_name} glyphshow\n") @@ -1072,19 +1069,18 @@ def print_figure_impl(fh): print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for font_path, chars \ - in ps_renderer._character_tracker.used.items(): - if not chars: + for font_path, glyphs in ps_renderer._character_tracker.used.items(): + if not glyphs: continue fonttype = mpl.rcParams['ps.fonttype'] # Can't use more than 255 chars from a single Type 3 font. - if len(chars) > 255: + if len(glyphs) > 255: fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, chars)) + fh.write(_font_to_ps_type3(font_path, glyphs)) else: # Type 42 only. - _font_to_ps_type42(font_path, chars, fh) + _font_to_ps_type42(font_path, glyphs, fh) print("end", file=fh) print("%%EndProlog", file=fh) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..d85ba4893958 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1023,19 +1023,19 @@ def _update_glyph_map_defs(self, glyph_map_new): writer = self.writer if glyph_map_new: writer.start('defs') - for char_id, (vertices, codes) in glyph_map_new.items(): - char_id = self._adjust_char_id(char_id) + for glyph_id, (vertices, codes) in glyph_map_new.items(): + glyph_id = self._adjust_glyph_id(glyph_id) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( Path(vertices * 64, codes), simplify=False) writer.element( - 'path', id=char_id, d=path_data, + 'path', id=glyph_id, d=path_data, transform=_generate_transform([('scale', (1 / 64,))])) writer.end('defs') self._glyph_map.update(glyph_map_new) - def _adjust_char_id(self, char_id): - return char_id.replace("%20", "_") + def _adjust_glyph_id(self, glyph_id): + return glyph_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # docstring inherited @@ -1067,9 +1067,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) - _glyphs = text2path.get_glyphs_with_font( + glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) - glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: @@ -1091,15 +1090,15 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) - for char_id, xposition, yposition, scale in glyph_info: - char_id = self._adjust_char_id(char_id) + for glyph_id, xposition, yposition, scale in glyph_info: + glyph_id = self._adjust_glyph_id(glyph_id) writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{char_id}'}) + attrib={'xlink:href': f'#{glyph_id}'}) for verts, codes in rects: path = Path(verts, codes) @@ -1223,7 +1222,12 @@ def _get_all_quoted_names(prop): # Sort the characters by font, and output one tspan for each. spans = {} - for font, fontsize, thetext, new_x, new_y in glyphs: + font_charmaps = {} + for font, fontsize, glyph_index, new_x, new_y in glyphs: + if font not in font_charmaps: + font_charmaps[font] = { + gind: ccode for ccode, gind in font.get_charmap().items()} + ccode = font_charmaps[font].get(glyph_index) entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes @@ -1238,9 +1242,9 @@ def _get_all_quoted_names(prop): if entry.stretch != 'normal': font_style['font-stretch'] = entry.stretch style = _generate_css({**font_style, **color_style}) - if thetext == 32: - thetext = 0xa0 # non-breaking space - spans.setdefault(style, []).append((new_x, -new_y, thetext)) + if ccode == 32: + ccode = 0xa0 # non-breaking space + spans.setdefault(style, []).append((new_x, -new_y, ccode)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index f126fb543e78..fdf0ebbfc871 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -361,13 +361,13 @@ def test_glyphs_subset(): # non-subsetted FT2Font nosubfont = FT2Font(fpath) nosubfont.set_text(chars) + nosubcmap = nosubfont.get_charmap() # subsetted FT2Font - with get_glyphs_subset(fpath, chars) as subset: + glyph_ids = {nosubcmap[ord(c)] for c in chars} + with get_glyphs_subset(fpath, glyph_ids) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) - - nosubcmap = nosubfont.get_charmap() subcmap = subfont.get_charmap() # all unique chars must be available in subsetted font diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..02e4cdf27b80 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -39,11 +39,9 @@ def _get_font(self, prop): def _get_hinting_flag(self): return LoadFlags.NO_HINTING - def _get_char_id(self, font, ccode): - """ - Return a unique id for the given font and character-code set. - """ - return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}") + def _get_glyph_id(self, font, glyph): + """Return a unique id for the given font and glyph index.""" + return urllib.parse.quote(f"{font.postscript_name}-{glyph:x}") def get_text_width_height_descent(self, s, prop, ismath): fontsize = prop.get_size_in_points() @@ -146,11 +144,11 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_ids = [] for item in _text_helpers.layout(s, font): - char_id = self._get_char_id(item.ft_object, ord(item.char)) - glyph_ids.append(char_id) + glyph_id = self._get_glyph_id(item.ft_object, item.glyph_index) + glyph_ids.append(glyph_id) xpositions.append(item.x) - if char_id not in glyph_map: - glyph_map_new[char_id] = item.ft_object.get_path() + if glyph_id not in glyph_map: + glyph_map_new[glyph_id] = item.ft_object.get_path() ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) @@ -185,17 +183,17 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, glyph_ids = [] sizes = [] - for font, fontsize, ccode, ox, oy in glyphs: - char_id = self._get_char_id(font, ccode) - if char_id not in glyph_map: + for font, fontsize, glyph_index, ox, oy in glyphs: + glyph_id = self._get_glyph_id(font, glyph_index) + if glyph_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LoadFlags.NO_HINTING) - glyph_map_new[char_id] = font.get_path() + font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) + glyph_map_new[glyph_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) - glyph_ids.append(char_id) + glyph_ids.append(glyph_id) size = fontsize / self.FONT_SCALE sizes.append(size) @@ -232,17 +230,16 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # Gather font information and do some setup for combining # characters into strings. - t1_encodings = {} for text in page.text: font = get_font(text.font_path) - char_id = self._get_char_id(font, text.glyph) - if char_id not in glyph_map: + glyph_id = self._get_glyph_id(font, text.index) + if glyph_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_glyph(text.index, flags=LoadFlags.TARGET_LIGHT) - glyph_map_new[char_id] = font.get_path() + glyph_map_new[glyph_id] = font.get_path() - glyph_ids.append(char_id) + glyph_ids.append(glyph_id) xpositions.append(text.x) ypositions.append(text.y) sizes.append(text.font_size / self.FONT_SCALE)