2 Commits

Author SHA1 Message Date
d416463242 fix(media): Add MPO format support for iPhone portrait photos
iPhones use MPO (Multi-Picture Object) format for depth/portrait photos.
This format contains multiple JPEG images (main + depth map). Pillow
opens these as MPO format which wasn't in our allowed types.

Added automatic MPO to JPEG conversion by extracting the primary image.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:23:23 -07:00
25b8cbd79d chore: Add format detection logging for debugging
Logs the detected image format when a file is rejected to help
diagnose why iPhone uploads are being rejected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:14:05 -07:00
2 changed files with 23 additions and 2 deletions

View File

@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- HEIC/HEIF image format support for iPhone photo uploads - HEIC/HEIF image format support for iPhone photo uploads
- Automatic HEIC to JPEG conversion (browsers cannot display HEIC) - MPO (Multi-Picture Object) format support for iPhone depth/portrait photos
- Automatic HEIC/MPO to JPEG conversion (browsers cannot display these formats)
- Graceful error handling if pillow-heif library not installed - Graceful error handling if pillow-heif library not installed
### Dependencies ### Dependencies

View File

@@ -160,6 +160,18 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
file_data = output.getvalue() file_data = output.getvalue()
img = Image.open(io.BytesIO(file_data)) img = Image.open(io.BytesIO(file_data))
# MPO (Multi-Picture Object) conversion (v1.4.2)
# MPO is used by iPhones for depth/portrait photos - extract primary image as JPEG
if img.format == 'MPO':
output = io.BytesIO()
# Convert to RGB if needed
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
img.save(output, format='JPEG', quality=95)
output.seek(0)
file_data = output.getvalue()
img = Image.open(io.BytesIO(file_data))
# Check format is allowed # Check format is allowed
if img.format: if img.format:
format_lower = img.format.lower() format_lower = img.format.lower()
@@ -170,7 +182,15 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
mime_type = 'image/jpeg' mime_type = 'image/jpeg'
if mime_type not in ALLOWED_MIME_TYPES: if mime_type not in ALLOWED_MIME_TYPES:
raise ValueError(f"Invalid image format. Accepted: JPEG, PNG, GIF, WebP") # Log the detected format for debugging (v1.4.2)
try:
current_app.logger.warning(
f'Media upload rejected format: filename="{filename}", '
f'detected_format="{img.format}", mime_type="{mime_type}"'
)
except RuntimeError:
pass # Outside app context
raise ValueError(f"Invalid image format '{img.format}'. Accepted: JPEG, PNG, GIF, WebP")
else: else:
raise ValueError("Could not determine image format") raise ValueError("Could not determine image format")