Documentation Menu

Pillow ImageDraw Module

Independent quick-reference. Canonical source: official ImageDraw module documentation.

ImageDraw draws shapes and text directly onto a Pillow image: boxes and labels on detection results, charts and badges, watermarks, social cards, or simple generated graphics. You create one Draw object per image and call its methods; every call changes the image in place.

Import
from PIL import Image, ImageDraw

ImageDraw.Draw()

Creates a drawing object that can be used to draw on the given image.

ParameterTypeDescription
imImageThe image to draw in. Note: image is modified in place.
modestrMode to use for drawing. Defaults to the image mode.
Basic Usage
img = Image.new("RGB", (400, 300), "white") draw = ImageDraw.Draw(img) draw.rectangle([50, 50, 350, 250], outline="black", width=2) img.save("output.png")

Drawing Methods: rectangle, ellipse, line & polygon

All methods accept fill (interior colour) and/or outline (border colour). Colours can be strings like "red", hex "#ff0000", or RGBA tuples (255, 0, 0, 128).

MethodSignatureDescription
arc()arc(xy, start, end, fill=None, width=1)Draw an arc between start and end angles (degrees, clockwise from 3 o'clock).
bitmap()bitmap(xy, bitmap, fill=None)Draw given bitmap over image at given offset, with fill for set pixels.
chord()chord(xy, start, end, fill=None, outline=None, width=1)Same as arc but connects endpoints with a straight line.
ellipse()ellipse(xy, fill=None, outline=None, width=1)Draw an ellipse inside a bounding box. Pass a 4-tuple [x0,y0,x1,y1].
circle()circle(xy, radius, fill=None, outline=None, width=1)Draw a circle. xy is the center point.
line()line(xy, fill=None, width=1, joint=None)Draw a line between all points in xy. joint can be "curve" for round joins.
pieslice()pieslice(xy, start, end, fill=None, outline=None, width=1)Same as arc but draws a pie slice from center to endpoints.
point()point(xy, fill=None)Draw individual pixels.
polygon()polygon(xy, fill=None, outline=None, width=1)Draw a polygon from a list of points [(x0,y0), (x1,y1), ...].
regular_polygon()regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1)Draw a regular polygon with n_sides sides inscribed within a circle.
rectangle()rectangle(xy, fill=None, outline=None, width=1)Draw a rectangle. xy is [x0,y0,x1,y1] or [(x0,y0),(x1,y1)].
rounded_rectangle()rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)Draw a rectangle with rounded corners.

Text Methods: text(), textbbox() & textlength()

MethodSignatureDescription
text()text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)Draw text at position xy.
multiline_text()multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)Draw multi-line text, splitting on \n.
textlength()textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)Returns the length of a string given the current font, in pixels.
textbbox()textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)Returns bounding box (left, top, right, bottom) of the given text. Useful for centering.
Centered Text
from PIL import Image, ImageDraw, ImageFont img = Image.new("RGB", (400, 200), "#1a1a2e") draw = ImageDraw.Draw(img) font = ImageFont.truetype("DejaVuSans.ttf", size=36) text = "Pillow Python" # Get bounding box for centering bbox = draw.textbbox((0, 0), text, font=font) x = (img.width - (bbox[2] - bbox[0])) // 2 y = (img.height - (bbox[3] - bbox[1])) // 2 # Draw with white stroke for visibility draw.text((x, y), text, fill="white", font=font, stroke_width=2, stroke_fill="black") img.save("text_demo.png")

Flood Fill

The ImageDraw.floodfill() function fills a bounded region of an image with a colour. Note: this is a function, not a Draw method.

Flood Fill
from PIL import ImageDraw # Fill all connected pixels matching img.getpixel((x,y)) with red ImageDraw.floodfill(img, xy=(50, 50), value=(255, 0, 0))

Full Example: Annotated Image

Draw Annotation Overlay
from PIL import Image, ImageDraw, ImageFont img = Image.open("photo.jpg").convert("RGBA") overlay = Image.new("RGBA", img.size, (0,0,0,0)) draw = ImageDraw.Draw(overlay) # Bounding box annotation draw.rectangle([100,100,300,300], outline=(255,0,0,255), width=3) # Semi-transparent fill overlay draw.rectangle([100,100,300,120], fill=(255,0,0,180)) # Label text font = ImageFont.load_default() draw.text((105,103), "Object: Cat (0.97)", fill="white", font=font) # Composite and save result = Image.alpha_composite(img, overlay).convert("RGB") result.save("annotated.jpg")

Tested examples

Everything in this section was run with Pillow 12.3.0; the images are the real output. Sample photos: NASA/Sam Lott, public domain.

Gallery of ImageDraw shapes: rectangle, rounded rectangle, ellipse, circle, polygon, regular polygon, arc, chord, pie slice and a curved line
Real output of the script below. circle() needs Pillow 10.4 or newer.
shape_gallery.py
from PIL import Image, ImageDraw, ImageFont

img = Image.new("RGB", (960, 420), "white")
d = ImageDraw.Draw(img)
font = ImageFont.load_default(size=18)
blue, red = "#2563eb", "#dc2626"

shapes = [
    ("rectangle", lambda b: d.rectangle(b, outline=blue, width=4)),
    ("rounded_rectangle", lambda b: d.rounded_rectangle(b, radius=24, fill=blue)),
    ("ellipse", lambda b: d.ellipse(b, fill=red)),
    ("circle", lambda b: d.circle(((b[0] + b[2]) / 2, (b[1] + b[3]) / 2), 60, outline=red, width=4)),
    ("polygon", lambda b: d.polygon([(b[0], b[3]), ((b[0] + b[2]) / 2, b[1]), (b[2], b[3])], fill=blue)),
    ("regular_polygon", lambda b: d.regular_polygon(((b[0] + b[2]) / 2, (b[1] + b[3]) / 2, 65), 6, fill=red)),
    ("arc", lambda b: d.arc(b, start=200, end=340, fill=blue, width=6)),
    ("chord", lambda b: d.chord(b, start=30, end=210, fill=red)),
    ("pieslice", lambda b: d.pieslice(b, start=-60, end=210, fill=blue)),
    ("line (joint='curve')", lambda b: d.line([(b[0], b[3]), (b[0] + 40, b[1]), (b[2] - 40, b[3]), (b[2], b[1])],
                                               fill=red, width=10, joint="curve")),
]
for i, (name, draw_it) in enumerate(shapes):
    col, row = i % 5, i // 5
    x, y = 30 + col * 190, 30 + row * 200
    draw_it((x + 15, y, x + 145, y + 130))
    d.text((x + 80, y + 150), name, font=font, fill="#0f172a", anchor="ma")
img.save("shapes.png")
Tested on ✓ Linux ✓ Windows ✓ macOS

Outlines grow inwards

The xy box is inclusive on all four sides, and a thick width is drawn inside it. A 5 px outline on the box (10, 10, 50, 50) covers pixels 10–14 and leaves the outer size unchanged:

outline_width.py
from PIL import Image, ImageDraw

img = Image.new("RGB", (60, 60), "white")
ImageDraw.Draw(img).rectangle((10, 10, 50, 50), outline="black", width=5)

for xy in [(9, 9), (10, 10), (14, 14), (15, 15), (50, 50), (51, 51)]:
    print(xy, "black" if img.getpixel(xy) == (0, 0, 0) else "white")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(9, 9) white
(10, 10) black
(14, 14) black
(15, 15) white
(50, 50) black
(51, 51) white

Smooth (anti-aliased) edges

ImageDraw does not anti-alias shapes, so curves and diagonals look jagged. The standard workaround is supersampling: draw at 4× the size and shrink with Image.LANCZOS. Zoomed in on the same edge:

Zoomed edge of a circle: jagged when drawn directly, smooth when supersampled
Real output. Left: drawn directly. Right: drawn at 4× and downscaled.
smooth_shapes.py
from PIL import Image, ImageDraw

SCALE = 4
size = (200, 200)

# Direct: ImageDraw shapes have hard, aliased edges.
plain = Image.new("RGB", size, "white")
ImageDraw.Draw(plain).ellipse((20, 20, 180, 180), outline="#2563eb", width=6)

# Supersampled: draw 4x larger, then shrink with a good filter.
big = Image.new("RGB", (size[0] * SCALE, size[1] * SCALE), "white")
ImageDraw.Draw(big).ellipse((20 * SCALE, 20 * SCALE, 180 * SCALE, 180 * SCALE),
                            outline="#2563eb", width=6 * SCALE)
smooth = big.resize(size, Image.LANCZOS)

# Zoom in on the edge so the difference is visible.
crop = (130, 20, 180, 70)
out = Image.new("RGB", (420, 210), "white")
out.paste(plain.crop(crop).resize((200, 200), Image.NEAREST), (0, 5))
out.paste(smooth.crop(crop).resize((200, 200), Image.NEAREST), (220, 5))
out.save("antialias_zoom.png")
Tested on ✓ Linux ✓ Windows ✓ macOS

Translucent fills

An alpha value in fill is ignored on an RGB image unless you create the drawing context with mode "RGBA", which blends each shape onto what is already there:

translucent_fill.py
from PIL import Image, ImageDraw

base = Image.new("RGB", (300, 120), "#2563eb")

# Default: the alpha value is ignored on an RGB image; the colour is written as-is.
a = base.copy()
ImageDraw.Draw(a).rectangle((20, 20, 280, 100), fill=(255, 255, 255, 128))
print("Draw(img):        ", a.getpixel((150, 60)))

# Draw(img, "RGBA"): the fill is blended onto the existing pixels.
b = base.copy()
ImageDraw.Draw(b, "RGBA").rectangle((20, 20, 280, 100), fill=(255, 255, 255, 128))
print('Draw(img, "RGBA"):', b.getpixel((150, 60)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
Draw(img):         (255, 255, 255)
Draw(img, "RGBA"): (146, 177, 245)

Labelled bounding boxes (object detection output)

draw_detections.py
from PIL import Image, ImageDraw, ImageFont

img = Image.open("photo.jpg")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 22)

detections = [((660, 462, 704, 772), "rocket", 0.97), ((872, 536, 958, 742), "water tower", 0.91),
              ((888, 700, 1068, 852), "tank", 0.86)]
for (x0, y0, x1, y1), label, score in detections:
    draw.rectangle((x0, y0, x1, y1), outline="#facc15", width=4)
    text = f"{label} {score:.2f}"
    l, t, r, b = draw.textbbox((x0, y0), text, font=font, anchor="lb")
    draw.rectangle((l - 6, t - 6, r + 6, b + 4), fill="#facc15")
    draw.text((x0, y0), text, font=font, fill="black", anchor="lb")
img.save("detections.jpg", quality=88)
Tested on ✓ Linux ✓ Windows ✓ macOS
Launch pad photo with yellow labelled boxes around the rocket, a water tower and a tank
Real output (photo: NASA/Sam Lott, public domain).

Round avatar with a mask

round_avatar.py
from PIL import Image, ImageDraw, ImageOps

photo = ImageOps.fit(Image.open("photo.jpg"), (400, 400))      # square crop

mask = Image.new("L", photo.size, 0)                          # 0 = transparent
ImageDraw.Draw(mask).ellipse((0, 0, 399, 399), fill=255)       # 255 = opaque

avatar = photo.convert("RGBA")
avatar.putalpha(mask)
avatar.save("avatar.png")                                      # PNG keeps transparency
print(avatar.mode, avatar.size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGBA (400, 400)
A square photo cropped to a circle with a transparent background
Real output (transparent corners shown as a checkerboard).

More on text: adding text to images (anchors, wrapping, outlines) and replacing the removed textsize().