diff --git a/.travis.yml b/.travis.yml index b88dc5ff2..a449b49e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,11 @@ python: - nightly 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..220a90d86 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -218,10 +218,10 @@ class ImageDraw(object): return text.split(split_character) def text(self, xy, text, fill=None, font=None, anchor=None, - *args, **kwargs): + *args, **kwargs, direction=None, features=[]): if self._multiline_check(text): return self.multiline_text(xy, text, fill, font, anchor, - *args, **kwargs) + *args, **kwargs, direction=direction, features=features) ink, fill = self._getink(fill) if font is None: @@ -230,17 +230,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, direction=direction, features=features) xy = xy[0] + offset[0], xy[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode) + mask = font.getmask(text, self.fontmode, direction, features) 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=[]): widths = [] max_width = 0 lines = self._multiline_split(text) @@ -259,7 +259,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..1af1887cb 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -143,13 +143,13 @@ class FreeTypeFont(object): 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=[]): + return self.getmask2(text, mode, direction, features)[0] - def getmask2(self, text, mode="", fill=Image.core.fill): + def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=[]): size, offset = self.font.getsize(text) 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/_imagingft.c b/_imagingft.c index ae62fc74e..93ef44c5d 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -48,6 +48,7 @@ struct { } ft_errors[] = #include FT_ERRORS_H +#include "raqm.h" /* -------------------------------------------------------------------- */ /* font objects */ @@ -329,11 +330,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,16 +338,127 @@ 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; + raqm_direction_t direction; + raqm_t *rq; + size_t count; + raqm_glyph_t *glyph_info; + PyObject *features = NULL; + + if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) return NULL; + rq = raqm_create(); + if (rq == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); + goto failed; + } + #if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { + if (PyUnicode_Check(string)) { + Py_ssize_t size = PyUnicode_GET_LENGTH(string); + Py_UCS4 text[size]; + PyUnicode_READY(string); + if (!PyUnicode_AsUCS4(string, text, size, 0)) + goto failed; + if (!raqm_set_text(rq, text, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + } #else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { + if (PyUnicode_Check(string)) { + Py_UNICODE *text = PyUnicode_AS_UNICODE(string); + Py_ssize_t size = PyUnicode_GET_SIZE(string); + if (!raqm_set_text(rq, text, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + } + 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"); - return NULL; + 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) { + 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; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_Check(item) || + PyUnicode_READY(string) != 0 || + PyUnicode_KIND(item) != PyUnicode_1BYTE_KIND) { + PyErr_SetString(PyExc_TypeError, "expected an ASCII string"); + goto failed; + } + + feature = PyUnicode_1BYTE_DATA(item); + size = PyUnicode_GET_LENGTH(item); +#else + if (!PyString_Check(item)) { + PyErr_SetString(PyExc_TypeError, "expected a string"); + goto failed; + } + 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; + } + + glyph_info = raqm_get_glyphs(rq, &count); + if (glyph_info == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + goto failed; } im = (Imaging) id; @@ -360,36 +468,36 @@ 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); + if (error) { + geterror(error); + goto failed; + } 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; - } + index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) - return geterror(error); + if (error){ + geterror(error); + goto failed; + } glyph = self->face->glyph; source = (unsigned char*) glyph->bitmap.buffer; xx = x + glyph->bitmap_left; + xx += PIXEL(glyph_info[i].x_offset); x0 = 0; x1 = glyph->bitmap.width; if (xx < 0) @@ -401,6 +509,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,6 +529,7 @@ 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; @@ -432,11 +542,14 @@ font_render(FontObject* self, PyObject* args) source += glyph->bitmap.pitch; } } - x += PIXEL(glyph->metrics.horiAdvance); - last_index = index; + x += PIXEL(glyph_info[i].x_advance); } Py_RETURN_NONE; + +failed: + raqm_destroy(rq); + return NULL; } static void diff --git a/setup.py b/setup.py index c34b8ed47..f184e8e45 100755 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ TIFF_ROOT = None FREETYPE_ROOT = None LCMS_ROOT = None +RAQM_ROOT = None def _pkg_config(name): try: @@ -132,7 +133,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 +514,11 @@ class pil_build_ext(build_ext): if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) + if feature.want('raqm'): + if _find_include_file(self, "raqm.h"): + if _find_library_file(self, "raqm"): + feature.raqm = "raqm" + if feature.want('lcms'): _dbg('Looking for lcms') if _find_include_file(self, "lcms2.h"): @@ -594,6 +600,9 @@ class pil_build_ext(build_ext): exts.append(Extension("PIL._imagingft", ["_imagingft.c"], libraries=["freetype"])) + if feature.freetype and feature.raqm: + exts.append(Extension( + "PIL._imagingft", ["_imagingft.c"], libraries=["freetype", "fribidi" , "harfbuzz", "raqm"])) if feature.lcms: extra = [] @@ -660,6 +669,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"),