From 2801c195104771097b1f6ee5da8ed107c74a5b19 Mon Sep 17 00:00:00 2001 From: shamsa Date: Wed, 13 Jan 2016 10:31:49 +0400 Subject: [PATCH] Add complex text support. This pull request adds support for languages that require complex text layout. We are using the Raqm library, that wraps FriBidi (for bidirectional text support) and HarfBuzz (for text shaping), and does proper BiDi and script itemization: https://github.com/HOST-Oman/libraqm This should fix #1089. --- .travis.yml | 6 +- PIL/ImageDraw.py | 12 ++-- PIL/ImageFont.py | 8 +-- _imagingft.c | 161 ++++++++++++++++++++++++++++++++++++++++------- setup.py | 15 +++-- 5 files changed, 163 insertions(+), 39 deletions(-) diff --git a/.travis.yml b/.travis.yml index b38c47c5a..2739f2419 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..78676fd29 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -241,9 +241,9 @@ class ImageDraw(object): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs): + def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs, direction=None, features=[]): if self._multiline_check(text): - return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) + return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs, direction=direction, features=features) ink, fill = self._getink(fill) if font is None: @@ -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, 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) @@ -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..1dad30b31 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -144,13 +144,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 dc1661f3e..e27e5603e 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -48,6 +48,7 @@ struct { } ft_errors[] = #include FT_ERRORS_H +#include "raqm.h" /* -------------------------------------------------------------------- */ /* font objects */ @@ -327,11 +328,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,16 +336,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; @@ -358,36 +466,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) @@ -399,6 +507,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,6 +527,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; @@ -430,11 +540,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 265c097ba..329be984e 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,11 @@ 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"): + feature.raqm = "raqm" + if feature.want('lcms'): if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): @@ -554,9 +560,9 @@ class pil_build_ext(build_ext): # # additional libraries - if feature.freetype: + if feature.freetype and feature.raqm: exts.append(Extension( - "PIL._imagingft", ["_imagingft.c"], libraries=["freetype"])) + "PIL._imagingft", ["_imagingft.c"], libraries=["freetype", "fribidi" , "harfbuzz", "raqm"])) if os.path.isfile("_imagingcms.c") and feature.lcms: extra = [] @@ -647,6 +653,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"), ]