Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add font feature API to FontProperties and Text#29695

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
QuLogic wants to merge1 commit intomatplotlib:main
base:main
Choose a base branch
Loading
fromQuLogic:font-features
Open
Show file tree
Hide file tree
Changes fromall 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
40 changes: 40 additions & 0 deletionsdoc/users/next_whats_new/font_features.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
Specifying font feature tags
----------------------------

OpenType fonts may support feature tags that specify alternate glyph shapes or
substitutions to be made optionally. The text API now supports setting a list of feature
tags to be used with the associated font. Feature tags can be set/get with:

- `matplotlib.text.Text.set_fontfeatures` / `matplotlib.text.Text.get_fontfeatures`
- Any API that creates a `.Text` object by passing the *fontfeatures* argument (e.g.,
``plt.xlabel(..., fontfeatures=...)``)

Font feature strings are eventually passed to HarfBuzz, and so all `string formats
supported by hb_feature_from_string()
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__ are
supported.

For example, the default font ``DejaVu Sans`` enables Standard Ligatures (the ``'liga'``
tag) by default, and also provides optional Discretionary Ligatures (the ``dlig`` tag.)
These may be toggled with ``+`` or ``-``.

.. plot::
:include-source:

fig = plt.figure(figsize=(7, 3))

fig.text(0.5, 0.85, 'Ligatures', fontsize=40, horizontalalignment='center')

# Default has Standard Ligatures (liga).
fig.text(0, 0.6, 'Default: fi ffi fl st', fontsize=40)

# Disable Standard Ligatures with -liga.
fig.text(0, 0.35, 'Disabled: fi ffi fl st', fontsize=40,
fontfeatures=['-liga'])

# Enable Discretionary Ligatures with dlig.
fig.text(0, 0.1, 'Discretionary: fi ffi fl st', fontsize=40,
fontfeatures=['dlig'])

Available font feature tags may be found at
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
6 changes: 4 additions & 2 deletionslib/matplotlib/_text_helpers.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames):
f"Matplotlib currently does not support {block} natively.")


def layout(string, font, *, kern_mode=Kerning.DEFAULT):
def layout(string, font, *,features=None,kern_mode=Kerning.DEFAULT):
"""
Render *string* with *font*.

Expand All@@ -56,6 +56,8 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT):
The string to be rendered.
font : FT2Font
The font.
features : tuple of str, optional
The font features to apply to the text.
kern_mode : Kerning
A FreeType kerning mode.

Expand All@@ -65,7 +67,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT):
"""
x = 0
prev_glyph_idx = None
char_to_font = font._get_fontmap(string)
char_to_font = font._get_fontmap(string) # TODO: Pass in features.
base_font = font
for char in string:
# This has done the fallback logic
Expand Down
3 changes: 2 additions & 1 deletionlib/matplotlib/backends/backend_agg.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -189,7 +189,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
font = self._prepare_font(prop)
# We pass '0' for angle here, since it will be rotated (in raster
# space) in the following call to draw_text_image).
font.set_text(s, 0, flags=get_hinting_flag())
font.set_text(s, 0, flags=get_hinting_flag(),
features=mtext.get_fontfeatures() if mtext is not None else None)
font.draw_glyphs_to_bitmap(
antialiased=gc.get_antialiased())
d = font.get_descent() / 64.0
Expand Down
6 changes: 4 additions & 2 deletionslib/matplotlib/backends/backend_pdf.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2338,6 +2338,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
return self.draw_mathtext(gc, x, y, s, prop, angle)

fontsize = prop.get_size_in_points()
features = mtext.get_fontfeatures() if mtext is not None else None

if mpl.rcParams['pdf.use14corefonts']:
font = self._get_font_afm(prop)
Expand All@@ -2348,7 +2349,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
fonttype = mpl.rcParams['pdf.fonttype']

if gc.get_url() is not None:
font.set_text(s)
font.set_text(s, features=features)
width, height = font.get_width_height()
self.file._annotations[-1][1].append(_get_link_annotation(
gc, x, y, width / 64, height / 64, angle))
Expand DownExpand Up@@ -2382,7 +2383,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
multibyte_glyphs = []
prev_was_multibyte = True
prev_font = font
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED):
for item in _text_helpers.layout(s, font, features=features,
kern_mode=Kerning.UNFITTED):
if _font_supports_glyph(fonttype, ord(item.char)):
if prev_was_multibyte or item.ft_object != prev_font:
singlebyte_chunks.append((item.ft_object, item.x, []))
Expand Down
3 changes: 2 additions & 1 deletionlib/matplotlib/backends/backend_ps.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,9 +795,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
thisx += width * scale

else:
features = mtext.get_fontfeatures() if mtext is not None else None
font = self._get_font_ttf(prop)
self._character_tracker.track(font, s)
for item in _text_helpers.layout(s, font):
for item in _text_helpers.layout(s, font, features=features):
ps_name = (item.ft_object.postscript_name
.encode("ascii", "replace").decode("ascii"))
glyph_name = item.ft_object.get_glyph_name(item.glyph_idx)
Expand Down
2 changes: 1 addition & 1 deletionlib/matplotlib/font_manager.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -536,7 +536,7 @@ def afmFontProperty(fontpath, font):

def _cleanup_fontproperties_init(init_method):
"""
A decorator to limit the call signature tosingle a positional argument
A decorator to limit the call signature toa single positional argument
or alternatively only keyword arguments.

We still accept but deprecate all other call signatures.
Expand Down
7 changes: 6 additions & 1 deletionlib/matplotlib/ft2font.pyi
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -236,7 +236,12 @@ class FT2Font(Buffer):
def set_charmap(self, i: int) -> None: ...
def set_size(self, ptsize: float, dpi: float) -> None: ...
def set_text(
self, string: str, angle: float = ..., flags: LoadFlags = ...
self,
string: str,
angle: float = ...,
flags: LoadFlags = ...,
*,
features: tuple[str] | None = ...,
) -> NDArray[np.float64]: ...
@property
def ascender(self) -> int: ...
Expand Down
13 changes: 13 additions & 0 deletionslib/matplotlib/tests/test_ft2font.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -199,6 +199,19 @@ def test_ft2font_set_size():
assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)


def test_ft2font_features():
# Smoke test that these are accepted as intended.
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file)
font.set_text('foo', features=None) # unset
font.set_text('foo', features=['calt', 'dlig']) # list
font.set_text('foo', features=('calt', 'dlig')) # tuple
with pytest.raises(TypeError):
font.set_text('foo', features=123)
with pytest.raises(TypeError):
font.set_text('foo', features=[123, 456])


def test_ft2font_charmaps():
def enc(name):
# We don't expose the encoding enum from FreeType, but can generate it here.
Expand Down
39 changes: 39 additions & 0 deletionslib/matplotlib/text.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,6 +136,7 @@
super().__init__()
self._x, self._y = x, y
self._text = ''
self._features = None
self._reset_visual_defaults(
text=text,
color=color,
Expand DownExpand Up@@ -847,6 +848,12 @@
"""
return self._fontproperties.get_family()

def get_fontfeatures(self):
"""
Return a tuple of font feature tags to enable.
"""
return self._features

def get_fontname(self):
"""
Return the font name as a string.
Expand DownExpand Up@@ -1094,6 +1101,38 @@
self._fontproperties.set_family(fontname)
self.stale = True

def set_fontfeatures(self, features):
"""
Set the feature tags to enable on the font.

Parameters
----------
features : list[str]
A list of feature tags to be used with the associated font. These strings
are eventually passed to HarfBuzz, and so all `string formats supported by
hb_feature_from_string()
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
are supported.

For example, if your desired font includes Stylistic Sets which enable
various typographic alternates including one that you do not wish to use
(e.g., Contextual Ligatures), then you can pass the following to enable one
and not the other::

fp.set_features([
'ss01', # Use Stylistic Set 1.
'-clig', # But disable Contextural Ligatures.
])

Available font feature tags may be found at
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
"""
_api.check_isinstance((list, tuple, None), features=features)

Check warning on line 1130 in lib/matplotlib/text.py

View check run for this annotation

Codecov/ codecov/patch

lib/matplotlib/text.py#L1130

Added line #L1130 was not covered by tests
if features is not None:
features = tuple(features)
self._features = features
self.stale = True

Check warning on line 1134 in lib/matplotlib/text.py

View check run for this annotation

Codecov/ codecov/patch

lib/matplotlib/text.py#L1132-L1134

Added lines #L1132 - L1134 were not covered by tests

def set_fontvariant(self, variant):
"""
Set the font variant.
Expand Down
2 changes: 2 additions & 0 deletionslib/matplotlib/text.pyi
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,7 @@ class Text(Artist):
def get_color(self) -> ColorType: ...
def get_fontproperties(self) -> FontProperties: ...
def get_fontfamily(self) -> list[str]: ...
def get_fontfeatures(self) -> tuple[str, ...] | None: ...
def get_fontname(self) -> str: ...
def get_fontstyle(self) -> Literal["normal", "italic", "oblique"]: ...
def get_fontsize(self) -> float | str: ...
Expand All@@ -80,6 +81,7 @@ class Text(Artist):
def set_multialignment(self, align: Literal["left", "center", "right"]) -> None: ...
def set_linespacing(self, spacing: float) -> None: ...
def set_fontfamily(self, fontname: str | Iterable[str]) -> None: ...
def set_fontfeatures(self, features: list[str] | tuple[str, ...] | None) -> None: ...
def set_fontvariant(self, variant: Literal["normal", "small-caps"]) -> None: ...
def set_fontstyle(
self, fontstyle: Literal["normal", "italic", "oblique"]
Expand Down
9 changes: 5 additions & 4 deletionslib/matplotlib/textpath.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
d /= 64.0
return w * scale, h * scale, d * scale

def get_text_path(self, prop, s, ismath=False):
def get_text_path(self, prop, s, ismath=False, *, features=None):
"""
Convert text *s* to path (a tuple of vertices and codes for
matplotlib.path.Path).
Expand DownExpand Up@@ -109,7 +109,8 @@ def get_text_path(self, prop, s, ismath=False):
glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
elif not ismath:
font = self._get_font(prop)
glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
glyph_info, glyph_map, rects = self.get_glyphs_with_font(
font, s, features=features)
else:
glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)

Expand All@@ -130,7 +131,7 @@ def get_text_path(self, prop, s, ismath=False):
return verts, codes

def get_glyphs_with_font(self, font, s, glyph_map=None,
return_new_glyphs_only=False):
return_new_glyphs_only=False, *, features=None):
"""
Convert string *s* to vertices and codes using the provided ttf font.
"""
Expand All@@ -145,7 +146,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,

xpositions = []
glyph_ids = []
for item in _text_helpers.layout(s, font):
for item in _text_helpers.layout(s, font, features=features):
char_id = self._get_char_id(item.ft_object, ord(item.char))
glyph_ids.append(char_id)
xpositions.append(item.x)
Expand Down
9 changes: 8 additions & 1 deletionlib/matplotlib/textpath.pyi
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,14 +16,21 @@ class TextToPath:
self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"]
) -> tuple[float, float, float]: ...
def get_text_path(
self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ...
self,
prop: FontProperties,
s: str,
ismath: bool | Literal["TeX"] = ...,
*,
features: tuple[str] | None = ...,
) -> list[np.ndarray]: ...
def get_glyphs_with_font(
self,
font: FT2Font,
s: str,
glyph_map: dict[str, tuple[np.ndarray, np.ndarray]] | None = ...,
return_new_glyphs_only: bool = ...,
*,
features: tuple[str] | None = ...,
) -> tuple[
list[tuple[str, float, float, float]],
dict[str, tuple[np.ndarray, np.ndarray]],
Expand Down
3 changes: 2 additions & 1 deletionsrc/ft2font.cpp
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -397,7 +397,8 @@ void FT2Font::set_kerning_factor(int factor)
}

void FT2Font::set_text(
std::u32string_view text, double angle, FT_Int32 flags, std::vector<double> &xys)
std::u32string_view text, double angle, FT_Int32 flags,
std::optional<std::vector<std::string>> features, std::vector<double> &xys)
{
FT_Matrix matrix; /* transformation matrix */

Expand Down
2 changes: 2 additions & 0 deletionssrc/ft2font.h
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
#ifndef MPL_FT2FONT_H
#define MPL_FT2FONT_H

#include <optional>
#include <set>
#include <string>
#include <string_view>
Expand DownExpand Up@@ -79,6 +80,7 @@ class FT2Font
void set_charmap(int i);
void select_charmap(unsigned long i);
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
std::optional<std::vector<std::string>> features,
std::vector<double> &xys);
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback);
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta);
Expand Down
13 changes: 10 additions & 3 deletionssrc/ft2font_wrapper.cpp
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -703,6 +703,11 @@ const char *PyFT2Font_set_text__doc__ = R"""(

.. versionchanged:: 3.10
This now takes an `.ft2font.LoadFlags` instead of an int.
features : tuple[str, ...]
The font feature tags to use for the font.

Available font feature tags may be found at
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

Returns
-------
Expand All@@ -712,7 +717,8 @@ const char *PyFT2Font_set_text__doc__ = R"""(

static py::array_t<double>
PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0,
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT)
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT,
std::optional<std::vector<std::string>> features = std::nullopt)
{
std::vector<double> xys;
LoadFlags flags;
Expand All@@ -732,7 +738,7 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0
throw py::type_error("flags must be LoadFlags or int");
}

self->x->set_text(text, angle, static_cast<FT_Int32>(flags), xys);
self->x->set_text(text, angle, static_cast<FT_Int32>(flags),features,xys);

py::ssize_t dims[] = { static_cast<py::ssize_t>(xys.size()) / 2, 2 };
py::array_t<double> result(dims);
Expand DownExpand Up@@ -1621,7 +1627,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
.def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a,
PyFT2Font_get_kerning__doc__)
.def("set_text", &PyFT2Font_set_text,
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT,
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(),
"features"_a=nullptr,
PyFT2Font_set_text__doc__)
.def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a,
PyFT2Font_get_fontmap__doc__)
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp