Skip to content

Commit da31a3f

Browse files
committed
Implement text shaping with libraqm
1 parent 3547379 commit da31a3f

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
@@ -775,9 +775,9 @@ def test_ft2font_set_text():
775775
xys = font.set_text('AADAT.XC-J')
776776
np.testing.assert_array_equal(
777777
xys,
778-
[(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0),
779-
(3200, 0), (3712, 0), (4032, 0)])
780-
assert font.get_width_height() == (4288, 768)
778+
[(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0),
779+
(3065, 0), (3577, 0), (3940, 0)])
780+
assert font.get_width_height() == (4196, 768)
781781
assert font.get_num_glyphs() == 10
782782
assert font.get_descent() == 192
783783
assert font.get_bitmap_offset() == (6, 0)

lib/matplotlib/tests/test_text.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ 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, but libraqm will order them
120+
# correctly for us.
121+
text = (
122+
'Arabic: \N{Arabic Letter REH}\N{Arabic FATHA}\N{Arabic Letter QAF}'
123+
'\N{Arabic SUKUN}\N{Arabic Letter MEEM}')
124+
fig = plt.figure(figsize=(3, 1))
125+
fig.text(0.5, 0.5, text, size=32, ha='center', va='center')
126+
127+
116128
@image_comparison(['multiline'])
117129
def test_multiline():
118130
plt.figure()
@@ -826,18 +838,6 @@ def test_pdf_kerning():
826838
plt.figtext(0.1, 0.5, "ATATATATATATATATATA", size=30)
827839

828840

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-
841841
# See gh-26152 for more information on this xfail
842842
@pytest.mark.xfail(pyparsing_version.release == (3, 1, 0),
843843
reason="Error messages are incorrect with pyparsing 3.1.0")

src/ft2font.cpp

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ void FT2Font::open(FT_Open_Args &open_args)
227227
if (open_args.stream != nullptr) {
228228
face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM;
229229
}
230+
231+
// This allows us to get back to our data if we need it, though it makes a pointer
232+
// loop, so don't set a free-function for it.
233+
face->generic.data = this;
234+
face->generic.finalizer = nullptr;
230235
}
231236

232237
void FT2Font::close()
@@ -333,60 +338,88 @@ void FT2Font::set_text(
333338
bbox.xMin = bbox.yMin = 32000;
334339
bbox.xMax = bbox.yMax = -32000;
335340

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

339-
for (auto codepoint : text) {
340-
FT_UInt glyph_index = 0;
341-
FT_BBox glyph_bbox;
342-
FT_Pos last_advance;
355+
if (!raqm_set_freetype_face(rq, face)) {
356+
throw std::runtime_error("failed to set text face for layout");
357+
}
343358

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

361-
// retrieve kerning distance and move pen position
362-
if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same
363-
ft_object_with_glyph->has_kerning() && // if the font knows how to kern
364-
previous && glyph_index // and we really have 2 glyphs
365-
) {
366-
pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT);
370+
371+
size_t num_glyphs = 0;
372+
auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs);
373+
374+
for (size_t i = 0; i < num_glyphs; i++) {
375+
auto const& rglyph = rq_glyphs[i];
376+
377+
// Warn for missing glyphs.
378+
if (rglyph.index == 0) {
379+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
380+
continue;
381+
}
382+
FT2Font *wrapped_font = static_cast<FT2Font *>(rglyph.ftface->generic.data);
383+
if (wrapped_font->warn_if_used) {
384+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
367385
}
368386

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

372-
last_advance = ft_object_with_glyph->get_face()->glyph->advance.x;
373402
FT_Glyph_Transform(thisGlyph, nullptr, &pen);
374403
FT_Glyph_Transform(thisGlyph, &matrix, nullptr);
375404
xys.push_back(pen.x);
376405
xys.push_back(pen.y);
377406

407+
FT_BBox glyph_bbox;
378408
FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox);
379409

380410
bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin);
381411
bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax);
382412
bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin);
383413
bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax);
384414

385-
pen.x += last_advance;
386-
387-
previous = glyph_index;
388-
previous_ft_object = ft_object_with_glyph;
415+
if ((flags & FT_LOAD_NO_HINTING) != 0) {
416+
pen.x += rglyph.x_advance - rglyph.x_offset;
417+
} else {
418+
pen.x += hinting_factor * rglyph.x_advance - rglyph.x_offset;
419+
}
420+
pen.y += rglyph.y_advance - rglyph.y_offset;
389421

422+
glyphs.push_back(thisGlyph);
390423
}
391424

392425
FT_Vector_Transform(&pen, &matrix);

0 commit comments

Comments
 (0)