1313# Maximum dimensions for profile pictures
1414MAX_PROFILE_PICTURE_WIDTH = 800
1515MAX_PROFILE_PICTURE_HEIGHT = 800
16- MAX_PROFILE_PICTURE_SIZE_KB = 500 # Target max size in KB
16+ MAX_PROFILE_PICTURE_SIZE_KB = 500 # Target max size in KB for avatars
17+
18+ # New limits for overall image uploads
19+ MAX_IMAGE_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
20+ COMPRESSION_THRESHOLD = 25 * 1024 * 1024 # 25MB
1721
1822# Quality settings
1923JPEG_QUALITY = 85 # Good balance between quality and size
@@ -24,8 +28,9 @@ def compress_image_base64(
2428 base64_string : str ,
2529 max_width : int = MAX_PROFILE_PICTURE_WIDTH ,
2630 max_height : int = MAX_PROFILE_PICTURE_HEIGHT ,
27- max_size_kb : int = MAX_PROFILE_PICTURE_SIZE_KB ,
28- quality : int = JPEG_QUALITY
31+ max_size_kb : Optional [int ] = None , # Changed to Optional
32+ quality : int = JPEG_QUALITY ,
33+ force_compression : bool = False
2934) -> Optional [str ]:
3035 """
3136 Compress a base64-encoded image.
@@ -34,22 +39,38 @@ def compress_image_base64(
3439 base64_string: Base64-encoded image string (with or without data URI prefix)
3540 max_width: Maximum width in pixels
3641 max_height: Maximum height in pixels
37- max_size_kb: Target maximum size in KB
42+ max_size_kb: Target maximum size in KB. If None, uses COMPRESSION_THRESHOLD logic.
3843 quality: JPEG quality (1-100, higher = better quality but larger file)
44+ force_compression: If True, always compress regardless of size
3945
4046 Returns:
4147 Compressed base64-encoded image string, or None if compression fails
4248 """
4349 try :
4450 # Remove data URI prefix if present (e.g., "data:image/jpeg;base64,")
51+ prefix = ""
4552 if ',' in base64_string :
46- base64_string = base64_string .split (',' )[1 ]
53+ prefix , base64_string = base64_string .split (',' , 1 )
54+ prefix += ","
4755
4856 # Decode base64 to bytes
4957 image_bytes = base64 .b64decode (base64_string )
50- original_size_kb = len (image_bytes ) / 1024
58+ original_size = len (image_bytes )
59+ original_size_kb = original_size / 1024
60+
61+ # Check against absolute limit
62+ if original_size > MAX_IMAGE_UPLOAD_SIZE :
63+ logger .error (f"Image too large: { original_size_kb :.2f} KB exceeds { MAX_IMAGE_UPLOAD_SIZE / 1024 / 1024 :.2f} MB" )
64+ return None
65+
66+ # Determine if compression is needed
67+ needs_compression = force_compression or original_size > COMPRESSION_THRESHOLD or max_size_kb is not None
68+
69+ if not needs_compression :
70+ logger .info (f"Image size { original_size_kb :.2f} KB is under threshold. Skipping compression." )
71+ return prefix + base64_string
5172
52- logger .info (f"Original image size: { original_size_kb :.2f} KB" )
73+ logger .info (f"Compressing image. Original size: { original_size_kb :.2f} KB" )
5374
5475 # Open image with PIL
5576 image = Image .open (io .BytesIO (image_bytes ))
@@ -65,16 +86,22 @@ def compress_image_base64(
6586 elif image .mode != 'RGB' :
6687 image = image .convert ('RGB' )
6788
68- # Resize if necessary
69- if image .width > max_width or image .height > max_height :
89+ # Resize if necessary (only if max_width/height are specifically small, like for avatars)
90+ # For general large images, we might want to keep the resolution unless it's insane.
91+ # If max_size_kb is set (e.g. 500KB for avatars), we resize.
92+ # Otherwise, if it's just > 25MB, we might just compress the quality first.
93+ if max_size_kb and (image .width > max_width or image .height > max_height ):
7094 image .thumbnail ((max_width , max_height ), Image .Resampling .LANCZOS )
7195 logger .info (f"Resized to: { image .width } x{ image .height } " )
7296
97+ # Target size in KB
98+ target_size_kb = max_size_kb if max_size_kb else 10240 # Default to 10MB if just over threshold
99+
73100 # Compress with quality adjustment
74101 output = io .BytesIO ()
75102 current_quality = quality
76103
77- # Try to get under max_size_kb by reducing quality if needed
104+ # Try to get under target_size_kb by reducing quality if needed
78105 for attempt in range (5 ): # Max 5 attempts
79106 output .seek (0 )
80107 output .truncate (0 )
@@ -85,20 +112,21 @@ def compress_image_base64(
85112 logger .info (f"Attempt { attempt + 1 } : Quality={ current_quality } , Size={ compressed_size_kb :.2f} KB" )
86113
87114 # If we're under the target size or quality is already low, we're done
88- if compressed_size_kb <= max_size_kb or current_quality <= 50 :
115+ if compressed_size_kb <= target_size_kb or current_quality <= 50 :
89116 break
90117
91118 # Reduce quality for next attempt
92119 current_quality = max (50 , int (current_quality * 0.85 ))
93120
94121 # Encode back to base64
95- compressed_base64 = base64 .b64encode (output .getvalue ()).decode ('utf-8' )
96- final_size_kb = len (output .getvalue ()) / 1024
122+ compressed_bytes = output .getvalue ()
123+ compressed_base64 = base64 .b64encode (compressed_bytes ).decode ('utf-8' )
124+ final_size_kb = len (compressed_bytes ) / 1024
97125
98126 compression_ratio = (1 - (final_size_kb / original_size_kb )) * 100 if original_size_kb > 0 else 0
99127 logger .info (f"Compressed image: { final_size_kb :.2f} KB ({ compression_ratio :.1f} % reduction)" )
100128
101- return compressed_base64
129+ return prefix + compressed_base64
102130
103131 except Exception as e :
104132 logger .error (f"Image compression failed: { str (e )} " , exc_info = True )
0 commit comments