2 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
3 changed files with 23 additions and 5 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()
@@ -182,7 +194,8 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
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')