30

I am using PIL' ImageFont module to load fonts to generate text images. I want the text to tightly bound to the edge, however, when using the ImageFont to get the font height, It seems that it includes the character's padding. As the red rectangle indicates.enter image description here

c = 'A'
font = ImageFont.truetype(font_path, font_size)
width = font.getsize(c)[0]
height = font.getsize(c)[1]
im = Image.new("RGBA", (width, height), (0, 0, 0))
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'A', (255, 255, 255), font=font)
im.show('charimg')

If I can get the actual height of the character, then I could skip the bounding rows in the bottom rectangle, could this info got from the font? Thank you.

5
  • Now I wrote a small function to scan vertically the generated image text to find the padding for every font I use. As the font character image contains only front and back colors, it works well. Commented Mar 28, 2017 at 4:21
  • What is c in font.getsize(c)[1] Commented May 7, 2017 at 3:41
  • Thanks for pointing out the error, I have fixed the code block, please check it. Commented May 8, 2017 at 9:10
  • @HassanBaig c stands for the character in question i think Commented Jan 27, 2020 at 11:37
  • You can get size in one line: width, height = font.getsize(c) Commented Oct 20, 2020 at 11:19

3 Answers 3

68

Exact size depends on many factors. I'll just show you how to calculate different metrics of font.

font = ImageFont.truetype('arial.ttf', font_size)
ascent, descent = font.getmetrics()
(width, baseline), (offset_x, offset_y) = font.font.getsize(text)
  • Height of red area: offset_y
  • Height of green area: ascent - offset_y
  • Height of blue area: descent
  • Black rectangle: font.getmask(text).getbbox()

Hope it helps.

Sign up to request clarification or add additional context in comments.

4 Comments

Correction: (width, height), (offset_x, offset_y) = font.font.getsize(text). Also, font.getmask(text) will return an image having the size (width, height), and a image_draw.text((0, 0), text, font) basically draws that "mask" (returned by getmask) at an offset of (offset_x, offset_y).
i guess your explanation only applies on English fonts as far as i test it is messy with other language fonts out there :/
This is outdated, and hopelessly broken with non-English text and non-standard fonts (e.g. significant italics). See my answer below using a new function in Pillow 8.0.0: stackoverflow.com/a/70636273/1648883
Also, you shouldn't use font.font.getsize, as this is a private API (although regrettably not prefixed by an underscore). OTOH font.getsize is broken for historical reasons and cannot be easily fixed.
28

The top voted answer is outdated. There is a new function in Pillow 8.0.0: ImageDraw.textbbox. See the release notes for other text-related functions added in Pillow 8.0.0.

Note that ImageDraw.textsize, ImageFont.getsize and ImageFont.getoffset are broken, and should not be used for new code. These have been effectively replaced by the new functions with a cleaner API. See the documentation for details.

To get a tight bounding box for a whole string you can use the following code:

from PIL import Image, ImageDraw, ImageFont
image = Image.new("RGB", (200, 80))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("arial.ttf", 30)

draw.text((20, 20), "Hello World", font=font)
bbox = draw.textbbox((20, 20), "Hello World", font=font)
draw.rectangle(bbox, outline="red")
print(bbox)
# (20, 26, 175, 48)

image.show()

bounding box example


You can combine it with the new ImageDraw.textlength to get individual bounding boxes per letter:


from PIL import Image, ImageDraw, ImageFont
image = Image.new("RGB", (200, 80))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("arial.ttf", 30)

xy = (20, 20)
text = "Example"
draw.text(xy, text, font=font)

x, y = xy
for c in text:
  bbox = draw.textbbox((x, y), c, font=font)
  draw.rectangle(bbox, outline="red")
  x += draw.textlength(c, font=font)

image.show()

letter bounding boxes example

Note that this ignores the effect of kerning. Kerning is currently broken with basic text layout, but could introduce a slight inaccuracy with Raqm layout. To fix it you would add the text length of pairs of letters instead:

for a, b in zip(text, text[1:] + " "):
  bbox = draw.textbbox((x, y), a, font=font)
  draw.rectangle(bbox, outline="red")
  x += draw.textlength(a + b, font=font) - draw.textlength(b, font=font)

3 Comments

FWIW: if you are looking for the fonts try ArialFont = "/System/Library/Fonts/Supplemental/Arial.ttf" or if you want to know your fonts, try this for MACOS github.com/JayRizzo/JayRizzoTools/blob/master/…
Can you (or someone) provide a link explaining why the alternatives "are broken", in what ways, and what specifics are being violated?
Do you know how can I determine if extra space was used at the bottom due to descenders? I'm trying to find an answer here: stackoverflow.com/questions/78044997/…
4
from PIL import Image, ImageDraw, ImageFont

im = Image.new('RGB', (400, 300), (200, 200, 200))
text = 'AQj'
font = ImageFont.truetype('arial.ttf', size=220)
ascent, descent = font.getmetrics()
(width, height), (offset_x, offset_y) = font.font.getsize(text)
draw = ImageDraw.Draw(im)
draw.rectangle([(0, 0), (width, offset_y)], fill=(237, 127, 130))  # Red
draw.rectangle([(0, offset_y), (width, ascent)], fill=(202, 229, 134))  # Green
draw.rectangle([(0, ascent), (width, ascent + descent)], fill=(134, 190, 229))  # Blue
draw.rectangle(font.getmask(text).getbbox(), outline=(0, 0, 0))  # Black
draw.text((0, 0), text, font=font, fill=(0, 0, 0))
im.save('result.jpg')

print(width, height)
print(offset_x, offset_y)
print('Red height', offset_y)
print('Green height', ascent - offset_y)
print('Blue height', descent)
print('Black', font.getmask(text).getbbox())

result

Calculate area pixel

from PIL import Image, ImageDraw, ImageFont

im = Image.new('RGB', (400, 300), (200, 200, 200))
text = 'AQj'
font = ImageFont.truetype('arial.ttf', size=220)
ascent, descent = font.getmetrics()
(width, height), (offset_x, offset_y) = font.font.getsize(text)
draw = ImageDraw.Draw(im)
draw.rectangle([(0, offset_y), (font.getmask(text).getbbox()[2], ascent + descent)], fill=(202, 229, 134))
draw.text((0, 0), text, font=font, fill=(0, 0, 0))
im.save('result.jpg')
print('Font pixel', (ascent + descent - offset_y) * (font.getmask(text).getbbox()[2]))

result

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.