diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png new file mode 100644 index 000000000..e55a93c74 Binary files /dev/null and b/Tests/images/test_language.png differ diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index d23f6d86f..7953b917d 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase): target_img = Image.open(target) self.assert_image_similar(im, target_img, .5) + + def test_language(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'абвг', font=ttf, fill=500, + language='sr') + + target = 'Tests/images/test_language.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) \ No newline at end of file diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 7c24bae93..8cafc5e26 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -255,7 +255,7 @@ Methods Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) +.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, language=None, features=None) Draws the string at the given position. @@ -274,6 +274,16 @@ Methods .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, @@ -316,7 +326,7 @@ Methods .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None) +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, language=None, features=None) Return the size of the given string, in pixels. @@ -330,7 +340,15 @@ Methods Requires libraqm. .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + .. versionadded:: 6.0.0 :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 55ce3d382..99eb0f1ad 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -47,11 +47,11 @@ Functions Methods ------- -.. py:method:: PIL.ImageFont.ImageFont.getsize(text) +.. py:method:: PIL.ImageFont.ImageFont.getsize(text, direction=None, language=None, features=[]) :return: (width, height) -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) +.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, language=None, features=[]) Create a bitmap for the text. @@ -72,6 +72,16 @@ Methods .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7454b4413..7b253717a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -158,17 +158,17 @@ class FreeTypeFont(object): def getmetrics(self): return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None): - size, offset = self.font.getsize(text, direction, features) + def getsize(self, text, direction=None, language=None, features=None): + size, offset = self.font.getsize(text, direction, language, features) return (size[0] + offset[0], size[1] + offset[1]) - def getsize_multiline(self, text, direction=None, + def getsize_multiline(self, text, direction=None, language=None, spacing=4, features=None): max_width = 0 lines = self._multiline_split(text) line_spacing = self.getsize('A')[1] + spacing for line in lines: - line_width, line_height = self.getsize(line, direction, features) + line_width, line_height = self.getsize(line, direction, language, features) max_width = max(max_width, line_width) return max_width, len(lines)*line_spacing - spacing @@ -181,10 +181,10 @@ class FreeTypeFont(object): features=features)[0] def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, - features=None, *args, **kwargs): - size, offset = self.font.getsize(text, direction, features) + language=None, features=None, *args, **kwargs): + size, offset = self.font.getsize(text, direction, language, features) im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features) + self.font.render(text, im.id, mode == "1", direction, language, features) return im, offset def font_variant(self, font=None, size=None, index=None, encoding=None, diff --git a/src/_imagingft.c b/src/_imagingft.c index f94e55803..e6b2b1cc5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -87,6 +87,10 @@ typedef bool (*t_raqm_set_text_utf8) (raqm_t *rq, size_t len); typedef bool (*t_raqm_set_par_direction) (raqm_t *rq, raqm_direction_t dir); +typedef bool (*t_raqm_set_language) (raqm_t *rq, + const char *lang, + size_t start, + size_t len); typedef bool (*t_raqm_add_font_feature) (raqm_t *rq, const char *feature, int len); @@ -106,6 +110,7 @@ typedef struct { t_raqm_set_text set_text; t_raqm_set_text_utf8 set_text_utf8; t_raqm_set_par_direction set_par_direction; + t_raqm_set_language set_language; t_raqm_add_font_feature add_font_feature; t_raqm_set_freetype_face set_freetype_face; t_raqm_layout layout; @@ -160,6 +165,7 @@ setraqm(void) p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)dlsym(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)dlsym(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)dlsym(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)dlsym(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)dlsym(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)dlsym(p_raqm.raqm, "raqm_layout"); @@ -176,6 +182,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -190,6 +197,7 @@ setraqm(void) p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)GetProcAddress(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)GetProcAddress(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)GetProcAddress(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)GetProcAddress(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)GetProcAddress(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)GetProcAddress(p_raqm.raqm, "raqm_layout"); @@ -205,6 +213,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -332,7 +341,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) } static size_t -text_layout_raqm(PyObject* string, FontObject* self, const char* dir, +text_layout_raqm(PyObject* string, FontObject* self, const char* dir, const char* lang, PyObject *features ,GlyphInfo **glyph_info, int mask) { int i = 0; @@ -341,6 +350,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, raqm_glyph_t *glyphs = NULL; raqm_glyph_t_01 *glyphs_01 = NULL; raqm_direction_t direction; + size_t start = 0; rq = (*p_raqm.create)(); if (rq == NULL) { @@ -360,6 +370,13 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; } + if (lang) { + if (!(*p_raqm.set_language)(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } + } #if PY_VERSION_HEX < 0x03000000 else if (PyString_Check(string)) { @@ -498,7 +515,7 @@ failed: } static size_t -text_layout_fallback(PyObject* string, FontObject* self, const char* dir, +text_layout_fallback(PyObject* string, FontObject* self, const char* dir, const char* lang, PyObject *features ,GlyphInfo **glyph_info, int mask) { int error, load_flags; @@ -509,8 +526,8 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, FT_UInt last_index = 0; int i; - if (features != Py_None || dir != NULL) { - PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + if (features != Py_None || dir != NULL || lang != NULL) { + PyErr_SetString(PyExc_KeyError, "setting text direction, language or font features is not supported without libraqm"); } #if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(string)) { @@ -564,15 +581,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, } static size_t -text_layout(PyObject* string, FontObject* self, const char* dir, +text_layout(PyObject* string, FontObject* self, const char* dir, const char* lang, PyObject *features, GlyphInfo **glyph_info, int mask) { size_t count; if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, glyph_info, mask); + count = text_layout_raqm(string, self, dir, lang, features, glyph_info, mask); } else { - count = text_layout_fallback(string, self, dir, features, glyph_info, mask); + count = text_layout_fallback(string, self, dir, lang, features, glyph_info, mask); } return count; } @@ -584,6 +601,7 @@ font_getsize(FontObject* self, PyObject* args) FT_Face face; int xoffset, yoffset; const char *dir = NULL; + const char *lang = NULL; size_t count; GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; @@ -591,14 +609,14 @@ font_getsize(FontObject* self, PyObject* args) /* calculate size and bearing for a given string */ PyObject* string; - if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) + if (!PyArg_ParseTuple(args, "O|zzO:getsize", &string, &dir, &lang, &features)) return NULL; face = NULL; xoffset = yoffset = 0; y_max = y_min = 0; - count = text_layout(string, self, dir, features, &glyph_info, 0); + count = text_layout(string, self, dir, lang, features, &glyph_info, 0); if (PyErr_Occurred()) { return NULL; } @@ -691,16 +709,17 @@ font_render(FontObject* self, PyObject* args) int temp; int xx, x0, x1; const char *dir = NULL; + const char *lang = NULL; size_t count; GlyphInfo *glyph_info; PyObject *features = NULL; - if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + if (!PyArg_ParseTuple(args, "On|izzO:render", &string, &id, &mask, &dir, &lang, &features)) { return NULL; } glyph_info = NULL; - count = text_layout(string, self, dir, features, &glyph_info, mask); + count = text_layout(string, self, dir, lang, features, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; }