Personal-Digital-Asset-Manager/image_processor.py
2025-05-24 10:39:31 -04:00

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)