# Pillow Image Editor

> Pillow Image Editor

- Skill: `buzzmatic/pillow-image-editor` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add buzzmatic/pillow-image-editor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/buzzmatic/pillow-image-editor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Buzzmatic (https://skillmd.com/u/buzzmatic)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/buzzmatic/pillow-image-editor

---


# Pillow Image Editor

Programmatic image editing via [Pillow](https://github.com/python-pillow/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:

```python
from PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageEnhance, ImageFont, ExifTags
```

### Minimal operations

```python
# 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

```python
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.

```python
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

```python
# 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.

```python
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

```python
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, faster
- `Image.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.

```python
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 with `Path("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 `RGB` first; 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 `DecompressionBombError` over ~179M pixels. Set `Image.MAX_IMAGE_PIXELS = None` only if you trust the source.
- **File handles on Windows:** Use `with Image.open(...) as img:` — not doing this can leave files locked.

## Further reading

- Handbook: https://pillow.readthedocs.io/en/stable/handbook/index.html
- API reference: https://pillow.readthedocs.io/en/stable/reference/Image.html
- GitHub: https://github.com/python-pillow/Pillow

