8000 Add font feature API to FontProperties and Text · matplotlib/matplotlib@ec912b1 · GitHub
[go: up one dir, main page]

Skip to content

Commit ec912b1

Browse files
committed
Add font feature API to FontProperties and Text
Font features allow font designers to provide alternate glyphs or shaping within a single font. These features may be accessed via special tags corresponding to internal tables of glyphs. The mplcairo backend supports font features via an elaborate re-use of the font file path [1]. This commit adds the API to make this officially supported in the main user API. At this time, nothing in Matplotlib itself uses these settings, but they will have an effect with libraqm. [1] https://github.com/matplotlib/mplcairo/blob/v0.6.1/README.rst#font-formats-and-features
1 parent 0b7a88a commit ec912b1

File tree

12 files changed

+187
-3
lines changed
  • lib/matplotlib
  • src
  • 12 files changed

    +187
    -3
    lines changed
    Lines changed: 42 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,42 @@
    1+
    Specifying font feature tags
    2+
    ----------------------------
    3+
    4+
    OpenType fonts may support feature tags that specify alternate glyph shapes or
    5+
    substitions to be made optionally. The text API now supports setting a list of feature
    6+
    tags to be used with the associated font. Feature tags can be set/get with:
    7+
    8+
    - `matplotlib.text.Text.set_fontfeatures` / `matplotlib.text.Text.get_fontfeatures`
    9+
    - `matplotlib.font_manager.FontProperties.set_features` /
    10+
    `matplotlib.font_manager.FontProperties.get_features`
    11+
    - Any API that creates a `.Text` object by passing the *fontfeatures* argument (e.g.,
    12+
    ``plt.xlabel(..., fontfeatures=...)``)
    13+
    14+
    Font feature strings are eventually passed to HarfBuzz, and so all `string formats
    15+
    supported by hb_feature_from_string()
    16+
    <https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__ are
    17+
    supported.
    18+
    19+
    For example, the default font ``DejaVu Sans`` enables Standard Ligatures (the ``'liga'``
    20+
    tag) by default, and also provides optional Discretionary Ligatures (the ``dlig`` tag.)
    21+
    These may be toggled with ``+`` or ``-``.
    22+
    23+
    .. plot::
    24+
    :include-source:
    25+
    26+
    fig = plt.figure(figsize=(7, 3))
    27+
    28+
    fig.text(0.5, 0.85, 'Ligatures', fontsize=40, horizontalalignment='center')
    29+
    30+
    # Default has Standard Ligatures (liga).
    31+
    fig.text(0, 0.6, 'Default: fi ffi fl st', fontsize=40)
    32+
    33+
    # Disable Standard Ligatures with -liga.
    34+
    fig.text(0, 0.35, 'Disabled: fi ffi fl st', fontsize=40,
    35+
    fontfeatures=['-liga'])
    36+
    37+
    # Enable Discretionary Ligatures with dlig.
    38+
    fig.text(0, 0.1, 'Discretionary: fi ffi fl st', fontsize=40,
    39+
    fontfeatures=['dlig'])
    40+
    41+
    Available font feature tags may be found at
    42+
    https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

    lib/matplotlib/backends/_backend_pdf_ps.py

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -181,6 +181,7 @@ def _get_font_ttf(self, prop):
    181181
    font = font_manager.get_font(fnames)
    182182
    font.clear()
    183183
    font.set_size(prop.get_size_in_points(), 72)
    184+
    font.set_features(prop.get_features())
    184185
    return font
    185186
    except RuntimeError:
    186187
    logging.getLogger(__name__).warning(

    lib/matplotlib/backends/backend_agg.py

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -253,6 +253,7 @@ def _prepare_font(self, font_prop):
    253253
    font.clear()
    254254
    size = font_prop.get_size_in_points()
    255255
    font.set_size(size, self.dpi)
    256+
    font.set_features(font_prop.get_features())
    256257
    return font
    257258

    258259
    def points_to_pixels(self, points):

    lib/matplotlib/font_manager.py

    Lines changed: 48 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -536,7 +536,7 @@ def afmFontProperty(fontpath, font):
    536536

    537537
    def _cleanup_fontproperties_init(init_method):
    538538
    """
    539-
    A decorator to limit the call signature to single a positional argument
    539+
    A decorator to limit the call signature to a single positional argument
    540540
    or alternatively only keyword arguments.
    541541
    542542
    We still accept but deprecate all other call signatures.
    @@ -624,6 +624,13 @@ class FontProperties:
    624624
    Supported values are: 'dejavusans', 'dejavuserif', 'cm',
    625625
    'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset`
    626626
    627+
    - features: A list of advanced font feature tags to enable. Font features are a
    628+
    component of OpenType fonts that allows picking from multiple stylistic variations
    629+
    within a single font. This may include small caps, ligatures, alternate forms of
    630+
    commonly-confused glyphs (e.g., capital I vs. lower-case l), and various other
    631+
    options. A `list of feature tags may be found here
    632+
    <https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist>`__.
    633+
    627634
    Alternatively, a font may be specified using the absolute path to a font
    628635
    file, by using the *fname* kwarg. However, in this case, it is typically
    629636
    simpler to just pass the path (as a `pathlib.Path`, not a `str`) to the
    @@ -657,7 +664,8 @@ class FontProperties:
    657664
    def __init__(self, family=None, style=None, variant=None, weight=None,
    658665
    stretch=None, size=None,
    659666
    fname=None, # if set, it's a hardcoded filename to use
    660-
    math_fontfamily=None):
    667+
    math_fontfamily=None,
    668+
    features=None):
    661669
    self.set_family(family)
    662670
    self.set_style(style)
    663671
    self.set_variant(variant)
    @@ -666,6 +674,7 @@ def __init__(self, family=None, style=None, variant=None, weight=None,
    666674
    self.set_file(fname)
    667675
    self.set_size(size)
    668676
    self.set_math_fontfamily(math_fontfamily)
    677+
    self.set_features(features)
    669678
    # Treat family as a fontconfig pattern if it is the only parameter
    670679
    # provided. Even in that case, call the other setters first to set
    671680
    # attributes not specified by the pattern to the rcParams defaults.
    @@ -705,7 +714,8 @@ def __hash__(self):
    705714
    self.get_stretch(),
    706715
    self.get_size(),
    707716
    self.get_file(),
    708-
    self.get_math_fontfamily())
    717+
    self.get_math_fontfamily(),
    718+
    self.get_features())
    709719
    return hash(l)
    710720

    711721
    def __eq__(self, other):
    @@ -952,6 +962,41 @@ def set_math_fontfamily(self, fontfamily):
    952962
    _api.check_in_list(valid_fonts, math_fontfamily=fontfamily)
    953963
    self._math_fontfamily = fontfamily
    954964

    965+
    def get_features(self):
    966+
    """Return a tuple of font feature tags to enable."""
    967+
    return self._features
    968+
    969+
    def set_features(self, features):
    970+
    """
    971+
    Set the font feature tags to enable on this font.
    972+
    973+
    Parameters
    974+
    ----------
    975+
    features : list[str]
    976+
    A list of feature tags to be used with the associated font. These strings
    977+
    are eventually passed to HarfBuzz, and so all `string formats supported by
    978+
    hb_feature_from_string()
    979+
    <https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
    980+
    are supported.
    981+
    982+
    For example, if your desired font includes Stylistic Sets which enable
    983+
    various typographic alternates including one that you do not wish to use
    984+
    (e.g., Contextual Ligatures), then you can pass the following to enable one
    985+
    and not the other::
    986+
    987+
    fp.set_features([
    988+
    'ss01', # Use Stylistic Set 1.
    989+
    '-clig', # But disable Contextural Ligatures.
    990+
    ])
    991+
    992+
    Available font feature tags may be found at
    993+
    https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
    994+
    """
    995+
    _api.check_isinstance((list, tuple, None), features=features)
    996+
    if features is not None:
    997+
    features = tuple(features)
    998+
    self._features = features
    999+
    9551000
    def copy(self):
    9561001
    """Return a copy of self."""
    9571002
    return copy.copy(self)

    lib/matplotlib/font_manager.pyi

    Lines changed: 3 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -52,6 +52,7 @@ class FontProperties:
    5252
    size: float | str | None = ...,
    5353
    fname: str | os.PathLike | Path | None = ...,
    5454
    math_fontfamily: str | None = ...,
    55+
    features: list[str] | None = ...,
    5556
    ) -> None: ...
    5657
    def __hash__(self) -> int: ...
    5758
    def __eq__(self, other: object) -> bool: ...
    @@ -76,6 +77,8 @@ class FontProperties:
    7677
    def set_fontconfig_pattern(self, pattern: str) -> None: ...
    7778
    def get_math_fontfamily 1241 (self) -> str: ...
    7879
    def set_math_fontfamily(self, fontfamily: str | None) -> None: ...
    80+
    def get_features(self) -> tuple[str, ...]: ...
    81+
    def set_features(self, features: list[str] | None) -> None: ...
    7982
    def copy(self) -> FontProperties: ...
    8083
    # Aliases
    8184
    set_name = set_family

    lib/matplotlib/ft2font.pyi

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -234,6 +234,7 @@ class FT2Font(Buffer):
    234234
    def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ...
    235235
    def select_charmap(self, i: int) -> None: ...
    236236
    def set_charmap(self, i: int) -> None: ...
    237+
    def set_features(self, features: tuple[str, ...]) -> None: ...
    237238
    def set_size(self, ptsize: float, dpi: float) -> None: ...
    238239
    def set_text(
    239240
    self, string: str, angle: float = ..., flags: LoadFlags = ...

    lib/matplotlib/tests/test_ft2font.py

    Lines changed: 13 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -198,6 +198,19 @@ def test_ft2font_set_size():
    198198
    assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)
    199199

    200200

    201+
    def test_ft2font_features():
    202+
    # Smoke test that these are accepted as intended.
    203+
    file = fm.findfont('DejaVu Sans')
    204+
    font = ft2font.FT2Font(file)
    205+
    font.set_features(None) # unset
    206+
    font.set_features(['calt', 'dlig']) # list
    207+
    font.set_features(('calt', 'dlig')) # tuple
    208+
    with pytest.raises(TypeError):
    209+
    font.set_features(123)
    210+
    with pytest.raises(TypeError):
    211+
    font.set_features([123, 456])
    212+
    213+
    201214
    def test_ft2font_charmaps():
    202215
    def enc(name):
    203216
    # We don't expose the encoding enum from FreeType, but can generate it here.

    lib/matplotlib/text.py

    Lines changed: 43 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -888,6 +888,16 @@ def get_fontweight(self):
    888888
    """
    889889
    return self._fontproperties.get_weight()
    890890

    891+
    def get_fontfeatures(self):
    892+
    """
    893+
    Return a tuple of font feature tags to enable.
    894+
    895+
    See Also
    896+
    --------
    897+
    .font_manager.FontProperties.get_features
    898+
    """
    899+
    return self._fontproperties.get_features()
    900+
    891901
    def get_stretch(self):
    892902
    """
    893903
    Return the font stretch as a string or a number.
    @@ -1198,6 +1208,39 @@ def set_fontstretch(self, stretch):
    11981208
    self._fontproperties.set_stretch(stretch)
    11991209
    self.stale = True
    12001210

    1211+
    def set_fontfeatures(self, features):
    1212+
    """
    1213+
    Set the feature tags to enable on the font.
    1214+
    1215+
    Parameters
    1216+
    ----------
    1217+
    features : list[str]
    1218+
    A list of feature tags to be used with the associated font. These strings
    1219+
    are eventually passed to HarfBuzz, and so all `string formats supported by
    1220+
    hb_feature_from_string()
    1221+
    <https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
    1222+
    are supported.
    1223+
    1224+
    For example, if your desired font includes Stylistic Sets which enable
    1225+
    various typographic alternates including one that you do not wish to use
    1226+
    (e.g., Contextual Ligatures), then you can pass the following to enable one
    1227+
    and not the other::
    1228+
    1229+
    fp.set_features([
    1230+
    'ss01', # Use Stylistic Set 1.
    1231+
    '-clig', # But disable Contextural Ligatures.
    1232+
    ])
    1233+
    1234+
    Available font feature tags may be found at
    1235+
    https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
    1236+
    1237+
    See Also
    1238+
    --------
    1239+
    .font_manager.FontProperties.set_features
    1240+
    """
    1241+
    self._fontproperties.set_features(features)
    1242+
    self.stale = True
    1243+
    12011244
    def set_position(self, xy):
    12021245
    """
    12031246
    Set the (*x*, *y*) position of the text.

    lib/matplotlib/textpath.py

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -34,6 +34,7 @@ def _get_font(self, prop):
    3434
    filenames = _fontManager._find_fonts_by_props(prop)
    3535
    font = get_font(filenames)
    3636
    font.set_size(self.FONT_SCALE, self.DPI)
    37+
    font.set_features(prop.get_features())
    3738
    return font
    3839

    3940
    def _get_hinting_flag(self):

    src/ft2font.cpp

    Lines changed: 5 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -336,6 +336,11 @@ void FT2Font::set_size(double ptsize, double dpi)
    336336
    }
    337337
    }
    338338

    339+
    void FT2Font::set_features(std::vector<std::string> features)
    340+
    {
    341+
    feature_tags = std::move(features);
    342+
    }
    343+
    339344
    void FT2Font::set_charmap(int i)
    340345
    {
    341346
    if (i >= face->num_charmaps) {

    src/ft2font.h

    Lines changed: 2 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -75,6 +75,7 @@ class FT2Font
    7575
    virtual ~FT2Font();
    7676
    void clear();
    7777
    void set_size(double ptsize, double dpi);
    78+
    void set_features(std::vector<std::string> features);
    7879
    void set_charmap(int i);
    7980
    void select_charmap(unsigned long i);
    8081
    void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
    @@ -150,6 +151,7 @@ class FT2Font
    150151
    FT_Pos advance;
    151152
    long hinting_factor;
    152153
    int kerning_factor;
    154+
    std::vector<std::string> feature_tags;
    153155

    154156
    // prevent copying
    155157
    FT2Font(const FT2Font &);

    src/ft2font_wrapper.cpp

    Lines changed: 27 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -524,6 +524,31 @@ PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi)
    524524
    self->x->set_size(ptsize, dpi);
    525525
    }
    526526

    527+
    const char *PyFT2Font_set_features__doc__ = R"""(
    528+
    Set the font feature tags used for the font.
    529+
    530+
    Parameters
    531+
    ----------
    532+
    features : tuple[str, ...]
    533+
    The font feature tags to use for the font.
    534+
    535+
    Available font feature tags may be found at
    536+
    https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
    537+
    )""";
    538+
    539+
    static void
    540+
    PyFT2Font_set_features(PyFT2Font *self, py::object features_obj)
    541+
    {
    542+
    std::vector<std::string> features;
    543+
    if (!features_obj.is_none()) {
    544+
    auto features_list = py::cast<py::tuple>(features_obj);
    545+
    for (auto &feature : features_list) {
    546+
    features.push_back(feature.cast<std::string>());
    547+
    }
    548+
    }
    549+
    self->x->set_features(std::move(features));
    550+
    }
    551+
    527552
    const char *PyFT2Font_set_charmap__doc__ = R"""(
    528553
    Make the i-th charmap current.
    529554
    @@ -1718,6 +1743,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
    17181743
    .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__)
    17191744
    .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a,
    17201745
    PyFT2Font_set_size__doc__)
    1746+
    .def("set_features", &PyFT2Font_set_features, "features"_a,
    1747+
    PyFT2Font_set_features__doc__)
    17211748
    .def("set_charmap", &PyFT2Font_set_charmap, "i"_a,
    17221749
    PyFT2Font_set_charmap__doc__)
    17231750
    .def("select_charmap", &PyFT2Font_select_charmap, "i"_a,

    0 commit comments

    Comments
     (0)
    0