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
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)}")
|