This commit is contained in:
Fahad Al-Saidi 2017-01-03 07:41:38 +00:00 committed by GitHub
commit 6a168ad9a4
26 changed files with 549 additions and 96 deletions

View File

@ -20,8 +20,9 @@ python:
dist: trusty dist: trusty
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

@ -222,7 +222,6 @@ class ImageDraw(object):
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)
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
if font is None: if font is None:
font = self.getfont() font = self.getfont()
@ -230,17 +229,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)
@ -259,7 +258,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

@ -136,20 +136,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

@ -4,6 +4,7 @@ modules = {
"pil": "PIL._imaging", "pil": "PIL._imaging",
"tkinter": "PIL._imagingtk", "tkinter": "PIL._imagingtk",
"freetype2": "PIL._imagingft", "freetype2": "PIL._imagingft",
"raqm": "PIL._imagingft",
"littlecms2": "PIL._imagingcms", "littlecms2": "PIL._imagingcms",
"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

141
Tests/test_imagefontctl.py Normal file
View File

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

View File

@ -41,6 +41,15 @@
#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>
#endif
typedef struct
{
int index, x_offset, x_advance, y_offset;
unsigned int cluster;
} GlyphInfo;
struct { struct {
int code; int code;
@ -188,60 +197,284 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
return 0; return 0;
} }
static PyObject* #ifdef HAVE_RAQM
font_getsize(FontObject* self, PyObject* args) static size_t
text_layout_raqm(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask)
{ {
int i, x, y_max, y_min; 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;
}
#endif
static size_t
text_layout_fallback(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask)
{
int error, load_flags;
FT_ULong ch; FT_ULong ch;
FT_Face face; Py_ssize_t count;
int xoffset, yoffset; FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face); FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0; FT_UInt last_index = 0;
int i;
/* calculate size and bearing for a given string */ if (features != Py_None || dir != NULL) {
PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm");
PyObject* string; }
if (!PyArg_ParseTuple(args, "O:getsize", &string))
return NULL;
#if PY_VERSION_HEX >= 0x03000000 #if PY_VERSION_HEX >= 0x03000000
if (!PyUnicode_Check(string)) { if (!PyUnicode_Check(string)) {
#else #else
if (!PyUnicode_Check(string) && !PyString_Check(string)) { if (!PyUnicode_Check(string) && !PyString_Check(string)) {
#endif #endif
PyErr_SetString(PyExc_TypeError, "expected string"); PyErr_SetString(PyExc_TypeError, "expected string");
return NULL; 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;
}
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;
}
static size_t
text_layout(PyObject* string, FontObject* self, const char* dir,
PyObject *features, GlyphInfo **glyph_info, int mask)
{
size_t count;
#ifdef HAVE_RAQM
count = text_layout_raqm(string, self, dir, features, glyph_info, mask);
#else
count = text_layout_fallback(string, self, dir, features, glyph_info, mask);
#endif
return count;
}
static PyObject*
font_getsize(FontObject* self, PyObject* args)
{
int i, x, y_max, y_min;
FT_Face face;
int xoffset, yoffset;
const char *dir = NULL;
size_t count;
GlyphInfo *glyph_info = NULL;
PyObject *features = Py_None;
/* calculate size and bearing for a given string */
PyObject* string;
if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features))
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)
@ -251,23 +484,16 @@ font_getsize(FontObject* self, PyObject* args)
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);
@ -306,7 +532,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);
@ -329,11 +555,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,15 +563,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;
return NULL; size_t count;
GlyphInfo *glyph_info;
PyObject *features = NULL;
#if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) {
if (!PyUnicode_Check(string)) { return NULL;
#else }
if (!PyUnicode_Check(string) && !PyString_Check(string)) {
#endif glyph_info = NULL;
PyErr_SetString(PyExc_TypeError, "expected string"); count = text_layout(string, self, dir, features, &glyph_info, mask);
if (count == 0) {
return NULL; return NULL;
} }
@ -360,36 +585,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)
@ -401,6 +627,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,8 +647,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++) {
@ -432,10 +661,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;
} }
@ -593,6 +822,14 @@ setup_module(PyObject* m) {
#endif #endif
PyDict_SetItemString(d, "freetype2_version", v); PyDict_SetItemString(d, "freetype2_version", v);
#ifdef HAVE_RAQM
v = PyBool_FromLong(1);
#else
v = PyBool_FromLong(0);
#endif
PyDict_SetItemString(d, "have_raqm", v);
return 0; return 0;
} }

View File

@ -11,7 +11,8 @@ sudo apt-get -y install python-dev python-setuptools \
python3-dev python-virtualenv cmake python3-dev python-virtualenv cmake
sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \
python-tk python3-tk python-tk python3-tk libharfbuzz-dev libfribidi-dev
./install_openjpeg.sh ./install_openjpeg.sh
./install_imagequant.sh ./install_imagequant.sh
./install_raqm.sh

View File

@ -15,4 +15,4 @@ sudo dnf install python-devel python3-devel python-virtualenv make gcc
sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \
lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \
tcl-devel tk-devel tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel

View File

@ -8,4 +8,6 @@ sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27
# Openjpeg fails badly using the openjpeg package. # Openjpeg fails badly using the openjpeg package.
# I can't find a python3.4 version of tkinter # I can't find a python3.4 version of tkinter
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 py27-tkinter sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter
./install_raqm.sh

19
depends/install_raqm.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# install raqm
if [ ! -f raqm-0.2.0.tar.gz ]; then
wget -O 'raqm-0.2.0.tar.gz' 'https://github.com/HOST-Oman/libraqm/releases/download/v0.2.0/raqm-0.2.0.tar.gz?raw=true'
fi
rm -r raqm-0.2.0
tar -xvzf raqm-0.2.0.tar.gz
pushd raqm-0.2.0
./configure --prefix=/usr && make -j4 && sudo make -j4 install
popd

View File

@ -4,12 +4,13 @@
# Installs all of the dependencies for Pillow for Ubuntu 14.04 # Installs all of the dependencies for Pillow for Ubuntu 14.04
# for both system Pythons 2.7 and 3.4 # for both system Pythons 2.7 and 3.4
# #
sudo add-apt-repository -y ppa:as-bahanta/raqm
sudo apt-get update
sudo apt-get -y install python-dev python-setuptools \ sudo apt-get -y install python-dev python-setuptools \
python3-dev python-virtualenv cmake python3-dev python-virtualenv cmake
sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \
python-tk python3-tk python-tk python3-tk libharfbuzz-dev libfribidi-dev libraqm-dev
./install_openjpeg.sh ./install_openjpeg.sh
./install_imagequant.sh ./install_imagequant.sh

View File

@ -170,6 +170,11 @@ Many of Pillow's features require external libraries:
* Windows support: Libimagequant requires VS2013/MSVC 18 to compile, * Windows support: Libimagequant requires VS2013/MSVC 18 to compile,
so it is unlikely to work with any Python prior to 3.5 on Windows. so it is unlikely to work with any Python prior to 3.5 on Windows.
* **libraqm** provides complex text layout support.
* libraqm provides bidirectional text support (using FriBiDi), shaping (using HarfBuzz), and proper script itemization. As a result, Raqm can support most writing systems covered by Unicode.
* libraqm depends on the following libraries: FreeType, HarfBuzz, FriBiDi, make sure that install them before install libraqm if not available as package in your system.
* setting text direction or font features is not supported without libraqm.
Once you have installed the prerequisites, run:: Once you have installed the prerequisites, run::
$ pip install Pillow $ pip install Pillow
@ -201,14 +206,16 @@ 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-webp``,
``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. ``--disable-webpmux``, ``--disable-jpeg2000``,
``--disable-imagequant``, ``--disable-raqm``.
Disable building the corresponding feature even if the development Disable building the corresponding feature even if the development
libraries are present on the building machine. libraries are present 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-webp``,
``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. ``--enable-webpmux``, ``--enable-jpeg2000``,
``--enable-imagequant``, ``--enable-raqm``.
Require that the corresponding feature is built. The build will raise Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata) an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together. relies on WebP support. Tcl and Tk also must be used together.
@ -222,7 +229,6 @@ Build Options
library search process to dump all paths searched for and found to library search process to dump all paths searched for and found to
stdout. stdout.
Sample Usage:: Sample Usage::
$ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install
@ -247,7 +253,16 @@ The easiest way to install external libraries is via `Homebrew
$ brew install libtiff libjpeg webp little-cms2 $ brew install libtiff libjpeg webp little-cms2
Install Pillow with:: To install libraqm on MaxOS use Homebrew::
$ brew install freetype harfbuzz fribidi
Once you have `libraqm source code <https://github.com/HOST-Oman/libraqm>`_ and the dependencies , run the customary sequence of commands in the source code
directory::
$ ./configure
$ make
$ make install
Now install Pillow with::
$ pip install Pillow $ pip install Pillow
@ -277,7 +292,7 @@ Or for Python 3::
Prerequisites are installed on **FreeBSD 10** with:: Prerequisites are installed on **FreeBSD 10** with::
$ sudo pkg install jpeg tiff webp lcms2 freetype2 $ sudo pkg install jpeg tiff webp lcms2 freetype2 harfbuzz fribidi
Building on Linux Building on Linux
@ -318,7 +333,7 @@ Prerequisites are installed on **Ubuntu 14.04 LTS** with::
Prerequisites are installed on **Fedora 23** with:: Prerequisites are installed on **Fedora 23** with::
$ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \
lcms2-devel libwebp-devel tcl-devel tk-devel lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel

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,16 @@ 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'. Requires libraqm
:param features: A list of font feature to be used during text layout.
This is usually used to turn on optional font features that are not enabled by
default, for example 'dlig' or 'ss01', but can be also used to turn off default
font features for example '-liga' to disable ligatures or '-kern' to disable kerning.
To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm
Requires libraqm.
.. 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 +259,13 @@ 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. It can be 'rtl', 'ltr', 'ttb' or 'btt'. Requires libraqm.
:param features: A list of font feature to be used during text layout.
This is usually used to turn on optional font features that are not enabled by
default, for example 'dlig' or 'ss01', but can be also used to turn off default
font features for example '-liga' to disable ligatures or '-kern' to disable kerning.
To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm
Requires libraqm.
.. 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,13 @@ 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. It can be 'rtl', 'ltr', 'ttb' or 'btt'. Requires libraqm
:param features: A list of font feature to be used during text layout. This is
usually used to turn on optional font features that are not enabled by default,
for example 'dlig' or 'ss01', but can be also used to turn off default font
features for example '-liga' to disable ligatures or '-kern' to disable kerning.
To get all supported features, see https://www.microsoft.com/typography/otspec/featurelist.htm
Requires libraqm
.. 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

@ -178,7 +178,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

@ -114,7 +114,7 @@ IMAGEQUANT_ROOT = None
TIFF_ROOT = None 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:
@ -132,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'}
@ -513,6 +513,14 @@ 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'):
_dbg('Looking for 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'):
_dbg('Looking for lcms') _dbg('Looking for lcms')
if _find_include_file(self, "lcms2.h"): if _find_include_file(self, "lcms2.h"):
@ -591,9 +599,14 @@ class pil_build_ext(build_ext):
# additional libraries # additional libraries
if feature.freetype: if feature.freetype:
exts.append(Extension("PIL._imagingft", libs = ["freetype"]
["_imagingft.c"], defs = []
libraries=["freetype"])) if feature.raqm:
libs.extend(feature.raqm)
defs.append(('HAVE_RAQM', None))
exts.append(Extension(
"PIL._imagingft", ["_imagingft.c"], libraries=libs,
define_macros=defs))
if feature.lcms: if feature.lcms:
extra = [] extra = []
@ -660,6 +673,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"),