Pillow thumbnail + EXIF orientation stripped: working versions (tested 2026-10-16)
Build aspect-correct JPEG thumbnails with Pillow, auto-rotating by EXIF orientation before resizing and stripping the stale orientation tag on save.
passed5 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
pillow@12.3.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pillow-image-thumbnail") (install), or see them on GitHub.
requirements.txt
pillow==12.3.0 pytest==9.1.1
thumbnailer.py
from PIL import Image, ImageOps
def make_thumbnail(src, dst, size=(128, 128), quality=85):
with Image.open(src) as image:
image = ImageOps.exif_transpose(image)
image.thumbnail(size, Image.Resampling.LANCZOS)
kwargs = {}
exif = image.getexif()
if exif:
kwargs["exif"] = exif.tobytes()
image.save(dst, quality=quality, **kwargs)
tests/test_thumbnailer.py
from PIL import Image
from thumbnailer import make_thumbnail
def make_jpeg(path, size, orientation=None, artist=None):
image = Image.new("RGB", size, (200, 100, 50))
exif = image.getexif()
if orientation is not None:
exif[274] = orientation
if artist is not None:
exif[315] = artist
if exif:
image.save(path, format="JPEG", exif=exif)
else:
image.save(path, format="JPEG")
return path
def test_thumbnail_scales_down_keeping_aspect(tmp_path):
src = make_jpeg(tmp_path / "src.jpg", (400, 200))
dst = tmp_path / "thumb.jpg"
make_thumbnail(src, dst, size=(128, 128))
with Image.open(dst) as out:
assert out.size == (128, 64)
def test_thumbnail_does_not_upscale(tmp_path):
src = make_jpeg(tmp_path / "src.jpg", (100, 80))
dst = tmp_path / "thumb.jpg"
make_thumbnail(src, dst, size=(128, 128))
with Image.open(dst) as out:
assert out.size == (100, 80)
def test_orientation_applied_before_scaling(tmp_path):
src = make_jpeg(tmp_path / "src.jpg", (400, 200), orientation=6)
dst = tmp_path / "thumb.jpg"
make_thumbnail(src, dst, size=(128, 128))
with Image.open(dst) as out:
assert out.size == (64, 128)
def test_orientation_tag_is_stripped(tmp_path):
src = make_jpeg(tmp_path / "src.jpg", (400, 200), orientation=6)
dst = tmp_path / "thumb.jpg"
make_thumbnail(src, dst, size=(128, 128))
with Image.open(dst) as out:
assert out.getexif().get(274) is None
def test_other_exif_tags_survive(tmp_path):
src = make_jpeg(tmp_path / "src.jpg", (400, 200), orientation=6, artist="provenpin")
dst = tmp_path / "thumb.jpg"
make_thumbnail(src, dst, size=(128, 128))
with Image.open(dst) as out:
assert out.getexif().get(315) == "provenpin"
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.