---
name: pil-photo-compression-pipeline
description: PIL/Pillow photo compression pipeline — thumbnail() with LANCZOS + JPEG quality 82 progressive; re-encoding already-compressed JPEGs gives no size benefit
type: concept
---

# PIL Photo Compression Pipeline

## The Standard Pipeline

```python
from PIL import Image
import io

def compress_photo(image_bytes: bytes, max_dimension: int = 1200) -> bytes:
    with Image.open(io.BytesIO(image_bytes)) as img:
        # Convert RGBA/P to RGB (JPEG doesn't support alpha)
        if img.mode in ('RGBA', 'P'):
            img = img.convert('RGB')

        # thumbnail() is in-place and respects aspect ratio
        # LANCZOS = best quality for downscaling
        img.thumbnail((max_dimension, max_dimension), Image.LANCZOS)

        output = io.BytesIO()
        img.save(
            output,
            format='JPEG',
            quality=82,       # sweet spot: good quality, ~60% size reduction vs 100
            progressive=True, # progressive JPEG loads from blurry to sharp (better UX)
            optimize=True,    # run Huffman optimizer
        )
        return output.getvalue()
```

## Key Choices

| Choice | Why |
|--------|-----|
| `thumbnail()` | Preserves aspect ratio; no-ops if already smaller |
| `LANCZOS` | Best resampling for photo downscaling (Pillow 10+: `Image.Resampling.LANCZOS`) |
| JPEG quality 82 | ~60% file size vs 100; visually indistinguishable for photos |
| `progressive=True` | Browser shows low-res preview while loading — better perceived perf |
| `optimize=True` | Lossless size reduction via better Huffman tables (~5% extra) |

## WebP Re-encoding Gotcha

Re-encoding a JPEG that was already compressed at ~82% quality to WebP gives **no meaningful size benefit**:

- Input: JPEG 82% (e.g., 450 KB)
- Output WebP: 440 KB — essentially the same
- The information loss from the first JPEG compression is already done; WebP can't recover it

**Use WebP only** for images you control from source (screenshots, graphics, PNGs), not for re-encoding existing JPEGs.

## Pillow Version Notes

- Pillow 10+: `Image.LANCZOS` → `Image.Resampling.LANCZOS` (old name still works as alias)
- Pillow 9+: `Image.Transpose.FLIP_LEFT_RIGHT` etc. (use `Image.Resampling` for resample filters)

## GCS Upload After Compression

```python
from google.cloud import storage

def upload_compressed_photo(bucket_name: str, blob_name: str, compressed: bytes) -> str:
    client = storage.Client()
    bucket = client.bucket(bucket_name)
    blob = bucket.blob(blob_name)
    blob.upload_from_string(compressed, content_type='image/jpeg')
    return blob.public_url
```

**Source:** daily/2026-05-13.md | 2026-05-13
