Python Imaging Library (PIL) Drawing--Rounded rectangle with gradient
Asked Answered
A

5

9

I am trying to use PIL to draw a rectangle with rounded corners and a gradient fill for the color. I found a cool web site ( http://web.archive.org/web/20130306020911/http://nadiana.com/pil-tutorial-basic-advanced-drawing#Drawing_Rounded_Corners_Rectangle ) that shows how to draw a solid-color rounded rectangle and I am happy with this, but I want to be able to draw one that starts light red at the top and goes to dark red at the bottom.

My initial thought was to use the code in the website above to draw a rounded rectangle, and then overlay a second white to black rectangle over the rounded rectangle, using alpha blending. Everything that I've tried ends up blowing up in my face.

I have seen some near-miss solutions using numpy, but I am not skilled enough to commute those code fragments to a successful solution. I would be grateful if someone could show how to modify the code in the link above, implement my overlay idea, or show a completely better solution for getting a rounded rectangle with gradient fill in Python.

Cheers, Ferris

Angary answered 16/10, 2011 at 21:13 Comment(1)
pillow.readthedocs.io/en/stable/reference/…Cayenne
S
14

This is a very brute force method, but it gets the job done. Code to produce the gradients was borrowed from here.

from PIL import Image, ImageDraw

def channel(i, c, size, startFill, stopFill):
    """calculate the value of a single color channel for a single pixel"""
    return startFill[c] + int((i * 1.0 / size) * (stopFill[c] - startFill[c]))

def color(i, size, startFill, stopFill):
    """calculate the RGB value of a single pixel"""
    return tuple([channel(i, c, size, startFill, stopFill) for c in range(3)])

def round_corner(radius):
    """Draw a round corner"""
    corner = Image.new('RGBA', (radius, radius), (0, 0, 0, 0))
    draw = ImageDraw.Draw(corner)
    draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill="blue")
    return corner

def apply_grad_to_corner(corner, gradient, backwards = False, topBottom = False):
    width, height = corner.size
    widthIter = range(width)

    if backwards:
        widthIter.reverse()

    for i in xrange(height):
        gradPos = 0
    for j in widthIter:
                if topBottom:
                    pos = (i,j)
                else:
                    pos = (j,i)
        pix = corner.getpixel(pos)
            gradPos+=1
        if pix[3] != 0:
            corner.putpixel(pos,gradient[gradPos])

    return corner

def round_rectangle(size, radius, startFill, stopFill, runTopBottom = False):
    """Draw a rounded rectangle"""
    width, height = size
    rectangle = Image.new('RGBA', size)

    if runTopBottom:
      si = height
    else:
      si = width

    gradient = [ color(i, width, startFill, stopFill) for i in xrange(si) ]

    if runTopBottom:
        modGrad = []
        for i in xrange(height):
           modGrad += [gradient[i]] * width
        rectangle.putdata(modGrad)
    else:
        rectangle.putdata(gradient*height)

    origCorner = round_corner(radius)

    # upper left
    corner = origCorner
    apply_grad_to_corner(corner,gradient,False,runTopBottom)
    rectangle.paste(corner, (0, 0))

    # lower left
    if runTopBottom: 
        gradient.reverse()
        backwards = True
    else:
        backwards = False


    corner = origCorner.rotate(90)
    apply_grad_to_corner(corner,gradient,backwards,runTopBottom)
    rectangle.paste(corner, (0, height - radius))

    # lower right
    if not runTopBottom: 
        gradient.reverse()

    corner = origCorner.rotate(180)
    apply_grad_to_corner(corner,gradient,True,runTopBottom)
    rectangle.paste(corner, (width - radius, height - radius))

    # upper right
    if runTopBottom: 
        gradient.reverse()
        backwards = False
    else:
        backwards = True

    corner = origCorner.rotate(270)
    apply_grad_to_corner(corner,gradient,backwards,runTopBottom)
    rectangle.paste(corner, (width - radius, 0))

    return rectangle

img = round_rectangle((200, 200), 70, (255,0,0), (0,255,0), True)
img.save("test.png", 'PNG')

Running from left to right (runTopBottom = False):

enter image description here

Running from top to bottom (runTopBottom = True):

enter image description here

Swan answered 17/10, 2011 at 0:25 Comment(3)
Brute force is good enough for my amateur statistics experiment. thanks.Angary
Am I using this wrong or something, when I paste the image onto another the transparent corners turn black, and the inner corners turn the startFill colorLaaland
@Laaland when you paste Set the mask to the image you are pasting. paste(im, box=None, mask=None) <== set the mask to the image pillow.readthedocs.io/en/stable/reference/…Subrogate
O
12

In case someone in the future is looking for a slightly more turn-key solution that can be monkey patched onto ImageDraw, I wrote the following.

Hopefully it helps.

Example: enter image description here Code:

from PIL.ImageDraw import ImageDraw


def rounded_rectangle(self: ImageDraw, xy, corner_radius, fill=None, outline=None):
    upper_left_point = xy[0]
    bottom_right_point = xy[1]
    self.rectangle(
        [
            (upper_left_point[0], upper_left_point[1] + corner_radius),
            (bottom_right_point[0], bottom_right_point[1] - corner_radius)
        ],
        fill=fill,
        outline=outline
    )
    self.rectangle(
        [
            (upper_left_point[0] + corner_radius, upper_left_point[1]),
            (bottom_right_point[0] - corner_radius, bottom_right_point[1])
        ],
        fill=fill,
        outline=outline
    )
    self.pieslice([upper_left_point, (upper_left_point[0] + corner_radius * 2, upper_left_point[1] + corner_radius * 2)],
        180,
        270,
        fill=fill,
        outline=outline
    )
    self.pieslice([(bottom_right_point[0] - corner_radius * 2, bottom_right_point[1] - corner_radius * 2), bottom_right_point],
        0,
        90,
        fill=fill,
        outline=outline
    )
    self.pieslice([(upper_left_point[0], bottom_right_point[1] - corner_radius * 2), (upper_left_point[0] + corner_radius * 2, bottom_right_point[1])],
        90,
        180,
        fill=fill,
        outline=outline
    )
    self.pieslice([(bottom_right_point[0] - corner_radius * 2, upper_left_point[1]), (bottom_right_point[0], upper_left_point[1] + corner_radius * 2)],
        270,
        360,
        fill=fill,
        outline=outline
    )


ImageDraw.rounded_rectangle = rounded_rectangle
Octameter answered 2/5, 2018 at 23:45 Comment(2)
Hey, how would I then use this?Its exactly what I needBoyse
` # An example: from PIL import Image, ImageDraw, ImageFont new_image: Image = Image.new("RGBA", size, (255, 255, 255, 0)) d: ImageDraw = ImageDraw.Draw(new_image) d.rounded_rectangle(xy, corner_radius)`Octameter
C
9

Rounded rect is now officially provided in Pillow 8.2.0, rounded_rectangle https://github.com/python-pillow/Pillow/pull/5208

from PIL import Image, ImageDraw
result = Image.new('RGBA', (100, 100))
draw = ImageDraw.Draw(result)
draw.rounded_rectangle(((0, 0), (100, 100)), 20, fill="blue")
result.show()

blue rectangle with rounded corners

If smooth one is needed however have a look at https://github.com/python-pillow/Pillow/issues/4765

Commissure answered 7/6, 2021 at 12:30 Comment(0)
C
3

For anyone looking for an updated version, this is a modified version of Whelchel's answer using Pillow 7.2.0 instead of PIL. (I had a problem with outlines using the previous version)

Code:

def rounded_rectangle(self: ImageDraw, xy, corner_radius, fill=None, outline=None):
    upper_left_point = xy[0]
    bottom_right_point = xy[1]


    self.pieslice([upper_left_point, (upper_left_point[0] + corner_radius * 2, upper_left_point[1] + corner_radius * 2)],
        180,
        270,
        fill=fill,
        outline=outline
    )
    self.pieslice([(bottom_right_point[0] - corner_radius * 2, bottom_right_point[1] - corner_radius * 2), bottom_right_point],
        0,
        90,
        fill=fill,
        outline=outline
    )
    self.pieslice([(upper_left_point[0], bottom_right_point[1] - corner_radius * 2), (upper_left_point[0] + corner_radius * 2, bottom_right_point[1])],
        90,
        180,
        fill=fill,
        outline=outline
    )
    self.pieslice([(bottom_right_point[0] - corner_radius * 2, upper_left_point[1]), (bottom_right_point[0], upper_left_point[1] + corner_radius * 2)],
        270,
        360,
        fill=fill,
        outline=outline
    )
    self.rectangle(
        [
            (upper_left_point[0], upper_left_point[1] + corner_radius),
            (bottom_right_point[0], bottom_right_point[1] - corner_radius)
        ],
        fill=fill,
        outline=fill
    )
    self.rectangle(
        [
            (upper_left_point[0] + corner_radius, upper_left_point[1]),
            (bottom_right_point[0] - corner_radius, bottom_right_point[1])
        ],
        fill=fill,
        outline=fill
    )
    self.line([(upper_left_point[0] + corner_radius, upper_left_point[1]), (bottom_right_point[0] - corner_radius, upper_left_point[1])], fill=outline)
    self.line([(upper_left_point[0] + corner_radius, bottom_right_point[1]), (bottom_right_point[0] - corner_radius, bottom_right_point[1])], fill=outline)
    self.line([(upper_left_point[0], upper_left_point[1] + corner_radius), (upper_left_point[0], bottom_right_point[1] - corner_radius)], fill=outline)
    self.line([(bottom_right_point[0], upper_left_point[1] + corner_radius), (bottom_right_point[0], bottom_right_point[1] - corner_radius)], fill=outline)
Chamkis answered 28/8, 2020 at 19:4 Comment(1)
How do you use this?Glacier
G
1
# set offset equal to 0 to get circular image
def round_corner(image, offset, width, height, filled_pixel):

    im = Image.open(image).convert('RGBA').resize((width - offset, height - offset))
    im_base = Image.new('RGBA', (width, height), (255, 255, 255, 255))
    im_base.paste(im, (offset / 2, offset / 2))
    im = im_base

    im_new = Image.new('RGBA', (width, height))

    half_w, half_h = width / 2, height / 2

    for x in range(width):
        for y in range(height):
            if (x - half_w) * (x - half_w) + (y - half_h) * (y - half_h) <= half_w * half_h:
                pixel = im.getpixel((x, y))
            else:
                pixel = filled_pixel
            im_new.putpixel((x, y), pixel)

    return im_new

how to use

round_corner(img, 0, 160, 160, (0, 0, 0, 0))

main idea

It is simple enough, the idea is to put filled_pixel where the actual pixel should be hidden, thus only the filled_pixel will be revealed and if the background is black, and we use black pixel as the filled pixel, then everything will be ok. I know this may not be the smartest way, but it will work for certain occasions.

And I found another way to implement this:

def circle_pic(img):
    scale = 3
    w, h = img.size
    r = w * scale
    alpha_layer = Image.new('L', (r, r), 0)
    draw = ImageDraw.Draw(alpha_layer)
    draw.ellipse((0, 0, r, r), fill=255)
    alpha_layer = alpha_layer.resize((w, w), Image.ANTIALIAS)
    return img, alpha_layer

here is how to use this function

icon_base = Image.new('RGBA', base.size, (255, 255, 255, 0))
iss = (28, 28)
icon, alpha_layer = circle_pic(icon)
icon = icon.resize(iss)
alpha_layer = alpha_layer.resize(iss)
icon_base.paste(icon, (ip[0], ip[1], ip[0] + iss[0], ip[1] + iss[1]), alpha_layer)
Gull answered 19/8, 2021 at 9:12 Comment(2)
Can you please give a brief description of how your code worksKan
hope the modification could helpGull

© 2022 - 2024 — McMap. All rights reserved.