Skip to content

Commit ae2d73e

Browse files
QuLogicanntzer
andcommitted
ft2font: Convert kerning mode into an enum
This follows some of the idea from matplotlib#9763, but is based on code from mplcairo. Co-authored-by: Antony Lee <anntzer.lee@gmail.com>
1 parent 8b3c01e commit ae2d73e

File tree

12 files changed

+255
-33
lines changed

12 files changed

+255
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ft2font module-level constants replaced by enums
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The `.ft2font`-level constants have been converted to `enum` classes, and all API using
5+
them now take/return the new types.
6+
7+
The following constants are now part of `.ft2font.Kerning` (without the ``KERNING_``
8+
prefix):
9+
10+
- ``KERNING_DEFAULT``
11+
- ``KERNING_UNFITTED``
12+
- ``KERNING_UNSCALED``

galleries/examples/misc/font_indexing.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import os
1010

1111
import matplotlib
12-
from matplotlib.ft2font import (KERNING_DEFAULT, KERNING_UNFITTED,
13-
KERNING_UNSCALED, FT2Font)
12+
from matplotlib.ft2font import FT2Font, Kerning
1413

1514
font = FT2Font(
1615
os.path.join(matplotlib.get_data_path(), 'fonts/ttf/DejaVuSans.ttf'))
@@ -31,7 +30,7 @@
3130
glyph = font.load_char(code)
3231
print(glyph.bbox)
3332
print(glyphd['A'], glyphd['V'], coded['A'], coded['V'])
34-
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_DEFAULT))
35-
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNFITTED))
36-
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNSCALED))
37-
print('AT', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED))
33+
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.DEFAULT))
34+
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNFITTED))
35+
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNSCALED))
36+
print('AT', font.get_kerning(glyphd['A'], glyphd['T'], Kerning.UNSCALED))

lib/matplotlib/_mathtext.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from ._mathtext_data import (
3030
latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
3131
from .font_manager import FontProperties, findfont, get_font
32-
from .ft2font import FT2Font, FT2Image, KERNING_DEFAULT
32+
from .ft2font import FT2Font, FT2Image, Kerning
3333

3434
from packaging.version import parse as parse_version
3535
from pyparsing import __version__ as pyparsing_version
@@ -426,7 +426,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
426426
info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
427427
info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
428428
font = info1.font
429-
return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64
429+
return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64
430430
return super().get_kern(font1, fontclass1, sym1, fontsize1,
431431
font2, fontclass2, sym2, fontsize2, dpi)
432432

lib/matplotlib/_text_helpers.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import dataclasses
88

99
from . import _api
10-
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING, FT2Font
10+
from .ft2font import LOAD_NO_HINTING, FT2Font, Kerning
1111

1212

1313
@dataclasses.dataclass(frozen=True)
@@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames):
4343
f"Matplotlib currently does not support {block} natively.")
4444

4545

46-
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
46+
def layout(string, font, *, kern_mode=Kerning.DEFAULT):
4747
"""
4848
Render *string* with *font*.
4949
@@ -56,7 +56,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
5656
The string to be rendered.
5757
font : FT2Font
5858
The font.
59-
kern_mode : int
59+
kern_mode : Kerning
6060
A FreeType kerning mode.
6161
6262
Yields

lib/matplotlib/backends/backend_pdf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from matplotlib.font_manager import get_font, fontManager as _fontManager
3737
from matplotlib._afm import AFM
3838
from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE,
39-
LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font)
39+
LOAD_NO_HINTING, FT2Font, Kerning)
4040
from matplotlib.transforms import Affine2D, BboxBase
4141
from matplotlib.path import Path
4242
from matplotlib.dates import UTC
@@ -2378,7 +2378,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23782378
multibyte_glyphs = []
23792379
prev_was_multibyte = True
23802380
prev_font = font
2381-
for item in _text_helpers.layout(s, font, kern_mode=KERNING_UNFITTED):
2381+
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED):
23822382
if _font_supports_glyph(fonttype, ord(item.char)):
23832383
if prev_was_multibyte or item.ft_object != prev_font:
23842384
singlebyte_chunks.append((item.ft_object, item.x, []))

lib/matplotlib/ft2font.pyi

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
import sys
23
from typing import BinaryIO, Literal, TypedDict, final, overload
34
from typing_extensions import Buffer # < Py 3.12
@@ -16,9 +17,6 @@ GLYPH_NAMES: int
1617
HORIZONTAL: int
1718
ITALIC: int
1819
KERNING: int
19-
KERNING_DEFAULT: int
20-
KERNING_UNFITTED: int
21-
KERNING_UNSCALED: int
2220
LOAD_CROP_BITMAP: int
2321
LOAD_DEFAULT: int
2422
LOAD_FORCE_AUTOHINT: int
@@ -44,6 +42,11 @@ SCALABLE: int
4442
SFNT: int
4543
VERTICAL: int
4644

45+
class Kerning(Enum):
46+
DEFAULT: int
47+
UNFITTED: int
48+
UNSCALED: int
49+
4750
class _SfntHeadDict(TypedDict):
4851
version: tuple[int, int]
4952
fontRevision: tuple[int, int]
@@ -184,7 +187,7 @@ class FT2Font(Buffer):
184187
def get_descent(self) -> int: ...
185188
def get_glyph_name(self, index: int) -> str: ...
186189
def get_image(self) -> NDArray[np.uint8]: ...
187-
def get_kerning(self, left: int, right: int, mode: int) -> int: ...
190+
def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ...
188191
def get_name_index(self, name: str) -> int: ...
189192
def get_num_glyphs(self) -> int: ...
190193
def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ...

lib/matplotlib/tests/test_ft2font.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import pytest
77

8+
import matplotlib as mpl
89
from matplotlib import ft2font
910
from matplotlib.testing.decorators import check_figures_equal
1011
import matplotlib.font_manager as fm
@@ -714,13 +715,37 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default):
714715
font.set_size(100, 100)
715716
assert font.get_kerning(font.get_char_index(ord(left)),
716717
font.get_char_index(ord(right)),
717-
ft2font.KERNING_UNSCALED) == unscaled
718+
ft2font.Kerning.UNSCALED) == unscaled
718719
assert font.get_kerning(font.get_char_index(ord(left)),
719720
font.get_char_index(ord(right)),
720-
ft2font.KERNING_UNFITTED) == unfitted
721+
ft2font.Kerning.UNFITTED) == unfitted
721722
assert font.get_kerning(font.get_char_index(ord(left)),
722723
font.get_char_index(ord(right)),
723-
ft2font.KERNING_DEFAULT) == default
724+
ft2font.Kerning.DEFAULT) == default
725+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
726+
match='Use Kerning.UNSCALED instead'):
727+
k = ft2font.KERNING_UNSCALED
728+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
729+
match='Use Kerning enum values instead'):
730+
assert font.get_kerning(font.get_char_index(ord(left)),
731+
font.get_char_index(ord(right)),
732+
int(k)) == unscaled
733+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
734+
match='Use Kerning.UNFITTED instead'):
735+
k = ft2font.KERNING_UNFITTED
736+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
737+
match='Use Kerning enum values instead'):
738+
assert font.get_kerning(font.get_char_index(ord(left)),
739+
font.get_char_index(ord(right)),
740+
int(k)) == unfitted
741+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
742+
match='Use Kerning.DEFAULT instead'):
743+
k = ft2font.KERNING_DEFAULT
744+
with pytest.warns(mpl.MatplotlibDeprecationWarning,
745+
match='Use Kerning enum values instead'):
746+
assert font.get_kerning(font.get_char_index(ord(left)),
747+
font.get_char_index(ord(right)),
748+
int(k)) == default
724749

725750

726751
def test_ft2font_set_text():

src/_enums.h

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#ifndef MPL_ENUMS_H
2+
#define MPL_ENUMS_H
3+
4+
#include <pybind11/pybind11.h>
5+
6+
// Extension for pybind11: Pythonic enums.
7+
// This allows creating classes based on ``enum.*`` types.
8+
// This code was copied from mplcairo, with some slight tweaks.
9+
// The API is:
10+
//
11+
// - P11X_DECLARE_ENUM(py_name: str, py_base_cls: str, ...: {str, enum value}):
12+
// py_name: The name to expose in the module.
13+
// py_base_cls: The name of the enum base class to use.
14+
// ...: The enum name/value pairs to expose.
15+
//
16+
// Use this macro to declare an enum and its values.
17+
//
18+
// - py11x::bind_enums(m: pybind11::module):
19+
// m: The module to use to register the enum classes.
20+
//
21+
// Place this in PYBIND11_MODULE to register the enums declared by P11X_DECLARE_ENUM.
22+
23+
// a1 includes the opening brace and a2 the closing brace.
24+
// This definition is compatible with older compiler versions compared to
25+
// #define P11X_ENUM_TYPE(...) decltype(std::map{std::pair __VA_ARGS__})::mapped_type
26+
#define P11X_ENUM_TYPE(a1, a2, ...) decltype(std::pair a1, a2)::second_type
27+
28+
#define P11X_CAT2(a, b) a##b
29+
#define P11X_CAT(a, b) P11X_CAT2(a, b)
30+
31+
namespace p11x {
32+
namespace {
33+
namespace py = pybind11;
34+
35+
// Holder is (py_base_cls, [(name, value), ...]) before module init;
36+
// converted to the Python class object after init.
37+
auto enums = std::unordered_map<std::string, py::object>{};
38+
39+
auto bind_enums(py::module mod) -> void
40+
{
41+
for (auto& [py_name, spec]: enums) {
42+
auto const& [py_base_cls, pairs] =
43+
spec.cast<std::pair<std::string, py::object>>();
44+
mod.attr(py::cast(py_name)) = spec =
45+
py::module::import("enum").attr(py_base_cls.c_str())(
46+
py_name, pairs, py::arg("module") = mod.attr("__name__"));
47+
}
48+
}
49+
}
50+
}
51+
52+
// Immediately converting the args to a vector outside of the lambda avoids
53+
// name collisions.
54+
#define P11X_DECLARE_ENUM(py_name, py_base_cls, ...) \
55+
namespace p11x { \
56+
namespace { \
57+
[[maybe_unused]] auto const P11X_CAT(enum_placeholder_, __COUNTER__) = \
58+
[](auto args) { \
59+
py::gil_scoped_acquire gil; \
60+
using int_t = std::underlying_type_t<decltype(args[0].second)>; \
61+
auto pairs = std::vector<std::pair<std::string, int_t>>{}; \
62+
for (auto& [k, v]: args) { \
63+
pairs.emplace_back(k, int_t(v)); \
64+
} \
65+
p11x::enums[py_name] = pybind11::cast(std::pair{py_base_cls, pairs}); \
66+
return 0; \
67+
} (std::vector{std::pair __VA_ARGS__}); \
68+
} \
69+
} \
70+
namespace pybind11::detail { \
71+
template<> struct type_caster<P11X_ENUM_TYPE(__VA_ARGS__)> { \
72+
using type = P11X_ENUM_TYPE(__VA_ARGS__); \
73+
static_assert(std::is_enum_v<type>, "Not an enum"); \
74+
PYBIND11_TYPE_CASTER(type, _(py_name)); \
75+
bool load(handle src, bool) { \
76+
auto cls = p11x::enums.at(py_name); \
77+
PyObject* tmp = nullptr; \
78+
if (pybind11::isinstance(src, cls) \
79+
&& (tmp = PyNumber_Index(src.attr("value").ptr()))) { \
80+
auto ival = PyLong_AsLong(tmp); \
81+
value = decltype(value)(ival); \
82+
Py_DECREF(tmp); \
83+
return !(ival == -1 && !PyErr_Occurred()); \
84+
} else { \
85+
return false; \
86+
} \
87+
} \
88+
static handle cast(decltype(value) obj, return_value_policy, handle) { \
89+
auto cls = p11x::enums.at(py_name); \
90+
return cls(std::underlying_type_t<type>(obj)).inc_ref(); \
91+
} \
92+
}; \
93+
}
94+
95+
#endif /* MPL_ENUMS_H */

src/ft2font.cpp

+4-2
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,8 @@ void FT2Font::select_charmap(unsigned long i)
354354
}
355355
}
356356

357-
int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback = false)
357+
int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode,
358+
bool fallback = false)
358359
{
359360
if (fallback && glyph_to_font.find(left) != glyph_to_font.end() &&
360361
glyph_to_font.find(right) != glyph_to_font.end()) {
@@ -375,7 +376,8 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallbac
375376
}
376377
}
377378

378-
int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta)
379+
int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode,
380+
FT_Vector &delta)
379381
{
380382
if (!FT_HAS_KERNING(face)) {
381383
return 0;

src/ft2font.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ class FT2Font
7979
void select_charmap(unsigned long i);
8080
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
8181
std::vector<double> &xys);
82-
int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback);
83-
int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta);
82+
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback);
83+
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta);
8484
void set_kerning_factor(int factor);
8585
void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback);
8686
bool load_char_with_fallback(FT2Font *&ft_object_with_glyph,

0 commit comments

Comments
 (0)