Merge pull request #2576 from wiredfool/pr_2284

Complex Text Support
This commit is contained in:
wiredfool 2017-07-01 10:45:18 +01:00 committed by GitHub
commit 8feac899dd
31 changed files with 1108 additions and 507 deletions

View File

@ -4,7 +4,9 @@ set -e
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
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\
libharfbuzz-dev libfribidi-dev
pip install cffi
pip install nose
pip install check-manifest

View File

@ -207,7 +207,6 @@ class ImageDraw(object):
if self._multiline_check(text):
return self.multiline_text(xy, text, fill, font, anchor,
*args, **kwargs)
ink, fill = self._getink(fill)
if font is None:
font = self.getfont()
@ -215,17 +214,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, *args, **kwargs)
xy = xy[0] + offset[0], xy[1] + offset[1]
except AttributeError:
try:
mask = font.getmask(text, self.fontmode)
mask = font.getmask(text, self.fontmode, *args, **kwargs)
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=None):
widths = []
max_width = 0
lines = self._multiline_split(text)
@ -244,25 +243,30 @@ 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]
def textsize(self, text, font=None, *args, **kwargs):
def textsize(self, text, font=None, spacing=4, direction=None,
features=None):
"""Get the size of a given string, in pixels."""
if self._multiline_check(text):
return self.multiline_textsize(text, font, *args, **kwargs)
return self.multiline_textsize(text, font, spacing,
direction, features)
if font is None:
font = self.getfont()
return font.getsize(text)
return font.getsize(text, direction, features)
def multiline_textsize(self, text, font=None, spacing=4):
def multiline_textsize(self, text, font=None, spacing=4, direction=None,
features=None):
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines:
line_width, line_height = self.textsize(line, font)
line_width, line_height = self.textsize(line, font, spacing,
direction, features)
max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing

View File

@ -41,6 +41,9 @@ try:
except ImportError:
core = _imagingft_not_installed()
LAYOUT_BASIC = 0
LAYOUT_RAQM = 1
# FIXME: add support for pilfont2 format (see FontFile.py)
# --------------------------------------------------------------------
@ -103,9 +106,12 @@ class ImageFont(object):
self.font = Image.core.font(image.im, data)
# delegate critical operations to internal type
self.getsize = self.font.getsize
self.getmask = self.font.getmask
def getsize(self, text, *args, **kwargs):
return self.font.getsize(text)
def getmask(self, text, mode="", *args, **kwargs):
return self.font.getmask(text, mode)
##
@ -115,7 +121,8 @@ class ImageFont(object):
class FreeTypeFont(object):
"FreeType font wrapper (requires _imagingft service)"
def __init__(self, font=None, size=10, index=0, encoding=""):
def __init__(self, font=None, size=10, index=0, encoding="",
layout_engine=None):
# FIXME: use service provider instead
self.path = font
@ -123,12 +130,21 @@ class FreeTypeFont(object):
self.index = index
self.encoding = encoding
if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM):
layout_engine = LAYOUT_BASIC
if core.HAVE_RAQM:
layout_engine = LAYOUT_RAQM
if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM:
layout_engine = LAYOUT_BASIC
self.layout_engine = layout_engine
if isPath(font):
self.font = core.getfont(font, size, index, encoding)
self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine)
else:
self.font_bytes = font.read()
self.font = core.getfont(
"", size, index, encoding, self.font_bytes)
"", size, index, encoding, self.font_bytes, layout_engine)
def getname(self):
return self.font.family, self.font.style
@ -136,23 +152,24 @@ class FreeTypeFont(object):
def getmetrics(self):
return self.font.ascent, self.font.descent
def getsize(self, text):
size, offset = self.font.getsize(text)
def getsize(self, text, direction=None, features=None):
size, offset = self.font.getsize(text, direction, features)
return (size[0] + offset[0], size[1] + offset[1])
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=None):
return self.getmask2(text, mode, direction=direction, features=features)[0]
def getmask2(self, text, mode="", fill=Image.core.fill):
size, offset = self.font.getsize(text)
def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None):
size, offset = self.font.getsize(text, direction, features)
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):
def font_variant(self, font=None, size=None, index=None, encoding=None,
layout_engine=None):
"""
Create a copy of this FreeTypeFont object,
using any specified arguments to override the settings.
@ -165,8 +182,9 @@ class FreeTypeFont(object):
return FreeTypeFont(font=self.path if font is None else font,
size=self.size if size is None else size,
index=self.index if index is None else index,
encoding=self.encoding if encoding is None else
encoding)
encoding=self.encoding if encoding is None else encoding,
layout_engine=self.layout_engine if layout_engine is None else layout_engine
)
class TransposedFont(object):
@ -185,14 +203,14 @@ class TransposedFont(object):
self.font = font
self.orientation = orientation # any 'transpose' argument, or None
def getsize(self, text):
def getsize(self, text, *args, **kwargs):
w, h = self.font.getsize(text)
if self.orientation in (Image.ROTATE_90, Image.ROTATE_270):
return h, w
return w, h
def getmask(self, text, mode=""):
im = self.font.getmask(text, mode)
def getmask(self, text, mode="", *args, **kwargs):
im = self.font.getmask(text, mode, *args, **kwargs)
if self.orientation is not None:
return im.transpose(self.orientation)
return im
@ -212,7 +230,8 @@ def load(filename):
return f
def truetype(font=None, size=10, index=0, encoding=""):
def truetype(font=None, size=10, index=0, encoding="",
layout_engine=None):
"""
Load a TrueType or OpenType font file, and create a font object.
This function loads a font object from the given file, and creates
@ -230,12 +249,14 @@ def truetype(font=None, size=10, index=0, encoding=""):
Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert),
and "armn" (Apple Roman). See the FreeType documentation
for more information.
:param layout_engine: Which layout engine to use, if available:
`ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.
:return: A font object.
:exception IOError: If the file could not be read.
"""
try:
return FreeTypeFont(font, size, index, encoding)
return FreeTypeFont(font, size, index, encoding, layout_engine)
except IOError:
ttf_filename = os.path.basename(font)
@ -266,16 +287,16 @@ def truetype(font=None, size=10, index=0, encoding=""):
for walkfilename in walkfilenames:
if ext and walkfilename == ttf_filename:
fontpath = os.path.join(walkroot, walkfilename)
return FreeTypeFont(fontpath, size, index, encoding)
return FreeTypeFont(fontpath, size, index, encoding, layout_engine)
elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
fontpath = os.path.join(walkroot, walkfilename)
if os.path.splitext(fontpath)[1] == '.ttf':
return FreeTypeFont(fontpath, size, index, encoding)
return FreeTypeFont(fontpath, size, index, encoding, layout_engine)
if not ext and first_font_with_a_different_extension is None:
first_font_with_a_different_extension = fontpath
if first_font_with_a_different_extension:
return FreeTypeFont(first_font_with_a_different_extension, size,
index, encoding)
index, encoding, layout_engine)
raise

View File

@ -45,6 +45,7 @@ def get_supported_codecs():
features = {
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
"raqm": ("PIL._imagingft", "HAVE_RAQM")
}
def check_feature(feature):

6
Tests/fonts/LICENSE.txt Normal file
View File

@ -0,0 +1,6 @@
NotoNastaliqUrdu-Regular.ttf:
(from https://github.com/googlei18n/noto-fonts)
All Noto fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.

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

View File

@ -1,7 +1,6 @@
from helper import unittest, PillowTestCase
from PIL import Image
from PIL import ImageDraw
from PIL import Image, ImageDraw, ImageFont, features
from io import BytesIO
import os
import sys
@ -12,10 +11,9 @@ FONT_SIZE = 20
TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward"
HAS_FREETYPE = features.check('freetype2')
HAS_RAQM = features.check('raqm')
try:
from PIL import ImageFont
ImageFont.core.getfont # check if freetype is available
class SimplePatcher(object):
def __init__(self, parent_obj, attr_name, value):
@ -42,14 +40,20 @@ try:
else:
delattr(self._parent_obj, self._attr_name)
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available")
class TestImageFont(PillowTestCase):
LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC
def get_font(self):
return ImageFont.truetype(FONT_PATH, FONT_SIZE,
layout_engine=self.LAYOUT_ENGINE)
def test_sanity(self):
self.assertRegexpMatches(
ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$")
def test_font_properties(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
self.assertEqual(ttf.path, FONT_PATH)
self.assertEqual(ttf.size, FONT_SIZE)
@ -65,9 +69,8 @@ try:
self.assertEqual(ttf_copy.path, second_font_path)
def test_font_with_name(self):
ImageFont.truetype(FONT_PATH, FONT_SIZE)
self.get_font()
self._render(FONT_PATH)
self._clean()
def _font_as_bytes(self):
with open(FONT_PATH, 'rb') as f:
@ -75,34 +78,30 @@ try:
return font_bytes
def test_font_with_filelike(self):
ImageFont.truetype(self._font_as_bytes(), FONT_SIZE)
ImageFont.truetype(self._font_as_bytes(), FONT_SIZE,
layout_engine=self.LAYOUT_ENGINE)
self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes()
# self._render(shared_bytes)
# self.assertRaises(Exception, lambda: _render(shared_bytes))
self._clean()
def test_font_with_open_file(self):
with open(FONT_PATH, 'rb') as f:
self._render(f)
self._clean()
def _render(self, font):
txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE)
ttf = ImageFont.truetype(font, FONT_SIZE,
layout_engine=self.LAYOUT_ENGINE)
ttf.getsize(txt)
img = Image.new("RGB", (256, 64), "white")
d = ImageDraw.Draw(img)
d.text((10, 10), txt, font=ttf, fill='black')
img.save('font.png')
return img
def _clean(self):
os.unlink('font.png')
def test_render_equal(self):
img_path = self._render(FONT_PATH)
with open(FONT_PATH, 'rb') as f:
@ -110,12 +109,11 @@ try:
img_filelike = self._render(font_filelike)
self.assert_image_equal(img_path, img_filelike)
self._clean()
def test_textsize_equal(self):
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
txt = "Hello World!"
size = draw.textsize(txt, ttf)
@ -132,7 +130,7 @@ try:
def test_render_multiline(self):
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
line_spacing = draw.textsize('A', font=ttf)[1] + 4
lines = TEST_TEXT.split("\n")
y = 0
@ -149,7 +147,7 @@ try:
self.assert_image_similar(im, target_img, 6.2)
def test_render_multiline_text(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
@ -187,7 +185,7 @@ try:
def test_unknown_align(self):
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
# Act/Assert
self.assertRaises(AssertionError,
@ -196,7 +194,7 @@ try:
align="unknown"))
def test_multiline_size(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
@ -211,7 +209,7 @@ try:
del draw
def test_multiline_width(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
@ -221,7 +219,7 @@ try:
del draw
def test_multiline_spacing(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = self.get_font()
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
@ -238,7 +236,7 @@ try:
img_grey = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_grey)
word = "testing"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
orientation = Image.ROTATE_90
transposed_font = ImageFont.TransposedFont(
@ -261,7 +259,7 @@ try:
img_grey = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_grey)
word = "testing"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
orientation = None
transposed_font = ImageFont.TransposedFont(
@ -282,7 +280,7 @@ try:
def test_rotated_transposed_font_get_mask(self):
# Arrange
text = "mask this"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
orientation = Image.ROTATE_90
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
@ -296,7 +294,7 @@ try:
def test_unrotated_transposed_font_get_mask(self):
# Arrange
text = "mask this"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
orientation = None
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
@ -309,7 +307,7 @@ try:
def test_free_type_font_get_name(self):
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
# Act
name = font.getname()
@ -319,7 +317,7 @@ try:
def test_free_type_font_get_metrics(self):
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
# Act
ascent, descent = font.getmetrics()
@ -331,7 +329,7 @@ try:
def test_free_type_font_get_offset(self):
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
text = "offset this"
# Act
@ -342,7 +340,7 @@ try:
def test_free_type_font_get_mask(self):
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
font = self.get_font()
text = "mask this"
# Act
@ -379,12 +377,12 @@ try:
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font):
def loadable_font(filepath, size, index, encoding):
def loadable_font(filepath, size, index, encoding, *args, **kwargs):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(FONT_PATH, size, index,
encoding)
encoding, *args, **kwargs)
return ImageFont._FreeTypeFont(filepath, size, index,
encoding)
encoding, *args, **kwargs)
with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font):
font = ImageFont.truetype(fontname)
# Make sure it's loaded
@ -450,7 +448,7 @@ try:
def test_imagefont_getters(self):
# Arrange
t = ImageFont.truetype(FONT_PATH, FONT_SIZE)
t = self.get_font()
# Act / Assert
self.assertEqual(t.getmetrics(), (16, 4))
@ -467,11 +465,9 @@ try:
self.assertEqual(t.getsize('a'), (12, 16))
except ImportError:
class TestImageFont(PillowTestCase):
def test_skip(self):
self.skipTest("ImportError")
@unittest.skipUnless(HAS_RAQM, "Raqm not Available")
class TestImageFont_RaqmLayout(TestImageFont):
LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM
if __name__ == '__main__':
unittest.main()

133
Tests/test_imagefontctl.py Normal file
View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
from helper import unittest, PillowTestCase
from PIL import Image, ImageDraw, ImageFont, features
FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans.ttf"
@unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.")
class TestImagecomplextext(PillowTestCase):
def test_english(self):
#smoke test, this should not fail
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')
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, 1.7)
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), 'السلام عليكم', 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)
liga_size = ttf.getsize('fi', features=['-liga'])
self.assertEqual(liga_size,(13,19))
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)
if __name__ == '__main__':
unittest.main()
# End of file

View File

@ -41,6 +41,18 @@
#define FT_ERRORDEF( e, v, s ) { e, s },
#define FT_ERROR_START_LIST {
#define FT_ERROR_END_LIST { 0, 0 } };
#ifdef HAVE_RAQM
#include <raqm.h>
#endif
#define LAYOUT_FALLBACK 0
#define LAYOUT_RAQM 1
typedef struct
{
int index, x_offset, x_advance, y_offset;
unsigned int cluster;
} GlyphInfo;
struct {
int code;
@ -58,6 +70,7 @@ typedef struct {
PyObject_HEAD
FT_Face face;
unsigned char *font_bytes;
int layout_engine;
} FontObject;
static PyTypeObject Font_Type;
@ -91,11 +104,13 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
char* filename = NULL;
int size;
int index = 0;
int layout_engine = 0;
unsigned char* encoding;
unsigned char* font_bytes;
int font_bytes_size = 0;
static char* kwlist[] = {
"filename", "size", "index", "encoding", "font_bytes", NULL
"filename", "size", "index", "encoding", "font_bytes",
"layout_engine", NULL
};
if (!library) {
@ -106,10 +121,10 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
return NULL;
}
if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#", kwlist,
if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#i", kwlist,
Py_FileSystemDefaultEncoding, &filename,
&size, &index, &encoding, &font_bytes,
&font_bytes_size)) {
&font_bytes_size, &layout_engine)) {
return NULL;
}
@ -121,6 +136,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
}
self->face = NULL;
self->layout_engine = layout_engine;
if (filename && font_bytes_size <= 0) {
self->font_bytes = NULL;
@ -188,60 +204,288 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
return 0;
}
static PyObject*
font_getsize(FontObject* self, PyObject* args)
#ifdef HAVE_RAQM
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_Face face;
int xoffset, yoffset;
Py_ssize_t count;
FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0;
int i;
/* calculate size and bearing for a given string */
PyObject* string;
if (!PyArg_ParseTuple(args, "O:getsize", &string))
return NULL;
if (features != Py_None || dir != NULL) {
PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm");
}
#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;
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
if (self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(string, self, dir, features, glyph_info, mask);
} else {
count = text_layout_fallback(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;
xoffset = yoffset = 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;
FT_BBox bbox;
FT_Glyph glyph;
face = self->face;
index = FT_Get_Char_Index(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;
}
index = glyph_info[i].index;
/* 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);
if (error)
return geterror(error);
if (i == 0)
if (i == 0 && face->glyph->metrics.horiBearingX < 0) {
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_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)
y_max = bbox.yMax;
if (bbox.yMin < y_min)
@ -251,23 +495,16 @@ font_getsize(FontObject* self, PyObject* args)
if (face->glyph->metrics.horiBearingY > yoffset)
yoffset = face->glyph->metrics.horiBearingY;
last_index = index;
FT_Done_Glyph(glyph);
}
if (face) {
int offset;
/* left bearing */
if (xoffset < 0)
x -= xoffset;
else
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
* the baseline from the top */
yoffset = PIXEL(self->face->size->metrics.ascender - yoffset);
@ -329,11 +566,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,15 +574,18 @@ 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))
return NULL;
const char *dir = NULL;
size_t count;
GlyphInfo *glyph_info;
PyObject *features = 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");
if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) {
return NULL;
}
glyph_info = NULL;
count = text_layout(string, self, dir, features, &glyph_info, mask);
if (count == 0) {
return NULL;
}
@ -360,36 +596,37 @@ 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);
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;
}
x = -self->face->glyph->metrics.horiBearingX;
index = glyph_info[i].index;
error = FT_Load_Glyph(self->face, index, load_flags);
if (error)
return geterror(error);
if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) {
x = -self->face->glyph->metrics.horiBearingX;
}
glyph = self->face->glyph;
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;
x1 = glyph->bitmap.width;
if (xx < 0)
@ -401,6 +638,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,8 +658,10 @@ 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;
unsigned char *target = im->image8[yy] + xx;
for (i = x0; i < x1; i++) {
@ -432,10 +672,10 @@ font_render(FontObject* self, PyObject* args)
source += glyph->bitmap.pitch;
}
}
x += PIXEL(glyph->metrics.horiAdvance);
last_index = index;
x += glyph_info[i].x_advance;
}
PyMem_Del(glyph_info);
Py_RETURN_NONE;
}
@ -593,6 +833,14 @@ setup_module(PyObject* m) {
#endif
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;
}

View File

@ -11,7 +11,8 @@ sudo apt-get -y install python-dev python-setuptools \
python3-dev python-virtualenv cmake
sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-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_imagequant.sh
./install_raqm.sh

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz
archive=$1

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

@ -4,8 +4,10 @@
# Installs all of the dependencies for Pillow for Freebsd 10.x
# for both system Pythons 2.7 and 3.4
#
sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27
sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake
# Openjpeg fails badly using the openjpeg package.
# 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_cmake.sh

14
depends/install_raqm.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# install raqm
archive=raqm-0.2.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
pushd $archive
./configure --prefix=/usr && make -j4 && sudo make -j4 install
popd

18
depends/install_raqm_cmake.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# install raqm
archive=raqm-cmake-b517ba80
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
pushd $archive
mkdir build
cd build
cmake ..
make && sudo make install
cd ..
popd

View File

@ -4,12 +4,13 @@
# Installs all of the dependencies for Pillow for Ubuntu 14.04
# for both system Pythons 2.7 and 3.4
#
sudo apt-get update
sudo apt-get -y install python-dev python-setuptools \
python3-dev python-virtualenv cmake
sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-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_imagequant.sh
./install_raqm.sh

View File

@ -28,12 +28,6 @@ Basic Installation
most common image formats. See :ref:`external-libraries` for a
full list of external libraries supported.
.. note::
The basic installation works on Windows and macOS using the binaries
from PyPI. Other installations require building from source as
detailed below.
Install Pillow with :command:`pip`::
$ pip install Pillow
@ -72,11 +66,15 @@ except OpenJPEG::
Linux Installation
^^^^^^^^^^^^^^^^^^
We do not provide binaries for Linux. Most major Linux distributions,
including Fedora, Debian/Ubuntu and ArchLinux include Pillow in
packages that previously contained PIL e.g. ``python-imaging``. Please
consider using native operating system packages first to avoid
installation problems and/or missing library support later.
We provide binaries for Linux for each of the supported Python
versions in the manylinux wheel format. These include support for all
optional libraries except Raqm::
$ pip install Pillow
Most major Linux distributions, including Fedora, Debian/Ubuntu and
ArchLinux also include Pillow in packages that previously contained
PIL e.g. ``python-imaging``.
FreeBSD Installation
^^^^^^^^^^^^^^^^^^^^
@ -120,7 +118,9 @@ External Libraries
.. note::
There are scripts to install the dependencies for some operating
systems included in the ``depends`` directory.
systems included in the ``depends`` directory. Also see the
Dockerfiles in our `docker images repo
<https://github.com/python-pillow/docker-images>`_.
Many of Pillow's features require external libraries:
@ -170,6 +170,18 @@ Many of Pillow's features require external libraries:
* Windows support: Libimagequant requires VS2013/MSVC 18 to compile,
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.
* Windows support: Raqm support is currently unsupported on Windows.
Once you have installed the prerequisites, run::
$ pip install Pillow
@ -201,14 +213,16 @@ Build Options
* Build flags: ``--disable-zlib``, ``--disable-jpeg``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``,
``--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
libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``,
``--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
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
@ -247,7 +261,12 @@ The easiest way to install external libraries is via `Homebrew
$ brew install libtiff libjpeg webp little-cms2
Install Pillow with::
To install libraqm on MaxOS use Homebrew to install its dependencies::
$ brew install freetype harfbuzz fribidi
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Now install Pillow with::
$ pip install Pillow
@ -277,7 +296,9 @@ Or for Python 3::
Prerequisites are installed on **FreeBSD 10 or 11** with::
$ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg
$ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Building on Linux
@ -313,12 +334,15 @@ Prerequisites are installed on **Ubuntu 12.04 LTS** or **Raspian Wheezy
Prerequisites are installed on **Ubuntu 14.04 LTS** with::
$ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk
libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \
tcl8.6-dev tk8.6-dev python-tk
Then see ``depends/install_raqm.sh`` to install libraqm.
Prerequisites are installed on **Fedora 23** with::
$ 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.
.. 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.
@ -240,9 +240,28 @@ Methods
the number of pixels between lines.
:param align: If the text is passed on to multiline_text(),
"left", "center" or "right".
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right), 'ttb' (top to
bottom) or 'btt' (bottom to top). Requires
libraqm.
.. versionadded:: 4.2.0
.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left")
:param features: A list of OpenType font features 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:: 4.2.0
.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left",
direction=None, features=None)
Draws the string at the given position.
@ -252,8 +271,28 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param spacing: The number of pixels between lines.
:param align: "left", "center" or "right".
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right), 'ttb' (top to
bottom) or 'btt' (bottom to top). Requires
libraqm.
.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0)
.. versionadded:: 4.2.0
:param features: A list of OpenType font features 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:: 4.2.0
.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=4, direction=None,
features=None)
Return the size of the given string, in pixels.
@ -262,11 +301,51 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param spacing: If the text is passed on to multiline_textsize(),
the number of pixels between lines.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right), 'ttb' (top to
bottom) or 'btt' (bottom to top). Requires
libraqm.
.. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=0)
.. versionadded:: 4.2.0
:param features: A list of OpenType font features 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:: 4.2.0
.. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=4, direction=None,
features=None)
Return the size of the given string, in pixels.
:param text: Text to be measured.
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param spacing: The number of pixels between lines.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right), 'ttb' (top to
bottom) or 'btt' (bottom to top). Requires
libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features 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:: 4.2.0

View File

@ -51,7 +51,7 @@ Methods
: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.
@ -65,5 +65,26 @@ Methods
C-level implementations.
.. versionadded:: 1.1.5
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right), 'ttb' (top to
bottom) or 'btt' (bottom to top). Requires
libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features 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:: 4.2.0
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.

View File

@ -1,11 +1,26 @@
4.2.0
-----
Added Complex Text Rendering
============================
Pillow now supports complex text rendering for scripts requiring glyph
composition and bidirectional flow. This optional feature adds three
dependencies: harfbuzz, fribidi, and raqm. See the install
documentation for further details. This feature is tested and works on
Un*x and Mac, but has not yet been built on Windows platforms.
Removed Deprecated Items
========================
Several deprecated items have been removed.
* The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, :py:meth:`PIL.ImageWin.Dib.tostring` and :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have been removed.
* The methods :py:meth:`PIL.ImageWin.Dib.fromstring`,
:py:meth:`PIL.ImageWin.Dib.tostring` and
:py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have
been removed.
* Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would discard the alpha channel. From Pillow 3.4.0, a deprecation warning was shown. From Pillow 4.2.0, the deprecation warning is removed and an :py:exc:`IOError` is raised.
* Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would
discard the alpha channel. From Pillow 3.4.0, a deprecation warning
was shown. From Pillow 4.2.0, the deprecation warning is removed and
an :py:exc:`IOError` is raised.

View File

@ -183,7 +183,8 @@ if __name__ == "__main__":
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),
("libtiff", "LIBTIFF")
("libtiff", "LIBTIFF"),
("raqm", "RAQM (Bidirectional Text)")
]:
if features.check(name):
print("---", feature, "support ok")

View File

@ -119,7 +119,7 @@ IMAGEQUANT_ROOT = None
TIFF_ROOT = None
FREETYPE_ROOT = None
LCMS_ROOT = None
RAQM_ROOT = None
def _pkg_config(name):
try:
@ -137,7 +137,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'}
@ -522,6 +522,14 @@ class pil_build_ext(build_ext):
if subdir:
_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'):
_dbg('Looking for lcms')
if _find_include_file(self, "lcms2.h"):
@ -605,9 +613,14 @@ class pil_build_ext(build_ext):
# additional libraries
if feature.freetype:
exts.append(Extension("PIL._imagingft",
["_imagingft.c"],
libraries=["freetype"]))
libs = ["freetype"]
defs = []
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:
extra = []
@ -669,6 +682,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"),