From 856cec6a0e7c489f77f35c34f0e416222a5cc692 Mon Sep 17 00:00:00 2001 From: JohnGrubba Date: Wed, 2 Oct 2024 10:23:18 +0200 Subject: [PATCH] Add GIF Functionality --- config/configtemplate.json | 5 +- src/api/profile.py | 71 +++++++++++++++++++++---- src/tools/conf/AccountFeaturesConfig.py | 40 ++++++++++++++ src/tools/conf/testing_config.json | 5 +- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/config/configtemplate.json b/config/configtemplate.json index 187f138..a3e5524 100644 --- a/config/configtemplate.json +++ b/config/configtemplate.json @@ -51,7 +51,10 @@ "width": 200, "height": 200 }, - "quality": 80 + "quality": 80, + "allow_gif": true, + "max_gif_frames": 200, + "max_size_mb": 15 } }, "security": { diff --git a/src/api/profile.py b/src/api/profile.py index deb1ad6..31ffe7d 100644 --- a/src/api/profile.py +++ b/src/api/profile.py @@ -24,7 +24,7 @@ import bson import json import io -from PIL import Image +from PIL import Image, ImageSequence router = APIRouter(prefix="/profile", tags=["Profile"]) @@ -54,9 +54,11 @@ async def update_profile( ## Description This endpoint is used to update the profile information of the user. """ - return bson_to_json(update_public_user( - user["_id"], update_data.model_dump(exclude_none=True), background_tasks - )) + return bson_to_json( + update_public_user( + user["_id"], update_data.model_dump(exclude_none=True), background_tasks + ) + ) @router.post("/confirm-email-change", status_code=204) @@ -155,13 +157,60 @@ async def upload_profile_picture( # Check if the uploaded file is an image if not pic.content_type.startswith("image/"): raise HTTPException( - status_code=400, detail="Invalid file type. Only images are allowed." + status_code=400, detail="Invalid file type. Only images / gifs are allowed." ) + + # Check file size (limit to 10 MB) + if pic.size > AccountFeaturesConfig.max_size_mb * 1024 * 1024: + raise HTTPException( + status_code=400, + detail=f"File size exceeds the {AccountFeaturesConfig.max_size_mb} MB limit.", + ) + try: image = Image.open(io.BytesIO(await pic.read())) except Exception: raise HTTPException(status_code=400, detail="Invalid Image File.") + if image.format == "GIF" and not AccountFeaturesConfig.allow_gif: + raise HTTPException( + status_code=400, detail="GIFs are not allowed for profile pictures." + ) + + # Handle GIFs + if image.format == "GIF" and AccountFeaturesConfig.allow_gif: + frames = [frame.copy() for frame in ImageSequence.Iterator(image)] + if len(frames) > AccountFeaturesConfig.max_gif_frames: + raise HTTPException( + status_code=400, + detail="GIFs with more than 200 frames are not allowed.", + ) + resized_frames = [] + for frame in frames: + frame = frame.convert("RGBA") + frame = resize_and_crop_image(frame) + resized_frames.append(frame) + image = resized_frames[0] + image.save( + f"/uploads/{user['_id']}.webp", + save_all=True, + append_images=resized_frames[1:], + format="webp", + optimize=True, + quality=AccountFeaturesConfig.profile_picture_quality, + ) + else: + image = resize_and_crop_image(image) + save_path = f"/uploads/{user['_id']}.webp" + image.save( + save_path, + "webp", + optimize=True, + quality=AccountFeaturesConfig.profile_picture_quality, + ) + + +def resize_and_crop_image(image): # Crop the image to a square from the center width, height = image.size min_dim = min(width, height) @@ -173,10 +222,10 @@ async def upload_profile_picture( # Resize the image to 128x128 image = image.resize( - (AccountFeaturesConfig.profile_picture_resize_width, AccountFeaturesConfig.profile_picture_resize_height), - Image.Resampling.NEAREST + ( + AccountFeaturesConfig.profile_picture_resize_width, + AccountFeaturesConfig.profile_picture_resize_height, + ), + Image.Resampling.NEAREST, ) - - # Save the image - save_path = f"/uploads/{user["_id"]}.webp" - image.save(save_path, "webp", optimize=True, quality=AccountFeaturesConfig.profile_picture_quality) + return image diff --git a/src/tools/conf/AccountFeaturesConfig.py b/src/tools/conf/AccountFeaturesConfig.py index 465330e..0c1fa4e 100644 --- a/src/tools/conf/AccountFeaturesConfig.py +++ b/src/tools/conf/AccountFeaturesConfig.py @@ -43,6 +43,11 @@ class AccountFeaturesConfig: profile_picture_quality: int = config["account_features"]["profile_picture"][ "quality" ] + allow_gif: bool = config["account_features"]["profile_picture"]["allow_gif"] + max_size_mb: float = config["account_features"]["profile_picture"]["max_size_mb"] + max_gif_frames: int = config["account_features"]["profile_picture"][ + "max_gif_frames" + ] def validate_types(self) -> bool: """This is to Type Check the Configuration""" @@ -117,6 +122,27 @@ def validate_types(self) -> bool: ) ) + if not isinstance(self.allow_gif, bool): + raise ValueError( + "account_features.profile_picture.allow_gif must be a boolean (got type {})".format( + type(self.allow_gif) + ) + ) + + if not isinstance(self.max_size_mb, (float, int)): + raise ValueError( + "account_features.profile_picture.max_size_mb must be a float (got type {})".format( + type(self.max_size_mb) + ) + ) + + if not isinstance(self.max_gif_frames, int): + raise ValueError( + "account_features.profile_picture.max_gif_frames must be an integer (got type {})".format( + type(self.max_gif_frames) + ) + ) + def validate_values(self) -> bool: """This is to Value Check the Configuration""" if not self.issuer_name_2fa: @@ -149,6 +175,20 @@ def validate_values(self) -> bool: ) ) + if not self.max_size_mb > 0: + raise ValueError( + "account_features.profile_picture.max_size_mb must be a positive float (got {})".format( + self.max_size_mb + ) + ) + + if not self.max_gif_frames > 0: + raise ValueError( + "account_features.profile_picture.max_gif_frames must be a positive integer (got {})".format( + self.max_gif_frames + ) + ) + AccountFeaturesConfig().validate_types() AccountFeaturesConfig().validate_values() diff --git a/src/tools/conf/testing_config.json b/src/tools/conf/testing_config.json index 8ca6be3..c13f250 100644 --- a/src/tools/conf/testing_config.json +++ b/src/tools/conf/testing_config.json @@ -55,7 +55,10 @@ "width": 200, "height": 200 }, - "quality": 100 + "quality": 100, + "allow_gif": true, + "max_gif_frames": 200, + "max_size_mb": 15 } }, "security": {