Skip to content

Commit 2027e7e

Browse files
committed
Implement text shaping with libraqm
1 parent 142bf69 commit 2027e7e

File tree

4 files changed

+82
-66
lines changed

4 files changed

+82
-66
lines changed

lib/matplotlib/_text_helpers.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,6 @@ def warn_on_missing_glyph(codepoint, fontnames):
2525
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
2626
f"missing from font(s) {fontnames}.")
2727

28-
block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else
29-
"Arabic" if 0x0600 <= codepoint <= 0x06ff else
30-
"Devanagari" if 0x0900 <= codepoint <= 0x097f else
31-
"Bengali" if 0x0980 <= codepoint <= 0x09ff else
32-
"Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else
33-
"Gujarati" if 0x0a80 <= codepoint <= 0x0aff else
34-
"Oriya" if 0x0b00 <= codepoint <= 0x0b7f else
35-
"Tamil" if 0x0b80 <= codepoint <= 0x0bff else
36-
"Telugu" if 0x0c00 <= codepoint <= 0x0c7f else
37-
"Kannada" if 0x0c80 <= codepoint <= 0x0cff else
38-
"Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else
39-
"Sinhala" if 0x0d80 <= codepoint <= 0x0dff else
40-
None)
41-
if block:
42-
_api.warn_external(
43-
f"Matplotlib currently does not support {block} natively.")
44-
4528

4629
def layout(string, font, *, kern_mode=Kerning.DEFAULT):
4730
"""

lib/matplotlib/tests/test_ft2font.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -767,9 +767,9 @@ def test_ft2font_set_text():
767767
xys = font.set_text('AADAT.XC-J')
768768
np.testing.assert_array_equal(
769769
xys,
770-
[(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0),
771-
(3200, 0), (3712, 0), (4032, 0)])
772-
assert font.get_width_height() == (4288, 768)
770+
[(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0),
771+
(3065, 0), (3577, 0), (3940, 0)])
772+
assert font.get_width_height() == (4196, 768)
773773
assert font.get_num_glyphs() == 10
774774
assert font.get_descent() == 192
775775
assert font.get_bitmap_offset() == (6, 0)

lib/matplotlib/tests/test_text.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ def find_matplotlib_font(**kw):
113113
ax.set_yticks([])
114114

115115

116+
@image_comparison(['complex.png'])
117+
def test_complex_shaping():
118+
# Raqm is Arabic for writing; note that because Arabic is RTL, the characters here
119+
# may seem to be in a different order than expected.
120+
text = (
121+
'Arabic: \N{Arabic Letter REH}\N{Arabic FATHA}\N{Arabic Letter QAF}'
122+
'\N{Arabic SUKUN}\N{Arabic Letter MEEM}')
123+
fig = plt.figure(figsize=(3, 1))
124+
fig.text(0.5, 0.5, text, size=32, ha='center', va='center')
125+
126+
116127
@image_comparison(['multiline'])
117128
def test_multiline():
118129
plt.figure()
@@ -826,18 +837,6 @@ def test_pdf_kerning():
826837
plt.figtext(0.1, 0.5, "ATATATATATATATATATA", size=30)
827838

828839

829-
def test_unsupported_script(recwarn):
830-
fig = plt.figure()
831-
t = fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}")
832-
fig.canvas.draw()
833-
assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
834-
assert (
835-
[warn.message.args for warn in recwarn] ==
836-
[(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from font(s) "
837-
+ f"{t.get_fontname()}.",),
838-
(r"Matplotlib currently does not support Bengali natively.",)])
839-
840-
841840
# See gh-26152 for more information on this xfail
842841
@pytest.mark.xfail(pyparsing_version.release == (3, 1, 0),
843842
reason="Error messages are incorrect with pyparsing 3.1.0")

src/ft2font.cpp

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ FT2Font::FT2Font(FT_Open_Args &open_args,
218218
{
219219
clear();
220220
FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face);
221+
222+
// This allows us to get back to our data if we need it, though it makes a pointer
223+
// loop, so don't set a free-function for it.
224+
face->generic.data = this;
225+
face->generic.finalizer = nullptr;
226+
221227
if (open_args.stream != nullptr) {
222228
face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM;
223229
}
@@ -329,60 +335,88 @@ void FT2Font::set_text(
329335
bbox.xMin = bbox.yMin = 32000;
330336
bbox.xMax = bbox.yMax = -32000;
331337

332-
FT_UInt previous = 0;
333-
FT2Font *previous_ft_object = nullptr;
338+
auto rq = raqm_create();
339+
if (!rq) {
340+
throw std::runtime_error("failed to compute text layout");
341+
}
342+
[[maybe_unused]] auto const& rq_cleanup =
343+
std::unique_ptr<std::remove_pointer_t<raqm_t>, decltype(&raqm_destroy)>(
344+
rq, raqm_destroy);
345+
346+
if (!raqm_set_text(rq, reinterpret_cast<const uint32_t *>(text.data()),
347+
text.size()))
348+
{
349+
throw std::runtime_error("failed to set text for layout");
350+
}
334351

335-
for (auto codepoint : text) {
336-
FT_UInt glyph_index = 0;
337-
FT_BBox glyph_bbox;
338-
FT_Pos last_advance;
352+
if (!raqm_set_freetype_face(rq, face)) {
353+
throw std::runtime_error("failed to set text face for layout");
354+
}
339355

340-
FT_Error charcode_error, glyph_error;
341-
std::set<FT_String*> glyph_seen_fonts;
342-
FT2Font *ft_object_with_glyph = this;
343-
bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs,
344-
char_to_font, codepoint, flags,
345-
charcode_error, glyph_error, glyph_seen_fonts, false);
346-
if (!was_found) {
347-
ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts);
348-
// render missing glyph tofu
349-
// come back to top-most font
350-
ft_object_with_glyph = this;
351-
char_to_font[codepoint] = ft_object_with_glyph;
352-
ft_object_with_glyph->load_glyph(glyph_index, flags);
353-
} else if (ft_object_with_glyph->warn_if_used) {
354-
ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts);
355-
}
356+
if (!raqm_set_freetype_load_flags(rq, flags)) {
357+
throw std::runtime_error("failed to set text flags for layout");
358+
}
359+
360+
std::set<FT_String*> glyph_seen_fonts;
361+
glyph_seen_fonts.insert(face->family_name);
362+
363+
if (!raqm_layout(rq)) {
364+
throw std::runtime_error("failed to layout text");
365+
}
366+
367+
368+
size_t num_glyphs = 0;
369+
auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs);
370+
371+
for (size_t i = 0; i < num_glyphs; i++) {
372+
auto const& rglyph = rq_glyphs[i];
356373

357-
// retrieve kerning distance and move pen position
358-
if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same
359-
ft_object_with_glyph->has_kerning() && // if the font knows how to kern
360-
previous && glyph_index // and we really have 2 glyphs
361-
) {
362-
pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT);
374+
// Warn for missing glyphs.
375+
if (rglyph.index == 0) {
376+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
377+
continue;
378+
}
379+
FT2Font *wrapped_font = static_cast<FT2Font *>(rglyph.ftface->generic.data);
380+
if (wrapped_font->warn_if_used) {
381+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
363382
}
364383

365384
// extract glyph image and store it in our table
366-
FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1];
385+
FT_Error error;
386+
error = FT_Load_Glyph(rglyph.ftface, rglyph.index, flags);
387+
if (error) {
388+
throw std::runtime_error("failed to load glyph");
389+
}
390+
FT_Glyph thisGlyph;
391+
error = FT_Get_Glyph(rglyph.ftface->glyph, &thisGlyph);
392+
if (error) {
393+
throw std::runtime_error("failed to get glyph");
394+
}
395+
396+
pen.x += rglyph.x_offset;
397+
pen.y += rglyph.y_offset;
367398

368-
last_advance = ft_object_with_glyph->get_face()->glyph->advance.x;
369399
FT_Glyph_Transform(thisGlyph, nullptr, &pen);
370400
FT_Glyph_Transform(thisGlyph, &matrix, nullptr);
371401
xys.push_back(pen.x);
372402
xys.push_back(pen.y);
373403

404+
FT_BBox glyph_bbox;
374405
FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox);
375406

376407
bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin);
377408
bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax);
378409
bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin);
379410
bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax);
380411

381-
pen.x += last_advance;
382-
383-
previous = glyph_index;
384-
previous_ft_object = ft_object_with_glyph;
412+
if ((flags & FT_LOAD_NO_HINTING) != 0) {
413+
pen.x += rglyph.x_advance - rglyph.x_offset;
414+
} else {
415+
pen.x += hinting_factor * rglyph.x_advance - rglyph.x_offset;
416+
}
417+
pen.y += rglyph.y_advance - rglyph.y_offset;
385418

419+
glyphs.push_back(thisGlyph);
386420
}
387421

388422
FT_Vector_Transform(&pen, &matrix);

0 commit comments

Comments
 (0)