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>
This commit is contained in:
@@ -21,6 +21,7 @@ from starpunk.media import (
|
||||
MAX_DIMENSION,
|
||||
RESIZE_DIMENSION,
|
||||
MAX_IMAGES_PER_NOTE,
|
||||
HEIC_SUPPORTED,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,44 +46,75 @@ def create_test_image(width=800, height=600, format='PNG'):
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def create_test_heic(width=800, height=600):
|
||||
"""
|
||||
Generate test HEIC image using pillow-heif
|
||||
|
||||
Args:
|
||||
width: Image width in pixels
|
||||
height: Image height in pixels
|
||||
|
||||
Returns:
|
||||
Bytes of HEIC image data
|
||||
"""
|
||||
if not HEIC_SUPPORTED:
|
||||
pytest.skip("pillow-heif not available")
|
||||
|
||||
import pillow_heif
|
||||
|
||||
# Create a simple RGB image
|
||||
img = Image.new('RGB', (width, height), color='blue')
|
||||
|
||||
# Convert to HEIC
|
||||
buffer = io.BytesIO()
|
||||
heif_file = pillow_heif.from_pillow(img)
|
||||
heif_file.save(buffer, format='HEIF')
|
||||
buffer.seek(0)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class TestImageValidation:
|
||||
"""Test validate_image function"""
|
||||
|
||||
def test_valid_jpeg(self):
|
||||
"""Test validation of valid JPEG image"""
|
||||
image_data = create_test_image(800, 600, 'JPEG')
|
||||
mime_type, width, height = validate_image(image_data, 'test.jpg')
|
||||
file_data, mime_type, width, height = validate_image(image_data, 'test.jpg')
|
||||
|
||||
assert mime_type == 'image/jpeg'
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
assert file_data == image_data # No conversion
|
||||
|
||||
def test_valid_png(self):
|
||||
"""Test validation of valid PNG image"""
|
||||
image_data = create_test_image(800, 600, 'PNG')
|
||||
mime_type, width, height = validate_image(image_data, 'test.png')
|
||||
file_data, mime_type, width, height = validate_image(image_data, 'test.png')
|
||||
|
||||
assert mime_type == 'image/png'
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
assert file_data == image_data # No conversion
|
||||
|
||||
def test_valid_gif(self):
|
||||
"""Test validation of valid GIF image"""
|
||||
image_data = create_test_image(800, 600, 'GIF')
|
||||
mime_type, width, height = validate_image(image_data, 'test.gif')
|
||||
file_data, mime_type, width, height = validate_image(image_data, 'test.gif')
|
||||
|
||||
assert mime_type == 'image/gif'
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
assert file_data == image_data # No conversion
|
||||
|
||||
def test_valid_webp(self):
|
||||
"""Test validation of valid WebP image"""
|
||||
image_data = create_test_image(800, 600, 'WEBP')
|
||||
mime_type, width, height = validate_image(image_data, 'test.webp')
|
||||
file_data, mime_type, width, height = validate_image(image_data, 'test.webp')
|
||||
|
||||
assert mime_type == 'image/webp'
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
assert file_data == image_data # No conversion
|
||||
|
||||
def test_file_too_large(self):
|
||||
"""Test rejection of >10MB file (per Q6)"""
|
||||
@@ -113,6 +145,96 @@ class TestImageValidation:
|
||||
assert "Invalid or corrupted" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestHEICSupport:
|
||||
"""Test HEIC/HEIF image format support (v1.4.2)"""
|
||||
|
||||
def test_heic_detection_and_conversion(self):
|
||||
"""Test HEIC file is detected and converted to JPEG"""
|
||||
heic_data = create_test_heic(800, 600)
|
||||
file_data, mime_type, width, height = validate_image(heic_data, 'test.heic')
|
||||
|
||||
# Should be converted to JPEG
|
||||
assert mime_type == 'image/jpeg'
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
# file_data should be different (converted to JPEG)
|
||||
assert file_data != heic_data
|
||||
# Verify it's actually JPEG by opening it
|
||||
img = Image.open(io.BytesIO(file_data))
|
||||
assert img.format == 'JPEG'
|
||||
|
||||
def test_heic_with_rgba_mode(self):
|
||||
"""Test HEIC with alpha channel is converted to RGB JPEG"""
|
||||
if not HEIC_SUPPORTED:
|
||||
pytest.skip("pillow-heif not available")
|
||||
|
||||
import pillow_heif
|
||||
|
||||
# Create image with alpha channel
|
||||
img = Image.new('RGBA', (800, 600), color=(255, 0, 0, 128))
|
||||
buffer = io.BytesIO()
|
||||
heif_file = pillow_heif.from_pillow(img)
|
||||
heif_file.save(buffer, format='HEIF')
|
||||
buffer.seek(0)
|
||||
heic_data = buffer.getvalue()
|
||||
|
||||
file_data, mime_type, width, height = validate_image(heic_data, 'test.heic')
|
||||
|
||||
# Should be converted to JPEG (no alpha)
|
||||
assert mime_type == 'image/jpeg'
|
||||
# Verify it's RGB (no alpha)
|
||||
img = Image.open(io.BytesIO(file_data))
|
||||
assert img.mode == 'RGB'
|
||||
|
||||
def test_heic_dimensions_preserved(self):
|
||||
"""Test HEIC conversion preserves dimensions"""
|
||||
heic_data = create_test_heic(1024, 768)
|
||||
file_data, mime_type, width, height = validate_image(heic_data, 'photo.heic')
|
||||
|
||||
assert width == 1024
|
||||
assert height == 768
|
||||
assert mime_type == 'image/jpeg'
|
||||
|
||||
def test_heic_error_without_library(self, monkeypatch):
|
||||
"""Test appropriate error when HEIC uploaded but pillow-heif not available"""
|
||||
# Mock HEIC_SUPPORTED to False
|
||||
from starpunk import media
|
||||
monkeypatch.setattr(media, 'HEIC_SUPPORTED', False)
|
||||
|
||||
# Create a mock HEIC file (just needs to be recognized as HEIC by Pillow)
|
||||
# We'll create a real HEIC if library is available, otherwise skip
|
||||
if not HEIC_SUPPORTED:
|
||||
pytest.skip("pillow-heif not available to create test HEIC")
|
||||
|
||||
heic_data = create_test_heic(800, 600)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
validate_image(heic_data, 'test.heic')
|
||||
|
||||
assert "HEIC/HEIF images require pillow-heif library" in str(exc_info.value)
|
||||
assert "convert to JPEG" in str(exc_info.value)
|
||||
|
||||
def test_heic_full_upload_flow(self, app):
|
||||
"""Test complete HEIC upload through save_media"""
|
||||
heic_data = create_test_heic(800, 600)
|
||||
|
||||
with app.app_context():
|
||||
media_info = save_media(heic_data, 'iphone_photo.heic')
|
||||
|
||||
# Should be saved as JPEG
|
||||
assert media_info['mime_type'] == 'image/jpeg'
|
||||
assert media_info['width'] == 800
|
||||
assert media_info['height'] == 600
|
||||
|
||||
# Verify file was saved
|
||||
media_path = Path(app.config['DATA_PATH']) / 'media' / media_info['path']
|
||||
assert media_path.exists()
|
||||
|
||||
# Verify it's actually a JPEG file
|
||||
saved_img = Image.open(media_path)
|
||||
assert saved_img.format == 'JPEG'
|
||||
|
||||
|
||||
class TestImageOptimization:
|
||||
"""Test optimize_image function"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user