Skip to content

Commit fef0e54

Browse files
committed
Use libraqm for text in vector outputs
1 parent 7d53c12 commit fef0e54

File tree

8 files changed

+37
-109
lines changed

8 files changed

+37
-109
lines changed

lib/matplotlib/_text_helpers.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,21 @@
44

55
from __future__ import annotations
66

7-
import dataclasses
7+
from collections.abc import Iterator
88

99
from . import _api
10-
from .ft2font import FT2Font, GlyphIndexType, Kerning, LoadFlags
10+
from .ft2font import FT2Font, CharacterCodeType, LayoutItem, LoadFlags
1111

1212

13-
@dataclasses.dataclass(frozen=True)
14-
class LayoutItem:
15-
ft_object: FT2Font
16-
char: str
17-
glyph_index: GlyphIndexType
18-
x: float
19-
prev_kern: float
20-
21-
22-
def warn_on_missing_glyph(codepoint, fontnames):
13+
def warn_on_missing_glyph(codepoint: CharacterCodeType, fontnames: str):
2314
_api.warn_external(
2415
f"Glyph {codepoint} "
2516
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
2617
f"missing from font(s) {fontnames}.")
2718

2819

29-
def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=None):
20+
def layout(string: str, font: FT2Font, *, features=None,
21+
language=None) -> Iterator[LayoutItem]:
3022
"""
3123
Render *string* with *font*.
3224
@@ -41,8 +33,6 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N
4133
The font.
4234
features : tuple of str, optional
4335
The font features to apply to the text.
44-
kern_mode : Kerning
45-
A FreeType kerning mode.
4636
language : str, optional
4737
The language of the text in a format accepted by libraqm, namely `a BCP47
4838
language code <https://www.w3.org/International/articles/language-tags/>`_.
@@ -51,20 +41,8 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N
5141
------
5242
LayoutItem
5343
"""
54-
x = 0
55-
prev_glyph_index = None
56-
char_to_font = font._get_fontmap(string) # TODO: Pass in features and language.
57-
base_font = font
58-
for char in string:
59-
# This has done the fallback logic
60-
font = char_to_font.get(char, base_font)
61-
glyph_index = font.get_char_index(ord(char))
62-
kern = (
63-
base_font.get_kerning(prev_glyph_index, glyph_index, kern_mode) / 64
64-
if prev_glyph_index is not None else 0.
65-
)
66-
x += kern
67-
glyph = font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING)
68-
yield LayoutItem(font, char, glyph_index, x, kern)
69-
x += glyph.linearHoriAdvance / 65536
70-
prev_glyph_index = glyph_index
44+
for raqm_item in font._layout(string, LoadFlags.NO_HINTING,
45+
features=features, language=language):
46+
raqm_item.ft_object.load_glyph(raqm_item.glyph_index,
47+
flags=LoadFlags.NO_HINTING)
48+
yield raqm_item

lib/matplotlib/backends/_backend_pdf_ps.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,17 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]:
158158
and the character codes will be returned from the string unchanged.
159159
"""
160160
font_glyphs = []
161-
char_to_font = font._get_fontmap(s)
162-
for _c, _f in char_to_font.items():
163-
charcode = ord(_c)
164-
glyph_index = _f.get_char_index(charcode)
161+
for raqm_item in font._layout(s, ft2font.LoadFlags.NO_HINTING):
162+
font_path = raqm_item.ft_object.fname
163+
charcode = ord(raqm_item.char)
164+
glyph_index = raqm_item.glyph_index
165165
if self.subset_size != 0:
166166
subset = charcode // self.subset_size
167167
subset_charcode = charcode % self.subset_size
168168
else:
169169
subset = 0
170170
subset_charcode = charcode
171-
self.used.setdefault((_f.fname, subset), {})[subset_charcode] = glyph_index
171+
self.used.setdefault((font_path, subset), {})[subset_charcode] = glyph_index
172172
font_glyphs.append((subset, subset_charcode))
173173
return font_glyphs
174174

lib/matplotlib/backends/backend_pdf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from matplotlib.figure import Figure
3535
from matplotlib.font_manager import get_font, fontManager as _fontManager
3636
from matplotlib._afm import AFM
37-
from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags
37+
from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags
3838
from matplotlib.transforms import Affine2D, BboxBase
3939
from matplotlib.path import Path
4040
from matplotlib.dates import UTC
@@ -2286,6 +2286,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
22862286
# If fonttype is neither 3 nor 42, emit the whole string at once
22872287
# without manual kerning.
22882288
if fonttype not in [3, 42]:
2289+
if not mpl.rcParams['pdf.use14corefonts']:
2290+
self.file._character_tracker.track(font, s)
22892291
self.file.output(Op.begin_text,
22902292
self.file.fontName(prop), fontsize, Op.selectfont)
22912293
self._setup_textpos(x, y, angle)

lib/matplotlib/backends/backend_ps.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
769769
if ismath:
770770
return self.draw_mathtext(gc, x, y, s, prop, angle)
771771

772-
stream = [] # list of (ps_name, x, char_name)
772+
stream = [] # list of (ps_name, x, y, char_name)
773773

774774
if mpl.rcParams['ps.useafm']:
775775
font = self._get_font_afm(prop)
@@ -787,7 +787,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
787787
kern = font.get_kern_dist_from_name(last_name, name)
788788
last_name = name
789789
thisx += kern * scale
790-
stream.append((ps_name, thisx, name))
790+
stream.append((ps_name, thisx, 0, name))
791791
thisx += width * scale
792792

793793
else:
@@ -797,20 +797,19 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
797797
else:
798798
features = language = None
799799
font = self._get_font_ttf(prop)
800-
self._character_tracker.track(font, s)
801800
for item in _text_helpers.layout(s, font, features=features,
802801
language=language):
802+
self._character_tracker.track_glyph(item.ft_object, item.glyph_index)
803803
ps_name = (item.ft_object.postscript_name
804804
.encode("ascii", "replace").decode("ascii"))
805805
glyph_name = item.ft_object.get_glyph_name(item.glyph_index)
806-
stream.append((ps_name, item.x, glyph_name))
806+
stream.append((ps_name, item.x, item.y, glyph_name))
807807
self.set_color(*gc.get_rgb())
808808

809-
for ps_name, group in itertools. \
810-
groupby(stream, lambda entry: entry[0]):
809+
for ps_name, group in itertools.groupby(stream, lambda entry: entry[0]):
811810
self.set_font(ps_name, prop.get_size_in_points(), False)
812-
thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow"
813-
for _, x, name in group)
811+
thetext = "\n".join(f"{x:g} {y:g} m /{name:s} glyphshow"
812+
for _, x, y, name in group)
814813
self._pswriter.write(f"""\
815814
gsave
816815
{self._get_clip_cmd(gc)}

lib/matplotlib/ft2font.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ class FT2Font(Buffer):
203203
) -> None: ...
204204
if sys.version_info[:2] >= (3, 12):
205205
def __buffer__(self, flags: int) -> memoryview: ...
206-
def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ...
207206
def clear(self) -> None: ...
208207
def draw_glyph_to_bitmap(
209208
self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ...

lib/matplotlib/tests/test_ft2font.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,7 @@ def test_fallback_last_resort(recwarn):
972972
"Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)")
973973

974974

975-
def test__get_fontmap():
975+
def test__layout():
976976
fonts, test_str = _gen_multi_font_text()
977977
# Add some glyphs that don't exist in either font to check the Last Resort fallback.
978978
missing_glyphs = '\n几个汉字'
@@ -981,11 +981,11 @@ def test__get_fontmap():
981981
ft = fm.get_font(
982982
fm.fontManager._find_fonts_by_props(fm.FontProperties(family=fonts))
983983
)
984-
fontmap = ft._get_fontmap(test_str)
985-
for char, font in fontmap.items():
986-
if char in missing_glyphs:
987-
assert Path(font.fname).name == 'LastResortHE-Regular.ttf'
988-
elif ord(char) > 127:
989-
assert Path(font.fname).name == 'DejaVuSans.ttf'
990-
else:
991-
assert Path(font.fname).name == 'cmr10.ttf'
984+
for substr in test_str.split('\n'):
985+
for item in ft._layout(substr, ft2font.LoadFlags.DEFAULT):
986+
if item.char in missing_glyphs:
987+
assert Path(item.ft_object.fname).name == 'LastResortHE-Regular.ttf'
988+
elif ord(item.char) > 127:
989+
assert Path(item.ft_object.fname).name == 'DejaVuSans.ttf'
990+
else:
991+
assert Path(item.ft_object.fname).name == 'cmr10.ttf'

lib/matplotlib/textpath.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,16 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,
147147
glyph_map_new = glyph_map
148148

149149
xpositions = []
150+
ypositions = []
150151
glyph_reprs = []
151152
for item in _text_helpers.layout(s, font, features=features, language=language):
152153
glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index)
153154
glyph_reprs.append(glyph_repr)
154155
xpositions.append(item.x)
156+
ypositions.append(item.y)
155157
if glyph_repr not in glyph_map:
156158
glyph_map_new[glyph_repr] = item.ft_object.get_path()
157159

158-
ypositions = [0] * len(xpositions)
159160
sizes = [1.] * len(xpositions)
160161

161162
rects = []

src/ft2font_wrapper.cpp

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -623,54 +623,6 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right,
623623
return self->get_kerning(left, right, mode);
624624
}
625625

626-
const char *PyFT2Font_get_fontmap__doc__ = R"""(
627-
Get a mapping between characters and the font that includes them.
628-
629-
.. warning::
630-
This API uses the fallback list and is both private and provisional: do not use
631-
it directly.
632-
633-
Parameters
634-
----------
635-
text : str
636-
The characters for which to find fonts.
637-
638-
Returns
639-
-------
640-
dict[str, FT2Font]
641-
A dictionary mapping unicode characters to `.FT2Font` objects.
642-
)""";
643-
644-
static py::dict
645-
PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text)
646-
{
647-
std::set<FT_ULong> codepoints;
648-
649-
py::dict char_to_font;
650-
for (auto code : text) {
651-
if (!codepoints.insert(code).second) {
652-
continue;
653-
}
654-
655-
py::object target_font;
656-
int index;
657-
if (self->get_char_fallback_index(code, index)) {
658-
if (index >= 0) {
659-
target_font = self->fallbacks[index];
660-
} else {
661-
target_font = py::cast(self);
662-
}
663-
} else {
664-
// TODO Handle recursion!
665-
target_font = py::cast(self);
666-
}
667-
668-
auto key = py::cast(std::u32string(1, code));
669-
char_to_font[key] = target_font;
670-
}
671-
return char_to_font;
672-
}
673-
674626
const char *PyFT2Font_set_text__doc__ = R"""(
675627
Set the text *string* and *angle*.
676628
@@ -1683,15 +1635,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16831635
.def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a,
16841636
PyFT2Font_get_kerning__doc__)
16851637
.def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(),
1686-
"features"_a=nullptr,
1687-
"language"_a=nullptr,
1638+
"features"_a=nullptr, "language"_a=nullptr,
16881639
PyFT2Font_layout__doc__)
16891640
.def("set_text", &PyFT2Font_set_text,
16901641
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(),
16911642
"features"_a=nullptr, "language"_a=nullptr,
16921643
PyFT2Font_set_text__doc__)
1693-
.def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a,
1694-
PyFT2Font_get_fontmap__doc__)
16951644
.def("get_num_glyphs", &PyFT2Font::get_num_glyphs,
16961645
PyFT2Font_get_num_glyphs__doc__)
16971646
.def("load_char", &PyFT2Font_load_char,

0 commit comments

Comments
 (0)