"""Social Optimizer service for platform-specific image optimization.""" from __future__ import annotations import base64 import io from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from PIL import Image, ImageDraw, ImageFont from .templates import Platform from utils.logger_utils import get_service_logger logger = get_service_logger("image_studio.social_optimizer") @dataclass class SafeZone: """Safe zone configuration for text overlay.""" top: float = 0.1 # Percentage from top bottom: float = 0.1 # Percentage from bottom left: float = 0.1 # Percentage from left right: float = 0.1 # Percentage from right @dataclass class PlatformFormat: """Platform format specification.""" name: str width: int height: int ratio: str safe_zone: SafeZone file_type: str = "PNG" max_size_mb: float = 5.0 # Platform format definitions with safe zones PLATFORM_FORMATS: Dict[Platform, List[PlatformFormat]] = { Platform.INSTAGRAM: [ PlatformFormat( name="Feed Post (Square)", width=1080, height=1080, ratio="1:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Feed Post (Portrait)", width=1080, height=1350, ratio="4:5", safe_zone=SafeZone(top=0.2, bottom=0.2, left=0.1, right=0.1), ), PlatformFormat( name="Story", width=1080, height=1920, ratio="9:16", safe_zone=SafeZone(top=0.25, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Reel", width=1080, height=1920, ratio="9:16", safe_zone=SafeZone(top=0.25, bottom=0.15, left=0.1, right=0.1), ), ], Platform.FACEBOOK: [ PlatformFormat( name="Feed Post", width=1200, height=630, ratio="1.91:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Feed Post (Square)", width=1080, height=1080, ratio="1:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Story", width=1080, height=1920, ratio="9:16", safe_zone=SafeZone(top=0.25, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Cover Photo", width=820, height=312, ratio="16:9", safe_zone=SafeZone(top=0.2, bottom=0.1, left=0.15, right=0.15), ), ], Platform.TWITTER: [ PlatformFormat( name="Post", width=1200, height=675, ratio="16:9", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Card", width=1200, height=600, ratio="2:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Header", width=1500, height=500, ratio="3:1", safe_zone=SafeZone(top=0.2, bottom=0.1, left=0.15, right=0.15), ), ], Platform.LINKEDIN: [ PlatformFormat( name="Feed Post", width=1200, height=628, ratio="1.91:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Feed Post (Square)", width=1080, height=1080, ratio="1:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Article", width=1200, height=627, ratio="2:1", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Company Cover", width=1128, height=191, ratio="4:1", safe_zone=SafeZone(top=0.2, bottom=0.1, left=0.15, right=0.15), ), ], Platform.YOUTUBE: [ PlatformFormat( name="Thumbnail", width=1280, height=720, ratio="16:9", safe_zone=SafeZone(top=0.15, bottom=0.15, left=0.1, right=0.1), ), PlatformFormat( name="Channel Art", width=2560, height=1440, ratio="16:9", safe_zone=SafeZone(top=0.2, bottom=0.1, left=0.15, right=0.15), ), ], Platform.PINTEREST: [ PlatformFormat( name="Pin", width=1000, height=1500, ratio="2:3", safe_zone=SafeZone(top=0.2, bottom=0.2, left=0.1, right=0.1), ), PlatformFormat( name="Story Pin", width=1080, height=1920, ratio="9:16", safe_zone=SafeZone(top=0.25, bottom=0.15, left=0.1, right=0.1), ), ], Platform.TIKTOK: [ PlatformFormat( name="Video Cover", width=1080, height=1920, ratio="9:16", safe_zone=SafeZone(top=0.25, bottom=0.15, left=0.1, right=0.1), ), ], } @dataclass class SocialOptimizerRequest: """Request payload for social optimization.""" image_base64: str platforms: List[Platform] # List of platforms to optimize for format_names: Optional[Dict[Platform, str]] = None # Specific format per platform show_safe_zones: bool = False # Include safe zone overlay in output crop_mode: str = "smart" # "smart", "center", "fit" focal_point: Optional[Dict[str, float]] = None # {"x": 0.5, "y": 0.5} for smart crop output_format: str = "png" options: Dict[str, Any] = field(default_factory=dict) class SocialOptimizerService: """Service for optimizing images for social media platforms.""" def __init__(self): logger.info("[Social Optimizer] Initialized service") @staticmethod def _decode_base64_image(value: str) -> bytes: """Decode a base64 (or data URL) string to bytes.""" try: if value.startswith("data:"): _, b64data = value.split(",", 1) else: b64data = value return base64.b64decode(b64data) except Exception as exc: logger.error(f"[Social Optimizer] Failed to decode base64 image: {exc}") raise ValueError("Invalid base64 image payload") from exc @staticmethod def _bytes_to_base64(image_bytes: bytes, output_format: str = "png") -> str: """Convert raw bytes to base64 data URL.""" b64 = base64.b64encode(image_bytes).decode("utf-8") return f"data:image/{output_format};base64,{b64}" @staticmethod def _smart_crop( image: Image.Image, target_width: int, target_height: int, focal_point: Optional[Dict[str, float]] = None, ) -> Image.Image: """Smart crop image to target dimensions, preserving important content.""" img_width, img_height = image.size target_ratio = target_width / target_height img_ratio = img_width / img_height # If focal point is provided, use it for cropping if focal_point: focal_x = int(focal_point["x"] * img_width) focal_y = int(focal_point["y"] * img_height) else: # Default to center focal_x = img_width // 2 focal_y = img_height // 2 if img_ratio > target_ratio: # Image is wider than target - crop width new_width = int(img_height * target_ratio) left = max(0, min(focal_x - new_width // 2, img_width - new_width)) right = left + new_width cropped = image.crop((left, 0, right, img_height)) else: # Image is taller than target - crop height new_height = int(img_width / target_ratio) top = max(0, min(focal_y - new_height // 2, img_height - new_height)) bottom = top + new_height cropped = image.crop((0, top, img_width, bottom)) # Resize to exact target dimensions return cropped.resize((target_width, target_height), Image.Resampling.LANCZOS) @staticmethod def _fit_image( image: Image.Image, target_width: int, target_height: int, ) -> Image.Image: """Fit image to target dimensions while maintaining aspect ratio (adds padding if needed).""" img_width, img_height = image.size target_ratio = target_width / target_height img_ratio = img_width / img_height if img_ratio > target_ratio: # Image is wider - fit to height, pad width new_height = target_height new_width = int(img_width * (target_height / img_height)) resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Create new image with target size and paste centered result = Image.new("RGB", (target_width, target_height), (255, 255, 255)) paste_x = (target_width - new_width) // 2 result.paste(resized, (paste_x, 0)) return result else: # Image is taller - fit to width, pad height new_width = target_width new_height = int(img_height * (target_width / img_width)) resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Create new image with target size and paste centered result = Image.new("RGB", (target_width, target_height), (255, 255, 255)) paste_y = (target_height - new_height) // 2 result.paste(resized, (0, paste_y)) return result @staticmethod def _center_crop( image: Image.Image, target_width: int, target_height: int, ) -> Image.Image: """Center crop image to target dimensions.""" img_width, img_height = image.size target_ratio = target_width / target_height img_ratio = img_width / img_height if img_ratio > target_ratio: # Image is wider - crop width new_width = int(img_height * target_ratio) left = (img_width - new_width) // 2 cropped = image.crop((left, 0, left + new_width, img_height)) else: # Image is taller - crop height new_height = int(img_width / target_ratio) top = (img_height - new_height) // 2 cropped = image.crop((0, top, img_width, top + new_height)) return cropped.resize((target_width, target_height), Image.Resampling.LANCZOS) @staticmethod def _draw_safe_zone( image: Image.Image, safe_zone: SafeZone, ) -> Image.Image: """Draw safe zone overlay on image.""" draw = ImageDraw.Draw(image) width, height = image.size # Calculate safe zone boundaries top = int(height * safe_zone.top) bottom = int(height * (1 - safe_zone.bottom)) left = int(width * safe_zone.left) right = int(width * (1 - safe_zone.right)) # Draw semi-transparent overlay outside safe zone overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) # Top area overlay_draw.rectangle([(0, 0), (width, top)], fill=(0, 0, 0, 100)) # Bottom area overlay_draw.rectangle([(0, bottom), (width, height)], fill=(0, 0, 0, 100)) # Left area overlay_draw.rectangle([(0, top), (left, bottom)], fill=(0, 0, 0, 100)) # Right area overlay_draw.rectangle([(right, top), (width, bottom)], fill=(0, 0, 0, 100)) # Draw safe zone border border_color = (255, 255, 0, 200) # Yellow with transparency overlay_draw.rectangle( [(left, top), (right, bottom)], outline=border_color, width=2, ) # Composite overlay onto image if image.mode != "RGBA": image = image.convert("RGBA") image = Image.alpha_composite(image, overlay) return image def get_platform_formats(self, platform: Platform) -> List[Dict[str, Any]]: """Get available formats for a platform.""" formats = PLATFORM_FORMATS.get(platform, []) return [ { "name": fmt.name, "width": fmt.width, "height": fmt.height, "ratio": fmt.ratio, "safe_zone": { "top": fmt.safe_zone.top, "bottom": fmt.safe_zone.bottom, "left": fmt.safe_zone.left, "right": fmt.safe_zone.right, }, "file_type": fmt.file_type, "max_size_mb": fmt.max_size_mb, } for fmt in formats ] def optimize_image( self, request: SocialOptimizerRequest, ) -> Dict[str, Any]: """Optimize image for specified platforms.""" logger.info( f"[Social Optimizer] Processing optimization for {len(request.platforms)} platform(s)" ) # Decode input image image_bytes = self._decode_base64_image(request.image_base64) original_image = Image.open(io.BytesIO(image_bytes)) # Convert to RGB if needed if original_image.mode in ("RGBA", "LA", "P"): if original_image.mode == "P": original_image = original_image.convert("RGBA") background = Image.new("RGB", original_image.size, (255, 255, 255)) if original_image.mode == "RGBA": background.paste(original_image, mask=original_image.split()[-1]) else: background.paste(original_image) original_image = background elif original_image.mode != "RGB": original_image = original_image.convert("RGB") results = [] for platform in request.platforms: formats = PLATFORM_FORMATS.get(platform, []) if not formats: logger.warning(f"[Social Optimizer] No formats found for platform: {platform}") continue # Get format (use specified format or default to first) format_name = None if request.format_names and platform in request.format_names: format_name = request.format_names[platform] platform_format = None for fmt in formats: if format_name and fmt.name == format_name: platform_format = fmt break if not platform_format: platform_format = formats[0] # Default to first format # Crop/resize image based on mode if request.crop_mode == "smart": optimized_image = self._smart_crop( original_image, platform_format.width, platform_format.height, request.focal_point, ) elif request.crop_mode == "fit": optimized_image = self._fit_image( original_image, platform_format.width, platform_format.height, ) else: # center optimized_image = self._center_crop( original_image, platform_format.width, platform_format.height, ) # Add safe zone overlay if requested if request.show_safe_zones: optimized_image = self._draw_safe_zone(optimized_image, platform_format.safe_zone) # Convert to bytes output_buffer = io.BytesIO() output_format = request.output_format.lower() if output_format == "jpg" or output_format == "jpeg": optimized_image = optimized_image.convert("RGB") optimized_image.save(output_buffer, format="JPEG", quality=95) else: optimized_image.save(output_buffer, format="PNG") output_bytes = output_buffer.getvalue() results.append( { "platform": platform.value, "format": platform_format.name, "width": platform_format.width, "height": platform_format.height, "ratio": platform_format.ratio, "image_base64": self._bytes_to_base64(output_bytes, request.output_format), "safe_zone": { "top": platform_format.safe_zone.top, "bottom": platform_format.safe_zone.bottom, "left": platform_format.safe_zone.left, "right": platform_format.safe_zone.right, }, } ) logger.info(f"[Social Optimizer] ✅ Generated {len(results)} optimized images") return { "success": True, "results": results, "total_optimized": len(results), }