From 910511606bcc025f6aad6197c414190d278f28ce Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 00:24:03 +0000 Subject: [PATCH] font fallback for Raqm layout --- src/PIL/ImageFont.py | 40 ++++++--- src/_imagingft.c | 203 ++++++++++++++++++++++++++++++------------- 2 files changed, 171 insertions(+), 72 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6625a9e02..e7f9829e5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -725,18 +725,20 @@ class FreeTypeFontFamily: # example: # from PIL import Image, ImageDraw, ImageFont - # f1 = ImageFont.truetype("segoeui.ttf", 24) - # f2 = ImageFont.truetype("seguisym.ttf", 24) - # ff = ImageFont.FreeTypeFontFamily(f1, f2) - # s = "a↦ľ" - # im = Image.new("RGBA", (100, 100), "white") - # d = ImageDraw.Draw(im) - # d.text((10, 10), s, "black", f1) - # d.text((10, 40), s, "black", f2) - # d.text((10, 70), s, "black", ff) - # im.show() + # le = ImageFont.Layout.RAQM + # f1 = ImageFont.truetype(r"C:\Users\Nulano\AppData\Local\Microsoft\Windows\Fonts\SCRIPTIN.ttf", 24) + # f2 = ImageFont.truetype("segoeui.ttf", 24) + # f3 = ImageFont.truetype("seguisym.ttf", 24) + # ff = ImageFont.FreeTypeFontFamily(f1, f2, f3, layout_engine=le) + # for s in ("testčingšsšccčcč", "ية↦α,abc", "a↦ľ"): + # im = Image.new("RGBA", (300, 300), "white") + # d = ImageDraw.Draw(im) + # d.text((10, 60), s, "black", f1, direction="ltr", anchor="ls") + # d.text((10, 160), s, "black", f2, direction="ltr", anchor="ls") + # d.text((10, 260), s, "black", ff, direction="ltr", anchor="ls") + # im.show() - def __init__(self, *fonts): + def __init__(self, *fonts, layout_engine=None): fonts_list = [] for font in fonts: try: @@ -745,9 +747,21 @@ class FreeTypeFontFamily: ) except AttributeError: fonts_list.append((font.path, font.size, font.index, font.encoding)) - self.fonts = tuple(fonts_list) - self.font = core.getfamily(self.fonts, layout_engine=Layout.BASIC) + + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC + if core.HAVE_RAQM: + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC + self.layout_engine = layout_engine + + self.font = core.getfamily(self.fonts, layout_engine=self.layout_engine) def getlength(self, text, mode="", direction=None, features=None, language=None): """ diff --git a/src/_imagingft.c b/src/_imagingft.c index d06a7a51d..efaa9bf1e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -404,40 +404,35 @@ text_layout_raqm( raqm_glyph_t *glyphs = NULL; raqm_direction_t direction; + char *buffer = NULL; + Py_UCS4 *text = NULL; + Py_ssize_t size; + int *fallback = NULL; + rq = raqm_create(); if (rq == NULL) { PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); goto failed; } - Py_ssize_t size; - int set_text; if (PyUnicode_Check(string)) { - Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); + text = PyUnicode_AsUCS4Copy(string); size = PyUnicode_GET_LENGTH(string); if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - set_text = raqm_set_text(rq, text, size); - PyMem_Free(text); } else { - char *buffer; PyBytes_AsStringAndSize(string, &buffer, &size); if (!buffer || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - set_text = raqm_set_text_utf8(rq, buffer, size); } - if (!set_text) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); - goto failed; - } - if (lang && !raqm_set_language(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + if (!buffer && !text) { + PyErr_SetString(PyExc_ValueError, "expected string"); goto failed; } @@ -464,63 +459,142 @@ text_layout_raqm( } } - if (!raqm_set_par_direction(rq, direction)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); - goto failed; + if (family->font_count > 1) { + fallback = PyMem_New(int, size); + if (!fallback) { + PyErr_SetString(PyExc_ValueError, "failed to allocate fallback buffer."); + goto failed; + } + fallback[0] = -1; + for (size_t j = 1; j < size; j++) { + fallback[j] = -2; + } } - if (features != Py_None) { - int j, len; - PyObject *seq = PySequence_Fast(features, "expected a sequence"); - if (!seq) { + for (int face = 0; face < family->font_count; face++) { + raqm_clear_contents(rq); + + int set_text = text != NULL ? raqm_set_text(rq, text, size) : raqm_set_text_utf8(rq, buffer, size); + if (!set_text) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + if (lang) { + if (!raqm_set_language(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } + + if (!raqm_set_par_direction(rq, direction)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); goto failed; } - len = PySequence_Fast_GET_SIZE(seq); - for (j = 0; j < len; j++) { - PyObject *item = PySequence_Fast_GET_ITEM(seq, j); - char *feature = NULL; - Py_ssize_t size = 0; - PyObject *bytes; + if (features != Py_None) { + int j, len; + PyObject *seq = PySequence_Fast(features, "expected a sequence"); + if (!seq) { + goto failed; + } - if (!PyUnicode_Check(item)) { - Py_DECREF(seq); - PyErr_SetString(PyExc_TypeError, "expected a string"); - goto failed; - } - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { - Py_DECREF(seq); - goto failed; - } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); - if (!raqm_add_font_feature(rq, feature, size)) { - Py_DECREF(seq); + len = PySequence_Fast_GET_SIZE(seq); + for (j = 0; j < len; j++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, j); + char *feature = NULL; + Py_ssize_t size = 0; + PyObject *bytes; + + if (!PyUnicode_Check(item)) { + Py_DECREF(seq); + PyErr_SetString(PyExc_TypeError, "expected a string"); + goto failed; + } + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) { + Py_DECREF(seq); + goto failed; + } + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); + if (!raqm_add_font_feature(rq, feature, size)) { + Py_DECREF(seq); + Py_DECREF(bytes); + PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + goto failed; + } Py_DECREF(bytes); - PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + } + Py_DECREF(seq); + } + + if (face == 0) { + if (!raqm_set_freetype_face(rq, family->faces[0])) { + PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); goto failed; } - Py_DECREF(bytes); + } else { + start = 0; + for (size_t j = 0; j <= size; j++) { + if (j < size) { + if (fallback[j] == -2) { + /* not a cluster boundary */ + continue; + } + if (fallback[start] == fallback[j]) { + /* use same font face for this cluster */ + continue; + } + } + if (fallback[start] == -1) { + raqm_set_freetype_face_range( + rq, family->faces[face], start, j - start); + } else { + raqm_set_freetype_face_range( + rq, family->faces[fallback[start]], start, j - start); + } + start = j; + } } - Py_DECREF(seq); - } - if (!raqm_set_freetype_face(rq, family->faces[0])) { - PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); - goto failed; - } + if (!raqm_layout(rq)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); + goto failed; + } - if (!raqm_layout(rq)) { - PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); - goto failed; - } + glyphs = raqm_get_glyphs(rq, &count); + if (glyphs == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + count = 0; + goto failed; + } - glyphs = raqm_get_glyphs(rq, &count); - if (glyphs == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); - count = 0; - goto failed; + if (i + 1 == family->font_count) { + break; + } + + for (size_t j = 1; j < size; j++) { + if (fallback[j] == -1) { + fallback[j] = -2; + } + } + + int missing = 0; + for (size_t j = 0; j < count; j++) { + int cluster = glyphs[j].cluster; + if (glyphs[j].index == 0) { + /* cluster contains missing glyph */ + fallback[cluster] = -1; + missing = 1; + } else if (fallback[cluster] < 0) { + /* use current font face for this cluster */ + fallback[cluster] = face; + } + } + + if (!missing) { + break; + } } (*glyph_info) = PyMem_New(GlyphInfo, count); @@ -531,16 +605,27 @@ text_layout_raqm( } for (i = 0; i < count; i++) { - (*glyph_info)[i].face = family->faces[0]; (*glyph_info)[i].index = glyphs[i].index; (*glyph_info)[i].x_offset = glyphs[i].x_offset; (*glyph_info)[i].x_advance = glyphs[i].x_advance; (*glyph_info)[i].y_offset = glyphs[i].y_offset; (*glyph_info)[i].y_advance = glyphs[i].y_advance; - (*glyph_info)[i].cluster = glyphs[i].cluster; + + uint32_t cluster = glyphs[i].cluster; + (*glyph_info)[i].cluster = cluster; + if (fallback && fallback[cluster] >= 0) { + (*glyph_info)[i].face = family->faces[fallback[cluster]]; + } else { + /* FIXME use first font's missing glyph, not last font's */ + (*glyph_info)[i].face = family->faces[family->font_count - 1]; + } } failed: + PyMem_Free(text); + if (fallback) { + PyMem_Free(fallback); + } raqm_destroy(rq); return count; }