25

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.

Noldorin
  • 144,213
  • 56
  • 264
  • 302
binzhang
  • 1,171
  • 4
  • 12
  • 22

3 Answers3

60

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.

iimos
  • 4,767
  • 2
  • 33
  • 35
  • 2
    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)`. – John Dec 06 '19 at 02:46
  • i guess your explanation only applies on English fonts as far as i test it is messy with other language fonts out there :/ – Ahmed4end Jan 29 '21 at 08:56
  • 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: https://stackoverflow.com/a/70636273/1648883 – Nulano Jan 08 '22 at 20:44
  • 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. – Nulano Jan 08 '22 at 20:56
18

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 documentaion 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. Kering 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)
Nulano
  • 1,148
  • 13
  • 27
  • 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 https://github.com/JayRizzo/JayRizzoTools/blob/master/pyGenRandFontObj.py – JayRizzo Jun 30 '22 at 19:35
  • 2
    Can you (or someone) provide a link explaining why the alternatives `"are broken"`, in what ways, and what specifics are being violated? – Thomas Kimber Oct 10 '22 at 11:28
3
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

XerCis
  • 917
  • 7
  • 6