How to draw shapes and text on images using Pillow in Python

How to draw shapes and text on images using Pillow in Python

Pillow, the Python Imaging Library fork, makes drawing simpler and surprisingly flexible. At its core, you use ImageDraw to sketch directly onto an image canvas, whether you’re making a quick visualization or building up complex graphics programmatically.

First, you create a blank image to draw on. This usually means setting a mode (like “RGB”) and a size tuple. From there, instantiate ImageDraw.Draw to get your drawing context. That context offers primitives for shapes: rectangles, ellipses, lines, and polygons – all with customizable fill and outline parameters.

from PIL import Image, ImageDraw

img = Image.new("RGB", (200, 200), "white")
draw = ImageDraw.Draw(img)

# Draw a red rectangle with a black outline
rect_coords = (20, 20, 180, 100)
draw.rectangle(rect_coords, fill="red", outline="black")

# Draw a blue ellipse inside the rectangle bounds
draw.ellipse(rect_coords, fill="blue", outline="green")

# Draw a diagonal line across the image
draw.line((0, 0, 200, 200), fill="purple", width=3)

# Draw a polygon (triangle) with a yellow fill
triangle = [(50, 150), (150, 150), (100, 50)]
draw.polygon(triangle, fill="yellow", outline="black")

img.show()

Notice how the coordinate parameters work: tuples of (x0, y0, x1, y1) define rectangular bounding boxes. These pads the shapes inside pixel regions you specify, so a rectangle from (20,20) to (180,100) spans that exact box. For a line or polygon, you just list points sequentially.

Control over outlines and fills gives crisp separation or transparent looks by simply omitting parameters (pass None). For example, if you want a circle with only an outline, set fill=None and specify an outline color. Adjust line width where applicable by using the width argument.

When you need arcs or pieslices, ImageDraw also supports those using angle parameters measured in degrees, starting at 3 o’clock and moving counterclockwise. Try this:

# Draw an arc from 45 to 270 degrees inside the rectangle
draw.arc(rect_coords, start=45, end=270, fill="orange", width=4)

# Draw a pieslice (filled arc)
draw.pieslice(rect_coords, start=0, end=120, fill="cyan", outline="blue")

This low-level approach means you can build quite detailed images piece by piece. It’s ideal when you want procedural graphics without depending on SVGs or external vector formats. Just remember: the coordinate system’s origin is top-left, with y increasing downward, which can trip up newcomers.

Before you move on, keep in mind transparency—use the “RGBA” mode for your base image if you want colors with alpha channels. Also, you can layer drawings by drawing multiple shapes on the same ImageDraw instance before saving or showing the image.

Rendering text with custom fonts and styles

Adding text to images with Pillow requires the ImageFont module in addition to ImageDraw. You start by loading a TrueType font file (*.ttf) at a chosen size, which gives you control over style and weight beyond the default bitmap font. This very important if you want professional or custom typography rather than a generic system fallback.

Here’s a minimal example that draws a string using a custom font:

from PIL import Image, ImageDraw, ImageFont

img = Image.new("RGB", (300, 100), "white")
draw = ImageDraw.Draw(img)

# Load a TrueType font file at size 40
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 40)

# Position to start text drawing
text_position = (10, 20)

# Draw text with fill color
draw.text(text_position, "Hello, Pillow!", font=font, fill="navy")

img.show()

If you don’t specify a font, Pillow defaults to a very basic bitmap font which lacks sizing and styling—making truetype() essential for any serious work. Fonts can be loaded from absolute paths or relative ones if you package your assets correctly.

To style text further, you can manipulate colors, apply shadows, or even simulate bold and italic by drawing multiple offset copies or skewing glyphs manually. For example, emulate a drop shadow by drawing the text twice with pixel offset and contrasting colors:

shadow_offset = (2, 2)
base_position = (20, 30)

# Draw shadow in gray, slightly offset
draw.text((base_position[0] + shadow_offset[0], base_position[1] + shadow_offset[1]), 
          "Shadow Text", font=font, fill="gray")

# Draw main text on top in black
draw.text(base_position, "Shadow Text", font=font, fill="black")

Text measurement is another useful feature. Before placing text, you often need to know its pixel size to center or wrap it properly. Use font.getsize() or the more modern draw.textbbox() to find bounding box dimensions:

text = "Centered"
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]

# Image center coords
img_center = (img.width // 2, img.height // 2)

# Calculate top-left coords to center the text
x = img_center[0] - text_width // 2
y = img_center[1] - text_height // 2

draw.text((x, y), text, font=font, fill="darkgreen")

Note that textbbox returns a 4-tuple (left, top, right, bottom) relative to the origin you supply. This means offsets, ascenders, and descenders are taken into account for precise alignment, unlike getsize() which can be more approximate.

For languages and scripts requiring complex layout or shaping (like Arabic or Indic scripts), Pillow’s native text rendering falls short, since it lacks built-in support for ligatures and glyph positioning. In those cases, pairing Pillow with libraries such as HarfBuzz or Pango, or pre-processing text into images with higher-level engines, is recommended.

Lastly, color management extends beyond simple names or RGB tuples. Pillow accepts RGBA tuples if your image has transparency enabled. This lets you render semi-transparent text efficiently, e.g.:

rgba_color = (255, 0, 0, 128)  # Red at 50% opacity

img = Image.new("RGBA", (300, 100), (255, 255, 255, 255))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 40)

draw.text((20, 20), "Semi-Transparent", font=font, fill=rgba_color)

img.show()

Using transparency in text unlocks effects like fading captions, watermarks, or overlay labels, all composable with your existing artwork layers.

Combining shapes and text for composite images

Combining shapes and text in Pillow allows for the creation of composite images that can convey information or simply enhance visual charm. To achieve this, you can layer your drawings by first sketching the shapes and then overlaying the text, or vice versa, depending on the desired outcome.

To illustrate this, let’s start with a simple example that draws a rectangle and then adds text on top of it. This layering approach is straightforward:

from PIL import Image, ImageDraw, ImageFont

# Create a blank image
img = Image.new("RGB", (400, 200), "white")
draw = ImageDraw.Draw(img)

# Draw a blue rectangle
rect_coords = (50, 50, 350, 150)
draw.rectangle(rect_coords, fill="lightblue", outline="black")

# Load a TrueType font
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 24)

# Position for the text
text_position = (100, 80)

# Add text on top of the rectangle
draw.text(text_position, "Welcome to Pillow!", font=font, fill="darkblue")

img.show()

In this example, the rectangle serves as a background for the text, creating a clean and visually appealing layout. You can adjust the text position to ensure it fits well within the shape, keeping in mind the dimensions of both the rectangle and the text size.

For more complex designs, consider using multiple shapes and text elements. Here’s how you can create a badge-like effect with a circle and text:

# Create a new image
img = Image.new("RGB", (300, 300), "white")
draw = ImageDraw.Draw(img)

# Draw a circle
circle_center = (150, 150)
circle_radius = 100
draw.ellipse((circle_center[0] - circle_radius, circle_center[1] - circle_radius,
               circle_center[0] + circle_radius, circle_center[1] + circle_radius),
              fill="lightgreen", outline="black")

# Add text in the center of the circle
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
text = "Badge"
text_size = draw.textsize(text, font=font)
text_position = (circle_center[0] - text_size[0] // 2, circle_center[1] - text_size[1] // 2)

draw.text(text_position, text, font=font, fill="darkgreen")

img.show()

This code demonstrates how to create a badge-like appearance by placing text centrally within a circle. The textsize() method especially important here, as it allows you to calculate the dimensions of the text, ensuring proper centering.

When combining elements, layering effects can enhance the visual interest. For instance, you can draw shadows beneath text to create depth. Here’s a way to add a shadow effect under the text:

# Create a new image
img = Image.new("RGB", (300, 300), "white")
draw = ImageDraw.Draw(img)

# Draw a rectangle as a background
draw.rectangle([0, 0, 300, 300], fill="lightgray")

# Define text properties
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 40)
text = "Hello, World!"

# Shadow parameters
shadow_offset = (2, 2)
shadow_color = "gray"

# Draw shadow
draw.text((50 + shadow_offset[0], 100 + shadow_offset[1]), text, font=font, fill=shadow_color)

# Draw main text
draw.text((50, 100), text, font=font, fill="black")

img.show()

In this example, the shadow is drawn slightly offset from the main text, adding a subtle three-dimensional effect. Adjusting the shadow’s color and position can yield various stylistic results.

Remember, when layering shapes and text, the order of drawing matters. The last drawn element appears on top of previously drawn elements. This principle allows for flexible designs where you can create intricate visuals by carefully planning your drawing sequence.

Moreover, you can experiment with different blending modes and transparency for more sophisticated compositions. By using RGBA images, you can achieve effects like translucent overlays, which can be particularly useful for creating watermarks or overlays that don’t completely obscure underlying content.

# Create an image with transparency
img = Image.new("RGBA", (400, 400), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)

# Draw a semi-transparent rectangle
draw.rectangle([50, 50, 350, 350], fill=(255, 0, 0, 128))

# Draw text on top
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
draw.text((100, 150), "Overlay Text", font=font, fill=(0, 0, 0, 255))

img.show()

This example showcases the power of RGBA images in Pillow. The semi-transparent rectangle allows the background to show through, creating a layered visual effect that can be adjusted by changing the alpha value.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *