You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

192 lines
6.1 KiB

"""
Image and video processing utilities for thumbnail generation
"""
import os
import subprocess
from PIL import Image
from django.conf import settings
def create_thumbnail(image_path, size=(300, 300), quality=85, format='WEBP'):
"""
Create a thumbnail for an uploaded image
Args:
image_path: Absolute path to original image
size: Tuple of (width, height) for thumbnail max dimensions
quality: Image quality (1-95 for JPEG, 1-100 for WebP)
format: Output format ('WEBP', 'JPEG', 'PNG')
Returns:
Absolute path to thumbnail file
Raises:
ValueError: If image cannot be processed
FileNotFoundError: If original image doesn't exist
"""
if not os.path.exists(image_path):
raise FileNotFoundError(f"Original image not found: {image_path}")
try:
# Open and process image
img = Image.open(image_path)
# Convert RGBA to RGB if needed (for JPEG/WebP compatibility)
if img.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'RGBA':
background.paste(img, mask=img.split()[-1])
else:
background.paste(img)
img = background
# Create thumbnail maintaining aspect ratio
img.thumbnail(size, Image.Resampling.LANCZOS)
# Generate thumbnail path
base, ext = os.path.splitext(image_path)
# Choose extension based on format
if format.upper() == 'WEBP':
thumb_path = f"{base}_thumb.webp"
img.save(thumb_path, 'WEBP', quality=quality, method=6)
elif format.upper() == 'JPEG':
thumb_path = f"{base}_thumb.jpg"
img.save(thumb_path, 'JPEG', quality=quality, optimize=True)
elif format.upper() == 'PNG':
thumb_path = f"{base}_thumb.png"
img.save(thumb_path, 'PNG', optimize=True)
else:
# Default to WebP
thumb_path = f"{base}_thumb.webp"
img.save(thumb_path, 'WEBP', quality=quality, method=6)
return thumb_path
except Exception as e:
raise ValueError(f"Failed to create thumbnail: {str(e)}")
def get_image_dimensions(image_path):
"""
Get dimensions of an image
Args:
image_path: Path to image file
Returns:
Tuple of (width, height) or None if error
"""
try:
with Image.open(image_path) as img:
return img.size
except Exception:
return None
def is_image_file(file_path):
"""
Check if a file is an image based on extension and content
Args:
file_path: Path to file
Returns:
Boolean indicating if file is an image
"""
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff'}
ext = os.path.splitext(file_path)[1].lower()
if ext not in image_extensions:
return False
try:
with Image.open(file_path) as img:
img.verify()
return True
except Exception:
return False
def is_video_file(file_path):
"""
Check if a file is a video based on extension
Args:
file_path: Path to file
Returns:
Boolean indicating if file is a video
"""
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm', '.m4v'}
ext = os.path.splitext(file_path)[1].lower()
return ext in video_extensions
def extract_video_thumbnail(video_path, time_offset='00:00:01', size=(300, 300), quality=85):
"""
Extract a frame from video as thumbnail using FFmpeg
Args:
video_path: Absolute path to video file
time_offset: Time position to extract frame (default: 1 second)
size: Tuple of (width, height) for thumbnail max dimensions
quality: JPEG quality (1-31 for FFmpeg, lower is better, we convert to FFmpeg scale)
Returns:
Absolute path to thumbnail file or None if failed
Raises:
FileNotFoundError: If video file doesn't exist
ValueError: If FFmpeg is not installed or extraction fails
"""
if not os.path.exists(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
# Check if FFmpeg is available
try:
subprocess.run(['ffmpeg', '-version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5)
except (subprocess.TimeoutExpired, FileNotFoundError):
raise ValueError("FFmpeg is not installed or not available in PATH")
try:
# Generate output path
base, ext = os.path.splitext(video_path)
output_path = f"{base}_thumb.jpg"
# Convert quality (85 -> 2 in FFmpeg scale)
# FFmpeg uses 1-31 where lower is better, opposite of our 0-100 scale
ffmpeg_quality = max(1, min(31, int((100 - quality) * 31 / 100)))
# FFmpeg command to extract frame
cmd = [
'ffmpeg',
'-ss', time_offset, # Seek to time position (before input for speed)
'-i', video_path, # Input video
'-vframes', '1', # Extract 1 frame
'-vf', f'scale={size[0]}:-1', # Scale to width, maintain aspect ratio
'-q:v', str(ffmpeg_quality), # Quality
'-y', # Overwrite output
output_path
]
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=15 # 15 second timeout
)
if result.returncode == 0 and os.path.exists(output_path):
return output_path
else:
error_msg = result.stderr.decode('utf-8', errors='ignore')
raise ValueError(f"FFmpeg extraction failed: {error_msg[:200]}")
except subprocess.TimeoutExpired:
raise ValueError("Video thumbnail extraction timed out (>15s)")
except Exception as e:
raise ValueError(f"Failed to extract video thumbnail: {str(e)}")