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 usewith 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=85as 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. UseImageOps.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 passfont=fonttodraw.text(). Without this, Pillow uses a tiny bitmap fallback font.
Useful operations#
| Task | Code |
|---|---|
| Open file | Image.open("path.jpg") |
| Get size | img.size β (width, height) |
| Resize exact | img.resize((w, h), Image.LANCZOS) |
| Fit in box | img.thumbnail((w, h)) |
| Crop | img.crop((x1, y1, x2, y2)) |
| Rotate | img.rotate(90, expand=True) |
| Flip | ImageOps.flip(img) / ImageOps.mirror(img) |
| Convert mode | img.convert("L") (grayscale), img.convert("RGBA") |
| Save | img.save("out.png") |
| Read EXIF | img._getexif() or img.getexif() (Pillow β₯ 6) |