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
from flask import Blueprint, current_app, jsonify, request
from image_service import generate_variants, InvalidImageError
bp = Blueprint("uploads", __name__)
MAX_BYTES = 8 * 1024 * 1024
def _stream_size(stream):
stream.seek(0, 2)
size = stream.tell()
stream.seek(0)
return size
@bp.route("/uploads", methods=["POST"])
def upload_image():
file = request.files.get("image")
if file is None or file.filename == "":
return jsonify(error="no image field provided"), 400
if _stream_size(file.stream) > MAX_BYTES:
return jsonify(error="image exceeds 8MB limit"), 413
try:
urls = generate_variants(file.stream, current_app.config["MEDIA_DIR"])
except InvalidImageError as exc:
return jsonify(error=str(exc)), 400
return jsonify(variants=urls), 201
from flask import Flask
from upload_routes import bp as uploads_bp
def create_app():
app = Flask(__name__)
app.config["MEDIA_DIR"] = "/var/app/media"
app.config["MAX_CONTENT_LENGTH"] = 8 * 1024 * 1024
app.register_blueprint(uploads_bp)
return app
if __name__ == "__main__":
create_app().run(debug=True)
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
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
import cv2
image = cv2.imread('receipt.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresholded = cv2.adaptiveThreshold(
OpenCV image preprocessing for OCR and vision pipelines
# Installation
# rails active_storage:install
# rails db:migrate
# config/storage.yml
local:
ActiveStorage for file uploads and attachments
import axios from 'axios';
export type NormalizedErrors = {
fields: Record<string, string>;
formLevel: string | null;
};
Frontend: normalize and display server validation errors
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
Laravel form requests for validation
Share this code
Here's the card — post it anywhere.