Title: Find the bounding box for text drawn with PIL in Python
My post Align text drawn with PIL in Python shows how to draw aligned text in a PIL image, but it doesn't show you how to figure out exactly where the text goes. This example does that. The basic idea is simple but there are two catches.
Approach
Before we get to the catches, let's talk about how you get the basic bounding box. To get that value, you use the ImageDraw.Draw class's textbbox function, passing it the position where the text will be drawn, the text, and the font. That function returns the bounding box in the format (xmin, ymin, xmax, ymax).
Now for the catches.
First, the returned bounding box doesn't account for the text's anchor point. For example, as you learned in the previous post, if the text's anchor value is mm, the text is centered at the anchor point. However, the bounding box always returned by textbbox assumes that the anchor point is to the left and slightly above the text. We need to move the box to account for the text's anchor value.
Second, textbbox doesn't account for internal leading (pronounced "ledding"), which is space above the font's characters. Image draw calls that value the Y-offset. The result is that the bounding box's top edge is a bit too low. If the text has anchor la, the bounding box extends to the bottom of the text but doesn't quite reach up to the anchor point. To fix this, we need to find the font's Y-offset and move the bounding box's top edge up by that amount.
Similarly, the font may include some leading space to the left of the text. To account for that, we need to find the font's X-offset and subtract that from the bounding box's left edge. (This seems to only apply to some of the fancier fonts. For many fonts, the X-offset is 0.)
Python Code
The following code shows how this example draws a piece of sample text. The code in blue deals with the bounding box.
def draw_sample(self, dr, font, x, y, align, anchor):
# Draw a sample.
# For quick informtion about font metrics, see:
# https://stackoverflow.com/questions/43060479/how-to-get-the-font-pixel-height-using-pils-imagefont-class
radius = 3
text = f'A longish first line\nAnchor: {anchor}\nEnding line'
dr.multiline_text((x, y), text=text,
fill='blue', font=font, anchor=anchor, align=align)
dr.ellipse((x - radius, y - radius, x + radius, y + radius),
fill='yellow', outline='black')
# Get the bounding box.
bbox = dr.textbbox((x, y), text, font=font)
# Get the vertical offset from the anchor point
# to the top of the first letters.
(width, baseline), (offset_x, offset_y) = font.font.getsize(text)
# Move the bounding box's up by offset_y and left by offset_x.
bbox = [
bbox[0] - offset_x, bbox[1] - offset_y,
bbox[2], bbox[3]]
# Adjust the bounding box depending on the anchor value.
wid = bbox[2] - bbox[0]
hgt = bbox[3] - bbox[1]
print(f'point=({x}, {y}), {bbox=}, {offset_x=}, {offset_y=}, {wid=}, {hgt=}')
if anchor[0] == 'm': # Anchor point is in the middle horizontally
bbox[0] -= wid // 2
bbox[2] -= wid // 2
elif anchor[0] == 'r': # Anchor point is on the right horizontally
bbox[0] -= wid
bbox[2] -= wid
if anchor[1] == 'm': # Anchor point is in the middle vertically
bbox[1] -= hgt // 2
bbox[3] -= hgt // 2
elif anchor[1] == 'd': # Anchor point is on the bottom vertically
bbox[1] -= hgt
bbox[3] -= hgt
dr.rectangle(bbox, outline='red')
The blue code first calls the ImageDraw.Draw object's textbbox function, passing it the anchor point, text, and font. It then calls the font's getsize method to get some basic font metrics. In this example, we only need offset_y. (For a nice, concise summary of some very basic font metrics, see the first answer in the Stack Overflow post How to get the font pixel height using PIL's ImageFont class?.
Now that the code has the bounding box and the X- and Y-offsets, the code sets bbox equal to the bounding box with its xmin and ymin values adjusted. This makes the bounding box a little taller and possibly wider, and moves it to include the anchor point.
The original bounding box returned by the textbbox function was a tuple, so we couldn't modify it. The new value is a list, so the next piece of code can change its values.
The code calculates the adjusted bounding box's width and height. It then examines the anchor's first value. (For example, "r" in "rd.") If the anchor point is centered horizontally, the code subtracts half the bounding box's width from its X values to move the box to the left. If the anchor point is aligned on the right edge of the text, the code subtracts the bounding box's full width from its X values to move the box even farther to the left.
The code then makes similar adjustments to account for the anchor's vertical alignment.
After adjusting the bounding box, the code draws it in red.
If you look at the picture at the top of the post, you'll see that the anchor points lie exactly on the bounding boxes.
Conclusion
This code shows how you can find exactly where the text will appear in your PIL drawing. Use the combo box to try other fonts. (You may need to change the code if the fonts I've picked aren't installed on your system.) Use the alignment radio buttons to draw the text with different alignments.
Be warned that font metrics are intended as guidelines for font developers but some fonts don't follow the metrics exactly. For example, take a look at the picture on the right. The text extends a pixel or two out of the bounding box on the left. You can try to adjust for that if you like, but I think your results will be inconsistent with different fonts and perhaps on different operating systems. In any case, the results are probably good enough.
Download the example to see additional details.
|