3 Commits

Author SHA1 Message Date
6682339a86 fix(media): Increase max dimension to 12000px for modern phone cameras
Modern iPhones (48MP) and other phones produce images larger than 4096px.
Since optimize_image() resizes them anyway, the input limit was too
restrictive. Increased from 4096x4096 to 12000x12000.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:29:21 -07:00
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
3 changed files with 32 additions and 6 deletions

View File

@@ -12,9 +12,14 @@ 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
### Changed
- Increased maximum input image dimensions from 4096x4096 to 12000x12000 to support modern phone cameras (48MP+); images are still optimized to smaller sizes for web delivery
### Dependencies ### Dependencies
- Added `pillow-heif` for HEIC image processing - Added `pillow-heif` for HEIC image processing

View File

@@ -38,7 +38,7 @@ ALLOWED_MIME_TYPES = {
# Limits per Q&A and ADR-058 (updated in v1.4.0) # Limits per Q&A and ADR-058 (updated in v1.4.0)
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB (v1.4.0) MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB (v1.4.0)
MAX_OUTPUT_SIZE = 10 * 1024 * 1024 # 10MB target after optimization (v1.4.0) MAX_OUTPUT_SIZE = 10 * 1024 * 1024 # 10MB target after optimization (v1.4.0)
MAX_DIMENSION = 4096 # 4096x4096 max MAX_DIMENSION = 12000 # 12000x12000 max input (v1.4.2 - supports modern phone cameras)
RESIZE_DIMENSION = 2048 # Auto-resize to 2048px (default) RESIZE_DIMENSION = 2048 # Auto-resize to 2048px (default)
MIN_QUALITY = 70 # Minimum JPEG quality before rejection (v1.4.0) MIN_QUALITY = 70 # Minimum JPEG quality before rejection (v1.4.0)
MIN_DIMENSION = 640 # Minimum dimension before rejection (v1.4.0) MIN_DIMENSION = 640 # Minimum dimension before rejection (v1.4.0)
@@ -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,11 +182,20 @@ 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")
# Check dimensions # Check dimensions (v1.4.2: increased to 12000px to support modern phone cameras)
# Images will be resized by optimize_image() anyway
width, height = img.size width, height = img.size
if max(width, height) > MAX_DIMENSION: if max(width, height) > MAX_DIMENSION:
raise ValueError(f"Image dimensions too large. Maximum is {MAX_DIMENSION}x{MAX_DIMENSION} pixels") raise ValueError(f"Image dimensions too large. Maximum is {MAX_DIMENSION}x{MAX_DIMENSION} pixels")

View File

@@ -127,8 +127,8 @@ class TestImageValidation:
assert "File too large" in str(exc_info.value) assert "File too large" in str(exc_info.value)
def test_dimensions_too_large(self): def test_dimensions_too_large(self):
"""Test rejection of >4096px image (per ADR-058)""" """Test rejection of >12000px image (v1.4.2: increased from 4096)"""
large_image = create_test_image(5000, 5000, 'PNG') large_image = create_test_image(13000, 13000, 'PNG')
with pytest.raises(ValueError) as exc_info: with pytest.raises(ValueError) as exc_info:
validate_image(large_image, 'huge.png') validate_image(large_image, 'huge.png')