PIL Drawing Primitives
The ImageDraw module turns a Pillow image into a programmable
drawing canvas. Build graphics from coordinates using lines,
rectangles, ellipses, polygons, arcs, points and text.
Learning Objectives
- 12.6.2.3 Apply graphic primitives to create drawings
Conceptual Anchor
Canvas + Brush + Coordinates
Image.new() creates the canvas.
ImageDraw.Draw(img) gives Python a drawing tool.
Every shape is then positioned with coordinates, coloured with RGB values,
and finally written to a file with img.save().
Image.new()
Create the canvas
ImageDraw.Draw(img)
Create the drawing context
draw.rectangle(...)
Add primitives in order
img.save()
Save the finished drawing
Setting Up a Drawing
Every Pillow drawing begins with the same three ideas: create an image, create a drawing object for that image, then call drawing methods.
from PIL import Image, ImageDraw
# 1. Create a canvas
img = Image.new("RGB", (500, 350), (255, 255, 255))
# 2. Create a drawing object connected to the image
draw = ImageDraw.Draw(img)
# 3. Draw something
draw.rectangle(
[(50, 50), (200, 150)],
fill=(255, 0, 0),
outline=(0, 0, 0),
width=3
)
# 4. Save the image
img.save("drawing.png")Connection to the Previous Lessons
Coordinates use the same top-left origin as PIL Image Basics,
while colours use the same (R, G, B) tuples from
RGB Colors.
Visual Primitive Reference
A graphic primitive is a simple shape used as a building block. Complex drawings are produced by combining many simple primitives.
Line
Connect two or more coordinates.
draw.line(points, fill=..., width=...)
Rectangle
Defined by opposite corners of a bounding box.
draw.rectangle(box, fill=..., outline=...)
Ellipse
Fills an invisible rectangular bounding box.
draw.ellipse(box, fill=..., outline=...)
Polygon
Connect several vertices and close the shape.
draw.polygon(points, fill=..., outline=...)
Arc
Draw only part of an ellipse outline.
draw.arc(box, start=..., end=..., fill=...)
Point
Colour one pixel or a list of pixels.
draw.point((x, y), fill=...)
Text
Place text at a coordinate.
draw.text((x, y), "Text", fill=...)
Rounded Rectangle
A rectangle with rounded corners.
draw.rounded_rectangle(box, radius=...)
Drawing Commands
| Method | Main coordinates | Useful parameters |
|---|---|---|
draw.line() |
list of points | fill, width |
draw.rectangle() |
bounding box | fill, outline, width |
draw.rounded_rectangle() |
bounding box | radius, fill, outline, width
|
draw.ellipse() |
bounding box | fill, outline, width |
draw.polygon() |
list of vertices | fill, outline, width |
draw.arc() |
bounding box | start, end, fill, width |
draw.point() |
point or points | fill |
draw.text() |
anchor coordinate | fill, font |
Parameter Reference
| Parameter | Meaning | Example |
|---|---|---|
fill |
Colour inside the shape | fill=(255, 0, 0) |
outline |
Colour of the border | outline=(0, 0, 0) |
width |
Thickness of a line or outline in pixels | width=4 |
radius |
Corner radius for a rounded rectangle | radius=20 |
start / end |
Start and end angles for an arc | start=20, end=160 |
Interactive Primitive Lab
Select a primitive. The preview changes immediately and shows the matching Pillow command. This browser canvas models what the Python program will draw.
draw.line()
Python
draw.line([(60, 220), (460, 80)], fill=(13, 148, 136), width=6)
Understanding the Bounding Box
Rectangles, ellipses and arcs are positioned inside a
bounding box. The box is defined by two opposite corners:
(x1, y1) and (x2, y2).
Move the Bounding Box
90
65
400
275
[(x1,y1), (x2,y2)]
The box itself becomes the rectangle.
[(x1,y1), (x2,y2)]
The ellipse touches the four sides of the invisible box.
square bounding box
If box width = box height, the ellipse becomes a circle.
Bounding Box ≠ Centre + Radius
With draw.ellipse(), you usually supply the two corners of
the box. For a circle centred at (200, 150) with radius
50, the box is
[(150, 100), (250, 200)].
Worked Examples
1 Basic Shapes
from PIL import Image, ImageDraw
img = Image.new("RGB", (500, 320), (255, 255, 255))
draw = ImageDraw.Draw(img)
# Red rectangle
draw.rectangle(
[(50, 50), (180, 140)],
fill=(255, 0, 0),
outline=(0, 0, 0),
width=3
)
# Blue ellipse
draw.ellipse(
[(250, 50), (390, 150)],
fill=(0, 100, 255),
outline=(0, 0, 0),
width=3
)
# Green line
draw.line(
[(60, 230), (440, 230)],
fill=(0, 150, 80),
width=6
)
img.save("shapes.png")2 Polygon from Vertices
A polygon uses a sequence of coordinates. Pillow joins each point to the next and automatically closes the shape from the last point to the first.
points = [
(200, 50),
(300, 120),
(260, 240),
(140, 240),
(100, 120)
]
draw.polygon(
points,
fill=(139, 92, 246),
outline=(0, 0, 0),
width=4
)3 Arc
# Bounding box of the full ellipse
box = [(100, 70), (400, 260)]
draw.arc(
box,
start=20,
end=160,
fill=(249, 115, 22),
width=8
)Arc Angles
Pillow measures arc angles in degrees from the 3 o'clock direction, increasing clockwise.
4 Adding Text
from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (500, 220), (25, 30, 40))
draw = ImageDraw.Draw(img)
draw.text(
(30, 100),
"Hello, PIL!",
fill=(255, 255, 255)
)
# Optional custom TrueType font
try:
font = ImageFont.truetype("arial.ttf", 42)
draw.text(
(30, 35),
"Big Text",
fill=(255, 200, 0),
font=font
)
except OSError:
pass
img.save("text_demo.png")Build a Scene Step by Step
A complete drawing is simply a sequence of primitives. Click through the stages and notice that later shapes are drawn on top of earlier shapes.
Image.new() — create sky canvas
Added code
img = Image.new("RGB", (520, 360), (135, 206, 235))
rectangle()
rectangle()
polygon()
ellipse()
rectangle()
ellipse()
Complete Scene Program
5 Simple House and Landscape
from PIL import Image, ImageDraw
img = Image.new("RGB", (520, 360), (135, 206, 235))
draw = ImageDraw.Draw(img)
# Ground
draw.rectangle(
[(0, 270), (520, 360)],
fill=(60, 150, 70)
)
# House
draw.rectangle(
[(150, 160), (370, 285)],
fill=(220, 185, 135),
outline=(50, 40, 30),
width=3
)
# Roof
draw.polygon(
[(130, 165), (260, 75), (390, 165)],
fill=(140, 70, 35),
outline=(50, 40, 30)
)
# Door
draw.rectangle(
[(245, 220), (290, 285)],
fill=(100, 65, 35),
outline=(40, 30, 20)
)
# Windows
draw.rectangle(
[(180, 195), (220, 235)],
fill=(180, 225, 245),
outline=(40, 40, 40)
)
draw.rectangle(
[(310, 195), (350, 235)],
fill=(180, 225, 245),
outline=(40, 40, 40)
)
# Sun
draw.ellipse(
[(420, 35), (485, 100)],
fill=(255, 220, 40)
)
# Tree
draw.rectangle(
[(75, 210), (95, 285)],
fill=(105, 65, 35)
)
draw.ellipse(
[(35, 145), (135, 235)],
fill=(30, 130, 60)
)
img.save("house_scene.png")Drawing Order = Layer Order
ImageDraw paints directly onto the image. If a new shape overlaps an older one, the newer shape appears on top. Plan the order from background → large objects → details.
From Shapes to Reusable Functions
Repeating many drawing commands quickly becomes difficult to maintain. Functions let you package a group of primitives into one reusable object.
6 A Building Function
def draw_building(draw, x, y, width, height, color):
# Main building
draw.rectangle(
[(x, y), (x + width, y + height)],
fill=color,
outline=(30, 30, 30),
width=2
)
# Windows
for row in range(3):
for col in range(2):
wx = x + 15 + col * 40
wy = y + 20 + row * 45
draw.rectangle(
[(wx, wy), (wx + 22, wy + 28)],
fill=(180, 225, 255)
)Later you can call the same function several times with different positions, sizes and colours to build a full cityscape.
Video Demonstration
Using Image and ImageDraw to Draw Shapes
A practical coding demonstration of drawing shapes with Python Pillow and ImageDraw.
Watch after the primitive reference, then reproduce one of the shapes using your own coordinates and colours.
Common Pitfalls
1. Forgetting ImageDraw.Draw(img)
Image.new() creates the image, but drawing methods belong
to the draw object:
draw = ImageDraw.Draw(img).
2. Reversing the Bounding Box
Keep the first corner above/left of the second:
x2 ≥ x1 and y2 ≥ y1.
3. Circle vs Ellipse
draw.ellipse() creates a circle only when the bounding box
has equal width and height.
4. Wrong Drawing Order
A large rectangle drawn last may hide earlier details. Draw background elements first and details later.
5. Font File Not Found
A custom .ttf font may not exist on another computer.
Use a valid font path or fall back to Pillow's default font.
Practice Tasks
Write one line that draws a red line from
(20, 40) to (300, 200)
with width 5.
Explain why [(100, 50), (220, 170)] creates a circle
when used with draw.ellipse().
Draw a traffic light: a dark rectangular body containing three circles — red, yellow and green.
Draw a smiley face using an ellipse for the head and eyes,
and an arc for the mouth. Save it as smiley.png.
A student draws a roof before the large house-body rectangle and the lower part of the roof disappears. Explain why and fix the drawing order.
Create a landscape using at least five different primitives: sky, ground, sun, trees, a building and text.
Write either draw_tree(...) or
draw_building(...) as a reusable function,
then call it at least three times with different arguments.
Self-Check Quiz
Q1: How do you create a drawing object for img?
draw = ImageDraw.Draw(img)
Q2: What is the difference between fill and outline?
fill colours the interior; outline colours the border.
Q3: What coordinates does an ellipse normally use?
[(x1, y1), (x2, y2)].
Q4: When does draw.ellipse() produce a circle?
Q5: How is a polygon different from a rectangle?
Q6: Why does drawing order matter?