feat: structure 2

This commit is contained in:
2026-04-07 00:15:00 +02:00
parent 46a0d7aba8
commit 6d1b227fb9
15 changed files with 7433 additions and 94 deletions

View File

@@ -11,13 +11,26 @@ import json
from celery import shared_task
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
import pyvips
import rawpy
import imageio
from PIL import Image
import imageio
from pillow_heif import register_heif_opener
import ffmpeg
# Try to import optional libraries
try:
import pyvips
PYVIPS_AVAILABLE = True
except ImportError:
PYVIPS_AVAILABLE = False
print("pyvips not available, using Pillow for image processing")
try:
import rawpy
RAWPY_AVAILABLE = True
except ImportError:
RAWPY_AVAILABLE = False
print("rawpy not available, using exiftool for RAW preview extraction")
from app.database import AsyncSessionLocal
from app.models import Photo
from app.config import settings
@@ -40,24 +53,28 @@ def get_thumb_path(photo_id: str, size: str) -> str:
os.makedirs(thumb_dir, exist_ok=True)
return f"{thumb_dir}/{size}.{settings.thumbnails.format}"
def process_standard_image(filepath: str) -> pyvips.Image:
def process_standard_image(filepath: str) -> Image.Image:
"""Process standard image formats (JPEG, PNG, etc.)"""
return pyvips.Image.new_from_file(filepath, access='sequential')
return Image.open(filepath)
def process_raw_image(filepath: str) -> pyvips.Image:
def process_raw_image(filepath: str) -> Image.Image:
"""Process RAW image formats"""
try:
with rawpy.imread(filepath) as raw:
# Use half_size for faster processing
rgb = raw.postprocess(use_camera_wb=True, half_size=True)
# Convert numpy array to pyvips image
return pyvips.Image.new_from_array(rgb)
except Exception as e:
logger.error(f"Error processing RAW file {filepath}: {e}")
# Try to extract embedded JPEG preview
if RAWPY_AVAILABLE:
try:
with rawpy.imread(filepath) as raw:
# Use half_size for faster processing
rgb = raw.postprocess(use_camera_wb=True, half_size=True)
# Convert numpy array to PIL Image
return Image.fromarray(rgb, 'RGB')
except Exception as e:
logger.error(f"Error processing RAW file {filepath}: {e}")
# Try to extract embedded JPEG preview
return extract_raw_preview(filepath)
else:
# Use exiftool to extract embedded preview
return extract_raw_preview(filepath)
def extract_raw_preview(filepath: str) -> Optional[pyvips.Image]:
def extract_raw_preview(filepath: str) -> Optional[Image.Image]:
"""Extract embedded JPEG preview from RAW file"""
try:
# Use exiftool to extract preview
@@ -71,13 +88,13 @@ def extract_raw_preview(filepath: str) -> Optional[pyvips.Image]:
if result.returncode == 0 and result.stdout:
tmp.write(result.stdout)
tmp.flush()
return pyvips.Image.new_from_file(tmp.name, access='sequential')
return Image.open(tmp.name)
except Exception as e:
logger.error(f"Error extracting RAW preview from {filepath}: {e}")
return None
def process_heic_image(filepath: str) -> pyvips.Image:
def process_heic_image(filepath: str) -> Image.Image:
"""Process HEIC/HEIF image formats"""
try:
# Use pillow-heif to open the image
@@ -85,16 +102,12 @@ def process_heic_image(filepath: str) -> pyvips.Image:
# Convert to RGB if needed
if img.mode != 'RGB':
img = img.convert('RGB')
# Save to temp file and load with pyvips
import tempfile
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
img.save(tmp.name, 'JPEG')
return pyvips.Image.new_from_file(tmp.name, access='sequential')
return img
except Exception as e:
logger.error(f"Error processing HEIC file {filepath}: {e}")
raise
def process_video_thumbnail(filepath: str) -> pyvips.Image:
def process_video_thumbnail(filepath: str) -> Image.Image:
"""Extract thumbnail from video file"""
try:
# Get video duration
@@ -111,57 +124,50 @@ def process_video_thumbnail(filepath: str) -> pyvips.Image:
stream = ffmpeg.output(stream, tmp.name, vframes=1, format='image2', vcodec='mjpeg')
ffmpeg.run(stream, capture_stdout=True, capture_stderr=True)
return pyvips.Image.new_from_file(tmp.name, access='sequential')
return Image.open(tmp.name)
except Exception as e:
logger.error(f"Error extracting video thumbnail from {filepath}: {e}")
# Create a placeholder thumbnail
return create_placeholder_thumbnail('video')
def create_placeholder_thumbnail(media_type: str) -> pyvips.Image:
def create_placeholder_thumbnail(media_type: str) -> Image.Image:
"""Create a placeholder thumbnail for failed processing"""
# Create a simple gray placeholder
placeholder = pyvips.Image.black(640, 480)
placeholder = placeholder + [128, 128, 128] # Make it gray
return placeholder
img = Image.new('RGB', (640, 480), color=(128, 128, 128))
return img
def auto_rotate_image(image: pyvips.Image) -> pyvips.Image:
def auto_rotate_image(image: Image.Image) -> Image.Image:
"""Auto-rotate image based on EXIF orientation"""
try:
orientation = image.get('orientation')
rotation_map = {
3: 180,
6: 90,
8: 270
}
if orientation in rotation_map:
image = image.rot(rotation_map[orientation])
# Get EXIF data
exif = image._getexif()
if exif:
orientation = exif.get(274) # Orientation tag
rotation_map = {
3: 180,
6: 270, # Note: PIL uses different rotation values than vips
8: 90
}
if orientation in rotation_map:
image = image.rotate(rotation_map[orientation], expand=True)
except:
pass # No orientation data available
return image
def generate_thumbnail(image: pyvips.Image, size: int, output_path: str):
def generate_thumbnail(image: Image.Image, size: int, output_path: str):
"""Generate a thumbnail of the specified size"""
# Calculate scale to fit within size (longest edge)
width = image.width
height = image.height
if width > height:
scale = size / width
else:
scale = size / height
# Only downscale, never upscale
if scale < 1:
image = image.resize(scale)
# Maintain aspect ratio
image.thumbnail((size, size), Image.Resampling.LANCZOS)
# Save as WebP with specified quality
image.webpsave(
image.save(
output_path,
Q=settings.thumbnails.quality,
effort=4 # Balance between speed and compression
'WEBP',
quality=settings.thumbnails.quality,
method=4 # Balance between speed and compression
)
@shared_task(bind=True, name='generate_thumbnails')