Title: Fill text with radial lines in Python
This example draws text that is filled with a collection of lines radiating out from a central point. To do that, it follows a pattern similar to the one used by several previous examples: it draws an image and then uses the ImageBrush class to fill a shape (in this case text) with that image.
Before I show you the code, however, I want to talk about aliasing and anti-aliasing.
Aliasing
The picture at the top of this post looks pretty smooth, but take a look at the closeups on the right. You can clearly see that the R in the first picture is filled with jagged lines. That jagginess is called "aliasing" and it occurs whenever you draw something at a resolution that is too large to capture the true drawing. PIL (and tkinter) draws shapes by coloring the pixels that define that shape. Any pixels that are part of the shape get its color (in this case, red), and pixels that are not part of the shape do not.
One way you could reduce the jagginess would be to buy a super-high resolution monitor with 300 or more pixels per square inch, about the typical resolution of a smartphone. Unfortunately it would be pretty expensive to buy a full-sized monitor with that resolution.
Another (less expensive) approach is to use pixels near the shape's edges to soften the shape's outline. Instead of making pixels red or not, some pixels are different shades of red. That gives a smoother, albeit slightly fuzzier, result. That process is called "anti-aliasing."
That's the approach taken by the second R, and you can see that it is much smoother. That's also the approach taken by the picture at the top of the approach so it looks smooth, too.
Filling the Text
As I mentioned earlier, this example takes an approach similar to those used by earlier posts: make an image and use an ImageBrush to fill the text. Part of the purpose of this example is to show how you can use PIL to anti-alias the result.
Unfortunately, PIL doesn't provide anti-aliasing for shapes like lines, so we'll have to trick it into doing our bidding. The way to do that is to draw everything at an enlarged scale and then use the PIL's resize method to shrink the image to its final size. PIL doesn't provide anti-aliasing for shapes, but it does use a resizing algorithm that can give us a similar result.
Here's the code that the example uses to draw its text. The code that draws at enlarged scale and then shrinks the final image is highlighted in blue.
def fill_shapes(self):
# Make an image to draw on.
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
scale = 2 # Scale for anti-aliasing.
image = Image.new('RGBA', (scale * wid, scale * hgt))
dr = ImageDraw.Draw(image)
# Fill with a gradient.
colors = [
[255, 255, 0, 255], # Yellow
[255, 0, 0, 255], # Red
[178, 34, 34, 255], # Firebrick
]
start_point = (0, 0)
end_point = (0, scale * hgt)
brush = LinearMultiGradientBrush(start_point, end_point, colors)
bounds = (0, 0, scale * wid, scale * hgt)
pil_fill_rectangle(image, brush, bounds)
# Get text metrics.
text = 'Radial\nLines'
font = ImageFont.truetype('times.ttf', scale * 150)
align = 'center'
anchor = 'mm'
cx = scale * wid / 2
cy = scale * hgt / 2
text_bounds = dr.textbbox((cx, cy), text, font=font,
align=align, anchor=anchor)
# Make the text filled image.
text_xmin, text_ymin, text_xmax, text_ymax = text_bounds
text_wid = math.floor(text_xmax - text_xmin)
text_hgt = math.floor(text_ymax - text_ymin)
fill_image = Image.new('RGBA', (text_wid, text_hgt))
fill_dr = ImageDraw.Draw(fill_image)
cx -= text_xmin
cy -= text_ymin
# Fill the image with random lines.
radius = max(text_wid, text_hgt)
num_lines = 100
theta = 0
dtheta = math.pi * 2 / num_lines
thickness = 1
color = (255, 0, 0, 255)
#cy += 20
thickness = scale * 2
for i in range(num_lines):
x = cx - radius * math.cos(theta)
y = cy - radius * math.sin(theta)
theta += dtheta
# Draw the line.
fill_dr.line((cx, cy, x, y), fill=color, width=thickness)
# Make the image brush.
brush = ImageBrush(fill_image)
# Fill the text.
cx = scale * wid / 2
cy = scale * hgt / 2
thickness = scale * 1
pil_draw_outline_text(dr, cx, cy, text, 'black', font, anchor, align,
thickness)
pil_fill_text(image, brush, text, font, cx, cy, align, anchor)
# Resize to fit the canvas.
image = image.resize((wid, hgt))
# Display the result.
self.photo_image = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image)
This code gets the canvas's dimensions and then uses them to create a new image. It scales the desired width and height by a scale factor, which I set to 2 for this example. Usually a scale factor of 2 gives a pretty good result.
The code fills the image with a color gradient and then uses the textbbox method to see how big the text will be. It makes an image of that size and fills it with lines radiating from the center of that image. Note that text bounding boxes often have extra space around them such as inter-line spacing and different letters have different heights and widths. As a result, the center of the text's bounding box may not feel like the center of the text. In this example, the center is slightly below the "d" in "Radial" so the bottom of that "d" is completely red. You may get a more pleasing result if you move the center down a bit, but I'll leave that for you.
After it creates the radial lines image, the program uses it to create an ImageBrush. It outlines the text and then uses the brush to fill the text.
Next, the code resizes the resulting image so it fits the canvas and displays the result.
Notice how the code must scale everything while it is drawing. It scales the image's dimensions, line thicknesses, font size, ..., the works! If it drew shapes like rectangles and ellipses, the code would need to scale them, too.
Conclusion
This post, like previous ones, shows how easy it is to fill shapes with brushes. This example also shows how you can provide anti-aliasing to make your images smoother.
In my next post, I'll give one last example of filling text with a brush.
Download the example to experiment with it and to see additional details.
|