feat(media): Add HEIC/HEIF image support - v1.4.2
- Add pillow-heif dependency for iPhone photo support - Auto-convert HEIC to JPEG (browsers can't display HEIC) - Graceful error if pillow-heif not installed - Handles RGBA/P mode conversion to RGB 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,14 @@ import io
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
from flask import current_app
|
||||
|
||||
# HEIC/HEIF support - import registers with Pillow automatically
|
||||
try:
|
||||
import pillow_heif
|
||||
pillow_heif.register_heif_opener()
|
||||
HEIC_SUPPORTED = True
|
||||
except ImportError:
|
||||
HEIC_SUPPORTED = False
|
||||
|
||||
# Allowed MIME types per Q11
|
||||
ALLOWED_MIME_TYPES = {
|
||||
'image/jpeg': ['.jpg', '.jpeg'],
|
||||
@@ -68,19 +76,21 @@ def get_optimization_params(file_size: int) -> Tuple[int, int]:
|
||||
return (1280, 85)
|
||||
|
||||
|
||||
def validate_image(file_data: bytes, filename: str) -> Tuple[str, int, int]:
|
||||
def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, int]:
|
||||
"""
|
||||
Validate image file
|
||||
|
||||
Per Q11: Validate MIME type using Pillow
|
||||
Per Q6: Reject if >50MB or >4096px (updated v1.4.0)
|
||||
Per v1.4.2: Convert HEIC to JPEG (browsers cannot display HEIC)
|
||||
|
||||
Args:
|
||||
file_data: Raw file bytes
|
||||
filename: Original filename
|
||||
|
||||
Returns:
|
||||
Tuple of (mime_type, width, height)
|
||||
Tuple of (file_data, mime_type, width, height)
|
||||
Note: file_data may be converted (e.g., HEIC to JPEG)
|
||||
|
||||
Raises:
|
||||
ValueError: If file is invalid
|
||||
@@ -100,6 +110,25 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[str, int, int]:
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid or corrupted image: {e}")
|
||||
|
||||
# HEIC/HEIF conversion (v1.4.2)
|
||||
# HEIC cannot be displayed in browsers, convert to JPEG
|
||||
if img.format in ('HEIF', 'HEIC'):
|
||||
if not HEIC_SUPPORTED:
|
||||
raise ValueError(
|
||||
"HEIC/HEIF images require pillow-heif library. "
|
||||
"Please convert to JPEG before uploading."
|
||||
)
|
||||
# Convert HEIC to JPEG in memory
|
||||
output = io.BytesIO()
|
||||
# Convert to RGB if needed (HEIC may have alpha channel)
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
img = img.convert('RGB')
|
||||
img.save(output, format='JPEG', quality=95)
|
||||
output.seek(0)
|
||||
# Re-open as JPEG for further processing
|
||||
file_data = output.getvalue()
|
||||
img = Image.open(io.BytesIO(file_data))
|
||||
|
||||
# Check format is allowed
|
||||
if img.format:
|
||||
format_lower = img.format.lower()
|
||||
@@ -135,7 +164,7 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[str, int, int]:
|
||||
# Not animated, continue normally
|
||||
pass
|
||||
|
||||
return mime_type, width, height
|
||||
return file_data, mime_type, width, height
|
||||
|
||||
|
||||
def optimize_image(image_data: bytes, original_size: int = None) -> Tuple[Image.Image, int, int, bytes]:
|
||||
@@ -395,9 +424,9 @@ def save_media(file_data: bytes, filename: str) -> Dict:
|
||||
file_size = len(file_data)
|
||||
|
||||
try:
|
||||
# Validate image (returns 3-tuple, signature unchanged)
|
||||
# Validate image (returns 4-tuple with potentially converted bytes)
|
||||
try:
|
||||
mime_type, orig_width, orig_height = validate_image(file_data, filename)
|
||||
file_data, mime_type, orig_width, orig_height = validate_image(file_data, filename)
|
||||
except ValueError as e:
|
||||
current_app.logger.warning(
|
||||
f'Media upload validation failed: filename="{filename}", '
|
||||
|
||||
Reference in New Issue
Block a user