Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/imagedraw_stroke_different.png
Loading
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
Loading
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
Loading
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
Loading
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

Comment thread
hugovk marked this conversation as 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
Comment thread
hugovk marked this conversation as 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
Original file line number Diff line number Diff line change
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
Loading