Skip to content

Commit 00037f8

Browse files
committed
Implement text shaping with libraqm
1 parent 42d3509 commit 00037f8

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
@@ -774,9 +774,9 @@ def test_ft2font_set_text():
774774
xys = font.set_text('AADAT.XC-J')
775775
np.testing.assert_array_equal(
776776
xys,
777-
[(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0),
778-
(3200, 0), (3712, 0), (4032, 0)])
779-
assert font.get_width_height() == (4288, 768)
777+
[(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0),
778+
(3065, 0), (3577, 0), (3940, 0)])
779+
assert font.get_width_height() == (4196, 768)
780780
assert font.get_num_glyphs() == 10
781781
assert font.get_descent() == 192
782782
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
}
@@ -323,60 +329,88 @@ void FT2Font::set_text(
323329
bbox.xMin = bbox.yMin = 32000;
324330
bbox.xMax = bbox.yMax = -32000;
325331

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

329-
for (auto codepoint : text) {
330-
FT_UInt glyph_index = 0;
331-
FT_BBox glyph_bbox;
332-
FT_Pos last_advance;
346+
if (!raqm_set_freetype_face(rq, face)) {
347+
throw std::runtime_error("failed to set text face for layout");
348+
}
333349

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

351-
// retrieve kerning distance and move pen position
352-
if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same
353-
ft_object_with_glyph->has_kerning() && // if the font knows how to kern
354-
previous && glyph_index // and we really have 2 glyphs
355-
) {
356-
pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT);
368+
// Warn for missing glyphs.
369+
if (rglyph.index == 0) {
370+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
371+
continue;
372+
}
373+
FT2Font *wrapped_font = static_cast<FT2Font *>(rglyph.ftface->generic.data);
374+
if (wrapped_font->warn_if_used) {
375+
ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts);
357376
}
358377

359378
// extract glyph image and store it in our table
360-
FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1];
379+
FT_Error error;
380+
error = FT_Load_Glyph(rglyph.ftface, rglyph.index, flags);
381+
if (error) {
382+
throw std::runtime_error("failed to load glyph");
383+
}
384+
FT_Glyph thisGlyph;
385+
error = FT_Get_Glyph(rglyph.ftface->glyph, &thisGlyph);
386+
if (error) {
387+
throw std::runtime_error("failed to get glyph");
388+
}
389+
390+
pen.x += rglyph.x_offset;
391+
pen.y += rglyph.y_offset;
361392

362-
last_advance = ft_object_with_glyph->get_face()->glyph->advance.x;
363393
FT_Glyph_Transform(thisGlyph, nullptr, &pen);
364394
FT_Glyph_Transform(thisGlyph, &matrix, nullptr);
365395
xys.push_back(pen.x);
366396
xys.push_back(pen.y);
367397

398+
FT_BBox glyph_bbox;
368399
FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox);
369400

370401
bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin);
371402
bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax);
372403
bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin);
373404
bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax);
374405

375-
pen.x += last_advance;
376-
377-
previous = glyph_index;
378-
previous_ft_object = ft_object_with_glyph;
406+
if ((flags & FT_LOAD_NO_HINTING) != 0) {
407+
pen.x += rglyph.x_advance - rglyph.x_offset;
408+
} else {
409+
pen.x += hinting_factor * rglyph.x_advance - rglyph.x_offset;
410+
}
411+
pen.y += rglyph.y_advance - rglyph.y_offset;
379412

413+
glyphs.push_back(thisGlyph);
380414
}
381415

382416
FT_Vector_Transform(&pen, &matrix);

0 commit comments

Comments
 (0)