6 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
042505d5a6 fix(media): Add MAX_CONTENT_LENGTH and debug file capture - v1.4.2
- Set Flask MAX_CONTENT_LENGTH to 50MB (matches MAX_FILE_SIZE)
- Save failed uploads to data/debug/ for analysis
- Log magic bytes when both Pillow and HEIC parsers fail

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:08:36 -07:00
72f3d4a55e fix(media): Save failed uploads to debug/ for analysis - v1.4.2
When an image fails both Pillow and HEIC parsing, save the file
to data/debug/ for manual analysis.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:06:51 -07:00
e8ff0a0dcb fix(media): Add debug logging for unrecognized image formats - v1.4.2
Logs magic bytes when both Pillow and HEIC parsers fail,
to help diagnose what format the file actually is.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 18:06:09 -07:00
9bc6780a8e fix(media): Handle HEIC files with wrong extension - v1.4.2
iOS sometimes saves HEIC with .jpeg extension. Pillow fails to open
these as JPEG, so now we fallback to trying pillow-heif directly
when initial open fails.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 17:54:50 -07:00
3 changed files with 59 additions and 3 deletions

View File

@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- 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
### Dependencies

View File

@@ -86,6 +86,10 @@ def load_config(app, config_override=None):
app.config["FEED_CACHE_ENABLED"] = os.getenv("FEED_CACHE_ENABLED", "true").lower() == "true"
app.config["FEED_CACHE_MAX_SIZE"] = int(os.getenv("FEED_CACHE_MAX_SIZE", "50"))
# Upload limits (v1.4.2)
# Flask MAX_CONTENT_LENGTH limits request body size (matches media.py MAX_FILE_SIZE)
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 # 50MB
# Metrics configuration (v1.1.2 Phase 1)
app.config["METRICS_ENABLED"] = os.getenv("METRICS_ENABLED", "true").lower() == "true"
app.config["METRICS_SLOW_QUERY_THRESHOLD"] = float(os.getenv("METRICS_SLOW_QUERY_THRESHOLD", "1.0"))

View File

@@ -108,6 +108,37 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
# Re-open after verify (verify() closes the file)
img = Image.open(io.BytesIO(file_data))
except Exception as e:
# v1.4.2: If Pillow can't open, try explicitly as HEIC
# iOS sometimes saves HEIC with .jpeg extension
if HEIC_SUPPORTED:
try:
heif_file = pillow_heif.read_heif(file_data)
img = Image.frombytes(
heif_file.mode,
heif_file.size,
heif_file.data,
"raw",
)
# Mark as HEIF so conversion happens below
img.format = 'HEIF'
except Exception as heic_error:
# Log the magic bytes and save file for debugging (if in app context)
try:
magic = file_data[:12].hex() if len(file_data) >= 12 else file_data.hex()
current_app.logger.warning(
f'Media upload failed both Pillow and HEIC: filename="{filename}", '
f'magic_bytes={magic}, pillow_error="{e}", heic_error="{heic_error}"'
)
# Save failed file for analysis
debug_dir = Path(current_app.config.get('DATA_PATH', 'data')) / 'debug'
debug_dir.mkdir(parents=True, exist_ok=True)
debug_file = debug_dir / f"failed_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{filename}"
debug_file.write_bytes(file_data)
current_app.logger.info(f'Saved failed upload for analysis: {debug_file}')
except RuntimeError:
pass # Outside app context (e.g., tests)
raise ValueError(f"Invalid or corrupted image: {e}")
else:
raise ValueError(f"Invalid or corrupted image: {e}")
# HEIC/HEIF conversion (v1.4.2)
@@ -129,6 +160,18 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
file_data = output.getvalue()
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
if img.format:
format_lower = img.format.lower()
@@ -139,7 +182,15 @@ def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, in
mime_type = 'image/jpeg'
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:
raise ValueError("Could not determine image format")