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

Skip to content

Add support for loading all fonts from collections #30334

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

Draft
wants to merge 3 commits into
base: text-overhaul
Choose a base branch
from
Draft
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
59 changes: 45 additions & 14 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ def findSystemFonts(fontpaths=None, fontext='ttf'):
return [fname for fname in fontfiles if os.path.exists(fname)]


class FontPath(str):
__match_args__ = ('path', 'face_index')

def __new__(cls, path, face_index):
ret = super().__new__(cls, path)
ret.face_index = face_index
return ret

@property
def path(self):
return str(self)


@dataclasses.dataclass(frozen=True)
class FontEntry:
"""
Expand All @@ -319,6 +332,7 @@ class FontEntry:
"""

fname: str = ''
index: int = 0
name: str = ''
style: str = 'normal'
variant: str = 'normal'
Expand Down Expand Up @@ -465,7 +479,8 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal.
raise NotImplementedError("Non-scalable fonts are not supported")
size = 'scalable'

return FontEntry(font.fname, name, style, variant, weight, stretch, size)
return FontEntry(font.fname, font.face_index, name,
style, variant, weight, stretch, size)


def afmFontProperty(fontpath, font):
Expand Down Expand Up @@ -535,7 +550,7 @@ def afmFontProperty(fontpath, font):

size = 'scalable'

return FontEntry(fontpath, name, style, variant, weight, stretch, size)
return FontEntry(fontpath, 0, name, style, variant, weight, stretch, size)


def _cleanup_fontproperties_init(init_method):
Expand Down Expand Up @@ -1069,7 +1084,7 @@ class FontManager:
# Increment this version number whenever the font cache data
# format or behavior has changed and requires an existing font
# cache files to be rebuilt.
__version__ = '3.11.0a1'
__version__ = '3.11.0a2'

def __init__(self, size=None, weight='normal'):
self._version = self.__version__
Expand Down Expand Up @@ -1134,6 +1149,10 @@ def addfont(self, path):
font = ft2font.FT2Font(path)
prop = ttfFontProperty(font)
self.ttflist.append(prop)
for face_index in range(1, font.num_faces):
subfont = ft2font.FT2Font(path, face_index=face_index)
prop = ttfFontProperty(subfont)
self.ttflist.append(prop)
self._findfont_cached.cache_clear()

@property
Expand Down Expand Up @@ -1536,7 +1555,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
# actually raised.
return cbook._ExceptionInfo(ValueError, "No valid font could be found")

return _cached_realpath(result)
return FontPath(_cached_realpath(result), best_font.index)


@_api.deprecated("3.11")
Expand All @@ -1556,15 +1575,16 @@ def is_opentype_cff_font(filename):
@lru_cache(64)
def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
enable_last_resort):
first_fontpath, *rest = font_filepaths
(first_fontpath, first_fontindex), *rest = font_filepaths
fallback_list = [
ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor)
for fpath in rest
ft2font.FT2Font(fpath, hinting_factor, face_index=index,
_kerning_factor=_kerning_factor)
for fpath, index in rest
]
last_resort_path = _cached_realpath(
cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf'))
try:
last_resort_index = font_filepaths.index(last_resort_path)
last_resort_index = font_filepaths.index((last_resort_path, 0))
except ValueError:
last_resort_index = -1
# Add Last Resort font so we always have glyphs regardless of font, unless we're
Expand All @@ -1576,7 +1596,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
_warn_if_used=True))
last_resort_index = len(fallback_list)
font = ft2font.FT2Font(
first_fontpath, hinting_factor,
first_fontpath, hinting_factor, face_index=first_fontindex,
_fallback_list=fallback_list,
_kerning_factor=_kerning_factor
)
Expand Down Expand Up @@ -1611,7 +1631,9 @@ def get_font(font_filepaths, hinting_factor=None):

Parameters
----------
font_filepaths : Iterable[str, Path, bytes], str, Path, bytes
font_filepaths : Iterable[str, Path, bytes, FontPath, \
tuple[str | Path | bytes, int, FontPath]], \
str, Path, bytes, FontPath, tuple[str | Path | bytes, int]
Relative or absolute paths to the font files to be used.

If a single string, bytes, or `pathlib.Path`, then it will be treated
Expand All @@ -1626,10 +1648,19 @@ def get_font(font_filepaths, hinting_factor=None):
`.ft2font.FT2Font`

"""
if isinstance(font_filepaths, (str, Path, bytes)):
paths = (_cached_realpath(font_filepaths),)
else:
paths = tuple(_cached_realpath(fname) for fname in font_filepaths)
match font_filepaths:
case FontPath(path, index):
paths = ((_cached_realpath(path), index), )
case str() | Path() | bytes() as path:
paths = ((_cached_realpath(path), 0), )
case (str() | Path() | bytes() as path, int() as index):
paths = ((_cached_realpath(path), index), )
case _:
paths = tuple(
(_cached_realpath(fname[0]), fname[1]) if isinstance(fname, tuple)
else (_cached_realpath(fname.path), fname.face_index)
if isinstance(fname, FontPath) else (_cached_realpath(fname), 0)
for fname in font_filepaths)

hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor')

Expand Down
16 changes: 13 additions & 3 deletions lib/matplotlib/font_manager.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ from dataclasses import dataclass
from numbers import Integral
import os
from pathlib import Path
from typing import Any, Literal
from typing import Any, Literal, TypeAlias

from matplotlib._afm import AFM
from matplotlib import ft2font

FontFace: TypeAlias = tuple[str | Path | bytes, int]

font_scalings: dict[str | None, float]
stretch_dict: dict[str, int]
weight_dict: dict[str, int]
Expand All @@ -26,9 +28,17 @@ def _get_fontconfig_fonts() -> list[Path]: ...
def findSystemFonts(
fontpaths: Iterable[str | os.PathLike | Path] | None = ..., fontext: str = ...
) -> list[str]: ...

class FontPath(str):
face_index: int
def __new__(cls: type[str], path: str, face_index: int) -> FontPath: ...
@property
def path(self) -> str: ...

@dataclass
class FontEntry:
fname: str = ...
index: int = ...
name: str = ...
style: str = ...
variant: str = ...
Expand Down Expand Up @@ -115,12 +125,12 @@ class FontManager:
directory: str | None = ...,
fallback_to_default: bool = ...,
rebuild_if_missing: bool = ...,
) -> str: ...
) -> FontPath: ...
def get_font_names(self) -> list[str]: ...

def is_opentype_cff_font(filename: str) -> bool: ...
def get_font(
font_filepaths: Iterable[str | Path | bytes] | str | Path | bytes,
font_filepaths: Iterable[str | Path | bytes | FontFace | FontPath] | str | Path | bytes | FontFace | FontPath,
hinting_factor: int | None = ...,
) -> ft2font.FT2Font: ...

Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/ft2font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class FT2Font(Buffer):
filename: str | BinaryIO,
hinting_factor: int = ...,
*,
face_index: int = ...,
_fallback_list: list[FT2Font] | None = ...,
_kerning_factor: int = ...
) -> None: ...
Expand Down Expand Up @@ -247,6 +248,8 @@ class FT2Font(Buffer):
@property
def face_flags(self) -> FaceFlags: ...
@property
def face_index(self) -> int: ...
@property
def family_name(self) -> str: ...
@property
def fname(self) -> str: ...
Expand Down
15 changes: 14 additions & 1 deletion lib/matplotlib/tests/test_font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,17 @@ def test_utf16m_sfnt():

def test_find_ttc():
fp = FontProperties(family=["WenQuanYi Zen Hei"])
if Path(findfont(fp)).name != "wqy-zenhei.ttc":
fontpath = findfont(fp)
if Path(fontpath).name != "wqy-zenhei.ttc":
pytest.skip("Font wqy-zenhei.ttc may be missing")
# All fonts from this collection should have loaded as well.
for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]:
subfontpath = findfont(FontProperties(family=[name]), fallback_to_default=False)
assert subfontpath.path == fontpath.path
assert subfontpath.face_index != fontpath.face_index
subfont = get_font(subfontpath)
assert subfont.fname == subfontpath.path
assert subfont.face_index == subfontpath.face_index
fig, ax = plt.subplots()
ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp)
for fmt in ["raw", "svg", "pdf", "ps"]:
Expand Down Expand Up @@ -342,6 +351,10 @@ def test_get_font_names():
font = ft2font.FT2Font(path)
prop = ttfFontProperty(font)
ttf_fonts.append(prop.name)
for face_index in range(1, font.num_faces):
font = ft2font.FT2Font(path, face_index=face_index)
prop = ttfFontProperty(font)
ttf_fonts.append(prop.name)
except Exception:
pass
available_fonts = sorted(list(set(ttf_fonts)))
Expand Down
18 changes: 18 additions & 0 deletions lib/matplotlib/tests/test_ft2font.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,24 @@ def test_ft2font_invalid_args(tmp_path):
ft2font.FT2Font(file, _kerning_factor=1.3)


@pytest.mark.parametrize('name, size, skippable',
[('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)])
def test_ft2font_face_index(name, size, skippable):
try:
file = fm.findfont(name, fallback_to_default=False)
except ValueError:
if skippable:
pytest.skip(r'Font {name} may be missing')
raise
for index in range(size):
font = ft2font.FT2Font(file, face_index=index)
assert font.num_faces >= size
with pytest.raises(ValueError, match='must be between'): # out of bounds for spec
ft2font.FT2Font(file, face_index=0x1ffff)
with pytest.raises(RuntimeError, match='invalid argument'): # invalid for this font
ft2font.FT2Font(file, face_index=0xff)


def test_ft2font_clear():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file)
Expand Down
5 changes: 2 additions & 3 deletions src/ft2font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ FT2Font::get_path(std::vector<double> &vertices, std::vector<unsigned char> &cod
codes.push_back(CLOSEPOLY);
}

FT2Font::FT2Font(FT_Open_Args &open_args,
long hinting_factor_,
FT2Font::FT2Font(FT_Long face_index, FT_Open_Args &open_args, long hinting_factor_,
std::vector<FT2Font *> &fallback_list,
FT2Font::WarnFunc warn, bool warn_if_used)
: ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr),
Expand All @@ -217,7 +216,7 @@ FT2Font::FT2Font(FT_Open_Args &open_args,
kerning_factor(0)
{
clear();
FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face);
FT_CHECK(FT_Open_Face, _ft2Library, &open_args, face_index, &face);
if (open_args.stream != nullptr) {
face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ft2font.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class FT2Font
typedef void (*WarnFunc)(FT_ULong charcode, std::set<FT_String*> family_names);

public:
FT2Font(FT_Open_Args &open_args, long hinting_factor,
FT2Font(FT_Long face_index, FT_Open_Args &open_args, long hinting_factor,
std::vector<FT2Font *> &fallback_list,
WarnFunc warn, bool warn_if_used);
virtual ~FT2Font();
Expand Down
21 changes: 16 additions & 5 deletions src/ft2font_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ const char *PyFT2Font_init__doc__ = R"""(
hinting_factor : int, optional
Must be positive. Used to scale the hinting in the x-direction.

face_index : int, optional
The index of the face in the font file to load.

_fallback_list : list of FT2Font, optional
A list of FT2Font objects used to find missing glyphs.

Expand All @@ -446,14 +449,18 @@ const char *PyFT2Font_init__doc__ = R"""(
)""";

static PyFT2Font *
PyFT2Font_init(py::object filename, long hinting_factor = 8,
PyFT2Font_init(py::object filename, long hinting_factor = 8, FT_Long face_index = 0,
std::optional<std::vector<PyFT2Font *>> fallback_list = std::nullopt,
int kerning_factor = 0, bool warn_if_used = false)
{
if (hinting_factor <= 0) {
throw py::value_error("hinting_factor must be greater than 0");
}

if (face_index < 0 || face_index >= 1<<16) {
throw std::range_error("face_index must be between 0 and 65535, inclusive");
}

PyFT2Font *self = new PyFT2Font();
self->x = nullptr;
memset(&self->stream, 0, sizeof(FT_StreamRec));
Expand Down Expand Up @@ -497,8 +504,8 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
self->stream.close = nullptr;
}

self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn,
warn_if_used);
self->x = new FT2Font(face_index, open_args, hinting_factor, fallback_fonts,
ft_glyph_warn, warn_if_used);

self->x->set_kerning_factor(kerning_factor);

Expand Down Expand Up @@ -1604,7 +1611,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
PyFT2Font__doc__)
.def(py::init(&PyFT2Font_init),
"filename"_a, "hinting_factor"_a=8, py::kw_only(),
"filename"_a, "hinting_factor"_a=8, py::kw_only(), "face_index"_a=0,
"_fallback_list"_a=py::none(), "_kerning_factor"_a=0,
"_warn_if_used"_a=false,
PyFT2Font_init__doc__)
Expand Down Expand Up @@ -1677,8 +1684,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
}, "PostScript name of the font.")
.def_property_readonly(
"num_faces", [](PyFT2Font *self) {
return self->x->get_face()->num_faces;
return self->x->get_face()->num_faces & 0xffff;
}, "Number of faces in file.")
.def_property_readonly(
"face_index", [](PyFT2Font *self) {
return self->x->get_face()->face_index;
}, "The index of the font in the file.")
.def_property_readonly(
"family_name", [](PyFT2Font *self) {
if (const char *name = self->x->get_face()->family_name) {
Expand Down
Loading