Personal-Digital-Asset-Manager/image_processor.py

95 lines
3.8 KiB
Python
Raw Permalink Normal View History

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)