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

@ -3,8 +3,10 @@
set -e set -e
sudo apt-get update sudo apt-get update
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk \ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\
libharfbuzz-dev libfribidi-dev
pip install cffi pip install cffi
pip install nose pip install nose
pip install check-manifest pip install check-manifest

View File

@ -207,7 +207,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()
@ -215,17 +214,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)
@ -244,25 +243,30 @@ 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]
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.""" """Get the size of a given string, in pixels."""
if self._multiline_check(text): 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: if font is None:
font = self.getfont() 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 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines: 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) max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing return max_width, len(lines)*line_spacing

View File

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

View File

@ -45,6 +45,7 @@ def get_supported_codecs():
features = { features = {
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
"raqm": ("PIL._imagingft", "HAVE_RAQM")
} }
def check_feature(feature): 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 helper import unittest, PillowTestCase
from PIL import Image from PIL import Image, ImageDraw, ImageFont, features
from PIL import ImageDraw
from io import BytesIO from io import BytesIO
import os import os
import sys import sys
@ -12,466 +11,463 @@ FONT_SIZE = 20
TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" 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): class SimplePatcher(object):
def __init__(self, parent_obj, attr_name, value): def __init__(self, parent_obj, attr_name, value):
self._parent_obj = parent_obj self._parent_obj = parent_obj
self._attr_name = attr_name self._attr_name = attr_name
self._saved = None self._saved = None
self._is_saved = False
self._value = value
def __enter__(self):
# Patch the attr on the object
if hasattr(self._parent_obj, self._attr_name):
self._saved = getattr(self._parent_obj, self._attr_name)
setattr(self._parent_obj, self._attr_name, self._value)
self._is_saved = True
else:
setattr(self._parent_obj, self._attr_name, self._value)
self._is_saved = False self._is_saved = False
self._value = value
def __enter__(self): def __exit__(self, type, value, traceback):
# Patch the attr on the object # Restore the original value
if hasattr(self._parent_obj, self._attr_name): if self._is_saved:
self._saved = getattr(self._parent_obj, self._attr_name) setattr(self._parent_obj, self._attr_name, self._saved)
setattr(self._parent_obj, self._attr_name, self._value) else:
self._is_saved = True delattr(self._parent_obj, self._attr_name)
else:
setattr(self._parent_obj, self._attr_name, self._value)
self._is_saved = False
def __exit__(self, type, value, traceback): @unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available")
# Restore the original value class TestImageFont(PillowTestCase):
if self._is_saved: LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC
setattr(self._parent_obj, self._attr_name, self._saved)
else:
delattr(self._parent_obj, self._attr_name)
class TestImageFont(PillowTestCase): 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_sanity(self): def test_font_properties(self):
self.assertRegexpMatches( ttf = self.get_font()
ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") self.assertEqual(ttf.path, FONT_PATH)
self.assertEqual(ttf.size, FONT_SIZE)
def test_font_properties(self): ttf_copy = ttf.font_variant()
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) self.assertEqual(ttf_copy.path, FONT_PATH)
self.assertEqual(ttf.path, FONT_PATH) self.assertEqual(ttf_copy.size, FONT_SIZE)
self.assertEqual(ttf.size, FONT_SIZE)
ttf_copy = ttf.font_variant() ttf_copy = ttf.font_variant(size=FONT_SIZE+1)
self.assertEqual(ttf_copy.path, FONT_PATH) self.assertEqual(ttf_copy.size, FONT_SIZE+1)
self.assertEqual(ttf_copy.size, FONT_SIZE)
ttf_copy = ttf.font_variant(size=FONT_SIZE+1) second_font_path = "Tests/fonts/DejaVuSans.ttf"
self.assertEqual(ttf_copy.size, FONT_SIZE+1) ttf_copy = ttf.font_variant(font=second_font_path)
self.assertEqual(ttf_copy.path, second_font_path)
second_font_path = "Tests/fonts/DejaVuSans.ttf" def test_font_with_name(self):
ttf_copy = ttf.font_variant(font=second_font_path) self.get_font()
self.assertEqual(ttf_copy.path, second_font_path) self._render(FONT_PATH)
def test_font_with_name(self): def _font_as_bytes(self):
ImageFont.truetype(FONT_PATH, FONT_SIZE) with open(FONT_PATH, 'rb') as f:
self._render(FONT_PATH) font_bytes = BytesIO(f.read())
self._clean() return font_bytes
def _font_as_bytes(self): def test_font_with_filelike(self):
with open(FONT_PATH, 'rb') as f: ImageFont.truetype(self._font_as_bytes(), FONT_SIZE,
font_bytes = BytesIO(f.read()) layout_engine=self.LAYOUT_ENGINE)
return font_bytes 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))
def test_font_with_filelike(self): def test_font_with_open_file(self):
ImageFont.truetype(self._font_as_bytes(), FONT_SIZE) with open(FONT_PATH, 'rb') as f:
self._render(self._font_as_bytes()) self._render(f)
# 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): def _render(self, font):
with open(FONT_PATH, 'rb') as f: txt = "Hello World!"
self._render(f) ttf = ImageFont.truetype(font, FONT_SIZE,
self._clean() layout_engine=self.LAYOUT_ENGINE)
ttf.getsize(txt)
def _render(self, font): img = Image.new("RGB", (256, 64), "white")
txt = "Hello World!" d = ImageDraw.Draw(img)
ttf = ImageFont.truetype(font, FONT_SIZE) d.text((10, 10), txt, font=ttf, fill='black')
ttf.getsize(txt)
img = Image.new("RGB", (256, 64), "white") return img
d = ImageDraw.Draw(img)
d.text((10, 10), txt, font=ttf, fill='black')
img.save('font.png') def test_render_equal(self):
return img img_path = self._render(FONT_PATH)
with open(FONT_PATH, 'rb') as f:
font_filelike = BytesIO(f.read())
img_filelike = self._render(font_filelike)
def _clean(self): self.assert_image_equal(img_path, img_filelike)
os.unlink('font.png')
def test_render_equal(self): def test_textsize_equal(self):
img_path = self._render(FONT_PATH) im = Image.new(mode='RGB', size=(300, 100))
with open(FONT_PATH, 'rb') as f: draw = ImageDraw.Draw(im)
font_filelike = BytesIO(f.read()) ttf = self.get_font()
img_filelike = self._render(font_filelike)
self.assert_image_equal(img_path, img_filelike) txt = "Hello World!"
self._clean() size = draw.textsize(txt, ttf)
draw.text((10, 10), txt, font=ttf)
draw.rectangle((10, 10, 10 + size[0], 10 + size[1]))
del draw
def test_textsize_equal(self): target = 'Tests/images/rectangle_surrounding_text.png'
target_img = Image.open(target)
# Epsilon ~.5 fails with FreeType 2.7
self.assert_image_similar(im, target_img, 2.5)
def test_render_multiline(self):
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
ttf = self.get_font()
line_spacing = draw.textsize('A', font=ttf)[1] + 4
lines = TEST_TEXT.split("\n")
y = 0
for line in lines:
draw.text((0, y), line, font=ttf)
y += line_spacing
target = 'Tests/images/multiline_text.png'
target_img = Image.open(target)
# some versions of freetype have different horizontal spacing.
# setting a tight epsilon, I'm showing the original test failure
# at epsilon = ~38.
self.assert_image_similar(im, target_img, 6.2)
def test_render_multiline_text(self):
ttf = self.get_font()
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
draw.text((0, 0), TEST_TEXT, font=ttf)
target = 'Tests/images/multiline_text.png'
target_img = Image.open(target)
# Epsilon ~.5 fails with FreeType 2.7
self.assert_image_similar(im, target_img, 6.2)
# Test that text() can pass on additional arguments
# to multiline_text()
draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None,
spacing=4, align="left")
draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left")
del draw
# Test align center and right
for align, ext in {"center": "_center",
"right": "_right"}.items():
im = Image.new(mode='RGB', size=(300, 100)) im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align)
txt = "Hello World!"
size = draw.textsize(txt, ttf)
draw.text((10, 10), txt, font=ttf)
draw.rectangle((10, 10, 10 + size[0], 10 + size[1]))
del draw del draw
target = 'Tests/images/rectangle_surrounding_text.png' target = 'Tests/images/multiline_text'+ext+'.png'
target_img = Image.open(target)
# Epsilon ~.5 fails with FreeType 2.7
self.assert_image_similar(im, target_img, 2.5)
def test_render_multiline(self):
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
line_spacing = draw.textsize('A', font=ttf)[1] + 4
lines = TEST_TEXT.split("\n")
y = 0
for line in lines:
draw.text((0, y), line, font=ttf)
y += line_spacing
target = 'Tests/images/multiline_text.png'
target_img = Image.open(target)
# some versions of freetype have different horizontal spacing.
# setting a tight epsilon, I'm showing the original test failure
# at epsilon = ~38.
self.assert_image_similar(im, target_img, 6.2)
def test_render_multiline_text(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
draw.text((0, 0), TEST_TEXT, font=ttf)
target = 'Tests/images/multiline_text.png'
target_img = Image.open(target) target_img = Image.open(target)
# Epsilon ~.5 fails with FreeType 2.7 # Epsilon ~.5 fails with FreeType 2.7
self.assert_image_similar(im, target_img, 6.2) self.assert_image_similar(im, target_img, 6.2)
# Test that text() can pass on additional arguments def test_unknown_align(self):
# to multiline_text() im = Image.new(mode='RGB', size=(300, 100))
draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, draw = ImageDraw.Draw(im)
spacing=4, align="left") ttf = self.get_font()
draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left")
del draw
# Test align center and right # Act/Assert
for align, ext in {"center": "_center", self.assertRaises(AssertionError,
"right": "_right"}.items(): lambda: draw.multiline_text((0, 0), TEST_TEXT,
im = Image.new(mode='RGB', size=(300, 100)) font=ttf,
draw = ImageDraw.Draw(im) align="unknown"))
draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align)
del draw
target = 'Tests/images/multiline_text'+ext+'.png' def test_multiline_size(self):
target_img = Image.open(target) ttf = self.get_font()
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
# Epsilon ~.5 fails with FreeType 2.7 # Test that textsize() correctly connects to multiline_textsize()
self.assert_image_similar(im, target_img, 6.2) self.assertEqual(draw.textsize(TEST_TEXT, font=ttf),
draw.multiline_textsize(TEST_TEXT, font=ttf))
def test_unknown_align(self): # Test that textsize() can pass on additional arguments
im = Image.new(mode='RGB', size=(300, 100)) # to multiline_textsize()
draw = ImageDraw.Draw(im) draw.textsize(TEST_TEXT, font=ttf, spacing=4)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) draw.textsize(TEST_TEXT, ttf, 4)
del draw
# Act/Assert def test_multiline_width(self):
self.assertRaises(AssertionError, ttf = self.get_font()
lambda: draw.multiline_text((0, 0), TEST_TEXT, im = Image.new(mode='RGB', size=(300, 100))
font=ttf, draw = ImageDraw.Draw(im)
align="unknown"))
def test_multiline_size(self): self.assertEqual(draw.textsize("longest line", font=ttf)[0],
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) draw.multiline_textsize("longest line\nline",
im = Image.new(mode='RGB', size=(300, 100)) font=ttf)[0])
draw = ImageDraw.Draw(im) del draw
# Test that textsize() correctly connects to multiline_textsize() def test_multiline_spacing(self):
self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), ttf = self.get_font()
draw.multiline_textsize(TEST_TEXT, font=ttf))
# Test that textsize() can pass on additional arguments im = Image.new(mode='RGB', size=(300, 100))
# to multiline_textsize() draw = ImageDraw.Draw(im)
draw.textsize(TEST_TEXT, font=ttf, spacing=4) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10)
draw.textsize(TEST_TEXT, ttf, 4) del draw
del draw
def test_multiline_width(self): target = 'Tests/images/multiline_text_spacing.png'
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) target_img = Image.open(target)
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
self.assertEqual(draw.textsize("longest line", font=ttf)[0], # Epsilon ~.5 fails with FreeType 2.7
draw.multiline_textsize("longest line\nline", self.assert_image_similar(im, target_img, 6.2)
font=ttf)[0])
del draw
def test_multiline_spacing(self): def test_rotated_transposed_font(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) img_grey = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_grey)
word = "testing"
font = self.get_font()
im = Image.new(mode='RGB', size=(300, 100)) orientation = Image.ROTATE_90
draw = ImageDraw.Draw(im) transposed_font = ImageFont.TransposedFont(
draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) font, orientation=orientation)
del draw
target = 'Tests/images/multiline_text_spacing.png' # Original font
target_img = Image.open(target) draw.font = font
box_size_a = draw.textsize(word)
# Epsilon ~.5 fails with FreeType 2.7 # Rotated font
self.assert_image_similar(im, target_img, 6.2) draw.font = transposed_font
box_size_b = draw.textsize(word)
del draw
def test_rotated_transposed_font(self): # Check (w,h) of box a is (h,w) of box b
img_grey = Image.new("L", (100, 100)) self.assertEqual(box_size_a[0], box_size_b[1])
draw = ImageDraw.Draw(img_grey) self.assertEqual(box_size_a[1], box_size_b[0])
word = "testing"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
orientation = Image.ROTATE_90 def test_unrotated_transposed_font(self):
transposed_font = ImageFont.TransposedFont( img_grey = Image.new("L", (100, 100))
font, orientation=orientation) draw = ImageDraw.Draw(img_grey)
word = "testing"
font = self.get_font()
# Original font orientation = None
draw.font = font transposed_font = ImageFont.TransposedFont(
box_size_a = draw.textsize(word) font, orientation=orientation)
# Rotated font # Original font
draw.font = transposed_font draw.font = font
box_size_b = draw.textsize(word) box_size_a = draw.textsize(word)
del draw
# Check (w,h) of box a is (h,w) of box b # Rotated font
self.assertEqual(box_size_a[0], box_size_b[1]) draw.font = transposed_font
self.assertEqual(box_size_a[1], box_size_b[0]) box_size_b = draw.textsize(word)
del draw
def test_unrotated_transposed_font(self): # Check boxes a and b are same size
img_grey = Image.new("L", (100, 100)) self.assertEqual(box_size_a, box_size_b)
draw = ImageDraw.Draw(img_grey)
word = "testing"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
orientation = None def test_rotated_transposed_font_get_mask(self):
transposed_font = ImageFont.TransposedFont( # Arrange
font, orientation=orientation) text = "mask this"
font = self.get_font()
orientation = Image.ROTATE_90
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
# Original font # Act
draw.font = font mask = transposed_font.getmask(text)
box_size_a = draw.textsize(word)
# Rotated font # Assert
draw.font = transposed_font self.assertEqual(mask.size, (13, 108))
box_size_b = draw.textsize(word)
del draw
# Check boxes a and b are same size def test_unrotated_transposed_font_get_mask(self):
self.assertEqual(box_size_a, box_size_b) # Arrange
text = "mask this"
font = self.get_font()
orientation = None
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
def test_rotated_transposed_font_get_mask(self): # Act
# Arrange mask = transposed_font.getmask(text)
text = "mask this"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
orientation = Image.ROTATE_90
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
# Act # Assert
mask = transposed_font.getmask(text) self.assertEqual(mask.size, (108, 13))
# Assert def test_free_type_font_get_name(self):
self.assertEqual(mask.size, (13, 108)) # Arrange
font = self.get_font()
def test_unrotated_transposed_font_get_mask(self): # Act
# Arrange name = font.getname()
text = "mask this"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
orientation = None
transposed_font = ImageFont.TransposedFont(
font, orientation=orientation)
# Act # Assert
mask = transposed_font.getmask(text) self.assertEqual(('FreeMono', 'Regular'), name)
# Assert def test_free_type_font_get_metrics(self):
self.assertEqual(mask.size, (108, 13)) # Arrange
font = self.get_font()
def test_free_type_font_get_name(self): # Act
# Arrange ascent, descent = font.getmetrics()
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
# Act # Assert
name = font.getname() self.assertIsInstance(ascent, int)
self.assertIsInstance(descent, int)
self.assertEqual((ascent, descent), (16, 4)) # too exact check?
# Assert def test_free_type_font_get_offset(self):
self.assertEqual(('FreeMono', 'Regular'), name) # Arrange
font = self.get_font()
text = "offset this"
def test_free_type_font_get_metrics(self): # Act
# Arrange offset = font.getoffset(text)
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
# Act # Assert
ascent, descent = font.getmetrics() self.assertEqual(offset, (0, 3))
# Assert def test_free_type_font_get_mask(self):
self.assertIsInstance(ascent, int) # Arrange
self.assertIsInstance(descent, int) font = self.get_font()
self.assertEqual((ascent, descent), (16, 4)) # too exact check? text = "mask this"
def test_free_type_font_get_offset(self): # Act
# Arrange mask = font.getmask(text)
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
text = "offset this"
# Act # Assert
offset = font.getoffset(text) self.assertEqual(mask.size, (108, 13))
# Assert def test_load_path_not_found(self):
self.assertEqual(offset, (0, 3)) # Arrange
filename = "somefilenamethatdoesntexist.ttf"
def test_free_type_font_get_mask(self): # Act/Assert
# Arrange self.assertRaises(IOError, lambda: ImageFont.load_path(filename))
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
text = "mask this"
# Act def test_default_font(self):
mask = font.getmask(text) # Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
# Assert target = 'Tests/images/default_font.png'
self.assertEqual(mask.size, (108, 13)) target_img = Image.open(target)
def test_load_path_not_found(self): # Act
# Arrange default_font = ImageFont.load_default()
filename = "somefilenamethatdoesntexist.ttf" draw.text((10, 10), txt, font=default_font)
del draw
# Act/Assert # Assert
self.assertRaises(IOError, lambda: ImageFont.load_path(filename)) self.assert_image_equal(im, target_img)
def test_default_font(self): def _test_fake_loading_font(self, path_to_fake, fontname):
# Arrange # Make a copy of FreeTypeFont so we can patch the original
txt = 'This is a "better than nothing" default font.' free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
im = Image.new(mode='RGB', size=(300, 100)) with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font):
draw = ImageDraw.Draw(im) def loadable_font(filepath, size, index, encoding, *args, **kwargs):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(FONT_PATH, size, index,
encoding, *args, **kwargs)
return ImageFont._FreeTypeFont(filepath, size, index,
encoding, *args, **kwargs)
with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font):
font = ImageFont.truetype(fontname)
# Make sure it's loaded
name = font.getname()
self.assertEqual(('FreeMono', 'Regular'), name)
target = 'Tests/images/default_font.png' @unittest.skipIf(sys.platform.startswith('win32'),
target_img = Image.open(target) "requires Unix or MacOS")
def test_find_linux_font(self):
# Act # A lot of mocking here - this is more for hitting code and
default_font = ImageFont.load_default() # catching syntax like errors
draw.text((10, 10), txt, font=default_font) font_directory = '/usr/local/share/fonts'
del draw with SimplePatcher(sys, 'platform', 'linux'):
patched_env = copy.deepcopy(os.environ)
# Assert patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/'
self.assert_image_equal(im, target_img) with SimplePatcher(os, 'environ', patched_env):
def _test_fake_loading_font(self, path_to_fake, fontname):
# 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):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(FONT_PATH, size, index,
encoding)
return ImageFont._FreeTypeFont(filepath, size, index,
encoding)
with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font):
font = ImageFont.truetype(fontname)
# Make sure it's loaded
name = font.getname()
self.assertEqual(('FreeMono', 'Regular'), name)
@unittest.skipIf(sys.platform.startswith('win32'),
"requires Unix or MacOS")
def test_find_linux_font(self):
# A lot of mocking here - this is more for hitting code and
# catching syntax like errors
font_directory = '/usr/local/share/fonts'
with SimplePatcher(sys, 'platform', 'linux'):
patched_env = copy.deepcopy(os.environ)
patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/'
with SimplePatcher(os, 'environ', patched_env):
def fake_walker(path):
if path == font_directory:
return [(path, [], [
'Arial.ttf', 'Single.otf', 'Duplicate.otf',
'Duplicate.ttf'], )]
return [(path, [], ['some_random_font.ttf'], )]
with SimplePatcher(os, 'walk', fake_walker):
# Test that the font loads both with and without the
# extension
self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial.ttf')
self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial')
# Test that non-ttf fonts can be found without the
# extension
self._test_fake_loading_font(
font_directory+'/Single.otf', 'Single')
# Test that ttf fonts are preferred if the extension is
# not specified
self._test_fake_loading_font(
font_directory+'/Duplicate.ttf', 'Duplicate')
@unittest.skipIf(sys.platform.startswith('win32'),
"requires Unix or MacOS")
def test_find_macos_font(self):
# Like the linux test, more cover hitting code rather than testing
# correctness.
font_directory = '/System/Library/Fonts'
with SimplePatcher(sys, 'platform', 'darwin'):
def fake_walker(path): def fake_walker(path):
if path == font_directory: if path == font_directory:
return [(path, [], return [(path, [], [
['Arial.ttf', 'Single.otf', 'Arial.ttf', 'Single.otf', 'Duplicate.otf',
'Duplicate.otf', 'Duplicate.ttf'], )] 'Duplicate.ttf'], )]
return [(path, [], ['some_random_font.ttf'], )] return [(path, [], ['some_random_font.ttf'], )]
with SimplePatcher(os, 'walk', fake_walker): with SimplePatcher(os, 'walk', fake_walker):
# Test that the font loads both with and without the
# extension
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial.ttf') font_directory+'/Arial.ttf', 'Arial.ttf')
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial') font_directory+'/Arial.ttf', 'Arial')
# Test that non-ttf fonts can be found without the
# extension
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory+'/Single.otf', 'Single') font_directory+'/Single.otf', 'Single')
# Test that ttf fonts are preferred if the extension is
# not specified
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory+'/Duplicate.ttf', 'Duplicate') font_directory+'/Duplicate.ttf', 'Duplicate')
def test_imagefont_getters(self): @unittest.skipIf(sys.platform.startswith('win32'),
# Arrange "requires Unix or MacOS")
t = ImageFont.truetype(FONT_PATH, FONT_SIZE) def test_find_macos_font(self):
# Like the linux test, more cover hitting code rather than testing
# correctness.
font_directory = '/System/Library/Fonts'
with SimplePatcher(sys, 'platform', 'darwin'):
def fake_walker(path):
if path == font_directory:
return [(path, [],
['Arial.ttf', 'Single.otf',
'Duplicate.otf', 'Duplicate.ttf'], )]
return [(path, [], ['some_random_font.ttf'], )]
with SimplePatcher(os, 'walk', fake_walker):
self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial.ttf')
self._test_fake_loading_font(
font_directory+'/Arial.ttf', 'Arial')
self._test_fake_loading_font(
font_directory+'/Single.otf', 'Single')
self._test_fake_loading_font(
font_directory+'/Duplicate.ttf', 'Duplicate')
# Act / Assert def test_imagefont_getters(self):
self.assertEqual(t.getmetrics(), (16, 4)) # Arrange
self.assertEqual(t.font.ascent, 16) t = self.get_font()
self.assertEqual(t.font.descent, 4)
self.assertEqual(t.font.height, 20) # Act / Assert
self.assertEqual(t.font.x_ppem, 20) self.assertEqual(t.getmetrics(), (16, 4))
self.assertEqual(t.font.y_ppem, 20) self.assertEqual(t.font.ascent, 16)
self.assertEqual(t.font.glyphs, 4177) self.assertEqual(t.font.descent, 4)
self.assertEqual(t.getsize('A'), (12, 16)) self.assertEqual(t.font.height, 20)
self.assertEqual(t.getsize('AB'), (24, 16)) self.assertEqual(t.font.x_ppem, 20)
self.assertEqual(t.getsize('M'), (12, 16)) self.assertEqual(t.font.y_ppem, 20)
self.assertEqual(t.getsize('y'), (12, 20)) self.assertEqual(t.font.glyphs, 4177)
self.assertEqual(t.getsize('a'), (12, 16)) self.assertEqual(t.getsize('A'), (12, 16))
self.assertEqual(t.getsize('AB'), (24, 16))
self.assertEqual(t.getsize('M'), (12, 16))
self.assertEqual(t.getsize('y'), (12, 20))
self.assertEqual(t.getsize('a'), (12, 16))
except ImportError: @unittest.skipUnless(HAS_RAQM, "Raqm not Available")
class TestImageFont(PillowTestCase): class TestImageFont_RaqmLayout(TestImageFont):
def test_skip(self): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM
self.skipTest("ImportError")
if __name__ == '__main__': if __name__ == '__main__':
unittest.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_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
#define LAYOUT_FALLBACK 0
#define LAYOUT_RAQM 1
typedef struct
{
int index, x_offset, x_advance, y_offset;
unsigned int cluster;
} GlyphInfo;
struct { struct {
int code; int code;
@ -58,6 +70,7 @@ typedef struct {
PyObject_HEAD PyObject_HEAD
FT_Face face; FT_Face face;
unsigned char *font_bytes; unsigned char *font_bytes;
int layout_engine;
} FontObject; } FontObject;
static PyTypeObject Font_Type; static PyTypeObject Font_Type;
@ -91,11 +104,13 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
char* filename = NULL; char* filename = NULL;
int size; int size;
int index = 0; int index = 0;
int layout_engine = 0;
unsigned char* encoding; unsigned char* encoding;
unsigned char* font_bytes; unsigned char* font_bytes;
int font_bytes_size = 0; int font_bytes_size = 0;
static char* kwlist[] = { static char* kwlist[] = {
"filename", "size", "index", "encoding", "font_bytes", NULL "filename", "size", "index", "encoding", "font_bytes",
"layout_engine", NULL
}; };
if (!library) { if (!library) {
@ -106,10 +121,10 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
return NULL; return NULL;
} }
if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#", kwlist, if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#i", kwlist,
Py_FileSystemDefaultEncoding, &filename, Py_FileSystemDefaultEncoding, &filename,
&size, &index, &encoding, &font_bytes, &size, &index, &encoding, &font_bytes,
&font_bytes_size)) { &font_bytes_size, &layout_engine)) {
return NULL; return NULL;
} }
@ -121,6 +136,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
} }
self->face = NULL; self->face = NULL;
self->layout_engine = layout_engine;
if (filename && font_bytes_size <= 0) { if (filename && font_bytes_size <= 0) {
self->font_bytes = NULL; self->font_bytes = NULL;
@ -188,60 +204,288 @@ 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
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; 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 +495,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 +543,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 +566,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 +574,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 +596,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 +638,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 +658,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 +672,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 +833,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

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
# Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz # Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz
archive=$1 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 \ 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

@ -4,8 +4,10 @@
# Installs all of the dependencies for Pillow for Freebsd 10.x # Installs all of the dependencies for Pillow for Freebsd 10.x
# for both system Pythons 2.7 and 3.4 # 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. # 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_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 # 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 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
./install_openjpeg.sh ./install_openjpeg.sh
./install_imagequant.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 most common image formats. See :ref:`external-libraries` for a
full list of external libraries supported. 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`:: Install Pillow with :command:`pip`::
$ pip install Pillow $ pip install Pillow
@ -72,11 +66,15 @@ except OpenJPEG::
Linux Installation Linux Installation
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
We do not provide binaries for Linux. Most major Linux distributions, We provide binaries for Linux for each of the supported Python
including Fedora, Debian/Ubuntu and ArchLinux include Pillow in versions in the manylinux wheel format. These include support for all
packages that previously contained PIL e.g. ``python-imaging``. Please optional libraries except Raqm::
consider using native operating system packages first to avoid
installation problems and/or missing library support later. $ 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 FreeBSD Installation
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
@ -120,7 +118,9 @@ External Libraries
.. note:: .. note::
There are scripts to install the dependencies for some operating 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: 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, * 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.
* Windows support: Raqm support is currently unsupported on Windows.
Once you have installed the prerequisites, run:: Once you have installed the prerequisites, run::
$ pip install Pillow $ pip install Pillow
@ -201,14 +213,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.
@ -247,7 +261,12 @@ 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 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 $ pip install Pillow
@ -277,7 +296,9 @@ Or for Python 3::
Prerequisites are installed on **FreeBSD 10 or 11** with:: 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 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:: Prerequisites are installed on **Ubuntu 14.04 LTS** with::
$ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ $ 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:: 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,28 @@ 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' (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. Draws the string at the given position.
@ -252,8 +271,28 @@ 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' (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. Return the size of the given string, in pixels.
@ -262,11 +301,51 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param spacing: If the text is passed on to multiline_textsize(), :param spacing: If the text is passed on to multiline_textsize(),
the number of pixels between lines. 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. Return the size of the given string, in pixels.
:param text: Text to be measured. :param text: Text to be measured.
: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 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) :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.
@ -65,5 +65,26 @@ Methods
C-level implementations. C-level implementations.
.. versionadded:: 1.1.5 .. 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 :return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module. :py:mod:`PIL.Image.core` interface module.

View File

@ -1,11 +1,26 @@
4.2.0 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 Removed Deprecated Items
======================== ========================
Several deprecated items have been removed. 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", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"), ("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"), ("zlib", "ZLIB (PNG/ZIP)"),
("libtiff", "LIBTIFF") ("libtiff", "LIBTIFF"),
("raqm", "RAQM (Bidirectional Text)")
]: ]:
if features.check(name): if features.check(name):
print("---", feature, "support ok") print("---", feature, "support ok")

View File

@ -119,7 +119,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:
@ -137,7 +137,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'}
@ -522,6 +522,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"):
@ -605,9 +613,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 = []
@ -669,6 +682,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"),