Files
StarPunk/docs/design/v1.4.2/heic-support-design.md
Phil Skentelbery e4e481d7cf 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>
2025-12-16 17:45:53 -07:00

6.8 KiB

HEIC Image Support Design - v1.4.2

Status: Ready for Implementation Type: Patch Release (backward compatible bug fix) Date: 2025-12-16

Problem Statement

iPhones save photos in HEIC format by default (since iOS 11). When users upload photos from iPhones to StarPunk, they receive "Invalid image format" errors because:

  1. HEIC is not in ALLOWED_MIME_TYPES
  2. Pillow cannot open HEIC files without the pillow-heif plugin
  3. iOS sometimes renames .heic files to .jpeg without converting, causing confusion

HEIC files cannot be displayed directly in browsers, so conversion to JPEG is required.

Design Overview

This is a minimal patch release with a single conceptual change: detect HEIC/HEIF images and convert them to JPEG before processing. The implementation requires:

  1. Dependency Addition: Add pillow-heif to requirements.txt
  2. Code Change: Modify validate_image() in starpunk/media.py to detect and convert HEIC

Implementation Specification

1. Dependency Update

File: /home/phil/Projects/starpunk/requirements.txt

Add after Pillow:

# HEIC/HEIF Support (v1.4.2 - iPhone photos)
pillow-heif==0.18.*

Note: pillow-heif automatically registers with Pillow on import, enabling HEIC support.

2. Code Changes

File: /home/phil/Projects/starpunk/starpunk/media.py

2.1 Add Import (top of file, after existing imports)

# 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

Rationale: Conditional import allows graceful degradation if pillow-heif is not installed (e.g., during development).

2.2 Modify validate_image() Function

Insert HEIC detection and conversion immediately after the Pillow image verification (line ~99), before the format check (line ~104).

Insert after line 99 (after img = Image.open(io.BytesIO(file_data))):

    # 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))

Modify the return statement to return the potentially converted file_data:

The current function signature returns Tuple[str, int, int] (mime_type, width, height). We need to also return the converted bytes when HEIC conversion occurs.

Change return type (line 84):

def validate_image(file_data: bytes, filename: str) -> Tuple[bytes, str, int, int]:

Change return statement (line 138):

    return file_data, mime_type, width, height

2.3 Update save_media() to Handle New Return Value

Modify line 400 in save_media():

        # Validate image (returns 4-tuple with potentially converted bytes)
        try:
            file_data, mime_type, orig_width, orig_height = validate_image(file_data, filename)
        except ValueError as e:

Note: The file_data variable is already in scope, so this reassignment handles the HEIC conversion case transparently.

Design Decisions

D1: Convert at Validation Time

Decision: Convert HEIC to JPEG during validate_image() rather than in a separate step.

Rationale:

  • Keeps the change minimal (single function modification)
  • Converted data flows naturally through existing optimize_image() pipeline
  • No new function signatures or abstractions needed
  • Validation is the logical place to normalize input formats

D2: Convert to JPEG (not WebP or PNG)

Decision: Convert HEIC to JPEG format.

Rationale:

  • JPEG has universal browser support
  • HEIC photos are typically photographic content, which JPEG handles well
  • Quality 95 preserves detail while reducing file size
  • Consistent with existing JPEG optimization pipeline

D3: Graceful Degradation

Decision: Use conditional import with HEIC_SUPPORTED flag.

Rationale:

  • Allows code to work without pillow-heif during development
  • Provides clear error message if HEIC upload attempted without library
  • No runtime crash if dependency missing

D4: Quality Setting

Decision: Use quality=95 for HEIC to JPEG conversion.

Rationale:

  • Preserves most detail from the original
  • Subsequent optimize_image() call will further compress if needed
  • Matches existing optimization tier behavior for high-quality inputs

Testing Requirements

Unit Tests

Add to existing media tests in /home/phil/Projects/starpunk/tests/test_media.py:

  1. Test HEIC detection and conversion

    • Upload valid HEIC file
    • Verify output is JPEG format
    • Verify dimensions preserved
  2. Test HEIC with alpha channel

    • Upload HEIC with transparency
    • Verify conversion to RGB (no alpha in JPEG)
  3. Test error handling without pillow-heif

    • Mock HEIC_SUPPORTED = False
    • Verify appropriate error message

Test Files

A sample HEIC file is needed for testing. Options:

  • Create programmatically using pillow-heif
  • Download from a public test file repository
  • Use iPhone simulator to generate

Migration Notes

  • Database: No changes required
  • Configuration: No changes required
  • Existing uploads: Not affected (HEIC was previously rejected)
  • Backward compatibility: Fully backward compatible

Files Changed

File Change
requirements.txt Add pillow-heif==0.18.*
starpunk/media.py Add HEIC import, modify validate_image()
starpunk/__init__.py Update __version__ to "1.4.2"
CHANGELOG.md Add v1.4.2 release notes
tests/test_media.py Add HEIC test cases

Changelog Entry

## [1.4.2] - 2025-12-XX

### Added
- HEIC/HEIF image format support for iPhone photo uploads
- Automatic HEIC to JPEG conversion (browsers cannot display HEIC)

### Dependencies
- Added `pillow-heif` for HEIC image processing

Implementation Checklist

  • Add pillow-heif==0.18.* to requirements.txt
  • Add HEIC import and registration to media.py
  • Modify validate_image() return type to include bytes
  • Add HEIC detection and conversion logic to validate_image()
  • Update save_media() to handle new return value
  • Update __version__ to "1.4.2"
  • Add HEIC test cases
  • Update CHANGELOG.md
  • Run full test suite