Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added text stroking #3978

Merged
merged 2 commits into from Sep 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -771,6 +773,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

hugovk marked this conversation as resolved.
Show resolved Hide resolved
.. 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
hugovk marked this conversation as resolved.
Show resolved Hide resolved

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