95 lines
No EOL
3.8 KiB
Python
95 lines
No EOL
3.8 KiB
Python
import os
|
|
from PIL import Image
|
|
from wand.image import Image as WandImage
|
|
import io
|
|
from typing import BinaryIO, Tuple, Optional
|
|
|
|
class ImageProcessor:
|
|
@staticmethod
|
|
def is_animated_gif(file_storage) -> bool:
|
|
"""Check if the image is an animated GIF"""
|
|
try:
|
|
# Save current position
|
|
pos = file_storage.tell()
|
|
# Go to beginning
|
|
file_storage.seek(0)
|
|
|
|
with Image.open(file_storage) as img:
|
|
try:
|
|
img.seek(1) # Try to move to the second frame
|
|
is_animated = True
|
|
except EOFError:
|
|
is_animated = False
|
|
|
|
# Restore position
|
|
file_storage.seek(pos)
|
|
return is_animated
|
|
except Exception:
|
|
# Restore position in case of error
|
|
file_storage.seek(pos)
|
|
return False
|
|
|
|
@staticmethod
|
|
def convert_to_webp(file_storage, quality: int = 90) -> Tuple[BinaryIO, str]:
|
|
"""
|
|
Convert an image to WebP format.
|
|
Returns a tuple of (file_object, extension)
|
|
"""
|
|
# Save current position
|
|
pos = file_storage.tell()
|
|
# Go to beginning
|
|
file_storage.seek(0)
|
|
|
|
try:
|
|
# Check if it's an animated GIF
|
|
if ImageProcessor.is_animated_gif(file_storage):
|
|
# Convert animated GIF to animated WebP
|
|
file_storage.seek(0)
|
|
with WandImage(file=file_storage) as img:
|
|
# Configure WebP animation settings
|
|
img.format = 'WEBP'
|
|
|
|
# Higher quality settings for animation
|
|
img.options['webp:lossless'] = 'true' # Use lossless for animations
|
|
img.options['webp:method'] = '6' # Best compression method
|
|
img.options['webp:image-hint'] = 'graph' # Better for animations
|
|
img.options['webp:minimize-size'] = 'false' # Prioritize quality
|
|
|
|
# Animation specific settings
|
|
img.options['webp:animation-type'] = 'default'
|
|
img.options['webp:loop'] = '0' # Infinite loop
|
|
|
|
# Save with high quality
|
|
webp_bytes = io.BytesIO(img.make_blob(format='webp'))
|
|
webp_bytes.seek(0)
|
|
return webp_bytes, '.webp'
|
|
else:
|
|
# Handle static images
|
|
file_storage.seek(0)
|
|
with Image.open(file_storage) as img:
|
|
# Convert RGBA to RGB if necessary
|
|
if img.mode in ('RGBA', 'LA'):
|
|
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
background.paste(img, mask=img.getchannel('A'))
|
|
img = background
|
|
elif img.mode != 'RGB':
|
|
img = img.convert('RGB')
|
|
|
|
# Save as WebP with high quality
|
|
output = io.BytesIO()
|
|
img.save(output,
|
|
format='WEBP',
|
|
quality=quality, # Higher quality
|
|
method=6, # Best compression method
|
|
lossless=False, # Use lossy for static images
|
|
exact=True) # Preserve color exactness
|
|
output.seek(0)
|
|
return output, '.webp'
|
|
finally:
|
|
# Restore original position
|
|
file_storage.seek(pos)
|
|
|
|
@staticmethod
|
|
def process_featured_image(file_storage) -> Tuple[BinaryIO, str]:
|
|
"""Process featured image, converting to WebP format"""
|
|
return ImageProcessor.convert_to_webp(file_storage, quality=90) |