ImageDraw and fonts

Pillow · Practical · 12 min

Watermarks, labels, charts on the fly and generated social images all rely on drawing text precisely.

ImageDraw.Draw(im) returns a drawing context that modifies `im` in place. Its methods take coordinates as (x, y) tuples or bounding boxes [x0, y0, x1, y1]:

  • line, rectangle, rounded_rectangle, ellipse, polygon, arc, pieslice
  • fill= for the interior colour, outline= and width= for the border

For text, load a font with ImageFont.truetype("DejaVuSans.ttf", size); a path or a font name your system can find. ImageFont.load_default(size=...) gives a built-in scalable font in recent versions, good for quick labels.

draw.text((x, y), text, font=font, fill=...) draws text; anchor= changes what (x, y) refers to, for example "mm" centres the text on the point and "ls" puts it on the left baseline. Multi-line strings work, with spacing= and align=.

To lay out text, measure it: draw.textbbox((x, y), text, font=font) returns the box (left, top, right, bottom) the text would occupy, and draw.textlength(text, font=font) the advance width. The old textsize method was removed in Pillow 10.

For semi-transparent overlays (watermarks), draw on a separate transparent RGBA layer and combine it with Image.alpha_composite; drawing a translucent fill straight onto an RGB image replaces pixels rather than blending.

Example

from PIL import Image, ImageDraw, ImageFont

im = Image.new("RGB", (400, 200), "white")
draw = ImageDraw.Draw(im)                       # draws on im in place
draw.rectangle([20, 20, 180, 120], outline="navy", width=3)
draw.ellipse([220, 30, 320, 130], fill="tomato")
draw.line([(0, 190), (399, 150)], fill="gray", width=2)

font = ImageFont.load_default(size=28)          # or ImageFont.truetype(path, 28)
text = "Hello Pillow"
box = draw.textbbox((0, 0), text, font=font)
w, h = box[2] - box[0], box[3] - box[1]
print(w > 0 and h > 0)                          # True: measured before drawing

draw.text((200, 170), text, font=font, fill="black", anchor="mm")  # centred
print(im.getpixel((270, 80)))                   # (255, 99, 71) tomato
print(draw.textlength("ab", font=font) > draw.textlength("a", font=font))  # True

Remember

  • ImageDraw.Draw draws in place on the image
  • Load fonts with ImageFont.truetype or load_default(size=...)
  • Measure text with textbbox/textlength; textsize is gone
  • anchor='mm' centres text on a point

Watch out for

  • Calling the removed draw.textsize() from old tutorials; use textbbox instead.

Check yourself

  1. You want text centred exactly on (200, 100). Which call does it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/reference/ImageDraw.html