Title: Draw rotated text that is rotated, outlined, and filled with an image with PIL in Python
This example combines the techniques described in several previous posts to draw rotated, filled, outlined text. The following section explains how the main program orchestrates the drawing. The next section explains the method that actually draws the text, and the two sections after that describe two helper methods.
Main Program
Here's the code that draws the program's text.
def fill_shapes(self):
# Make an image to fill the canvas.
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
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(image, brush, bbox)
# Draw some text.
cx = wid / 2
cy = hgt / 2
text = 'Python'
fill = 'lightblue'
outline = 'blue'
font = ImageFont.truetype('times.ttf', 170)
anchor = 'mm'
align = 'center'
thickness = 1
dr = ImageDraw.Draw(image)
pil_draw_outline_text(dr, cx, cy, text, fill, outline, font,
anchor, align, thickness)
# Draw rotated text on top.
brush_type = 5
if brush_type == 1: # Checkerboard, filled.
brush_image = pil_draw_checkerboard(100, 100, 10, 10,
'yellow', 'red', filled=True)
brush = ImageBrush(brush_image)
elif brush_type == 2: # Checkerboard, unfilled.
brush_image = pil_draw_checkerboard(100, 100, 10, 10,
'yellow', 'red', filled=False, thickness=1)
brush = ImageBrush(brush_image)
elif brush_type == 3: # Diamonds, filled.
brush_image = pil_draw_diamonds(100, 100, 5, 5,
'white', 'purple', filled=True)
brush = ImageBrush(brush_image)
elif brush_type == 4: # Diamonds, unfilled.
brush_image = pil_draw_diamonds(100, 100, 5, 5,
'white', 'purple', filled=False, thickness=2)
brush = ImageBrush(brush_image)
elif brush_type == 5: # Linear gradient brush.
# Get the text's bounding box.
bbox = dr.textbbox((0, 0), text, font=font, align=align,
anchor=anchor)
start_point = (0, bbox[1])
end_point = (0, bbox[3])
colors = []
for _ in range(10):
colors.append((255, 255, 255, 255)) # White
colors.append((255, 0, 0, 255)) # Red
brush = LinearMultiGradientBrush(start_point, end_point, colors)
text = 'UNCHAINED'
font = ImageFont.truetype('times.ttf', 100)
angle = 30 # Degrees
fill = 'yellow'
outline = 'black'
thickness = 2
pil_rotated_outlined_filled_text(image, dr, cx, cy, angle, outline,
thickness, brush, text, font)
# Display the result.
self.photo_image = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image)
The code begins, as is traditional, by finding the canvas widget's dimensions and making an image to fit it. It then fills the image with a color gradient.
Next, the code uses the pil_draw_outline_text method (see the post Draw outlined text with PIL in Python) to draw the word "Python" filed with light blue and outlined with blue.
The program then gets to work on the new piece: drawing rotated, outlined, filled text. For this example, I made two new helper methods: pil_draw_checkerboard and pil_draw_diamonds. Those methods create images filled with checkerboards and diamonds, respectively. For example, the picture on the right shows the text filled with a diamond pattern. Use the brush_type variable to tell the program which pattern to use.
The different values of brush_type make the program create different text fill images. If brush_type has a value between 1 and 4, the code calls one of the helper methods described in the following sections to fill an image with a checkerboard or diamonds. It uses that image to make an ImageBrush to fill the text with the image.
If brush_type is 5, the code gets the text's bounding box and creates a linear gradient brush to fill the area with alternating bands of white and red.
Having created either an ImageBrush or a LinearMultiGradientBrush, the code calls the pil_rotated_outlined_filled_text method to draw the rotated, outlined, filled text.
Finally, the program displays the result.
pil_rotated_outlined_filled_text
The following pil_rotated_outlined_filled_text method does the real work of drawing the rotated text.
def pil_rotated_outlined_filled_text(image, dr, cx, cy, angle, outline,
thickness, brush, text, font):
# Get the bounding box.
bbox = dr.textbbox((cx, cy), text, font=font)
# Make the text and mask images with a little extra room.
wid = math.ceil(bbox[2] - bbox[0] + 2 * thickness + 10)
hgt = math.ceil(bbox[3] - bbox[1] + 2 * thickness + 10)
text_image = Image.new('RGB', (wid, hgt), 'white')
text_dr = ImageDraw.Draw(text_image)
mask_image = Image.new('RGBA', (wid, hgt), (0,0,0,0))
mask_dr = ImageDraw.Draw(mask_image)
# Translate the center point.
x = wid / 2
y = hgt / 2
# Draw the text's outline on both images.
align = 'center'
anchor = 'mm'
pil_draw_outline_text(text_dr, x, y, text, outline, outline, font,
anchor, align, thickness)
pil_draw_outline_text(mask_dr, x, y, text, 'white', 'white', font,
anchor, align, thickness)
# Fill the text on both images.
pil_fill_text(text_image, brush, text, font, x, y, align, anchor)
pil_fill_text(mask_image, brush, text, font, x, y, align, anchor)
# Rotate the image and mask.
text_image = text_image.rotate(angle, expand=True,
resample=Image.Resampling.BILINEAR)
mask_image = mask_image.rotate(angle, expand=True,
resample=Image.Resampling.BILINEAR)
# See where to paste the image.
x = round(cx - text_image.width / 2)
y = round(cy - text_image.height / 2)
# Use the mask to paste the text onto the destination image.
image.paste(text_image, (x, y), mask_image)
The method gets the text's bounding box and makes drawing and mask images with a little extra room around the edges. It translates the center point so the text is drawn on the new images rather than in its final location similarly to how previous examples did that.
The code calls the pil_draw_outline_text method to draw the text on the text image and the mask image. It then calls pil_fill_text to fill the text using brush parameter to fill the text on both images.
The method then rotates the images. This is why the images include a little extra space around the edges. During the rotation, PIL uses techniques to make the result look smoother and that can make the edges of the text a bit fuzzy. If the images did not include a little extra space, some fuzzy pixels might be cut off and that would make the edges of the text look bad.
The method finishes by using the mask to paste the text onto the destination image.
pil_draw_checkerboard
The following code shows the pil_draw_checkerboard helper method.
def pil_draw_checkerboard(wid, hgt, num_rows, num_cols, fill, outline,
filled=True, thickness=1):
'''Draw a checkerboard image.'''
image= Image.new('RGBA', (wid, hgt), fill)
dr = ImageDraw.Draw(image)
col_wid = wid / num_cols
row_hgt = hgt / num_rows
if filled:
for r in range(num_rows):
for c in range(num_cols):
if (r + c) % 2 == 1:
x = c * col_wid
y = r * row_hgt
dr.rectangle((x, y, x + col_wid, y + row_hgt), fill=outline)
else:
for r in range(num_rows):
y = r * row_hgt
dr.line((0, y, wid, y), fill=outline, width=thickness)
for c in range(num_cols):
x = c * col_wid
dr.line((x, 0, x, hgt), fill=outline, width=thickness)
return image
This code creates an image of the desired size. It then uses loops to fill the image with a checkerboard that's either filled or a grid. The picture on the right shows the two options.
pil_draw_diamonds
The following code shows the pil_draw_diamonds helper method.
def pil_draw_diamonds(wid, hgt, num_rows, num_cols, fill, outline,
filled=True, thickness=1):
'''Draw a diamond image.'''
image= Image.new('RGBA', (wid, hgt), fill)
dr = ImageDraw.Draw(image)
col_wid = wid / num_cols
row_hgt = hgt / num_rows
for r in range(num_rows):
for c in range(num_cols):
x = c * col_wid
y = r * row_hgt
points = [
(x, y + row_hgt / 2),
(x + col_wid / 2, y),
(x + col_wid, y + row_hgt / 2),
(x + col_wid / 2, y + row_hgt),
]
if filled:
dr.polygon(points, fill=outline)
else:
dr.polygon(points, outline=outline, width=thickness)
return image
This code creates an image of the desired size. It then uses loops to fill the image with diamonds that are either filled or unfilled. The picture on the right shows the two options.
Conclusion
Okay, that's all of the PIL text drawing example I'm going to post, at least for a while. Download the example to experiment with it and to see additional details.
|