Coverage for server / utilities / image_processing.py: 75%
24 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1import io
2from typing import IO
4from fastapi import HTTPException, status
5from PIL import Image, ImageOps, UnidentifiedImageError
7# Fixed output target for profile pictures: regardless of the uploaded
8# image's original dimensions or format, storage size stays predictable.
9PROFILE_PICTURE_MAX_DIMENSION = 512
10PROFILE_PICTURE_JPEG_QUALITY = 85
13def compress_profile_picture(file_obj: IO) -> io.BytesIO:
14 """Resize and re-encode an uploaded image into a fixed-size JPEG.
16 Caps the longest side at ``PROFILE_PICTURE_MAX_DIMENSION`` (never
17 upscales smaller images) and always outputs JPEG so every profile
18 picture is compressed to the same fixed target, no matter the source
19 size or format (PNG/JPEG, with or without transparency).
20 """
21 file_obj.seek(0)
22 try:
23 with Image.open(file_obj) as image:
24 image = ImageOps.exif_transpose(image)
26 if image.mode in ("RGBA", "P", "LA"):
27 rgba_image = image.convert("RGBA")
28 background = Image.new("RGB", rgba_image.size, (255, 255, 255))
29 background.paste(rgba_image, mask=rgba_image.split()[-1])
30 image = background
31 else:
32 image = image.convert("RGB")
34 image.thumbnail(
35 (PROFILE_PICTURE_MAX_DIMENSION, PROFILE_PICTURE_MAX_DIMENSION),
36 Image.LANCZOS,
37 )
39 buffer = io.BytesIO()
40 image.save(
41 buffer,
42 format="JPEG",
43 quality=PROFILE_PICTURE_JPEG_QUALITY,
44 optimize=True,
45 )
46 except UnidentifiedImageError as exc:
47 raise HTTPException(
48 status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
49 detail="Unable to process image. Please upload a valid JPG or PNG image.",
50 ) from exc
52 buffer.seek(0)
53 return buffer