ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.