python 100 lines · 3 tabs

Validate and Generate Image Thumbnails on a Flask Upload Endpoint With Pillow

Shared by codesnips Aug 2026
3 tabs
import io
import os
from uuid import uuid4
from PIL import Image, UnidentifiedImageError

ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP"}
VARIANTS = {"thumb": (200, 200), "medium": (800, 800)}


class InvalidImageError(Exception):
    pass


def load_and_validate(stream):
    try:
        probe = Image.open(stream)
        probe.verify()  # consumes the stream, leaves probe unusable
    except (UnidentifiedImageError, OSError):
        raise InvalidImageError("file is not a valid image")

    if probe.format not in ALLOWED_FORMATS:
        raise InvalidImageError(f"unsupported format: {probe.format}")

    stream.seek(0)
    return Image.open(stream)


def _flatten(img):
    if img.mode in ("RGBA", "LA", "P"):
        background = Image.new("RGB", img.size, (255, 255, 255))
        rgba = img.convert("RGBA")
        background.paste(rgba, mask=rgba.split()[-1])
        return background
    return img.convert("RGB")


def make_thumbnail(img, size):
    copy = img.copy()
    copy.thumbnail(size, Image.LANCZOS)
    return _flatten(copy)


def generate_variants(stream, dest_dir):
    img = load_and_validate(stream)
    stem = uuid4().hex
    os.makedirs(dest_dir, exist_ok=True)

    urls = {}
    for name, size in VARIANTS.items():
        variant = make_thumbnail(img, size)
        filename = f"{stem}_{name}.jpg"
        variant.save(os.path.join(dest_dir, filename), "JPEG", quality=85, optimize=True)
        urls[name] = f"/media/{filename}"
    return urls
3 files · python Explain with highlit

This snippet shows how a Flask endpoint accepts an image upload, validates it, and produces resized thumbnails using Pillow, keeping the risky image-handling logic separate from the HTTP layer.

The image_service module owns everything about decoding and resizing. load_and_validate opens the incoming stream with Image.open, then calls img.verify() to confirm the bytes actually decode as an image before trusting them. Because verify() consumes the file object and leaves the image unusable, the stream is rewound with stream.seek(0) and reopened — a well-known Pillow gotcha that catches many first-time implementations. The function also rejects unexpected formats via ALLOWED_FORMATS, so a renamed .php masquerading as .jpg never reaches disk.

make_thumbnail uses img.thumbnail(size), which resizes in place while preserving aspect ratio and never upscales past the original — it only shrinks to fit within the bounding box. Image.LANCZOS is chosen as the resampling filter because it gives the best quality for downscaling. Before saving, _flatten converts modes like RGBA or P onto a white RGB background, which avoids the black-box artifact that appears when saving transparent PNGs as JPEG. Each variant is written under a random uuid4 stem so uploads never collide or overwrite each other.

In upload_routes, the blueprint reads the file from request.files, guards against an empty filename, and enforces a byte ceiling by inspecting the stream length against MAX_BYTES before doing any decoding — cheap rejection first. It then delegates to the service, catches InvalidImageError to return a clean 400 instead of a stack trace, and responds with the generated variant URLs as JSON.

The key idea is layering: the HTTP handler stays thin and only deals with request shape and error mapping, while all format sniffing, resampling, and mode flattening live in a testable service. This separation makes the untrusted-input handling auditable in one place, and lets the same generate_variants function be reused by a background job or CLI without dragging in Flask's request context. A production version would stream large files to a temp path and offload resizing to a worker, but the validation and thumbnail flow shown here is the durable core.


Related snips

Share this code

Here's the card — post it anywhere.

Validate and Generate Image Thumbnails on a Flask Upload Endpoint With Pillow — share card
Link copied