mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 01:46:18 +03:00
Added text stroking
This commit is contained in:
parent
f3f45cfec5
commit
f93a5d0972
BIN
Tests/images/imagedraw_stroke_different.png
Normal file
BIN
Tests/images/imagedraw_stroke_different.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
Tests/images/imagedraw_stroke_multiline.png
Normal file
BIN
Tests/images/imagedraw_stroke_multiline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
Tests/images/imagedraw_stroke_same.png
Normal file
BIN
Tests/images/imagedraw_stroke_same.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
Tests/images/test_direction_ttb_stroke.png
Normal file
BIN
Tests/images/test_direction_ttb_stroke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
|
@ -1,8 +1,8 @@
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from PIL import Image, ImageColor, ImageDraw
|
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
|
||||||
|
|
||||||
from .helper import PillowTestCase, hopper
|
from .helper import PillowTestCase, hopper, unittest
|
||||||
|
|
||||||
BLACK = (0, 0, 0)
|
BLACK = (0, 0, 0)
|
||||||
WHITE = (255, 255, 255)
|
WHITE = (255, 255, 255)
|
||||||
|
@ -29,6 +29,8 @@ POINTS2 = [10, 10, 20, 40, 30, 30]
|
||||||
|
|
||||||
KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
|
KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
|
||||||
|
|
||||||
|
HAS_FREETYPE = features.check("freetype2")
|
||||||
|
|
||||||
|
|
||||||
class TestImageDraw(PillowTestCase):
|
class TestImageDraw(PillowTestCase):
|
||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
|
@ -771,6 +773,54 @@ class TestImageDraw(PillowTestCase):
|
||||||
draw.textsize("\n")
|
draw.textsize("\n")
|
||||||
draw.textsize("test\n")
|
draw.textsize("test\n")
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
|
||||||
|
def test_textsize_stroke(self):
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("RGB", (W, H))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
self.assertEqual(draw.textsize("A", font, stroke_width=2), (16, 20))
|
||||||
|
self.assertEqual(
|
||||||
|
draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2), (52, 44)
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
|
||||||
|
def test_stroke(self):
|
||||||
|
for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items():
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("RGB", (120, 130))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
draw.text(
|
||||||
|
(10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assert_image_similar(
|
||||||
|
im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 2.8
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
|
||||||
|
def test_stroke_multiline(self):
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("RGB", (100, 250))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
draw.multiline_text(
|
||||||
|
(10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assert_image_similar(
|
||||||
|
im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3
|
||||||
|
)
|
||||||
|
|
||||||
def test_same_color_outline(self):
|
def test_same_color_outline(self):
|
||||||
# Prepare shape
|
# Prepare shape
|
||||||
x0, y0 = 5, 5
|
x0, y0 = 5, 5
|
||||||
|
|
|
@ -605,6 +605,21 @@ class TestImageFont(PillowTestCase):
|
||||||
self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36))
|
self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36))
|
||||||
self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36))
|
self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36))
|
||||||
|
|
||||||
|
def test_getsize_stroke(self):
|
||||||
|
# Arrange
|
||||||
|
t = self.get_font()
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
for stroke_width in [0, 2]:
|
||||||
|
self.assertEqual(
|
||||||
|
t.getsize("A", stroke_width=stroke_width),
|
||||||
|
(12 + stroke_width * 2, 16 + stroke_width * 2),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width),
|
||||||
|
(48 + stroke_width * 2, 36 + stroke_width * 4),
|
||||||
|
)
|
||||||
|
|
||||||
def test_complex_font_settings(self):
|
def test_complex_font_settings(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
t = self.get_font()
|
t = self.get_font()
|
||||||
|
|
|
@ -115,6 +115,30 @@ class TestImagecomplextext(PillowTestCase):
|
||||||
|
|
||||||
self.assert_image_similar(im, target_img, 1.15)
|
self.assert_image_similar(im, target_img, 1.15)
|
||||||
|
|
||||||
|
def test_text_direction_ttb_stroke(self):
|
||||||
|
ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50)
|
||||||
|
|
||||||
|
im = Image.new(mode="RGB", size=(100, 300))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
try:
|
||||||
|
draw.text(
|
||||||
|
(25, 25),
|
||||||
|
"あい",
|
||||||
|
font=ttf,
|
||||||
|
fill=500,
|
||||||
|
direction="ttb",
|
||||||
|
stroke_width=2,
|
||||||
|
stroke_fill="#0f0",
|
||||||
|
)
|
||||||
|
except ValueError as ex:
|
||||||
|
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
||||||
|
self.skipTest("libraqm 0.7 or greater not available")
|
||||||
|
|
||||||
|
target = "Tests/images/test_direction_ttb_stroke.png"
|
||||||
|
target_img = Image.open(target)
|
||||||
|
|
||||||
|
self.assert_image_similar(im, target_img, 12.4)
|
||||||
|
|
||||||
def test_ligature_features(self):
|
def test_ligature_features(self):
|
||||||
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,7 @@ Methods
|
||||||
|
|
||||||
Draw a shape.
|
Draw a shape.
|
||||||
|
|
||||||
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
|
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None)
|
||||||
|
|
||||||
Draws the string at the given position.
|
Draws the string at the given position.
|
||||||
|
|
||||||
|
@ -297,6 +297,15 @@ Methods
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
|
:param stroke_fill: Color to use for the text stroke. If not given, will default to
|
||||||
|
the ``fill`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
|
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
|
||||||
|
|
||||||
Draws the string at the given position.
|
Draws the string at the given position.
|
||||||
|
@ -336,7 +345,7 @@ Methods
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
|
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
|
||||||
|
|
||||||
Return the size of the given string, in pixels.
|
Return the size of the given string, in pixels.
|
||||||
|
|
||||||
|
@ -372,7 +381,11 @@ Methods
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
|
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
|
||||||
|
|
||||||
Return the size of the given string, in pixels.
|
Return the size of the given string, in pixels.
|
||||||
|
|
||||||
|
@ -408,6 +421,10 @@ Methods
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
|
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
|
||||||
|
|
||||||
.. warning:: This method is experimental.
|
.. warning:: This method is experimental.
|
||||||
|
|
|
@ -261,24 +261,95 @@ class ImageDraw(object):
|
||||||
|
|
||||||
return text.split(split_character)
|
return text.split(split_character)
|
||||||
|
|
||||||
def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs):
|
def text(
|
||||||
|
self,
|
||||||
|
xy,
|
||||||
|
text,
|
||||||
|
fill=None,
|
||||||
|
font=None,
|
||||||
|
anchor=None,
|
||||||
|
spacing=4,
|
||||||
|
align="left",
|
||||||
|
direction=None,
|
||||||
|
features=None,
|
||||||
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
|
stroke_fill=None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
if self._multiline_check(text):
|
if self._multiline_check(text):
|
||||||
return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs)
|
return self.multiline_text(
|
||||||
ink, fill = self._getink(fill)
|
xy,
|
||||||
|
text,
|
||||||
|
fill,
|
||||||
|
font,
|
||||||
|
anchor,
|
||||||
|
spacing,
|
||||||
|
align,
|
||||||
|
direction,
|
||||||
|
features,
|
||||||
|
language,
|
||||||
|
stroke_width,
|
||||||
|
stroke_fill,
|
||||||
|
)
|
||||||
|
|
||||||
if font is None:
|
if font is None:
|
||||||
font = self.getfont()
|
font = self.getfont()
|
||||||
if ink is None:
|
|
||||||
ink = fill
|
def getink(fill):
|
||||||
if ink is not None:
|
ink, fill = self._getink(fill)
|
||||||
|
if ink is None:
|
||||||
|
return fill
|
||||||
|
return ink
|
||||||
|
|
||||||
|
def drawText(ink, stroke_width=0, stroke_offset=None):
|
||||||
|
coord = xy
|
||||||
try:
|
try:
|
||||||
mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs)
|
mask, offset = font.getmask2(
|
||||||
xy = xy[0] + offset[0], xy[1] + offset[1]
|
text,
|
||||||
|
self.fontmode,
|
||||||
|
direction=direction,
|
||||||
|
features=features,
|
||||||
|
language=language,
|
||||||
|
stroke_width=stroke_width,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
coord = coord[0] + offset[0], coord[1] + offset[1]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
try:
|
try:
|
||||||
mask = font.getmask(text, self.fontmode, *args, **kwargs)
|
mask = font.getmask(
|
||||||
|
text,
|
||||||
|
self.fontmode,
|
||||||
|
direction,
|
||||||
|
features,
|
||||||
|
language,
|
||||||
|
stroke_width,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
mask = font.getmask(text)
|
mask = font.getmask(text)
|
||||||
self.draw.draw_bitmap(xy, mask, ink)
|
if stroke_offset:
|
||||||
|
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
|
||||||
|
self.draw.draw_bitmap(coord, mask, ink)
|
||||||
|
|
||||||
|
ink = getink(fill)
|
||||||
|
if ink is not None:
|
||||||
|
stroke_ink = None
|
||||||
|
if stroke_width:
|
||||||
|
stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
|
||||||
|
|
||||||
|
if stroke_ink is not None:
|
||||||
|
# Draw stroked text
|
||||||
|
drawText(stroke_ink, stroke_width)
|
||||||
|
|
||||||
|
# Draw normal text
|
||||||
|
drawText(ink, 0, (stroke_width, stroke_width))
|
||||||
|
else:
|
||||||
|
# Only draw normal text
|
||||||
|
drawText(ink)
|
||||||
|
|
||||||
def multiline_text(
|
def multiline_text(
|
||||||
self,
|
self,
|
||||||
|
@ -292,14 +363,23 @@ class ImageDraw(object):
|
||||||
direction=None,
|
direction=None,
|
||||||
features=None,
|
features=None,
|
||||||
language=None,
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
|
stroke_fill=None,
|
||||||
):
|
):
|
||||||
widths = []
|
widths = []
|
||||||
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, stroke_width=stroke_width)[1] + spacing
|
||||||
|
)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line_width, line_height = self.textsize(
|
line_width, line_height = self.textsize(
|
||||||
line, font, direction=direction, features=features, language=language
|
line,
|
||||||
|
font,
|
||||||
|
direction=direction,
|
||||||
|
features=features,
|
||||||
|
language=language,
|
||||||
|
stroke_width=stroke_width,
|
||||||
)
|
)
|
||||||
widths.append(line_width)
|
widths.append(line_width)
|
||||||
max_width = max(max_width, line_width)
|
max_width = max(max_width, line_width)
|
||||||
|
@ -322,32 +402,50 @@ class ImageDraw(object):
|
||||||
direction=direction,
|
direction=direction,
|
||||||
features=features,
|
features=features,
|
||||||
language=language,
|
language=language,
|
||||||
|
stroke_width=stroke_width,
|
||||||
|
stroke_fill=stroke_fill,
|
||||||
)
|
)
|
||||||
top += line_spacing
|
top += line_spacing
|
||||||
left = xy[0]
|
left = xy[0]
|
||||||
|
|
||||||
def textsize(
|
def textsize(
|
||||||
self, text, font=None, spacing=4, direction=None, features=None, language=None
|
self,
|
||||||
|
text,
|
||||||
|
font=None,
|
||||||
|
spacing=4,
|
||||||
|
direction=None,
|
||||||
|
features=None,
|
||||||
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
):
|
):
|
||||||
"""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(
|
return self.multiline_textsize(
|
||||||
text, font, spacing, direction, features, language
|
text, font, spacing, direction, features, language, stroke_width
|
||||||
)
|
)
|
||||||
|
|
||||||
if font is None:
|
if font is None:
|
||||||
font = self.getfont()
|
font = self.getfont()
|
||||||
return font.getsize(text, direction, features, language)
|
return font.getsize(text, direction, features, language, stroke_width)
|
||||||
|
|
||||||
def multiline_textsize(
|
def multiline_textsize(
|
||||||
self, text, font=None, spacing=4, direction=None, features=None, language=None
|
self,
|
||||||
|
text,
|
||||||
|
font=None,
|
||||||
|
spacing=4,
|
||||||
|
direction=None,
|
||||||
|
features=None,
|
||||||
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
):
|
):
|
||||||
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, stroke_width=stroke_width)[1] + spacing
|
||||||
|
)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line_width, line_height = self.textsize(
|
line_width, line_height = self.textsize(
|
||||||
line, font, spacing, direction, features, language
|
line, font, spacing, direction, features, language, stroke_width
|
||||||
)
|
)
|
||||||
max_width = max(max_width, line_width)
|
max_width = max(max_width, line_width)
|
||||||
return max_width, len(lines) * line_spacing - spacing
|
return max_width, len(lines) * line_spacing - spacing
|
||||||
|
|
|
@ -207,7 +207,9 @@ class FreeTypeFont(object):
|
||||||
"""
|
"""
|
||||||
return self.font.ascent, self.font.descent
|
return self.font.ascent, self.font.descent
|
||||||
|
|
||||||
def getsize(self, text, direction=None, features=None, language=None):
|
def getsize(
|
||||||
|
self, text, direction=None, features=None, language=None, stroke_width=0
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Returns width and height (in pixels) of given text if rendered in font with
|
Returns width and height (in pixels) of given text if rendered in font with
|
||||||
provided direction, features, and language.
|
provided direction, features, and language.
|
||||||
|
@ -243,13 +245,26 @@ class FreeTypeFont(object):
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
:return: (width, height)
|
:return: (width, height)
|
||||||
"""
|
"""
|
||||||
size, offset = self.font.getsize(text, direction, features, language)
|
size, offset = self.font.getsize(text, direction, features, language)
|
||||||
return (size[0] + offset[0], size[1] + offset[1])
|
return (
|
||||||
|
size[0] + stroke_width * 2 + offset[0],
|
||||||
|
size[1] + stroke_width * 2 + offset[1],
|
||||||
|
)
|
||||||
|
|
||||||
def getsize_multiline(
|
def getsize_multiline(
|
||||||
self, text, direction=None, spacing=4, features=None, language=None
|
self,
|
||||||
|
text,
|
||||||
|
direction=None,
|
||||||
|
spacing=4,
|
||||||
|
features=None,
|
||||||
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Returns width and height (in pixels) of given text if rendered in font
|
Returns width and height (in pixels) of given text if rendered in font
|
||||||
|
@ -285,13 +300,19 @@ class FreeTypeFont(object):
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
:return: (width, height)
|
:return: (width, height)
|
||||||
"""
|
"""
|
||||||
max_width = 0
|
max_width = 0
|
||||||
lines = self._multiline_split(text)
|
lines = self._multiline_split(text)
|
||||||
line_spacing = self.getsize("A")[1] + spacing
|
line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line_width, line_height = self.getsize(line, direction, features, language)
|
line_width, line_height = self.getsize(
|
||||||
|
line, direction, features, language, stroke_width
|
||||||
|
)
|
||||||
max_width = max(max_width, line_width)
|
max_width = max(max_width, line_width)
|
||||||
|
|
||||||
return max_width, len(lines) * line_spacing - spacing
|
return max_width, len(lines) * line_spacing - spacing
|
||||||
|
@ -308,7 +329,15 @@ class FreeTypeFont(object):
|
||||||
"""
|
"""
|
||||||
return self.font.getsize(text)[1]
|
return self.font.getsize(text)[1]
|
||||||
|
|
||||||
def getmask(self, text, mode="", direction=None, features=None, language=None):
|
def getmask(
|
||||||
|
self,
|
||||||
|
text,
|
||||||
|
mode="",
|
||||||
|
direction=None,
|
||||||
|
features=None,
|
||||||
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Create a bitmap for the text.
|
Create a bitmap for the text.
|
||||||
|
|
||||||
|
@ -352,11 +381,20 @@ class FreeTypeFont(object):
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.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.
|
||||||
"""
|
"""
|
||||||
return self.getmask2(
|
return self.getmask2(
|
||||||
text, mode, direction=direction, features=features, language=language
|
text,
|
||||||
|
mode,
|
||||||
|
direction=direction,
|
||||||
|
features=features,
|
||||||
|
language=language,
|
||||||
|
stroke_width=stroke_width,
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
def getmask2(
|
def getmask2(
|
||||||
|
@ -367,6 +405,7 @@ class FreeTypeFont(object):
|
||||||
direction=None,
|
direction=None,
|
||||||
features=None,
|
features=None,
|
||||||
language=None,
|
language=None,
|
||||||
|
stroke_width=0,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
|
@ -413,13 +452,20 @@ class FreeTypeFont(object):
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
|
||||||
|
:param stroke_width: The width of the text stroke.
|
||||||
|
|
||||||
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
:return: A tuple of an internal PIL storage memory instance as defined by the
|
:return: A tuple of an internal PIL storage memory instance as defined by the
|
||||||
:py:mod:`PIL.Image.core` interface module, and the text offset, the
|
:py:mod:`PIL.Image.core` interface module, and the text offset, the
|
||||||
gap between the starting coordinate and the first marking
|
gap between the starting coordinate and the first marking
|
||||||
"""
|
"""
|
||||||
size, offset = self.font.getsize(text, direction, features, language)
|
size, offset = self.font.getsize(text, direction, features, language)
|
||||||
|
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
|
||||||
im = fill("L", size, 0)
|
im = fill("L", size, 0)
|
||||||
self.font.render(text, im.id, mode == "1", direction, features, language)
|
self.font.render(
|
||||||
|
text, im.id, mode == "1", direction, features, language, stroke_width
|
||||||
|
)
|
||||||
return im, offset
|
return im, offset
|
||||||
|
|
||||||
def font_variant(
|
def font_variant(
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include <ft2build.h>
|
#include <ft2build.h>
|
||||||
#include FT_FREETYPE_H
|
#include FT_FREETYPE_H
|
||||||
#include FT_GLYPH_H
|
#include FT_GLYPH_H
|
||||||
|
#include FT_STROKER_H
|
||||||
#include FT_MULTIPLE_MASTERS_H
|
#include FT_MULTIPLE_MASTERS_H
|
||||||
#include FT_SFNT_NAMES_H
|
#include FT_SFNT_NAMES_H
|
||||||
|
|
||||||
|
@ -790,7 +791,13 @@ font_render(FontObject* self, PyObject* args)
|
||||||
int index, error, ascender, horizontal_dir;
|
int index, error, ascender, horizontal_dir;
|
||||||
int load_flags;
|
int load_flags;
|
||||||
unsigned char *source;
|
unsigned char *source;
|
||||||
FT_GlyphSlot glyph;
|
FT_Glyph glyph;
|
||||||
|
FT_GlyphSlot glyph_slot;
|
||||||
|
FT_Bitmap bitmap;
|
||||||
|
FT_BitmapGlyph bitmap_glyph;
|
||||||
|
int stroke_width = 0;
|
||||||
|
FT_Stroker stroker = NULL;
|
||||||
|
FT_Int left;
|
||||||
/* 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;
|
||||||
|
@ -806,7 +813,8 @@ font_render(FontObject* self, PyObject* args)
|
||||||
GlyphInfo *glyph_info;
|
GlyphInfo *glyph_info;
|
||||||
PyObject *features = NULL;
|
PyObject *features = NULL;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) {
|
if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang,
|
||||||
|
&stroke_width)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -819,21 +827,37 @@ font_render(FontObject* self, PyObject* args)
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stroke_width) {
|
||||||
|
error = FT_Stroker_New(library, &stroker);
|
||||||
|
if (error) {
|
||||||
|
return geterror(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Stroker_Set(stroker, (FT_Fixed)stroke_width*64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
|
||||||
|
}
|
||||||
|
|
||||||
im = (Imaging) id;
|
im = (Imaging) id;
|
||||||
/* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */
|
/* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */
|
||||||
load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP;
|
load_flags = FT_LOAD_NO_BITMAP;
|
||||||
if (mask)
|
if (stroker == NULL) {
|
||||||
|
load_flags |= FT_LOAD_RENDER;
|
||||||
|
}
|
||||||
|
if (mask) {
|
||||||
load_flags |= FT_LOAD_TARGET_MONO;
|
load_flags |= FT_LOAD_TARGET_MONO;
|
||||||
|
}
|
||||||
|
|
||||||
ascender = 0;
|
ascender = 0;
|
||||||
for (i = 0; i < count; i++) {
|
for (i = 0; i < count; i++) {
|
||||||
index = glyph_info[i].index;
|
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_slot = self->face->glyph;
|
||||||
temp = glyph->bitmap.rows - glyph->bitmap_top;
|
bitmap = glyph_slot->bitmap;
|
||||||
|
|
||||||
|
temp = bitmap.rows - glyph_slot->bitmap_top;
|
||||||
temp -= PIXEL(glyph_info[i].y_offset);
|
temp -= PIXEL(glyph_info[i].y_offset);
|
||||||
if (temp > ascender)
|
if (temp > ascender)
|
||||||
ascender = temp;
|
ascender = temp;
|
||||||
|
@ -844,37 +868,62 @@ font_render(FontObject* self, PyObject* args)
|
||||||
for (i = 0; i < count; i++) {
|
for (i = 0; i < count; i++) {
|
||||||
index = glyph_info[i].index;
|
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_slot = self->face->glyph;
|
||||||
if (horizontal_dir) {
|
if (stroker != NULL) {
|
||||||
if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) {
|
error = FT_Get_Glyph(glyph_slot, &glyph);
|
||||||
x = -self->face->glyph->metrics.horiBearingX;
|
if (!error) {
|
||||||
|
error = FT_Glyph_Stroke(&glyph, stroker, 1);
|
||||||
}
|
}
|
||||||
xx = PIXEL(x) + glyph->bitmap_left;
|
if (!error) {
|
||||||
xx += PIXEL(glyph_info[i].x_offset);
|
FT_Vector origin = {0, 0};
|
||||||
|
error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return geterror(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap_glyph = (FT_BitmapGlyph)glyph;
|
||||||
|
|
||||||
|
bitmap = bitmap_glyph->bitmap;
|
||||||
|
left = bitmap_glyph->left;
|
||||||
|
|
||||||
|
FT_Done_Glyph(glyph);
|
||||||
} else {
|
} else {
|
||||||
if (self->face->glyph->metrics.vertBearingX < 0) {
|
bitmap = glyph_slot->bitmap;
|
||||||
x = -self->face->glyph->metrics.vertBearingX;
|
left = glyph_slot->bitmap_left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (horizontal_dir) {
|
||||||
|
if (i == 0 && glyph_slot->metrics.horiBearingX < 0) {
|
||||||
|
x = -glyph_slot->metrics.horiBearingX;
|
||||||
}
|
}
|
||||||
xx = im->xsize / 2 - glyph->bitmap.width / 2;
|
xx = PIXEL(x) + left;
|
||||||
|
xx += PIXEL(glyph_info[i].x_offset) + stroke_width;
|
||||||
|
} else {
|
||||||
|
if (glyph_slot->metrics.vertBearingX < 0) {
|
||||||
|
x = -glyph_slot->metrics.vertBearingX;
|
||||||
|
}
|
||||||
|
xx = im->xsize / 2 - bitmap.width / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
x0 = 0;
|
x0 = 0;
|
||||||
x1 = glyph->bitmap.width;
|
x1 = bitmap.width;
|
||||||
if (xx < 0)
|
if (xx < 0)
|
||||||
x0 = -xx;
|
x0 = -xx;
|
||||||
if (xx + x1 > im->xsize)
|
if (xx + x1 > im->xsize)
|
||||||
x1 = im->xsize - xx;
|
x1 = im->xsize - xx;
|
||||||
|
|
||||||
source = (unsigned char*) glyph->bitmap.buffer;
|
source = (unsigned char*) bitmap.buffer;
|
||||||
for (bitmap_y = 0; bitmap_y < glyph->bitmap.rows; bitmap_y++) {
|
for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) {
|
||||||
if (horizontal_dir) {
|
if (horizontal_dir) {
|
||||||
yy = bitmap_y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
|
yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender);
|
||||||
yy -= PIXEL(glyph_info[i].y_offset);
|
yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2;
|
||||||
} else {
|
} else {
|
||||||
yy = bitmap_y + PIXEL(y + glyph->metrics.vertBearingY) + ascender;
|
yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender;
|
||||||
yy += PIXEL(glyph_info[i].y_offset);
|
yy += PIXEL(glyph_info[i].y_offset);
|
||||||
}
|
}
|
||||||
if (yy >= 0 && yy < im->ysize) {
|
if (yy >= 0 && yy < im->ysize) {
|
||||||
|
@ -900,12 +949,13 @@ font_render(FontObject* self, PyObject* args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
source += glyph->bitmap.pitch;
|
source += bitmap.pitch;
|
||||||
}
|
}
|
||||||
x += glyph_info[i].x_advance;
|
x += glyph_info[i].x_advance;
|
||||||
y -= glyph_info[i].y_advance;
|
y -= glyph_info[i].y_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FT_Stroker_Done(stroker);
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user