""" Validators for image uploads. """ import magic from django.core.exceptions import ValidationError from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile from PIL import Image from typing import Optional # Allowed file types ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', ] ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.gif'] # Size limits (in bytes) MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB MIN_FILE_SIZE = 1024 # 1 KB # Dimension limits MIN_WIDTH = 100 MIN_HEIGHT = 100 MAX_WIDTH = 8000 MAX_HEIGHT = 8000 # Aspect ratio limits (for specific photo types) ASPECT_RATIO_LIMITS = { 'banner': {'min': 2.0, 'max': 4.0}, # Wide banners 'logo': {'min': 0.5, 'max': 2.0}, # Square-ish logos } def validate_image_file_type(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None: """ Validate that the uploaded file is an allowed image type. Uses python-magic to detect actual file type, not just extension. Args: file: The uploaded file object Raises: ValidationError: If file type is not allowed """ # Check file extension file_ext = None if hasattr(file, 'name') and file.name: file_ext = '.' + file.name.split('.')[-1].lower() if file_ext not in ALLOWED_EXTENSIONS: raise ValidationError( f"File extension {file_ext} not allowed. " f"Allowed extensions: {', '.join(ALLOWED_EXTENSIONS)}" ) # Check MIME type from content type if hasattr(file, 'content_type'): if file.content_type not in ALLOWED_MIME_TYPES: raise ValidationError( f"File type {file.content_type} not allowed. " f"Allowed types: {', '.join(ALLOWED_MIME_TYPES)}" ) # Verify actual file content using python-magic try: file.seek(0) mime = magic.from_buffer(file.read(2048), mime=True) file.seek(0) if mime not in ALLOWED_MIME_TYPES: raise ValidationError( f"File content type {mime} does not match allowed types. " "File may be corrupted or incorrectly labeled." ) except Exception as e: # If magic fails, we already validated content_type above pass def validate_image_file_size(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None: """ Validate that the file size is within allowed limits. Args: file: The uploaded file object Raises: ValidationError: If file size is not within limits """ file_size = file.size if file_size < MIN_FILE_SIZE: raise ValidationError( f"File size is too small. Minimum: {MIN_FILE_SIZE / 1024:.0f} KB" ) if file_size > MAX_FILE_SIZE: raise ValidationError( f"File size is too large. Maximum: {MAX_FILE_SIZE / (1024 * 1024):.0f} MB" ) def validate_image_dimensions( file: InMemoryUploadedFile | TemporaryUploadedFile, photo_type: Optional[str] = None ) -> None: """ Validate image dimensions and aspect ratio. Args: file: The uploaded file object photo_type: Optional photo type for specific validation Raises: ValidationError: If dimensions are not within limits """ try: file.seek(0) image = Image.open(file) width, height = image.size file.seek(0) except Exception as e: raise ValidationError(f"Could not read image dimensions: {str(e)}") # Check minimum dimensions if width < MIN_WIDTH or height < MIN_HEIGHT: raise ValidationError( f"Image dimensions too small. Minimum: {MIN_WIDTH}x{MIN_HEIGHT}px, " f"got: {width}x{height}px" ) # Check maximum dimensions if width > MAX_WIDTH or height > MAX_HEIGHT: raise ValidationError( f"Image dimensions too large. Maximum: {MAX_WIDTH}x{MAX_HEIGHT}px, " f"got: {width}x{height}px" ) # Check aspect ratio for specific photo types if photo_type and photo_type in ASPECT_RATIO_LIMITS: aspect_ratio = width / height limits = ASPECT_RATIO_LIMITS[photo_type] if aspect_ratio < limits['min'] or aspect_ratio > limits['max']: raise ValidationError( f"Invalid aspect ratio for {photo_type}. " f"Expected ratio between {limits['min']:.2f} and {limits['max']:.2f}, " f"got: {aspect_ratio:.2f}" ) def validate_image( file: InMemoryUploadedFile | TemporaryUploadedFile, photo_type: Optional[str] = None ) -> None: """ Run all image validations. Args: file: The uploaded file object photo_type: Optional photo type for specific validation Raises: ValidationError: If any validation fails """ validate_image_file_type(file) validate_image_file_size(file) validate_image_dimensions(file, photo_type) def validate_image_content_safety(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None: """ Placeholder for content safety validation. This could integrate with services like: - AWS Rekognition - Google Cloud Vision - Azure Content Moderator For now, this is a no-op but provides extension point. Args: file: The uploaded file object Raises: ValidationError: If content is deemed unsafe """ # TODO: Integrate with content moderation API pass