Unit 12.1A · Term 1

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

Lesson Presentation

12.1A-pil-drawing.pdf · Slides for classroom use

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.

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.

Primitive 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

x1 90
y1 65
x2 400
y2 275
draw.ellipse([(90, 65), (400, 275)], fill=(59, 130, 246), outline=(0, 0, 0), width=4)
Rectangle [(x1,y1), (x2,y2)]

The box itself becomes the rectangle.

Ellipse [(x1,y1), (x2,y2)]

The ellipse touches the four sides of the invisible box.

Circle 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.

Current step Image.new() — create sky canvas Added code img = Image.new("RGB", (520, 360), (135, 206, 235))
Ground rectangle()
House + door rectangle()
Roof polygon()
Sun ellipse()
Tree trunk rectangle()
Tree crown 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

Remember

Write one line that draws a red line from (20, 40) to (300, 200) with width 5.

Understand

Explain why [(100, 50), (220, 170)] creates a circle when used with draw.ellipse().

Apply

Draw a traffic light: a dark rectangular body containing three circles — red, yellow and green.

Apply

Draw a smiley face using an ellipse for the head and eyes, and an arc for the mouth. Save it as smiley.png.

Analyze

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

Create a landscape using at least five different primitives: sky, ground, sun, trees, a building and text.

Create

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?

Two opposite corners of its bounding box: [(x1, y1), (x2, y2)].

Q4: When does draw.ellipse() produce a circle?

When the bounding box is square — its width and height are equal.

Q5: How is a polygon different from a rectangle?

A polygon can use any number of vertices, while a rectangle is described by a rectangular bounding box.

Q6: Why does drawing order matter?

New primitives are painted onto the existing image, so later shapes can cover shapes drawn earlier.