skip to content

Pillow β€” Image Processing

Open, resize, crop, convert, and save images with Pillow (PIL fork). Covers format conversion, filters, drawing, and EXIF handling.

3 min read 9 snippets yesterday quick read

Pillow β€” Image Processing#

What it is#

Pillow is the maintained fork of the original Python Imaging Library (PIL). It supports reading and writing over 30 image formats (JPEG, PNG, GIF, TIFF, WebP, BMP, and more) and provides transforms, filters, color mode conversions, and drawing primitives.

Install#

pip install pillow

Quick example#

from PIL import Image

# Create a solid-color image (no file needed)
img = Image.new("RGB", (400, 200), color=(73, 109, 137))
print(f"Size: {img.size}, Mode: {img.mode}")
img.save("blue_rect.png")
print("Saved blue_rect.png")

Output:

Size: (400, 200), Mode: RGB
Saved blue_rect.png

When / why to use it#

  • Batch-resize or convert images (thumbnails, WebP conversion).
  • Add watermarks or text to images programmatically.
  • Preprocess images before passing to a model (resize, normalize, convert to grayscale).
  • Read image metadata (EXIF: camera settings, GPS coordinates).
  • Generate simple graphics or diagrams with ImageDraw.

Common pitfalls#

[!WARNING] File stays open β€” Image.open() opens a file handle lazily. If you plan to do many opens in a loop, call .load() or use with Image.open(...) as img: to ensure the handle is closed.

[!WARNING] JPEG quality loss β€” every JPEG save re-encodes and loses quality. If you need lossless editing, work in PNG until the final step. Set quality=85 as a reasonable default for JPEG output.

[!WARNING] EXIF orientation β€” JPEG photos often store rotation in EXIF rather than pixel data. img.resize() ignores EXIF orientation. Use ImageOps.exif_transpose(img) to apply the rotation first.

Richer example β€” resize, convert, and strip EXIF#

from PIL import Image, ImageOps, ImageFilter

with Image.open("photo.jpg") as img:
    # Apply EXIF rotation so the image is physically correct
    img = ImageOps.exif_transpose(img)

    print(f"Original: {img.size} {img.mode}")

    # Resize to fit within 800Γ—600 while preserving aspect ratio
    img.thumbnail((800, 600), Image.LANCZOS)
    print(f"Thumbnail: {img.size}")

    # Convert to grayscale and sharpen
    gray = img.convert("L")
    sharpened = gray.filter(ImageFilter.SHARPEN)

    sharpened.save("processed.jpg", quality=85, optimize=True)
    print("Saved processed.jpg")

Output:

Original: (3024, 4032) RGB
Thumbnail: (450, 600)
Saved processed.jpg

Format conversion#

from PIL import Image

# Convert JPEG to WebP (modern, smaller files)
with Image.open("photo.jpg") as img:
    img.save("photo.webp", "WEBP", quality=80)
    print("Saved photo.webp")

# Convert to PNG (lossless)
with Image.open("photo.jpg") as img:
    img.save("photo.png", "PNG")
    print("Saved photo.png")

Output:

Saved photo.webp
Saved photo.png

Drawing text and shapes#

from PIL import Image, ImageDraw, ImageFont

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

draw.rectangle([(20, 20), (380, 180)], outline="navy", width=3)
draw.ellipse([(150, 70), (250, 130)], fill="coral")
draw.text((160, 90), "Hello!", fill="white")

img.save("drawing.png")
print("Saved drawing.png")

Output:

Saved drawing.png

[!TIP] To use a TTF font: font = ImageFont.truetype("Arial.ttf", size=24) then pass font=font to draw.text(). Without this, Pillow uses a tiny bitmap fallback font.

Useful operations#

TaskCode
Open fileImage.open("path.jpg")
Get sizeimg.size β†’ (width, height)
Resize exactimg.resize((w, h), Image.LANCZOS)
Fit in boximg.thumbnail((w, h))
Cropimg.crop((x1, y1, x2, y2))
Rotateimg.rotate(90, expand=True)
FlipImageOps.flip(img) / ImageOps.mirror(img)
Convert modeimg.convert("L") (grayscale), img.convert("RGBA")
Saveimg.save("out.png")
Read EXIFimg._getexif() or img.getexif() (Pillow β‰₯ 6)