开发者

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

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 t开发者_JS百科o 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


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):

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

Running from top to bottom (runTopBottom = True):

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


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:

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

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


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()

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

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


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)


# 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)
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜