diff --git a/.travis.yml b/.travis.yml index b76f5dbdb..feab7477f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,13 @@ python: - 3.4 - nightly +sudo: required # needed for trusty beta +dist: trusty # needed for HarfBuzz + install: + - "travis_retry sudo add-apt-repository -y ppa:as-bahanta/raqm" - "travis_retry sudo apt-get update" - - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" + - "travis_retry sudo apt-get -qq install libfreetype6-dev libharfbuzz-dev libfribidi-dev libraqm-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - "travis_retry pip install cffi" - "travis_retry pip install nose" - "travis_retry pip install check-manifest" diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 1d5f53746..539b66273 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -252,17 +252,17 @@ class ImageDraw(object): ink = fill if ink is not None: try: - mask, offset = font.getmask2(text, self.fontmode) + mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) xy = xy[0] + offset[0], xy[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode) + mask = font.getmask(text, self.fontmode, *args, **kwargs) except TypeError: mask = font.getmask(text) self.draw.draw_bitmap(xy, mask, ink) def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left"): + spacing=4, align="left", direction=None, features=None): widths = [] max_width = 0 lines = self._multiline_split(text) @@ -281,7 +281,7 @@ class ImageDraw(object): left += (max_width - widths[idx]) else: assert False, 'align must be "left", "center" or "right"' - self.text((left, top), line, fill, font, anchor) + self.text((left, top), line, fill, font, anchor, direction=direction, features=features) top += line_spacing left = xy[0] diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index af1166dde..2541a21ed 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -137,20 +137,20 @@ class FreeTypeFont(object): def getmetrics(self): return self.font.ascent, self.font.descent - def getsize(self, text): - size, offset = self.font.getsize(text) + def getsize(self, text, direction=None, features=None): + size, offset = self.font.getsize(text, direction, features) return (size[0] + offset[0], size[1] + offset[1]) def getoffset(self, text): return self.font.getsize(text)[1] - def getmask(self, text, mode=""): - return self.getmask2(text, mode)[0] + def getmask(self, text, mode="", direction=None, features=None): + return self.getmask2(text, mode, direction=direction, features=features)[0] - def getmask2(self, text, mode="", fill=Image.core.fill): - size, offset = self.font.getsize(text) + def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None): + size, offset = self.font.getsize(text, direction, features) im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1") + self.font.render(text, im.id, mode == "1", direction, features) return im, offset def font_variant(self, font=None, size=None, index=None, encoding=None): diff --git a/PIL/features.py b/PIL/features.py index fd87f094f..53ce38bb0 100644 --- a/PIL/features.py +++ b/PIL/features.py @@ -5,6 +5,7 @@ modules = { "tkinter": "PIL._imagingtk", "freetype2": "PIL._imagingft", "littlecms2": "PIL._imagingcms", + "raqm": "PIL._imagingft", "webp": "PIL._webp", "transp_webp": ("WEBP", "WebPDecoderBuggyAlpha") } diff --git a/Tests/fonts/NotoNastaliqUrdu-Regular.ttf b/Tests/fonts/NotoNastaliqUrdu-Regular.ttf new file mode 100644 index 000000000..891f633d8 Binary files /dev/null and b/Tests/fonts/NotoNastaliqUrdu-Regular.ttf differ diff --git a/Tests/images/test_Nastalifont_text.png b/Tests/images/test_Nastalifont_text.png new file mode 100644 index 000000000..51d56a0de Binary files /dev/null and b/Tests/images/test_Nastalifont_text.png differ diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png new file mode 100644 index 000000000..9bfa5a931 Binary files /dev/null and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png new file mode 100644 index 000000000..f1a6f7ec6 Binary files /dev/null and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png new file mode 100644 index 000000000..42239334d Binary files /dev/null and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png new file mode 100644 index 000000000..966b67d6b Binary files /dev/null and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png new file mode 100644 index 000000000..ca895735c Binary files /dev/null and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png new file mode 100644 index 000000000..664e9929d Binary files /dev/null and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png new file mode 100644 index 000000000..c156399cd Binary files /dev/null and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png new file mode 100644 index 000000000..5a166be8c Binary files /dev/null and b/Tests/images/test_y_offset.png differ diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py new file mode 100644 index 000000000..6c03fb168 --- /dev/null +++ b/Tests/test_imagefontctl.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +from helper import unittest, PillowTestCase +from PIL import Image +from PIL import ImageDraw + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans.ttf" + +try: + from PIL import ImageFont + + # check if raqm is available + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') + + class TestImagecomplextext(PillowTestCase): + + def test_complex_text(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) + + target = 'Tests/images/test_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_y_offset(self): + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'العالم العربي', font=ttf, fill=500) + + target = 'Tests/images/test_y_offset.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_complex_unicode_text(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), u'السلام عليكم', font=ttf, fill=500) + + target = 'Tests/images/test_complex_unicode_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_rtl(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'English عربي', font=ttf, fill=500, direction='rtl') + + target = 'Tests/images/test_direction_rtl.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_ltr(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'سلطنة عمان Oman', font=ttf, fill=500, direction='ltr') + + target = 'Tests/images/test_direction_ltr.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_rtl2(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'Oman سلطنة عمان', font=ttf, fill=500, direction='rtl') + + target = 'Tests/images/test_direction_ltr.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_ligature_features(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'filling', font=ttf, fill=500, features=['-liga']) + + target = 'Tests/images/test_ligature_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_kerning_features(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'TeToAV', font=ttf, fill=500, features=['-kern']) + + target = 'Tests/images/test_kerning_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_arabictext_features(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, features=['-fina','-init','-medi']) + + target = 'Tests/images/test_arabictext_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + +except (KeyError, ImportError): + class TestImagecomplextext(PillowTestCase): + def test_skip(self): + self.skipTest("KeyError") + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/_imagingft.c b/_imagingft.c index dc1661f3e..633a812b3 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -41,6 +41,23 @@ #define FT_ERRORDEF( e, v, s ) { e, s }, #define FT_ERROR_START_LIST { #define FT_ERROR_END_LIST { 0, 0 } }; +#ifdef HAVE_RAQM +#include +#else +typedef enum +{ + RAQM_DIRECTION_DEFAULT, + RAQM_DIRECTION_RTL, + RAQM_DIRECTION_LTR, + RAQM_DIRECTION_TTB +} raqm_direction_t; +#endif + +typedef struct +{ + int index, x_offset, x_advance, y_offset; + unsigned int cluster; +} GlyphInfo; struct { int code; @@ -186,86 +203,88 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) return 0; } +static size_t +text_layout(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask); + static PyObject* font_getsize(FontObject* self, PyObject* args) { int i, x, y_max, y_min; - FT_ULong ch; FT_Face face; int xoffset, yoffset; - FT_Bool kerning = FT_HAS_KERNING(self->face); - FT_UInt last_index = 0; + const char *dir = NULL; + size_t count; + GlyphInfo *glyph_info = NULL;; + PyObject *features = Py_None; /* calculate size and bearing for a given string */ PyObject* string; - if (!PyArg_ParseTuple(args, "O:getsize", &string)) + if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) return NULL; -#if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif - PyErr_SetString(PyExc_TypeError, "expected string"); - return NULL; - } - face = NULL; xoffset = yoffset = 0; y_max = y_min = 0; - for (x = i = 0; font_getchar(string, i, &ch); i++) { + count = text_layout(string, self, dir, features, &glyph_info, 0); + if (count == 0) + return NULL; + + for (x = i = 0; i < count; i++) { int index, error; FT_BBox bbox; FT_Glyph glyph; face = self->face; - index = FT_Get_Char_Index(face, ch); - if (kerning && last_index && index) { - FT_Vector delta; - FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, - &delta); - x += delta.x; - } - - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ + index = glyph_info[i].index; + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 + * Yifu Yu, 2014-10-15 + */ error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) return geterror(error); - if (i == 0) + + if (i == 0 && face->glyph->metrics.horiBearingX < 0) { xoffset = face->glyph->metrics.horiBearingX; - x += face->glyph->metrics.horiAdvance; + x -= xoffset; + } + + x += glyph_info[i].x_advance; + + if (i == count - 1) + { + int offset; + offset = glyph_info[i].x_advance - + face->glyph->metrics.width - + face->glyph->metrics.horiBearingX; + if (offset < 0) + x -= offset; + } FT_Get_Glyph(face->glyph, &glyph); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); + bbox.yMax -= glyph_info[i].y_offset; + bbox.yMin -= glyph_info[i].y_offset; if (bbox.yMax > y_max) y_max = bbox.yMax; if (bbox.yMin < y_min) y_min = bbox.yMin; - + /* find max distance of baseline from top */ if (face->glyph->metrics.horiBearingY > yoffset) yoffset = face->glyph->metrics.horiBearingY; - - last_index = index; + FT_Done_Glyph(glyph); } if (face) { - int offset; + /* left bearing */ if (xoffset < 0) x -= xoffset; else xoffset = 0; - /* right bearing */ - offset = face->glyph->metrics.horiAdvance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) - x -= offset; /* difference between the font ascender and the distance of * the baseline from the top */ yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); @@ -304,7 +323,7 @@ font_getabc(FontObject* self, PyObject* args) int index, error; face = self->face; index = FT_Get_Char_Index(face, ch); - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) return geterror(error); @@ -319,6 +338,205 @@ font_getabc(FontObject* self, PyObject* args) return Py_BuildValue("ddd", a, b, c); } + +static size_t +text_layout(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask) +{ +#ifdef HAVE_RAQM + int i = 0; + raqm_t *rq; + size_t count = 0; + raqm_glyph_t *glyphs; + raqm_direction_t direction; + + rq = raqm_create(); + if (rq == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); + goto failed; + } + + if (PyUnicode_Check(string)) { + Py_UNICODE *text = PyUnicode_AS_UNICODE(string); + Py_ssize_t size = PyUnicode_GET_SIZE(string); + if (!raqm_set_text(rq, (const uint32_t *)(text), size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + } +#if PY_VERSION_HEX < 0x03000000 + else if (PyString_Check(string)) { + char *text = PyString_AS_STRING(string); + int size = PyString_GET_SIZE(string); + if (!raqm_set_text_utf8(rq, text, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); + goto failed; + } + } +#endif + else { + PyErr_SetString(PyExc_TypeError, "expected string"); + goto failed; + } + + direction = RAQM_DIRECTION_DEFAULT; + if (dir) { + if (strcmp(dir, "rtl") == 0) + direction = RAQM_DIRECTION_RTL; + else if (strcmp(dir, "ltr") == 0) + direction = RAQM_DIRECTION_LTR; + else if (strcmp(dir, "ttb") == 0) + direction = RAQM_DIRECTION_TTB; + else { + PyErr_SetString(PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); + goto failed; + } + } + + if (!raqm_set_par_direction(rq, direction)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); + goto failed; + } + + if (features != Py_None) { + int len; + PyObject *seq = PySequence_Fast(features, "expected a sequence"); + if (!seq) { + goto failed; + } + + len = PySequence_Size(seq); + for (i = 0; i < len; i++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, i); + char *feature = NULL; + Py_ssize_t size = 0; + PyObject *bytes; + +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_Check(item)) { +#else + if (!PyUnicode_Check(item) && !PyString_Check(item)) { +#endif + PyErr_SetString(PyExc_TypeError, "expected a string"); + goto failed; + } + + if (PyUnicode_Check(item)) { + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) + goto failed; + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); + } +#if PY_VERSION_HEX < 0x03000000 + else { + feature = PyString_AsString(item); + size = PyString_GET_SIZE(item); + } +#endif + if (!raqm_add_font_feature(rq, feature, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + goto failed; + } + } + } + + if (!raqm_set_freetype_face(rq, self->face)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() 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; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + count = 0; + goto failed; + } + + for (i = 0; i < count; i++) { + (*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].cluster = glyphs[i].cluster; + } + +failed: + raqm_destroy (rq); + return count; + +#else + if (features != Py_None || dir != NULL) + PyErr_SetString(PyExc_KeyError, "Raqm is missing."); + + int error, load_flags; + FT_ULong ch; + Py_ssize_t count; + FT_GlyphSlot glyph; + FT_Bool kerning = FT_HAS_KERNING(self->face); + FT_UInt last_index = 0; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_Check(string)) { +#else + if (!PyUnicode_Check(string) && !PyString_Check(string)) { +#endif + PyErr_SetString(PyExc_TypeError, "expected string"); + return 0; + } + + count = 0; + while (font_getchar(string, count, &ch)) + count++; + if (count == 0) + return 0; + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + return 0; + } + + load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; + if (mask) + load_flags |= FT_LOAD_TARGET_MONO; + int i; + for (i = 0; font_getchar(string, i, &ch); i++) { + (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); + error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); + if (error) { + geterror(error); + return 0; + } + glyph = self->face->glyph; + (*glyph_info)[i].x_offset=0; + (*glyph_info)[i].y_offset=0; + if (kerning && last_index && (*glyph_info)[i].index) { + FT_Vector delta; + if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, + ft_kerning_default,&delta) == 0) + (*glyph_info)[i-1].x_advance += PIXEL(delta.x); + } + + (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + last_index = (*glyph_info)[i].index; + (*glyph_info)[i].cluster = ch; + } + return count; +#endif +} + static PyObject* font_render(FontObject* self, PyObject* args) { @@ -327,11 +545,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender; int load_flags; unsigned char *source; - FT_ULong ch; FT_GlyphSlot glyph; - FT_Bool kerning = FT_HAS_KERNING(self->face); - FT_UInt last_index = 0; - /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ PyObject* string; @@ -339,17 +553,18 @@ font_render(FontObject* self, PyObject* args) int mask = 0; int temp; int xx, x0, x1; - if (!PyArg_ParseTuple(args, "On|i:render", &string, &id, &mask)) + const char *dir = NULL; + size_t count; + GlyphInfo *glyph_info; + PyObject *features = NULL; + + if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) return NULL; -#if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif - PyErr_SetString(PyExc_TypeError, "expected string"); + glyph_info = NULL; + count = text_layout(string, self, dir, features, &glyph_info, mask); + if (count == 0) return NULL; - } im = (Imaging) id; /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ @@ -358,36 +573,37 @@ font_render(FontObject* self, PyObject* args) load_flags |= FT_LOAD_TARGET_MONO; ascender = 0; - for (i = 0; font_getchar(string, i, &ch); i++) { - index = FT_Get_Char_Index(self->face, ch); + for (i = 0; i < count; i++) { + index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); if (error) return geterror(error); + glyph = self->face->glyph; temp = (glyph->bitmap.rows - glyph->bitmap_top); + temp -= PIXEL(glyph_info[i].y_offset); if (temp > ascender) ascender = temp; } - for (x = i = 0; font_getchar(string, i, &ch); i++) { + for (x = i = 0; i < count; i++) { if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) - x = -PIXEL(self->face->glyph->metrics.horiBearingX); - index = FT_Get_Char_Index(self->face, ch); - if (kerning && last_index && index) { - FT_Vector delta; - FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, - &delta); - x += delta.x >> 6; - } + x = -self->face->glyph->metrics.horiBearingX; + index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); if (error) return geterror(error); + if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { + x = -self->face->glyph->metrics.horiBearingX; + } + glyph = self->face->glyph; source = (unsigned char*) glyph->bitmap.buffer; - xx = x + glyph->bitmap_left; + xx = PIXEL(x) + glyph->bitmap_left; + xx += PIXEL(glyph_info[i].x_offset); x0 = 0; x1 = glyph->bitmap.width; if (xx < 0) @@ -399,6 +615,7 @@ font_render(FontObject* self, PyObject* args) /* use monochrome mask (on palette images, etc) */ for (y = 0; y < glyph->bitmap.rows; y++) { int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset); if (yy >= 0 && yy < im->ysize) { /* blend this glyph into the buffer */ unsigned char *target = im->image8[yy] + xx; @@ -418,8 +635,10 @@ font_render(FontObject* self, PyObject* args) /* use antialiased rendering */ for (y = 0; y < glyph->bitmap.rows; y++) { int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset); if (yy >= 0 && yy < im->ysize) { /* blend this glyph into the buffer */ + int i; unsigned char *target = im->image8[yy] + xx; for (i = x0; i < x1; i++) { @@ -430,10 +649,10 @@ font_render(FontObject* self, PyObject* args) source += glyph->bitmap.pitch; } } - x += PIXEL(glyph->metrics.horiAdvance); - last_index = index; + x += glyph_info[i].x_advance; } + PyMem_Del(glyph_info); Py_RETURN_NONE; } diff --git a/docs/installation.rst b/docs/installation.rst index 45b0c3351..1ce838efb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -138,6 +138,8 @@ Many of Pillow's features require external libraries: * Pillow does **not** support the earlier **1.5** series which ships with Ubuntu and Debian. +* **libraqm** provides complex text layout support. + Once you have installed the prerequisites, run:: $ pip install Pillow @@ -168,14 +170,14 @@ Build Options * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, - ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, + ``--disable-tk``, ``--disable-lcms``, ``--disable-raqm``, ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, - ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, + ``--enable-tk``, ``--enable-lcms``, ``--enable-raqm``, ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 842407c90..6e379475b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -227,7 +227,7 @@ Methods Draw a shape. -.. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") +.. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) Draws the string at the given position. @@ -240,9 +240,11 @@ Methods the number of pixels between lines. :param align: If the text is passed on to multiline_text(), "left", "center" or "right". + :param direction: Direction of the text. It can be 'rtl', 'ltr', 'ttb' or 'btt. + :param features: A list of font features used for text layout. For example, 'ligature, kerning, Medial ...etc. - -.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") +.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", + direction=None, features=[]) Draws the string at the given position. @@ -252,6 +254,8 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". + :param direction: Direction of the text. + :param features: Font features used for text layout. .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 166d977a6..40e7d7f46 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -51,7 +51,7 @@ Methods :return: (width, height) -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='') +.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) Create a bitmap for the text. @@ -63,6 +63,8 @@ Methods driver prefers; if empty, the renderer may return either mode. Note that the mode is always a string, to simplify C-level implementations. + :param direction: Direction of the text. + :param features: A list of font features used for text layout. .. versionadded:: 1.1.5 :return: An internal PIL storage memory instance as defined by the diff --git a/selftest.py b/selftest.py index 71f2354f8..b19e4a34b 100644 --- a/selftest.py +++ b/selftest.py @@ -180,7 +180,8 @@ if __name__ == "__main__": ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "Transparent WEBP") + ("transp_webp", "Transparent WEBP"), + ("raqm", "RAQM") ]: supported = features.check_module(name) diff --git a/setup.py b/setup.py index 9013270f3..f7302f1ed 100644 --- a/setup.py +++ b/setup.py @@ -99,11 +99,12 @@ TIFF_ROOT = None FREETYPE_ROOT = None LCMS_ROOT = None +RAQM_ROOT = None class pil_build_ext(build_ext): class feature: - zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = webpmux = None + zlib = jpeg = tiff = freetype = raqm = tcl = tk = lcms = webp = webpmux = None jpeg2000 = None required = set(['jpeg', 'zlib']) @@ -160,7 +161,7 @@ class pil_build_ext(build_ext): # add configured kits for root in (TCL_ROOT, JPEG_ROOT, JPEG2K_ROOT, TIFF_ROOT, ZLIB_ROOT, - FREETYPE_ROOT, LCMS_ROOT): + FREETYPE_ROOT, LCMS_ROOT, RAQM_ROOT): if isinstance(root, type(())): lib_root, include_root = root else: @@ -467,6 +468,13 @@ class pil_build_ext(build_ext): if dir: _add_directory(self.compiler.include_dirs, dir, 0) + if feature.want('raqm'): + if _find_include_file(self, "raqm.h"): + if _find_library_file(self, "raqm") and \ + _find_library_file(self, "harfbuzz") and \ + _find_library_file(self, "fribidi"): + feature.raqm = ["raqm", "harfbuzz", "fribidi"] + if feature.want('lcms'): if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): @@ -555,8 +563,15 @@ class pil_build_ext(build_ext): # additional libraries if feature.freetype: + libs = ["freetype"] + defs = [] + if feature.raqm: + libs.extend(feature.raqm) + defs.append(('HAVE_RAQM', None)) + exts.append(Extension( - "PIL._imagingft", ["_imagingft.c"], libraries=["freetype"])) + "PIL._imagingft", ["_imagingft.c"], libraries=libs, + define_macros=defs)) if os.path.isfile("_imagingcms.c") and feature.lcms: extra = [] @@ -647,6 +662,7 @@ class pil_build_ext(build_ext): (feature.zlib, "ZLIB (PNG/ZIP)"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), + (feature.raqm, "RAQM"), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), ]