Repository: python-pillow/Pillow

Issue:
Function for Rounded Rectangle
### Feature Request
I got a feature request for the `ImageDraw` module. 
For more modern design, it's very useful to create a rectangle with rounded corners. Currently, it requires a higher effort to create such a rounded rectangle.

My Code:
```py
def create_rounded_rectangle_mask(size, radius, alpha=255):
    factor = 5  # Factor to increase the image size that I can later antialiaze the corners
    radius = radius * factor
    image = Image.new('RGBA', (size[0] * factor, size[1] * factor), (0, 0, 0, 0))

    # create corner
    corner = Image.new('RGBA', (radius, radius), (0, 0, 0, 0))
    draw = ImageDraw.Draw(corner)
    # added the fill = .. you only drew a line, no fill
    draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill=(50, 50, 50, alpha + 55))

    # max_x, max_y
    mx, my = (size[0] * factor, size[1] * factor)

    # paste corner rotated as needed
    # use corners alpha channel as mask
    image.paste(corner, (0, 0), corner)
    image.paste(corner.rotate(90), (0, my - radius), corner.rotate(90))
    image.paste(corner.rotate(180), (mx - radius, my - radius), corner.rotate(180))
    image.paste(corner.rotate(270), (mx - radius, 0), corner.rotate(270))

    # draw both inner rects
    draw = ImageDraw.Draw(image)
    draw.rectangle([(radius, 0), (mx - radius, my)], fill=(50, 50, 50, alpha))
    draw.rectangle([(0, radius), (mx, my - radius)], fill=(50, 50, 50, alpha))
    image = image.resize(size, Image.ANTIALIAS)  # Smooth the corners

    return image
```
This mask will be used to paste with a normal rectangle.


Discussion:
So to be clear, you can already achieve the desired outcome with Pillow, you're just suggesting that this code become a part of Pillow?
Yeah I think there will be several ppl. which might use such function. I also found those functions in other image libraries (other languages) so might be worth considering to implement. My implementation also won't be the best, so you or other ppl. find ways to improve it.
I don't understand what you were trying to achieve with the `alpha` argument.
Using your code, 255 is normal.
```python
create_rounded_rectangle_mask((200, 100), 30, 255).show()
```
![255](https://user-images.githubusercontent.com/3112309/104695586-1cb4fe80-5761-11eb-9500-147106e0a17a.png)

But 127 and 0 just don't seem like they would be helpful.
```python
create_rounded_rectangle_mask((200, 100), 30, 127).show()
```
![127](https://user-images.githubusercontent.com/3112309/104695623-28082a00-5761-11eb-952d-a713ae2b1494.png)
```python
create_rounded_rectangle_mask((200, 100), 30, 0).show()
```
![0](https://user-images.githubusercontent.com/3112309/104695626-29d1ed80-5761-11eb-85e1-e13da3193819.png)
I've created PR #5208. See what you think
> I don't understand what you were trying to achieve with the `alpha` argument.
> Using your code, 255 is normal.

@radarhere Am not fully aware of how to use this value right. I encountered an issue if I create a rounded rectangle with alpha lower than 255. 
Eg. I created a translucent bar background with Alpha set to 30:
![DiscordPTB_kGBwbpMnIK](https://user-images.githubusercontent.com/24532065/104728497-3f322200-5737-11eb-9c8b-f893852dd63a.png)

This happens when you not adjusting alpha: 
![DiscordPTB_i9mppapcT4](https://user-images.githubusercontent.com/24532065/104728814-c2ec0e80-5737-11eb-9f99-14d3ca0bb3c3.png)

My version of just using the same alpha as the image is imperfect too. If you zoom in, you'll see that the corners got a different alpha.




Ok, sure. Using my PR, you should be able to just set `fill` to a color with an alpha value.
```python
from PIL import Image, ImageDraw
im = Image.new("RGB", (500, 100), (0, 0, 100))
draw = ImageDraw.Draw(im, "RGBA")
draw.rounded_rectangle((20, 20, 480, 80), 30, fill=(255,255,255,50))
draw.rounded_rectangle((20, 20, 280, 80), 30, fill=(255,0,0,100))
im.show()
```
![out](https://user-images.githubusercontent.com/3112309/104810029-22d0db00-5846-11eb-83e2-098ed96758b0.png)
@radarhere Sounds great! Looking forward to use this function in a future version. 
But I think it might be helpful to either implement corner smoothing or provide help how to do it in docs.
Your idea of using antialiasing to create a smoother result doesn't purely apply to rounded rectangles - it might equally be applied to other shapes.

While I acknowledge that it might be useful, I'm reluctant to officially endorse it as a strategy - if there is a need for it, my personal feeling is that it is more of a workaround than a proper solution. Feel free to create a PR yourself, or if you'd like to continue talking about smoothing, would you be able to open a new issue? It would be nice to keep this issue specifically about rounded rectangles.

Resolving patch:
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -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"


Test patch:
diff --git a/Tests/images/imagedraw_rounded_rectangle.png b/Tests/images/imagedraw_rounded_rectangle.png
new file mode 100644
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_both.png b/Tests/images/imagedraw_rounded_rectangle_both.png
new file mode 100644
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_both.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_x.png b/Tests/images/imagedraw_rounded_rectangle_x.png
new file mode 100644
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_y.png b/Tests/images/imagedraw_rounded_rectangle_y.png
new file mode 100644
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y.png differ
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -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")