Skip to content

Commit

Permalink
Merge pull request #3978 from radarhere/stroke
Browse files Browse the repository at this point in the history
Added text stroking
  • Loading branch information
radarhere committed Sep 6, 2019
2 parents 397a26b + e790a40 commit da39d40
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 54 deletions.
Binary file added Tests/images/imagedraw_stroke_different.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/imagedraw_stroke_multiline.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/imagedraw_stroke_same.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_direction_ttb_stroke.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 52 additions & 2 deletions Tests/test_imagedraw.py
@@ -1,8 +1,8 @@
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)
WHITE = (255, 255, 255)
Expand All @@ -29,6 +29,8 @@

KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]

HAS_FREETYPE = features.check("freetype2")


class TestImageDraw(PillowTestCase):
def test_sanity(self):
Expand Down Expand Up @@ -801,6 +803,54 @@ def test_textsize_empty_string(self):
draw.textsize("\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):
# Prepare shape
x0, y0 = 5, 5
Expand Down
15 changes: 15 additions & 0 deletions Tests/test_imagefont.py
Expand Up @@ -605,6 +605,21 @@ def test_imagefont_getters(self):
self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 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):
# Arrange
t = self.get_font()
Expand Down
24 changes: 24 additions & 0 deletions Tests/test_imagefontctl.py
Expand Up @@ -115,6 +115,30 @@ def test_text_direction_ttb(self):

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):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)

Expand Down
23 changes: 20 additions & 3 deletions docs/reference/ImageDraw.rst
Expand Up @@ -255,7 +255,7 @@ Methods

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.

Expand Down Expand Up @@ -297,6 +297,15 @@ Methods

.. 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)
Draws the string at the given position.
Expand Down Expand Up @@ -336,7 +345,7 @@ Methods

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

Expand Down Expand Up @@ -372,7 +381,11 @@ Methods

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

Expand Down Expand Up @@ -408,6 +421,10 @@ Methods

.. 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)
.. warning:: This method is experimental.
Expand Down
134 changes: 116 additions & 18 deletions src/PIL/ImageDraw.py
Expand Up @@ -261,24 +261,95 @@ def _multiline_split(self, text):

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):
return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs)
ink, fill = self._getink(fill)
return self.multiline_text(
xy,
text,
fill,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
stroke_fill,
)

if font is None:
font = self.getfont()
if ink is None:
ink = fill
if ink is not None:

def getink(fill):
ink, fill = self._getink(fill)
if ink is None:
return fill
return ink

def draw_text(ink, stroke_width=0, stroke_offset=None):
coord = xy
try:
mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs)
xy = xy[0] + offset[0], xy[1] + offset[1]
mask, offset = font.getmask2(
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:
try:
mask = font.getmask(text, self.fontmode, *args, **kwargs)
mask = font.getmask(
text,
self.fontmode,
direction,
features,
language,
stroke_width,
*args,
**kwargs
)
except TypeError:
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
draw_text(stroke_ink, stroke_width)

# Draw normal text
draw_text(ink, 0, (stroke_width, stroke_width))
else:
# Only draw normal text
draw_text(ink)

def multiline_text(
self,
Expand All @@ -292,14 +363,23 @@ def multiline_text(
direction=None,
features=None,
language=None,
stroke_width=0,
stroke_fill=None,
):
widths = []
max_width = 0
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:
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)
max_width = max(max_width, line_width)
Expand All @@ -322,32 +402,50 @@ def multiline_text(
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
stroke_fill=stroke_fill,
)
top += line_spacing
left = xy[0]

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."""
if self._multiline_check(text):
return self.multiline_textsize(
text, font, spacing, direction, features, language
text, font, spacing, direction, features, language, stroke_width
)

if font is None:
font = self.getfont()
return font.getsize(text, direction, features, language)
return font.getsize(text, direction, features, language, stroke_width)

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
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:
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)
return max_width, len(lines) * line_spacing - spacing
Expand Down

0 comments on commit da39d40

Please sign in to comment.