mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-29 09:23:11 +03:00
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:
parent
80ac338bf8
commit
0b178edbc8
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
sudo add-apt-repository -y ppa:as-bahanta/raqm
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk \
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\
|
||||||
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick
|
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\
|
||||||
|
libharfbuzz-dev libfribidi-dev libraqm-dev
|
||||||
|
|
||||||
pip install cffi
|
pip install cffi
|
||||||
pip install nose
|
pip install nose
|
||||||
pip install check-manifest
|
pip install check-manifest
|
||||||
|
|
|
@ -203,10 +203,10 @@ class ImageDraw(object):
|
||||||
return text.split(split_character)
|
return text.split(split_character)
|
||||||
|
|
||||||
def text(self, xy, text, fill=None, font=None, anchor=None,
|
def text(self, xy, text, fill=None, font=None, anchor=None,
|
||||||
*args, **kwargs):
|
*args, **kwargs, direction=None, features=[]):
|
||||||
if self._multiline_check(text):
|
if self._multiline_check(text):
|
||||||
return self.multiline_text(xy, text, fill, font, anchor,
|
return self.multiline_text(xy, text, fill, font, anchor,
|
||||||
*args, **kwargs)
|
*args, **kwargs, direction=direction, features=features)
|
||||||
|
|
||||||
ink, fill = self._getink(fill)
|
ink, fill = self._getink(fill)
|
||||||
if font is None:
|
if font is None:
|
||||||
|
@ -215,17 +215,17 @@ class ImageDraw(object):
|
||||||
ink = fill
|
ink = fill
|
||||||
if ink is not None:
|
if ink is not None:
|
||||||
try:
|
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]
|
xy = xy[0] + offset[0], xy[1] + offset[1]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
try:
|
try:
|
||||||
mask = font.getmask(text, self.fontmode)
|
mask = font.getmask(text, self.fontmode, direction, features)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
mask = font.getmask(text)
|
mask = font.getmask(text)
|
||||||
self.draw.draw_bitmap(xy, mask, ink)
|
self.draw.draw_bitmap(xy, mask, ink)
|
||||||
|
|
||||||
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
|
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
|
||||||
spacing=4, align="left"):
|
spacing=4, align="left", direction=None, features=[]):
|
||||||
widths = []
|
widths = []
|
||||||
max_width = 0
|
max_width = 0
|
||||||
lines = self._multiline_split(text)
|
lines = self._multiline_split(text)
|
||||||
|
@ -244,7 +244,7 @@ class ImageDraw(object):
|
||||||
left += (max_width - widths[idx])
|
left += (max_width - widths[idx])
|
||||||
else:
|
else:
|
||||||
assert False, 'align must be "left", "center" or "right"'
|
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
|
top += line_spacing
|
||||||
left = xy[0]
|
left = xy[0]
|
||||||
|
|
||||||
|
|
|
@ -143,13 +143,13 @@ class FreeTypeFont(object):
|
||||||
def getoffset(self, text):
|
def getoffset(self, text):
|
||||||
return self.font.getsize(text)[1]
|
return self.font.getsize(text)[1]
|
||||||
|
|
||||||
def getmask(self, text, mode=""):
|
def getmask(self, text, mode="", direction=None, features=[]):
|
||||||
return self.getmask2(text, mode)[0]
|
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)
|
size, offset = self.font.getsize(text)
|
||||||
im = fill("L", size, 0)
|
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
|
return im, offset
|
||||||
|
|
||||||
def font_variant(self, font=None, size=None, index=None, encoding=None):
|
def font_variant(self, font=None, size=None, index=None, encoding=None):
|
||||||
|
|
161
_imagingft.c
161
_imagingft.c
|
@ -48,6 +48,7 @@ struct {
|
||||||
} ft_errors[] =
|
} ft_errors[] =
|
||||||
|
|
||||||
#include FT_ERRORS_H
|
#include FT_ERRORS_H
|
||||||
|
#include "raqm.h"
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/* font objects */
|
/* font objects */
|
||||||
|
@ -329,11 +330,7 @@ font_render(FontObject* self, PyObject* args)
|
||||||
int index, error, ascender;
|
int index, error, ascender;
|
||||||
int load_flags;
|
int load_flags;
|
||||||
unsigned char *source;
|
unsigned char *source;
|
||||||
FT_ULong ch;
|
|
||||||
FT_GlyphSlot glyph;
|
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
|
/* render string into given buffer (the buffer *must* have
|
||||||
the right size, or this will crash) */
|
the right size, or this will crash) */
|
||||||
PyObject* string;
|
PyObject* string;
|
||||||
|
@ -341,16 +338,127 @@ font_render(FontObject* self, PyObject* args)
|
||||||
int mask = 0;
|
int mask = 0;
|
||||||
int temp;
|
int temp;
|
||||||
int xx, x0, x1;
|
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;
|
return NULL;
|
||||||
|
|
||||||
|
rq = raqm_create();
|
||||||
|
if (rq == NULL) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "raqm_create() failed.");
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
|
|
||||||
#if PY_VERSION_HEX >= 0x03000000
|
#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
|
#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
|
#endif
|
||||||
|
else {
|
||||||
PyErr_SetString(PyExc_TypeError, "expected string");
|
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;
|
im = (Imaging) id;
|
||||||
|
@ -360,36 +468,36 @@ font_render(FontObject* self, PyObject* args)
|
||||||
load_flags |= FT_LOAD_TARGET_MONO;
|
load_flags |= FT_LOAD_TARGET_MONO;
|
||||||
|
|
||||||
ascender = 0;
|
ascender = 0;
|
||||||
for (i = 0; font_getchar(string, i, &ch); i++) {
|
for (i = 0; i < count; i++) {
|
||||||
index = FT_Get_Char_Index(self->face, ch);
|
index = glyph_info[i].index;
|
||||||
error = FT_Load_Glyph(self->face, index, load_flags);
|
error = FT_Load_Glyph(self->face, index, load_flags);
|
||||||
if (error)
|
if (error) {
|
||||||
return geterror(error);
|
geterror(error);
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
glyph = self->face->glyph;
|
glyph = self->face->glyph;
|
||||||
temp = (glyph->bitmap.rows - glyph->bitmap_top);
|
temp = (glyph->bitmap.rows - glyph->bitmap_top);
|
||||||
|
temp -= PIXEL(glyph_info[i].y_offset);
|
||||||
if (temp > ascender)
|
if (temp > ascender)
|
||||||
ascender = temp;
|
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)
|
if (i == 0 && self->face->glyph->metrics.horiBearingX < 0)
|
||||||
x = -PIXEL(self->face->glyph->metrics.horiBearingX);
|
x = -PIXEL(self->face->glyph->metrics.horiBearingX);
|
||||||
index = FT_Get_Char_Index(self->face, ch);
|
index = glyph_info[i].index;
|
||||||
if (kerning && last_index && index) {
|
|
||||||
FT_Vector delta;
|
|
||||||
FT_Get_Kerning(self->face, last_index, index, ft_kerning_default,
|
|
||||||
&delta);
|
|
||||||
x += delta.x >> 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = FT_Load_Glyph(self->face, index, load_flags);
|
error = FT_Load_Glyph(self->face, index, load_flags);
|
||||||
if (error)
|
if (error){
|
||||||
return geterror(error);
|
geterror(error);
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
|
|
||||||
glyph = self->face->glyph;
|
glyph = self->face->glyph;
|
||||||
|
|
||||||
source = (unsigned char*) glyph->bitmap.buffer;
|
source = (unsigned char*) glyph->bitmap.buffer;
|
||||||
xx = x + glyph->bitmap_left;
|
xx = x + glyph->bitmap_left;
|
||||||
|
xx += PIXEL(glyph_info[i].x_offset);
|
||||||
x0 = 0;
|
x0 = 0;
|
||||||
x1 = glyph->bitmap.width;
|
x1 = glyph->bitmap.width;
|
||||||
if (xx < 0)
|
if (xx < 0)
|
||||||
|
@ -401,6 +509,7 @@ font_render(FontObject* self, PyObject* args)
|
||||||
/* use monochrome mask (on palette images, etc) */
|
/* use monochrome mask (on palette images, etc) */
|
||||||
for (y = 0; y < glyph->bitmap.rows; y++) {
|
for (y = 0; y < glyph->bitmap.rows; y++) {
|
||||||
int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
|
int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
|
||||||
|
yy -= PIXEL(glyph_info[i].y_offset);
|
||||||
if (yy >= 0 && yy < im->ysize) {
|
if (yy >= 0 && yy < im->ysize) {
|
||||||
/* blend this glyph into the buffer */
|
/* blend this glyph into the buffer */
|
||||||
unsigned char *target = im->image8[yy] + xx;
|
unsigned char *target = im->image8[yy] + xx;
|
||||||
|
@ -420,6 +529,7 @@ font_render(FontObject* self, PyObject* args)
|
||||||
/* use antialiased rendering */
|
/* use antialiased rendering */
|
||||||
for (y = 0; y < glyph->bitmap.rows; y++) {
|
for (y = 0; y < glyph->bitmap.rows; y++) {
|
||||||
int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
|
int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
|
||||||
|
yy -= PIXEL(glyph_info[i].y_offset);
|
||||||
if (yy >= 0 && yy < im->ysize) {
|
if (yy >= 0 && yy < im->ysize) {
|
||||||
/* blend this glyph into the buffer */
|
/* blend this glyph into the buffer */
|
||||||
int i;
|
int i;
|
||||||
|
@ -432,11 +542,14 @@ font_render(FontObject* self, PyObject* args)
|
||||||
source += glyph->bitmap.pitch;
|
source += glyph->bitmap.pitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x += PIXEL(glyph->metrics.horiAdvance);
|
x += PIXEL(glyph_info[i].x_advance);
|
||||||
last_index = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|
||||||
|
failed:
|
||||||
|
raqm_destroy(rq);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
12
setup.py
12
setup.py
|
@ -114,6 +114,7 @@ TIFF_ROOT = None
|
||||||
FREETYPE_ROOT = None
|
FREETYPE_ROOT = None
|
||||||
LCMS_ROOT = None
|
LCMS_ROOT = None
|
||||||
|
|
||||||
|
RAQM_ROOT = None
|
||||||
|
|
||||||
def _pkg_config(name):
|
def _pkg_config(name):
|
||||||
try:
|
try:
|
||||||
|
@ -131,7 +132,7 @@ def _pkg_config(name):
|
||||||
|
|
||||||
class pil_build_ext(build_ext):
|
class pil_build_ext(build_ext):
|
||||||
class feature:
|
class feature:
|
||||||
features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp',
|
features = ['zlib', 'jpeg', 'tiff', 'freetype', 'raqm', 'lcms', 'webp',
|
||||||
'webpmux', 'jpeg2000', 'imagequant']
|
'webpmux', 'jpeg2000', 'imagequant']
|
||||||
|
|
||||||
required = {'jpeg', 'zlib'}
|
required = {'jpeg', 'zlib'}
|
||||||
|
@ -516,6 +517,11 @@ class pil_build_ext(build_ext):
|
||||||
if subdir:
|
if subdir:
|
||||||
_add_directory(self.compiler.include_dirs, subdir, 0)
|
_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'):
|
if feature.want('lcms'):
|
||||||
_dbg('Looking for lcms')
|
_dbg('Looking for lcms')
|
||||||
if _find_include_file(self, "lcms2.h"):
|
if _find_include_file(self, "lcms2.h"):
|
||||||
|
@ -597,6 +603,9 @@ class pil_build_ext(build_ext):
|
||||||
exts.append(Extension("PIL._imagingft",
|
exts.append(Extension("PIL._imagingft",
|
||||||
["_imagingft.c"],
|
["_imagingft.c"],
|
||||||
libraries=["freetype"]))
|
libraries=["freetype"]))
|
||||||
|
if feature.freetype and feature.raqm:
|
||||||
|
exts.append(Extension(
|
||||||
|
"PIL._imagingft", ["_imagingft.c"], libraries=["freetype", "fribidi" , "harfbuzz", "raqm"]))
|
||||||
|
|
||||||
if feature.lcms:
|
if feature.lcms:
|
||||||
extra = []
|
extra = []
|
||||||
|
@ -658,6 +667,7 @@ class pil_build_ext(build_ext):
|
||||||
(feature.imagequant, "LIBIMAGEQUANT"),
|
(feature.imagequant, "LIBIMAGEQUANT"),
|
||||||
(feature.tiff, "LIBTIFF"),
|
(feature.tiff, "LIBTIFF"),
|
||||||
(feature.freetype, "FREETYPE2"),
|
(feature.freetype, "FREETYPE2"),
|
||||||
|
(feature.raqm, "RAQM"),
|
||||||
(feature.lcms, "LITTLECMS2"),
|
(feature.lcms, "LITTLECMS2"),
|
||||||
(feature.webp, "WEBP"),
|
(feature.webp, "WEBP"),
|
||||||
(feature.webpmux, "WEBPMUX"),
|
(feature.webpmux, "WEBPMUX"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user