|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
@ -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.
|
BIN
Tests/fonts/NotoNastaliqUrdu-Regular.ttf
Normal file
BIN
Tests/images/test_Nastalifont_text.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Tests/images/test_arabictext_features.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/test_complex_unicode_text.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/test_direction_ltr.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
Tests/images/test_direction_rtl.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Tests/images/test_kerning_features.png
Normal file
After Width: | Height: | Size: 963 B |
BIN
Tests/images/test_ligature_features.png
Normal file
After Width: | Height: | Size: 605 B |
BIN
Tests/images/test_text.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Tests/images/test_y_offset.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
|
@ -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
|
@ -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
|
376
_imagingft.c
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
24
setup.py
|
@ -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"),
|
||||||
|
|