import os from PIL import Image from wand.image import Image as WandImage import io from typing import BinaryIO, Tuple, Optional class ImageProcessor: @staticmethod def is_animated_gif(file_storage) -> bool: """Check if the image is an animated GIF""" try: # Save current position pos = file_storage.tell() # Go to beginning file_storage.seek(0) with Image.open(file_storage) as img: try: img.seek(1) # Try to move to the second frame is_animated = True except EOFError: is_animated = False # Restore position file_storage.seek(pos) return is_animated except Exception: # Restore position in case of error file_storage.seek(pos) return False @staticmethod def convert_to_webp(file_storage, quality: int = 90) -> Tuple[BinaryIO, str]: """ Convert an image to WebP format. Returns a tuple of (file_object, extension) """ # Save current position pos = file_storage.tell() # Go to beginning file_storage.seek(0) try: # Check if it's an animated GIF if ImageProcessor.is_animated_gif(file_storage): # Convert animated GIF to animated WebP file_storage.seek(0) with WandImage(file=file_storage) as img: # Configure WebP animation settings img.format = 'WEBP' # Higher quality settings for animation img.options['webp:lossless'] = 'true' # Use lossless for animations img.options['webp:method'] = '6' # Best compression method img.options['webp:image-hint'] = 'graph' # Better for animations img.options['webp:minimize-size'] = 'false' # Prioritize quality # Animation specific settings img.options['webp:animation-type'] = 'default' img.options['webp:loop'] = '0' # Infinite loop # Save with high quality webp_bytes = io.BytesIO(img.make_blob(format='webp')) webp_bytes.seek(0) return webp_bytes, '.webp' else: # Handle static images file_storage.seek(0) with Image.open(file_storage) as img: # Convert RGBA to RGB if necessary if img.mode in ('RGBA', 'LA'): background = Image.new('RGB', img.size, (255, 255, 255)) background.paste(img, mask=img.getchannel('A')) img = background elif img.mode != 'RGB': img = img.convert('RGB') # Save as WebP with high quality output = io.BytesIO() img.save(output, format='WEBP', quality=quality, # Higher quality method=6, # Best compression method lossless=False, # Use lossy for static images exact=True) # Preserve color exactness output.seek(0) return output, '.webp' finally: # Restore original position file_storage.seek(pos) @staticmethod def process_featured_image(file_storage) -> Tuple[BinaryIO, str]: """Process featured image, converting to WebP format""" return ImageProcessor.convert_to_webp(file_storage, quality=90)