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 ImageDraw rounded_rectangle method #5208

Merged
merged 3 commits into from Mar 8, 2021
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_rounded_rectangle.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_rounded_rectangle_both.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_rounded_rectangle_x.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_rounded_rectangle_y.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions Tests/test_imagedraw.py
Expand Up @@ -706,6 +706,58 @@ def test_rectangle_translucent_outline():
)


@pytest.mark.parametrize(
"xy",
[(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))],
)
def test_rounded_rectangle(xy):
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)

# Act
draw.rounded_rectangle(xy, 30, fill="red", outline="green", width=5)

# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png")


def test_rounded_rectangle_zero_radius():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)

# Act
draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5)

# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png")


@pytest.mark.parametrize(
"xy, suffix",
[
((20, 10, 80, 90), "x"),
((10, 20, 90, 80), "y"),
((20, 20, 80, 80), "both"),
],
)
def test_rounded_rectangle_translucent(xy, suffix):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")

# Act
draw.rounded_rectangle(
xy, 30, fill=(255, 0, 0, 127), outline=(0, 255, 0, 127), width=5
)

# Assert
assert_image_equal_tofile(
im, "Tests/images/imagedraw_rounded_rectangle_" + suffix + ".png"
)


def test_floodfill():
red = ImageColor.getrgb("red")

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/ImageDraw.rst
Expand Up @@ -285,6 +285,20 @@ Methods

.. versionadded:: 5.3.0

.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)

Draws a rounded rectangle.

:param xy: Two points to define the bounding box. Sequence of either
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
is just outside the drawn rectangle.
:param radius: Radius of the corners.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
:param width: The line width, in pixels.

.. versionadded:: 8.2.0

.. py:method:: ImageDraw.shape(shape, fill=None, outline=None)

.. warning:: This method is experimental.
Expand Down
16 changes: 13 additions & 3 deletions docs/releasenotes/8.2.0.rst
Expand Up @@ -13,10 +13,20 @@ when Tk/Tcl 8.5 will be the minimum supported.
API Changes
===========

TODO
^^^^
ImageDraw.rounded_rectangle
^^^^^^^^^^^^^^^^^^^^^^^^^^^

TODO
Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as
:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius``
argument. ``radius`` is limited to half of the width or the height, so that users can
create a circle, but not any other ellipse.

.. code-block:: python

from PIL import Image, ImageDraw
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red")

API Additions
=============
Expand Down
90 changes: 90 additions & 0 deletions src/PIL/ImageDraw.py
Expand Up @@ -257,6 +257,96 @@ def rectangle(self, xy, fill=None, outline=None, width=1):
if ink is not None and ink != fill and width != 0:
self.draw.draw_rectangle(xy, ink, 0, width)

def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1):
"""Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)):
(x0, y0), (x1, y1) = xy
else:
x0, y0, x1, y1 = xy

d = radius * 2

full_x = d >= x1 - x0
if full_x:
# The two left and two right corners are joined
d = x1 - x0
full_y = d >= y1 - y0
if full_y:
# The two top and two bottom corners are joined
d = y1 - y0
if full_x and full_y:
# If all corners are joined, that is a circle
return self.ellipse(xy, fill, outline, width)

if d == 0:
# If the corners have no curve, that is a rectangle
return self.rectangle(xy, fill, outline, width)

ink, fill = self._getink(outline, fill)

def draw_corners(pieslice):
if full_x:
# Draw top and bottom halves
parts = (
((x0, y0, x0 + d, y0 + d), 180, 360),
((x0, y1 - d, x0 + d, y1), 0, 180),
)
elif full_y:
# Draw left and right halves
parts = (
((x0, y0, x0 + d, y0 + d), 90, 270),
((x1 - d, y0, x1, y0 + d), 270, 90),
)
else:
# Draw four separate corners
parts = (
((x1 - d, y0, x1, y0 + d), 270, 360),
((x1 - d, y1 - d, x1, y1), 0, 90),
((x0, y1 - d, x0 + d, y1), 90, 180),
((x0, y0, x0 + d, y0 + d), 180, 270),
)
for part in parts:
if pieslice:
self.draw.draw_pieslice(*(part + (fill, 1)))
else:
self.draw.draw_arc(*(part + (ink, width)))

if fill is not None:
draw_corners(True)

if full_x:
self.draw.draw_rectangle(
(x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1
)
else:
self.draw.draw_rectangle(
(x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1
)
if not full_x and not full_y:
self.draw.draw_rectangle(
(x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1
)
self.draw.draw_rectangle(
(x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1
)
if ink is not None and ink != fill and width != 0:
draw_corners(False)

if not full_x:
self.draw.draw_rectangle(
(x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1
)
self.draw.draw_rectangle(
(x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1
)
if not full_y:
self.draw.draw_rectangle(
(x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1
)
self.draw.draw_rectangle(
(x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1
)

def _multiline_check(self, text):
"""Draw text."""
split_character = "\n" if isinstance(text, str) else b"\n"
Expand Down