83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
import io
|
|
import os
|
|
|
|
from django.core.files.base import ContentFile
|
|
from django.core.files.storage import FileSystemStorage
|
|
import pillow_heif
|
|
from PIL import Image, ImageSequence
|
|
|
|
pillow_heif.register_heif_opener()
|
|
from storages.backends.s3boto3 import S3Boto3Storage, S3StaticStorage
|
|
|
|
# All formats Pillow handles natively (no extra plugins needed).
|
|
# Add '.heic' / '.heif' if you install pillow-heif.
|
|
_IMAGE_EXTS = {
|
|
'.jpg', '.jpeg', '.jpe', '.jfif', # JPEG variants
|
|
'.png', # PNG
|
|
'.gif', # GIF (animated preserved)
|
|
'.bmp', '.dib', # BMP
|
|
'.tiff', '.tif', # TIFF
|
|
'.tga', # Truevision TGA
|
|
'.ico', # ICO (largest frame used)
|
|
'.ppm', '.pgm', '.pbm', '.pnm', # Portable pixmap family
|
|
'.pcx', # PCX
|
|
'.heic', '.heif', # Apple HEIC/HEIF (pillow-heif)
|
|
}
|
|
|
|
def _to_webp(content, quality: int = 85) -> io.BytesIO:
|
|
img = Image.open(content)
|
|
frames = list(ImageSequence.Iterator(img))
|
|
out = io.BytesIO()
|
|
if len(frames) > 1:
|
|
converted = [f.copy().convert('RGBA') for f in frames]
|
|
converted[0].save(
|
|
out,
|
|
format='WEBP',
|
|
save_all=True,
|
|
append_images=converted[1:],
|
|
loop=img.info.get('loop', 0),
|
|
quality=quality,
|
|
)
|
|
else:
|
|
img.convert('RGBA').save(out, format='WEBP', quality=quality)
|
|
out.seek(0)
|
|
return out
|
|
|
|
|
|
class WebPConversionMixin:
|
|
"""
|
|
Intercepts image/GIF uploads and converts them to WebP before storage.
|
|
Videos are saved as-is; use `vontor_cz.tasks.convert_video_to_webm` async
|
|
to transcode them to WebM after save.
|
|
"""
|
|
WEBP_QUALITY: int = 85
|
|
|
|
def _save(self, name: str, content) -> str:
|
|
root, ext = os.path.splitext(name)
|
|
if ext.lower() in _IMAGE_EXTS:
|
|
try:
|
|
webp_data = _to_webp(content, self.WEBP_QUALITY)
|
|
content = ContentFile(webp_data.read())
|
|
name = root + '.webp'
|
|
except Exception:
|
|
content.seek(0)
|
|
return super()._save(name, content)
|
|
|
|
|
|
class MediaStorage(WebPConversionMixin, S3Boto3Storage):
|
|
def exists(self, name):
|
|
if not name:
|
|
return False
|
|
return super().exists(name)
|
|
|
|
|
|
class LocalMediaStorage(WebPConversionMixin, FileSystemStorage):
|
|
pass
|
|
|
|
|
|
class StaticStorage(S3StaticStorage):
|
|
def exists(self, name):
|
|
if not name:
|
|
return False
|
|
return super().exists(name)
|