143 lines
5.1 KiB
Python
143 lines
5.1 KiB
Python
import logging
|
|
import io
|
|
from typing import Dict, Any, Tuple, Optional
|
|
from PIL import Image, ImageOps
|
|
from PIL.ExifTags import TAGS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ImageProcessor:
|
|
"""Service for image processing operations"""
|
|
|
|
def extract_metadata(self, image_data: bytes) -> Dict[str, Any]:
|
|
"""
|
|
Extract metadata from an image
|
|
|
|
Args:
|
|
image_data: Binary image data
|
|
|
|
Returns:
|
|
Dictionary of metadata
|
|
"""
|
|
try:
|
|
metadata = {}
|
|
|
|
# Open the image with PIL
|
|
with Image.open(io.BytesIO(image_data)) as img:
|
|
# Basic image info
|
|
metadata["width"] = img.width
|
|
metadata["height"] = img.height
|
|
metadata["format"] = img.format
|
|
metadata["mode"] = img.mode
|
|
|
|
# Try to extract EXIF data if available
|
|
if hasattr(img, '_getexif') and img._getexif():
|
|
exif = {}
|
|
for tag_id, value in img._getexif().items():
|
|
tag = TAGS.get(tag_id, tag_id)
|
|
# Skip binary data that might be in EXIF
|
|
if isinstance(value, (bytes, bytearray)):
|
|
value = "binary data"
|
|
exif[tag] = value
|
|
metadata["exif"] = exif
|
|
|
|
return metadata
|
|
except Exception as e:
|
|
logger.error(f"Error extracting image metadata: {e}")
|
|
# Return at least an empty dict rather than failing
|
|
return {}
|
|
|
|
def resize_image(self, image_data: bytes, max_width: int = 1200, max_height: int = 1200) -> Tuple[bytes, Dict[str, Any]]:
|
|
"""
|
|
Resize an image while maintaining aspect ratio
|
|
|
|
Args:
|
|
image_data: Binary image data
|
|
max_width: Maximum width
|
|
max_height: Maximum height
|
|
|
|
Returns:
|
|
Tuple of (resized_image_data, metadata)
|
|
"""
|
|
try:
|
|
# Open the image with PIL
|
|
img = Image.open(io.BytesIO(image_data))
|
|
|
|
# Get original size
|
|
original_width, original_height = img.size
|
|
|
|
# Calculate new size
|
|
width_ratio = max_width / original_width if original_width > max_width else 1
|
|
height_ratio = max_height / original_height if original_height > max_height else 1
|
|
|
|
# Use the smaller ratio to ensure image fits within max dimensions
|
|
ratio = min(width_ratio, height_ratio)
|
|
|
|
# Only resize if the image is larger than the max dimensions
|
|
if ratio < 1:
|
|
new_width = int(original_width * ratio)
|
|
new_height = int(original_height * ratio)
|
|
img = img.resize((new_width, new_height), Image.LANCZOS)
|
|
|
|
logger.info(f"Resized image from {original_width}x{original_height} to {new_width}x{new_height}")
|
|
else:
|
|
logger.info(f"No resizing needed, image size {original_width}x{original_height}")
|
|
|
|
# Save to bytes
|
|
output = io.BytesIO()
|
|
img.save(output, format=img.format)
|
|
resized_data = output.getvalue()
|
|
|
|
# Get new metadata
|
|
metadata = {
|
|
"width": img.width,
|
|
"height": img.height,
|
|
"format": img.format,
|
|
"mode": img.mode,
|
|
"resized": ratio < 1
|
|
}
|
|
|
|
return resized_data, metadata
|
|
except Exception as e:
|
|
logger.error(f"Error resizing image: {e}")
|
|
# Return original data on error
|
|
return image_data, {"error": str(e)}
|
|
|
|
def is_image(self, mime_type: str) -> bool:
|
|
"""
|
|
Check if a file is an image based on MIME type
|
|
|
|
Args:
|
|
mime_type: File MIME type
|
|
|
|
Returns:
|
|
True if the file is an image
|
|
"""
|
|
return mime_type.startswith('image/')
|
|
|
|
def validate_image(self, image_data: bytes, mime_type: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate an image file
|
|
|
|
Args:
|
|
image_data: Binary image data
|
|
mime_type: File MIME type
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not self.is_image(mime_type):
|
|
return False, "File is not an image"
|
|
|
|
try:
|
|
# Try opening the image with PIL
|
|
with Image.open(io.BytesIO(image_data)) as img:
|
|
# Verify image can be read
|
|
img.verify()
|
|
return True, None
|
|
except Exception as e:
|
|
logger.error(f"Invalid image: {e}")
|
|
return False, f"Invalid image: {str(e)}"
|
|
|
|
# Create a singleton service
|
|
image_processor = ImageProcessor() |