from PIL import Image, ImageOps, ImageEnhance import io from io import BytesIO import os from typing import Tuple, List, Dict, Optional, Union import logging import sys import json import requests # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class ImageProcessor: """Image processing utility for resizing, optimizing, and converting images.""" # Supported formats for conversion SUPPORTED_FORMATS = ['JPEG', 'PNG', 'WEBP', 'GIF', 'AVIF'] @staticmethod def open_image(image_data: Union[bytes, str]) -> Image.Image: """Open an image from bytes or file path.""" try: if isinstance(image_data, bytes): return Image.open(io.BytesIO(image_data)) else: return Image.open(image_data) except Exception as e: logger.error(f"Failed to open image: {e}") raise ValueError(f"Could not open image: {e}") @staticmethod def resize_image( img: Image.Image, width: Optional[int] = None, height: Optional[int] = None, maintain_aspect_ratio: bool = True ) -> Image.Image: """ Resize an image to specified dimensions. Args: img: PIL Image object width: Target width (None to auto-calculate from height) height: Target height (None to auto-calculate from width) maintain_aspect_ratio: Whether to maintain the original aspect ratio Returns: Resized PIL Image """ if width is None and height is None: return img # No resize needed original_width, original_height = img.size if maintain_aspect_ratio: if width and height: # Calculate the best fit while maintaining aspect ratio ratio = min(width / original_width, height / original_height) new_width = int(original_width * ratio) new_height = int(original_height * ratio) elif width: # Calculate height based on width ratio = width / original_width new_width = width new_height = int(original_height * ratio) else: # Calculate width based on height ratio = height / original_height new_width = int(original_width * ratio) new_height = height else: # Force exact dimensions new_width = width if width else original_width new_height = height if height else original_height return img.resize((new_width, new_height), Image.LANCZOS) @staticmethod def optimize_image( img: Image.Image, quality: int = 85, format: Optional[str] = None ) -> Tuple[bytes, str]: """ Optimize an image for web delivery. Args: img: PIL Image object quality: JPEG/WebP quality (0-100) format: Output format (JPEG, PNG, WEBP, etc.) Returns: Tuple of (image_bytes, format) """ if format is None: format = img.format or 'JPEG' format = format.upper() if format not in ImageProcessor.SUPPORTED_FORMATS: format = 'JPEG' # Default to JPEG if unsupported format # Convert mode if needed if format == 'JPEG' and img.mode in ('RGBA', 'P'): img = img.convert('RGB') # Save to bytes buffer = io.BytesIO() if format == 'JPEG': img.save(buffer, format=format, quality=quality, optimize=True) elif format == 'PNG': img.save(buffer, format=format, optimize=True) elif format == 'WEBP': img.save(buffer, format=format, quality=quality) elif format == 'AVIF': img.save(buffer, format=format, quality=quality) else: img.save(buffer, format=format) buffer.seek(0) return buffer.getvalue(), format.lower() @staticmethod def apply_filters( img: Image.Image, brightness: Optional[float] = None, contrast: Optional[float] = None, sharpness: Optional[float] = None, grayscale: bool = False ) -> Image.Image: """ Apply various filters and enhancements to an image. Args: img: PIL Image object brightness: Brightness factor (0.0-2.0, 1.0 is original) contrast: Contrast factor (0.0-2.0, 1.0 is original) sharpness: Sharpness factor (0.0-2.0, 1.0 is original) grayscale: Convert to grayscale if True Returns: Processed PIL Image """ # Apply grayscale first if requested if grayscale: img = ImageOps.grayscale(img) # Convert back to RGB if other filters will be applied if any(x is not None for x in [brightness, contrast, sharpness]): img = img.convert('RGB') # Apply enhancements if brightness is not None: img = ImageEnhance.Brightness(img).enhance(brightness) if contrast is not None: img = ImageEnhance.Contrast(img).enhance(contrast) if sharpness is not None: img = ImageEnhance.Sharpness(img).enhance(sharpness) return img @staticmethod def process_image( image_data: Union[bytes, str], width: Optional[int] = None, height: Optional[int] = None, maintain_aspect_ratio: bool = True, quality: int = 85, output_format: Optional[str] = None, brightness: Optional[float] = None, contrast: Optional[float] = None, sharpness: Optional[float] = None, grayscale: bool = False ) -> Dict: """ Process an image with all available options. Args: image_data: Image bytes or file path width: Target width height: Target height maintain_aspect_ratio: Whether to maintain aspect ratio quality: Output quality output_format: Output format brightness: Brightness adjustment contrast: Contrast adjustment sharpness: Sharpness adjustment grayscale: Convert to grayscale Returns: Dict with processed image data and metadata """ # Open the image img = ImageProcessor.open_image(image_data) original_format = img.format original_size = img.size # Apply filters img = ImageProcessor.apply_filters( img, brightness=brightness, contrast=contrast, sharpness=sharpness, grayscale=grayscale ) # Resize if needed if width or height: img = ImageProcessor.resize_image( img, width=width, height=height, maintain_aspect_ratio=maintain_aspect_ratio ) # Optimize and get bytes processed_bytes, actual_format = ImageProcessor.optimize_image( img, quality=quality, format=output_format ) # Return result with metadata return { "processed_image": processed_bytes, "format": actual_format, "original_format": original_format, "original_size": original_size, "new_size": img.size, "file_size_bytes": len(processed_bytes) } def process_image(url, height, width, quality): # Download image from URL response = requests.get(url) img = Image.open(BytesIO(response.content)) # Resize img = img.resize((int(width), int(height)), Image.Resampling.LANCZOS) # Save with quality setting output_path = f"/tmp/processed_{width}x{height}.jpg" img.save(output_path, "JPEG", quality=int(quality)) return output_path if __name__ == "__main__": url = sys.argv[1] height = int(sys.argv[2]) width = int(sys.argv[3]) quality = int(sys.argv[4]) maintain_aspect_ratio = sys.argv[5].lower() == 'true' output_format = sys.argv[6] brightness = float(sys.argv[7]) if sys.argv[7] != 'null' else None contrast = float(sys.argv[8]) if sys.argv[8] != 'null' else None sharpness = float(sys.argv[9]) if sys.argv[9] != 'null' else None grayscale = sys.argv[10].lower() == 'true' processor = ImageProcessor() result = processor.process_image( requests.get(url).content, width=width, height=height, maintain_aspect_ratio=maintain_aspect_ratio, quality=quality, output_format=output_format, brightness=brightness, contrast=contrast, sharpness=sharpness, grayscale=grayscale ) output_path = f"/tmp/processed_{width}x{height}.{result['format']}" with open(output_path, 'wb') as f: f.write(result['processed_image']) print(json.dumps({ "outputPath": output_path, "format": result['format'], "originalSize": result['original_size'], "newSize": result['new_size'], "fileSizeBytes": result['file_size_bytes'] }))