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.
This commit is contained in:
shamsa 2016-01-13 10:31:49 +04:00 committed by wiredfool
parent 80ac338bf8
commit 0b178edbc8
5 changed files with 163 additions and 37 deletions

View File

@ -2,9 +2,12 @@
set -e
sudo add-apt-repository -y ppa:as-bahanta/raqm
sudo apt-get update
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk \
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\
libharfbuzz-dev libfribidi-dev libraqm-dev
pip install cffi
pip install nose
pip install check-manifest

View File

@ -203,10 +203,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:
@ -215,17 +215,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)
@ -244,7 +244,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]

View File

@ -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):

View File

@ -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

View File

@ -114,6 +114,7 @@ TIFF_ROOT = None
FREETYPE_ROOT = None
LCMS_ROOT = None
RAQM_ROOT = None
def _pkg_config(name):
try:
@ -131,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'}
@ -516,6 +517,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"):
@ -597,6 +603,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 = []
@ -658,6 +667,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"),