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

1import io 

2from typing import IO 

3 

4from fastapi import HTTPException, status 

5from PIL import Image, ImageOps, UnidentifiedImageError 

6 

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 

11 

12 

13def compress_profile_picture(file_obj: IO) -> io.BytesIO: 

14 """Resize and re-encode an uploaded image into a fixed-size JPEG. 

15 

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) 

25 

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") 

33 

34 image.thumbnail( 

35 (PROFILE_PICTURE_MAX_DIMENSION, PROFILE_PICTURE_MAX_DIMENSION), 

36 Image.LANCZOS, 

37 ) 

38 

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 

51 

52 buffer.seek(0) 

53 return buffer