diff --git a/.travis.yml b/.travis.yml index 3478d0aa0..d3927940d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,8 +20,9 @@ python: dist: trusty 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 720403920..de3dfdcd6 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -222,7 +222,6 @@ class ImageDraw(object): if self._multiline_check(text): return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) - ink, fill = self._getink(fill) if font is None: font = self.getfont() @@ -230,17 +229,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) @@ -259,7 +258,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 49494b33f..b8059933c 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -136,20 +136,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 134d85abf..cfa05b473 100644 --- a/PIL/features.py +++ b/PIL/features.py @@ -4,6 +4,7 @@ modules = { "pil": "PIL._imaging", "tkinter": "PIL._imagingtk", "freetype2": "PIL._imagingft", + "raqm": "PIL._imagingft", "littlecms2": "PIL._imagingcms", "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..32999b169 --- /dev/null +++ b/Tests/test_imagefontctl.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +from helper import unittest, PillowTestCase +from PIL import Image +from PIL import ImageDraw, ImageFont + +#check if raqm installed +have_raqm = ImageFont.core.have_raqm + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans.ttf" + +try: + from PIL import ImageFont + + 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') + @unittest.skipIf(not have_raqm, "Raqm Library is not installed !") + 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 ImportError: + class TestImagecomplextext(PillowTestCase): + def test_skip(self): + self.skipTest("ImportError") +except KeyError: + 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 ae62fc74e..2725f3022 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -41,6 +41,15 @@ #define FT_ERRORDEF( e, v, s ) { e, s }, #define FT_ERROR_START_LIST { #define FT_ERROR_END_LIST { 0, 0 } }; +#ifdef HAVE_RAQM +#include +#endif + +typedef struct +{ + int index, x_offset, x_advance, y_offset; + unsigned int cluster; +} GlyphInfo; struct { int code; @@ -188,60 +197,284 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) return 0; } -static PyObject* -font_getsize(FontObject* self, PyObject* args) +#ifdef HAVE_RAQM +static size_t +text_layout_raqm(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask) { - int i, x, y_max, y_min; + 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; +} +#endif + +static size_t +text_layout_fallback(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask) +{ + int error, load_flags; FT_ULong ch; - FT_Face face; - int xoffset, yoffset; + Py_ssize_t count; + FT_GlyphSlot glyph; FT_Bool kerning = FT_HAS_KERNING(self->face); FT_UInt last_index = 0; + int i; - /* calculate size and bearing for a given string */ - - PyObject* string; - if (!PyArg_ParseTuple(args, "O:getsize", &string)) - return NULL; - + if (features != Py_None || dir != NULL) { + PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + } #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; + 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; + } + 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; +} + +static size_t +text_layout(PyObject* string, FontObject* self, const char* dir, + PyObject *features, GlyphInfo **glyph_info, int mask) +{ + size_t count; +#ifdef HAVE_RAQM + count = text_layout_raqm(string, self, dir, features, glyph_info, mask); +#else + count = text_layout_fallback(string, self, dir, features, glyph_info, mask); +#endif + return count; +} + +static PyObject* +font_getsize(FontObject* self, PyObject* args) +{ + int i, x, y_max, y_min; + FT_Face face; + int xoffset, yoffset; + 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|zO:getsize", &string, &dir, &features)) + 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) @@ -251,23 +484,16 @@ font_getsize(FontObject* self, PyObject* args) 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); @@ -306,7 +532,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); @@ -329,11 +555,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; @@ -341,15 +563,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)) - return NULL; + const char *dir = NULL; + size_t count; + GlyphInfo *glyph_info; + PyObject *features = 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"); + if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + return NULL; + } + + glyph_info = NULL; + count = text_layout(string, self, dir, features, &glyph_info, mask); + if (count == 0) { return NULL; } @@ -360,36 +585,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) @@ -401,6 +627,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; @@ -420,8 +647,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++) { @@ -432,10 +661,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; } @@ -593,6 +822,14 @@ setup_module(PyObject* m) { #endif PyDict_SetItemString(d, "freetype2_version", v); + +#ifdef HAVE_RAQM + v = PyBool_FromLong(1); +#else + v = PyBool_FromLong(0); +#endif + PyDict_SetItemString(d, "have_raqm", v); + return 0; } diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh index 96e6a8e2b..c4f72bf8e 100755 --- a/depends/debian_8.2.sh +++ b/depends/debian_8.2.sh @@ -11,7 +11,8 @@ sudo apt-get -y install python-dev python-setuptools \ python3-dev python-virtualenv cmake sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk + python-tk python3-tk libharfbuzz-dev libfribidi-dev ./install_openjpeg.sh ./install_imagequant.sh +./install_raqm.sh diff --git a/depends/fedora_23.sh b/depends/fedora_23.sh index bad03e764..5bdcf7f17 100755 --- a/depends/fedora_23.sh +++ b/depends/fedora_23.sh @@ -15,4 +15,4 @@ sudo dnf install python-devel python3-devel python-virtualenv make gcc sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ - tcl-devel tk-devel \ No newline at end of file + tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh index 99b4d6d0f..205b6660e 100755 --- a/depends/freebsd_10.sh +++ b/depends/freebsd_10.sh @@ -8,4 +8,6 @@ sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27 # Openjpeg fails badly using the openjpeg package. # I can't find a python3.4 version of tkinter -sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 py27-tkinter +sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter + +./install_raqm.sh \ No newline at end of file diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh new file mode 100755 index 000000000..31950aa4a --- /dev/null +++ b/depends/install_raqm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# install raqm + + +if [ ! -f raqm-0.2.0.tar.gz ]; then + wget -O 'raqm-0.2.0.tar.gz' 'https://github.com/HOST-Oman/libraqm/releases/download/v0.2.0/raqm-0.2.0.tar.gz?raw=true' + +fi + +rm -r raqm-0.2.0 +tar -xvzf raqm-0.2.0.tar.gz + + +pushd raqm-0.2.0 + +./configure --prefix=/usr && make -j4 && sudo make -j4 install + +popd + diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh index a548f74fa..997bb09cf 100755 --- a/depends/ubuntu_14.04.sh +++ b/depends/ubuntu_14.04.sh @@ -4,12 +4,13 @@ # Installs all of the dependencies for Pillow for Ubuntu 14.04 # for both system Pythons 2.7 and 3.4 # - +sudo add-apt-repository -y ppa:as-bahanta/raqm +sudo apt-get update sudo apt-get -y install python-dev python-setuptools \ python3-dev python-virtualenv cmake sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk + python-tk python3-tk libharfbuzz-dev libfribidi-dev libraqm-dev ./install_openjpeg.sh -./install_imagequant.sh +./install_imagequant.sh \ No newline at end of file diff --git a/docs/installation.rst b/docs/installation.rst index 5eb12c69c..2472fe5c5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -170,6 +170,11 @@ Many of Pillow's features require external libraries: * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, so it is unlikely to work with any Python prior to 3.5 on Windows. +* **libraqm** provides complex text layout support. + * libraqm provides bidirectional text support (using FriBiDi), shaping (using HarfBuzz), and proper script itemization. As a result, Raqm can support most writing systems covered by Unicode. + * libraqm depends on the following libraries: FreeType, HarfBuzz, FriBiDi, make sure that install them before install libraqm if not available as package in your system. + * setting text direction or font features is not supported without libraqm. + Once you have installed the prerequisites, run:: $ pip install Pillow @@ -201,14 +206,16 @@ Build Options * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. + ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-imagequant``, ``--disable-raqm``. 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-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. + ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-imagequant``, ``--enable-raqm``. 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 support. Tcl and Tk also must be used together. @@ -222,7 +229,6 @@ Build Options library search process to dump all paths searched for and found to stdout. - Sample Usage:: $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install @@ -247,7 +253,16 @@ The easiest way to install external libraries is via `Homebrew $ brew install libtiff libjpeg webp little-cms2 -Install Pillow with:: +To install libraqm on MaxOS use Homebrew:: + $ brew install freetype harfbuzz fribidi +Once you have `libraqm source code `_ and the dependencies , run the customary sequence of commands in the source code +directory:: + + $ ./configure + $ make + $ make install + +Now install Pillow with:: $ pip install Pillow @@ -277,7 +292,7 @@ Or for Python 3:: Prerequisites are installed on **FreeBSD 10** with:: - $ sudo pkg install jpeg tiff webp lcms2 freetype2 + $ sudo pkg install jpeg tiff webp lcms2 freetype2 harfbuzz fribidi Building on Linux @@ -318,7 +333,7 @@ Prerequisites are installed on **Ubuntu 14.04 LTS** with:: Prerequisites are installed on **Fedora 23** with:: $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel + lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 842407c90..2a83b9268 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,16 @@ 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'. Requires libraqm + :param features: A list of font feature to be used during text layout. + This is usually used to turn on optional font features that are not enabled by + default, for example 'dlig' or 'ss01', but can be also used to turn off default + font features for example '-liga' to disable ligatures or '-kern' to disable kerning. + To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. - -.. 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 +259,13 @@ 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. It can be 'rtl', 'ltr', 'ttb' or 'btt'. Requires libraqm. + :param features: A list of font feature to be used during text layout. + This is usually used to turn on optional font features that are not enabled by + default, for example 'dlig' or 'ss01', but can be also used to turn off default + font features for example '-liga' to disable ligatures or '-kern' to disable kerning. + To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. .. 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..46df1e4f7 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,13 @@ 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. It can be 'rtl', 'ltr', 'ttb' or 'btt'. Requires libraqm + :param features: A list of font feature to be used during text layout. This is + usually used to turn on optional font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also used to turn off default font + features for example '-liga' to disable ligatures or '-kern' to disable kerning. + To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm .. versionadded:: 1.1.5 :return: An internal PIL storage memory instance as defined by the diff --git a/selftest.py b/selftest.py index 067db4d79..cfb6a2b43 100755 --- a/selftest.py +++ b/selftest.py @@ -178,7 +178,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 b0209a399..5e4ca5353 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ IMAGEQUANT_ROOT = None TIFF_ROOT = None FREETYPE_ROOT = None LCMS_ROOT = None - +RAQM_ROOT = None def _pkg_config(name): try: @@ -132,7 +132,7 @@ def _pkg_config(name): class pil_build_ext(build_ext): class feature: - features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', + features = ['zlib', 'jpeg', 'tiff', 'freetype', 'raqm', 'lcms', 'webp', 'webpmux', 'jpeg2000', 'imagequant'] required = {'jpeg', 'zlib'} @@ -513,6 +513,14 @@ class pil_build_ext(build_ext): if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) + if feature.want('raqm'): + _dbg('Looking for 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'): _dbg('Looking for lcms') if _find_include_file(self, "lcms2.h"): @@ -591,9 +599,14 @@ class pil_build_ext(build_ext): # additional libraries if feature.freetype: - exts.append(Extension("PIL._imagingft", - ["_imagingft.c"], - libraries=["freetype"])) + libs = ["freetype"] + defs = [] + if feature.raqm: + libs.extend(feature.raqm) + defs.append(('HAVE_RAQM', None)) + exts.append(Extension( + "PIL._imagingft", ["_imagingft.c"], libraries=libs, + define_macros=defs)) if feature.lcms: extra = [] @@ -660,6 +673,7 @@ class pil_build_ext(build_ext): (feature.imagequant, "LIBIMAGEQUANT"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), + (feature.raqm, "RAQM"), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"),