This commit is contained in:
ShamsaHamed 2016-03-16 10:24:02 +00:00
commit 0c99360822
21 changed files with 471 additions and 86 deletions

View File

@ -18,9 +18,13 @@ python:
- 3.4 - 3.4
- nightly - nightly
sudo: required # needed for trusty beta
dist: trusty # needed for HarfBuzz
install: install:
- "travis_retry sudo add-apt-repository -y ppa:as-bahanta/raqm"
- "travis_retry sudo apt-get update" - "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 cffi"
- "travis_retry pip install nose" - "travis_retry pip install nose"
- "travis_retry pip install check-manifest" - "travis_retry pip install check-manifest"

View File

@ -252,17 +252,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, *args, **kwargs)
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, *args, **kwargs)
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=None):
widths = [] widths = []
max_width = 0 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
@ -281,7 +281,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]

View File

@ -137,20 +137,20 @@ class FreeTypeFont(object):
def getmetrics(self): def getmetrics(self):
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getsize(self, text): def getsize(self, text, direction=None, features=None):
size, offset = self.font.getsize(text) size, offset = self.font.getsize(text, direction, features)
return (size[0] + offset[0], size[1] + offset[1]) return (size[0] + offset[0], size[1] + offset[1])
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=None):
return self.getmask2(text, mode)[0] return self.getmask2(text, mode, direction=direction, features=features)[0]
def getmask2(self, text, mode="", fill=Image.core.fill): def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None):
size, offset = self.font.getsize(text) size, offset = self.font.getsize(text, direction, features)
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):

View File

@ -5,6 +5,7 @@ modules = {
"tkinter": "PIL._imagingtk", "tkinter": "PIL._imagingtk",
"freetype2": "PIL._imagingft", "freetype2": "PIL._imagingft",
"littlecms2": "PIL._imagingcms", "littlecms2": "PIL._imagingcms",
"raqm": "PIL._imagingft",
"webp": "PIL._webp", "webp": "PIL._webp",
"transp_webp": ("WEBP", "WebPDecoderBuggyAlpha") "transp_webp": ("WEBP", "WebPDecoderBuggyAlpha")
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

BIN
Tests/images/test_text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

136
Tests/test_imagefontctl.py Normal file
View File

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
from helper import unittest, PillowTestCase
from PIL import Image
from PIL import ImageDraw
FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans.ttf"
try:
from PIL import ImageFont
# check if raqm is available
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')
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 (KeyError, ImportError):
class TestImagecomplextext(PillowTestCase):
def test_skip(self):
self.skipTest("KeyError")
if __name__ == '__main__':
unittest.main()
# End of file

View File

@ -41,6 +41,23 @@
#define FT_ERRORDEF( e, v, s ) { e, s }, #define FT_ERRORDEF( e, v, s ) { e, s },
#define FT_ERROR_START_LIST { #define FT_ERROR_START_LIST {
#define FT_ERROR_END_LIST { 0, 0 } }; #define FT_ERROR_END_LIST { 0, 0 } };
#ifdef HAVE_RAQM
#include <raqm.h>
#else
typedef enum
{
RAQM_DIRECTION_DEFAULT,
RAQM_DIRECTION_RTL,
RAQM_DIRECTION_LTR,
RAQM_DIRECTION_TTB
} raqm_direction_t;
#endif
typedef struct
{
int index, x_offset, x_advance, y_offset;
unsigned int cluster;
} GlyphInfo;
struct { struct {
int code; int code;
@ -186,86 +203,88 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
return 0; return 0;
} }
static size_t
text_layout(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask);
static PyObject* static PyObject*
font_getsize(FontObject* self, PyObject* args) font_getsize(FontObject* self, PyObject* args)
{ {
int i, x, y_max, y_min; int i, x, y_max, y_min;
FT_ULong ch;
FT_Face face; FT_Face face;
int xoffset, yoffset; int xoffset, yoffset;
FT_Bool kerning = FT_HAS_KERNING(self->face); const char *dir = NULL;
FT_UInt last_index = 0; size_t count;
GlyphInfo *glyph_info = NULL;;
PyObject *features = Py_None;
/* calculate size and bearing for a given string */ /* calculate size and bearing for a given string */
PyObject* string; PyObject* string;
if (!PyArg_ParseTuple(args, "O:getsize", &string)) if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features))
return NULL; return 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");
return NULL;
}
face = NULL; face = NULL;
xoffset = yoffset = 0; xoffset = yoffset = 0;
y_max = y_min = 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; int index, error;
FT_BBox bbox; FT_BBox bbox;
FT_Glyph glyph; FT_Glyph glyph;
face = self->face; face = self->face;
index = FT_Get_Char_Index(face, ch); index = glyph_info[i].index;
if (kerning && last_index && index) { /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960
FT_Vector delta; * Yifu Yu<root@jackyyf.com>, 2014-10-15
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<root@jackyyf.com>, 2014-10-15
*/
error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP);
if (error) if (error)
return geterror(error); return geterror(error);
if (i == 0)
if (i == 0 && face->glyph->metrics.horiBearingX < 0) {
xoffset = face->glyph->metrics.horiBearingX; 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_Get_Glyph(face->glyph, &glyph);
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); 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) if (bbox.yMax > y_max)
y_max = bbox.yMax; y_max = bbox.yMax;
if (bbox.yMin < y_min) if (bbox.yMin < y_min)
y_min = bbox.yMin; y_min = bbox.yMin;
/* find max distance of baseline from top */ /* find max distance of baseline from top */
if (face->glyph->metrics.horiBearingY > yoffset) if (face->glyph->metrics.horiBearingY > yoffset)
yoffset = face->glyph->metrics.horiBearingY; yoffset = face->glyph->metrics.horiBearingY;
last_index = index;
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }
if (face) { if (face) {
int offset;
/* left bearing */ /* left bearing */
if (xoffset < 0) if (xoffset < 0)
x -= xoffset; x -= xoffset;
else else
xoffset = 0; 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 /* difference between the font ascender and the distance of
* the baseline from the top */ * the baseline from the top */
yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); yoffset = PIXEL(self->face->size->metrics.ascender - yoffset);
@ -304,7 +323,7 @@ font_getabc(FontObject* self, PyObject* args)
int index, error; int index, error;
face = self->face; face = self->face;
index = FT_Get_Char_Index(face, ch); 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); error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP);
if (error) if (error)
return geterror(error); return geterror(error);
@ -319,6 +338,205 @@ font_getabc(FontObject* self, PyObject* args)
return Py_BuildValue("ddd", a, b, c); return Py_BuildValue("ddd", a, b, c);
} }
static size_t
text_layout(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask)
{
#ifdef HAVE_RAQM
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;
#else
if (features != Py_None || dir != NULL)
PyErr_SetString(PyExc_KeyError, "Raqm is missing.");
int error, load_flags;
FT_ULong ch;
Py_ssize_t count;
FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0;
#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 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;
int i;
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;
#endif
}
static PyObject* static PyObject*
font_render(FontObject* self, PyObject* args) font_render(FontObject* self, PyObject* args)
{ {
@ -327,11 +545,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;
@ -339,17 +553,18 @@ 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;
size_t count;
GlyphInfo *glyph_info;
PyObject *features = NULL;
if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features))
return NULL; return NULL;
#if PY_VERSION_HEX >= 0x03000000 glyph_info = NULL;
if (!PyUnicode_Check(string)) { count = text_layout(string, self, dir, features, &glyph_info, mask);
#else if (count == 0)
if (!PyUnicode_Check(string) && !PyString_Check(string)) {
#endif
PyErr_SetString(PyExc_TypeError, "expected string");
return NULL; return NULL;
}
im = (Imaging) id; im = (Imaging) id;
/* 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 */
@ -358,36 +573,37 @@ 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); return geterror(error);
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 = -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); error = FT_Load_Glyph(self->face, index, load_flags);
if (error) if (error)
return geterror(error); return geterror(error);
if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) {
x = -self->face->glyph->metrics.horiBearingX;
}
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 = PIXEL(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)
@ -399,6 +615,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;
@ -418,8 +635,10 @@ 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;
unsigned char *target = im->image8[yy] + xx; unsigned char *target = im->image8[yy] + xx;
for (i = x0; i < x1; i++) { for (i = x0; i < x1; i++) {
@ -430,10 +649,10 @@ font_render(FontObject* self, PyObject* args)
source += glyph->bitmap.pitch; source += glyph->bitmap.pitch;
} }
} }
x += PIXEL(glyph->metrics.horiAdvance); x += glyph_info[i].x_advance;
last_index = index;
} }
PyMem_Del(glyph_info);
Py_RETURN_NONE; Py_RETURN_NONE;
} }

View File

@ -138,6 +138,8 @@ Many of Pillow's features require external libraries:
* Pillow does **not** support the earlier **1.5** series which ships * Pillow does **not** support the earlier **1.5** series which ships
with Ubuntu and Debian. with Ubuntu and Debian.
* **libraqm** provides complex text layout support.
Once you have installed the prerequisites, run:: Once you have installed the prerequisites, run::
$ pip install Pillow $ pip install Pillow
@ -168,14 +170,14 @@ Build Options
* Build flags: ``--disable-zlib``, ``--disable-jpeg``, * Build flags: ``--disable-zlib``, ``--disable-jpeg``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``,
``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, ``--disable-tk``, ``--disable-lcms``, ``--disable-raqm``, ``--disable-webp``,
``--disable-webpmux``, ``--disable-jpeg2000``. Disable building the ``--disable-webpmux``, ``--disable-jpeg2000``. Disable building the
corresponding feature even if the development libraries are present corresponding feature even if the development libraries are present
on the building machine. on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``, * Build flags: ``--enable-zlib``, ``--enable-jpeg``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``,
``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, ``--enable-tk``, ``--enable-lcms``, ``--enable-raqm``, ``--enable-webp``,
``--enable-webpmux``, ``--enable-jpeg2000``. Require that the ``--enable-webpmux``, ``--enable-jpeg2000``. Require that the
corresponding feature is built. The build will raise an exception if corresponding feature is built. The build will raise an exception if
the libraries are not found. Webpmux (WebP metadata) relies on WebP the libraries are not found. Webpmux (WebP metadata) relies on WebP

View File

@ -227,7 +227,7 @@ Methods
Draw a shape. 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. Draws the string at the given position.
@ -240,9 +240,11 @@ Methods
the number of pixels between lines. the number of pixels between lines.
:param align: If the text is passed on to multiline_text(), :param align: If the text is passed on to multiline_text(),
"left", "center" or "right". "left", "center" or "right".
:param direction: Direction of the text. It can be 'rtl', 'ltr', 'ttb' or 'btt.
:param features: A list of font features used for text layout. For example, 'ligature, kerning, Medial ...etc.
.. 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. Draws the string at the given position.
@ -252,6 +254,8 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param spacing: The number of pixels between lines. :param spacing: The number of pixels between lines.
:param align: "left", "center" or "right". :param align: "left", "center" or "right".
:param direction: Direction of the text.
:param features: Font features used for text layout.
.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0)

View File

@ -51,7 +51,7 @@ Methods
:return: (width, height) :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. Create a bitmap for the text.
@ -63,6 +63,8 @@ Methods
driver prefers; if empty, the renderer may return either driver prefers; if empty, the renderer may return either
mode. Note that the mode is always a string, to simplify mode. Note that the mode is always a string, to simplify
C-level implementations. C-level implementations.
:param direction: Direction of the text.
:param features: A list of font features used for text layout.
.. versionadded:: 1.1.5 .. versionadded:: 1.1.5
:return: An internal PIL storage memory instance as defined by the :return: An internal PIL storage memory instance as defined by the

View File

@ -180,7 +180,8 @@ if __name__ == "__main__":
("freetype2", "FREETYPE2"), ("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"), ("littlecms2", "LITTLECMS2"),
("webp", "WEBP"), ("webp", "WEBP"),
("transp_webp", "Transparent WEBP") ("transp_webp", "Transparent WEBP"),
("raqm", "RAQM")
]: ]:
supported = features.check_module(name) supported = features.check_module(name)

View File

@ -99,11 +99,12 @@ TIFF_ROOT = None
FREETYPE_ROOT = None FREETYPE_ROOT = None
LCMS_ROOT = None LCMS_ROOT = None
RAQM_ROOT = None
class pil_build_ext(build_ext): class pil_build_ext(build_ext):
class feature: 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 jpeg2000 = None
required = set(['jpeg', 'zlib']) required = set(['jpeg', 'zlib'])
@ -160,7 +161,7 @@ class pil_build_ext(build_ext):
# add configured kits # add configured kits
for root in (TCL_ROOT, JPEG_ROOT, JPEG2K_ROOT, TIFF_ROOT, ZLIB_ROOT, 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(())): if isinstance(root, type(())):
lib_root, include_root = root lib_root, include_root = root
else: else:
@ -467,6 +468,13 @@ class pil_build_ext(build_ext):
if dir: if dir:
_add_directory(self.compiler.include_dirs, dir, 0) _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") and \
_find_library_file(self, "harfbuzz") and \
_find_library_file(self, "fribidi"):
feature.raqm = ["raqm", "harfbuzz", "fribidi"]
if feature.want('lcms'): if feature.want('lcms'):
if _find_include_file(self, "lcms2.h"): if _find_include_file(self, "lcms2.h"):
if _find_library_file(self, "lcms2"): if _find_library_file(self, "lcms2"):
@ -555,8 +563,15 @@ class pil_build_ext(build_ext):
# additional libraries # additional libraries
if feature.freetype: if feature.freetype:
libs = ["freetype"]
defs = []
if feature.raqm:
libs.extend(feature.raqm)
defs.append(('HAVE_RAQM', None))
exts.append(Extension( exts.append(Extension(
"PIL._imagingft", ["_imagingft.c"], libraries=["freetype"])) "PIL._imagingft", ["_imagingft.c"], libraries=libs,
define_macros=defs))
if os.path.isfile("_imagingcms.c") and feature.lcms: if os.path.isfile("_imagingcms.c") and feature.lcms:
extra = [] extra = []
@ -647,6 +662,7 @@ class pil_build_ext(build_ext):
(feature.zlib, "ZLIB (PNG/ZIP)"), (feature.zlib, "ZLIB (PNG/ZIP)"),
(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"), ]