Pillow Image Editor
Programmatic image editing via Pillow (PIL fork). Pillow 10.4+ is installed in the system Python.
When to use
- Resize / crop / rotate / flip / transpose
- Format conversion (PNG, JPEG, WebP, GIF, AVIF, TIFF, BMP)
- Compression / quality tuning / file-size reduction
- Filters: blur, sharpen, edge detection, emboss, contour
- Adjustments: brightness, contrast, saturation, color balance
- Compositing: paste, alpha blend, transparency, masks
- Annotation: draw text, shapes, lines, watermarks, logos
- EXIF: read, strip, or preserve metadata
- Thumbnails, ICO/favicon generation, sprite sheets
- Batch processing a folder of images
For generating images from prompts, prefer gemini-imagegen or kie-ai. This skill is for deterministic pixel-level edits to existing files.
Quick start
Pillow is already installed. Import like this:
from PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageEnhance, ImageFont, ExifTags
Minimal operations
# Resize (preserve aspect ratio, fit within box)
img = Image.open("in.jpg")
img.thumbnail((1200, 1200)) # modifies in place, keeps aspect
img.save("out.jpg", quality=85, optimize=True)
# Hard resize to exact dimensions
img.resize((800, 600), Image.Resampling.LANCZOS).save("out.png")
# Crop (left, upper, right, lower)
img.crop((100, 50, 900, 650)).save("cropped.png")
# Rotate / flip
img.rotate(90, expand=True).save("rotated.png")
ImageOps.mirror(img).save("flipped_horizontal.png")
ImageOps.flip(img).save("flipped_vertical.png")
# Convert format / color mode
Image.open("in.png").convert("RGB").save("out.jpg", quality=90) # drop alpha for JPEG
Filters & adjustments
from PIL import ImageFilter, ImageEnhance
img.filter(ImageFilter.GaussianBlur(radius=5))
img.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
img.filter(ImageFilter.FIND_EDGES)
ImageEnhance.Brightness(img).enhance(1.2) # >1 brighter, <1 darker
ImageEnhance.Contrast(img).enhance(1.5)
ImageEnhance.Color(img).enhance(0.0) # 0.0 = grayscale, 1.0 = original
ImageEnhance.Sharpness(img).enhance(2.0)
Text and watermarks
Use ImageDraw + ImageFont.truetype for real typography. On Windows, common fonts live under C:/Windows/Fonts/ (e.g. arial.ttf, segoeui.ttf). Fall back to ImageFont.load_default() only for quick dev work — it looks bad.
from PIL import Image, ImageDraw, ImageFont
img = Image.open("photo.jpg").convert("RGBA")
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 48)
text = "(c) Example"
# Measure to position bottom-right with 24px padding
bbox = draw.textbbox((0, 0), text, font=font)
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
x = img.width - w - 24
y = img.height - h - 24
# Semi-transparent shadow then white text
draw.text((x + 2, y + 2), text, font=font, fill=(0, 0, 0, 160))
draw.text((x, y), text, font=font, fill=(255, 255, 255, 220))
Image.alpha_composite(img, overlay).convert("RGB").save("watermarked.jpg", quality=92)
Compositing with transparency
# Paste a logo onto a photo, respecting the logo's alpha
base = Image.open("photo.jpg").convert("RGBA")
logo = Image.open("logo.png").convert("RGBA")
# Optionally resize logo to ~15% of base width
target_w = base.width // 6
ratio = target_w / logo.width
logo = logo.resize((target_w, int(logo.height * ratio)), Image.Resampling.LANCZOS)
base.paste(logo, (base.width - logo.width - 20, base.height - logo.height - 20), logo)
base.convert("RGB").save("composed.jpg", quality=90)
EXIF / orientation
Phone photos are often saved rotated with EXIF orientation flags. Always run ImageOps.exif_transpose before processing, or the output will be sideways.
img = ImageOps.exif_transpose(Image.open("phone_photo.jpg"))
# Strip EXIF (for privacy / smaller files): re-save without info
data = list(img.getdata())
clean = Image.new(img.mode, img.size)
clean.putdata(data)
clean.save("no_exif.jpg", quality=90)
Thumbnails and favicons
img.thumbnail((256, 256))
img.save("thumb.png")
# Multi-size ICO for favicon
Image.open("logo.png").save(
"favicon.ico",
sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
Format cheat-sheet
| Format | Good for | Notes |
|---|---|---|
| PNG | Logos, UI, anything with alpha | Lossless; use optimize=True |
| JPEG | Photos | Requires RGB mode (convert first); quality=85-92 is usually right |
| WebP | Web delivery, smaller than JPEG/PNG | quality=80, method=6 for best compression; lossless=True available |
| AVIF | Even smaller, modern browsers | Needs pillow-avif-plugin; skip if not installed |
| GIF | Simple anims, 256 colors max | Use save_all=True, append_images=[...], duration=N, loop=0 |
| TIFF | Archival, print | Supports multi-page and 16-bit |
Common pitfall: saving a PNG-with-alpha as JPEG fails with OSError: cannot write mode RGBA as JPEG. Fix with .convert("RGB") first.
Resampling filters
When resizing, the resampling filter matters:
Image.Resampling.LANCZOS- best quality for downscaling photos (default pick)Image.Resampling.BICUBIC- good quality, fasterImage.Resampling.NEAREST- pixel art / when you want hard edges
Batch processing
For folders of images, prefer pathlib.Path.glob and process one file at a time. Always open inside a with block or call .close() — Pillow uses lazy loading and can leak file handles on Windows.
from pathlib import Path
from PIL import Image, ImageOps
src = Path("input/")
dst = Path("output/")
dst.mkdir(parents=True, exist_ok=True)
for path in list(src.glob("*.jpg")) + list(src.glob("*.png")):
with Image.open(path) as im:
im = ImageOps.exif_transpose(im)
im.thumbnail((1600, 1600))
out = dst / f"{path.stem}.webp"
im.save(out, "WEBP", quality=82, method=6)
print(f"{path.name} -> {out.name}")
File management
- Write outputs to
./output/under the current working directory (create it withPath("output").mkdir(parents=True, exist_ok=True)). - Never overwrite the user's source files unless they explicitly ask — save to a new path.
Gotchas
- Mode mismatch: Many operations require
"RGB"or"RGBA". Convert explicitly — don't assume. - JPEG + alpha: Convert to
RGBfirst; JPEG has no alpha channel. - EXIF rotation: Phone photos render sideways without
ImageOps.exif_transpose. - Windows fonts:
ImageFont.truetype("arial.ttf", 24)works on Windows because the fonts dir is on the font search path; absolute paths are safer cross-platform. - Palette images (GIF): Convert to
"RGB"or"RGBA"before filters/enhancements, or results look wrong. - Very large images: Pillow raises
DecompressionBombErrorover ~179M pixels. SetImage.MAX_IMAGE_PIXELS = Noneonly if you trust the source. - File handles on Windows: Use
with Image.open(...) as img:— not doing this can leave files locked.