Title: Make an ImageBrush class for use with PIL in Python
Now that we have the all-important smiley face method, we can use it to fill shapes. First, I'll show you the ImageBrush class. Then I'll show you how the program uses it to fill the shapes in the picture.
ImageBrush
The following code shows the ImageBrush class.
from PIL import Image, ImageTk
class ImageBrush(Brush):
'''Represents an image brush.'''
def __init__(self, image):
self.image = image
def make_image(self, bounds):
'''Make an image filled with the brush.'''
return make_tiled_image(bounds, self.image)
The constructor simply saves a reference to the brush's image.
The make_image method, which is the only really important part of any of these brush classes, calls the following make_tiled_Image method to create a new image filled with the brush's picture. (I put this code in a separate method rather than inside the ImageBrush class in case you want to fill an image with a picture more generally.)
def make_tiled_image(bounds, tile_image):
'''Return an image tiled with the image.'''
# Make the new image.
xmin, ymin, xmax, ymax = bounds
xmin = int(xmin)
ymin = int(ymin)
xmax = int(xmax)
ymax = int(ymax)
wid = xmax - xmin
hgt = ymax - ymin
result_image = Image.new('RGBA', (wid, hgt))
# Tile.
for x in range(0, wid, tile_image.width):
for y in range(0, hgt, tile_image.height):
result_image.paste(tile_image, (x, y))
return result_image
This method makes a new PIL image with the desired dimensions. It then loops over the new image's area pasting copies of the tile image onto it. The loops ensure that the new image is completely covered. For example, the for x loop begins with 0 and loops until x is at least the image's width. Each time through the loop, the range statement increments x by the tile image's width so each copy appears right next to the previous one.
Similarly, the for y loop moves down by the height of the tile image so each copy sits just below the one above.
There are other ways you could fill the new image with the tile image. For example, you could:
- Stretch the tile image to fill the new image
- Resize the tile image to be as large as possible without distorting it and while still fitting in the new image
- Resize the tile image without distorting it so it covers all of the new image
- Reflect the tile image vertically and/or horizontally for adjacent copies
You could modify the class to allow for all of those options.
Filling Shapes
A program can use the ImageBrush to fill shapes just as you can use other brushes like the LinearGradientBrush. The following code shows how the example program draws the picture at the top of this post.
def fill_shapes(self):
# Make a smiley face image.
wid = 100
hgt = 100
xmin = ymin = 3
xmax = wid - 3
ymax = hgt - 3
smiley_image = Image.new('RGBA', (wid, hgt))
# Fill with a linear gradient.
start_point = (0, 0)
end_point = (0, hgt)
start_color = (144, 238, 144, 255) # Light green.
end_color = (0, 100, 0, 255) # Darkgreen.
brush = LinearGradientBrush(start_point, end_point,
start_color, end_color)
bbox = (0, 0, wid, hgt)
pil_fill_rectangle(smiley_image, brush, bbox)
# Draw the smiley.
dr = ImageDraw.Draw(smiley_image)
pil_draw_smiley(dr, xmin, ymin, xmax, ymax)
# Make an image to draw on.
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
image = Image.new('RGBA', (wid, hgt), 'lightyellow')
dr = ImageDraw.Draw(image)
# Draw the smiley image.
margin = 10
image.paste(smiley_image, (margin, margin))
# Make an ImageBrush.
brush = ImageBrush(smiley_image)
# Fill an ellipse.
bounds = 2 * margin + 100, margin, margin + 320, margin + 150
pil_fill_ellipse(image, brush, bounds)
dr.ellipse(bounds, outline='red')
# Star.
xmin = 3 * margin + 300
ymin = margin
rect_wid = 200
rect_hgt = 200
bbox = [xmin, ymin, xmin + rect_wid, ymin + rect_hgt]
points = get_star_points(-math.pi / 2, 8, 3, bbox)
pil_fill_polygon(image, brush, points)
dr.polygon(points, fill=None, outline='black')
# Text.
cx = wid / 2
cy = hgt - 100
text = 'Python'
font = ImageFont.truetype('times.ttf', 150)
align = 'center'
anchor = 'mm'
thickness = 1
pil_draw_outline_text(dr, cx, cy, text, 'red', font, anchor, align,
thickness)
pil_fill_text(image, brush, text, font, cx, cy, align, anchor)
# Display the result.
self.photo_image = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image)
The program first creates a new 100 × 100 pixel image. It then uses a LinearGradientBrush and the pil_fill_rectangle method to fill the image starting with light green at the top and ending with dark green at the bottom.
Next, the code calls the pil_draw_smiley method described in the previous post to draw a smiley face on the small image. It then makes an image to fit the program's canvas and creates an associated ImageDraw.Draw object so it can draw on that image.
Now the code starts drawing. First, it draws the smiley face image directly on the main image.
The program then uses the smiley to create an ImageBrush. It calls pil_fill_ellipse to fill an ellipse with the brush and then uses the normal PIL ellipse method to outline the ellipse.
Next, the code makes a list holding the vertices of an eight-pointed star (see the post Draw stars in Python for details). It calls pil_fill_polygon to fill the star with the smiley brush and then uses PIL's polygon method to outline the star.
The program then uses the pil_draw_outline_text method to fill some text with the brush and then calls pil_fill_text to fill the center of the text with the smiley bush.
Finally, the code converts the image into a PhotoImage, saving the result in a class variable so it doesn't get garbage collected, and displays the final image on the program's canvas.
Conclusion
You can see how easy it is to fill rectangles, ellipses, polygons, text, and other shapes with a brush. Simply define the brush class, give it a make_image method, and pass it into the pil_fill_xxx methods that we created earlier. In my next few posts, I'll explain how you can make a few other kinds of brushes, partly to show how easy it is and partly because they're fun to make!
Meanwhile, download the example to experiment with it and to see additional details.
|