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
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 = ['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
original_width, original_height = img.size
if maintain_aspect_ratio:
if width and height:
ratio = min(width / original_width, height / original_height)
new_width = int(original_width * ratio)
new_height = int(original_height * ratio)
elif width:
ratio = width / original_width
new_width = width
new_height = int(original_height * ratio)
else:
ratio = height / original_height
new_width = int(original_width * ratio)
new_height = height
else:
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'
if format == 'JPEG' and img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
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
"""
if grayscale:
img = ImageOps.grayscale(img)
if any(x is not None for x in [brightness, contrast, sharpness]):
img = img.convert('RGB')
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
"""
img = ImageProcessor.open_image(image_data)
original_format = img.format
original_size = img.size
img = ImageProcessor.apply_filters(
img,
brightness=brightness,
contrast=contrast,
sharpness=sharpness,
grayscale=grayscale
)
if width or height:
img = ImageProcessor.resize_image(
img,
width=width,
height=height,
maintain_aspect_ratio=maintain_aspect_ratio
)
processed_bytes, actual_format = ImageProcessor.optimize_image(
img,
quality=quality,
format=output_format
)
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):
response = requests.get(url)
img = Image.open(BytesIO(response.content))
img = img.resize((int(width), int(height)), Image.Resampling.LANCZOS)
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']
}))