docs refactor
All checks were successful
All checks were successful
This commit is contained in:
204
src/server/image_optimizer.py
Normal file
204
src/server/image_optimizer.py
Normal file
@ -0,0 +1,204 @@
|
||||
"""
|
||||
Image optimization utilities for foldsite
|
||||
Follows grug principles: simple, focused functionality
|
||||
"""
|
||||
|
||||
import os
|
||||
from PIL import Image, ExifTags
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ImageOptimizer:
|
||||
"""Simple image optimization following grug principles - does one thing well"""
|
||||
|
||||
def __init__(self, max_width=2048, max_height=2048, quality=85):
|
||||
self.max_width = max_width
|
||||
self.max_height = max_height
|
||||
self.quality = quality
|
||||
|
||||
def optimize_image(self, input_path, output_path=None, preserve_exif=True):
|
||||
"""
|
||||
Optimize a single image - grug-simple approach
|
||||
"""
|
||||
if output_path is None:
|
||||
output_path = input_path
|
||||
|
||||
try:
|
||||
with Image.open(input_path) as img:
|
||||
# Handle EXIF orientation
|
||||
if preserve_exif and hasattr(img, '_getexif'):
|
||||
exif = img._getexif()
|
||||
if exif is not None:
|
||||
orientation = exif.get(0x0112, 1)
|
||||
if orientation == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
elif orientation == 6:
|
||||
img = img.rotate(270, expand=True)
|
||||
elif orientation == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
|
||||
# Resize if too large
|
||||
if img.width > self.max_width or img.height > self.max_height:
|
||||
img.thumbnail((self.max_width, self.max_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Convert to RGB if needed (for JPEG output)
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
||||
rgb_img.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
||||
img = rgb_img
|
||||
|
||||
# Save optimized image
|
||||
save_kwargs = {'quality': self.quality, 'optimize': True}
|
||||
|
||||
# Preserve some EXIF if possible
|
||||
if preserve_exif and hasattr(img, '_getexif'):
|
||||
exif_dict = img._getexif()
|
||||
if exif_dict:
|
||||
# Keep important EXIF data
|
||||
save_kwargs['exif'] = img.info.get('exif', b'')
|
||||
|
||||
img.save(output_path, **save_kwargs)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error optimizing {input_path}: {e}")
|
||||
return False
|
||||
|
||||
def optimize_folder(self, folder_path, backup=True):
|
||||
"""
|
||||
Optimize all images in a folder - simple batch processing
|
||||
"""
|
||||
folder_path = Path(folder_path)
|
||||
results = {'optimized': 0, 'errors': 0, 'skipped': 0}
|
||||
|
||||
image_extensions = {'.jpg', '.jpeg', '.png', '.gif'}
|
||||
|
||||
for file_path in folder_path.rglob('*'):
|
||||
if file_path.suffix.lower() in image_extensions:
|
||||
try:
|
||||
# Create backup if requested
|
||||
if backup:
|
||||
backup_path = file_path.with_suffix(f'{file_path.suffix}.backup')
|
||||
if not backup_path.exists():
|
||||
file_path.rename(backup_path)
|
||||
source_path = backup_path
|
||||
else:
|
||||
source_path = file_path
|
||||
else:
|
||||
source_path = file_path
|
||||
|
||||
# Optimize
|
||||
if self.optimize_image(source_path, file_path):
|
||||
results['optimized'] += 1
|
||||
else:
|
||||
results['errors'] += 1
|
||||
|
||||
except Exception:
|
||||
results['errors'] += 1
|
||||
|
||||
return results
|
||||
|
||||
def generate_thumbnails(self, image_path, thumbnail_dir, sizes=[150, 300, 600]):
|
||||
"""
|
||||
Generate thumbnails in multiple sizes - simple and useful
|
||||
"""
|
||||
image_path = Path(image_path)
|
||||
thumbnail_dir = Path(thumbnail_dir)
|
||||
thumbnail_dir.mkdir(exist_ok=True)
|
||||
|
||||
base_name = image_path.stem
|
||||
extension = image_path.suffix
|
||||
|
||||
generated = []
|
||||
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
for size in sizes:
|
||||
# Create thumbnail
|
||||
thumb = img.copy()
|
||||
thumb.thumbnail((size, size), Image.Resampling.LANCZOS)
|
||||
|
||||
# Save thumbnail
|
||||
thumb_name = f"{base_name}_{size}px{extension}"
|
||||
thumb_path = thumbnail_dir / thumb_name
|
||||
|
||||
# Convert to RGB for JPEG if needed
|
||||
if thumb.mode in ('RGBA', 'P') and extension.lower() in ['.jpg', '.jpeg']:
|
||||
rgb_thumb = Image.new('RGB', thumb.size, (255, 255, 255))
|
||||
rgb_thumb.paste(thumb, mask=thumb.split()[-1] if thumb.mode == 'RGBA' else None)
|
||||
thumb = rgb_thumb
|
||||
|
||||
thumb.save(thumb_path, quality=self.quality, optimize=True)
|
||||
generated.append(thumb_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating thumbnails for {image_path}: {e}")
|
||||
|
||||
return generated
|
||||
|
||||
def get_image_info(self, image_path):
|
||||
"""
|
||||
Get basic image information - simple and fast
|
||||
"""
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
info = {
|
||||
'width': img.width,
|
||||
'height': img.height,
|
||||
'format': img.format,
|
||||
'mode': img.mode,
|
||||
'size_kb': os.path.getsize(image_path) // 1024
|
||||
}
|
||||
|
||||
# Get EXIF data if available
|
||||
if hasattr(img, '_getexif'):
|
||||
exif = img._getexif()
|
||||
if exif:
|
||||
exif_data = {}
|
||||
for key, value in exif.items():
|
||||
tag = ExifTags.TAGS.get(key, key)
|
||||
exif_data[tag] = value
|
||||
info['exif'] = exif_data
|
||||
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def bulk_optimize_images(content_dir, max_width=2048, quality=85):
|
||||
"""
|
||||
Utility function for bulk optimization - grug simple
|
||||
"""
|
||||
optimizer = ImageOptimizer(max_width=max_width, quality=quality)
|
||||
return optimizer.optimize_folder(content_dir, backup=True)
|
||||
|
||||
|
||||
def create_image_gallery_data(folder_path):
|
||||
"""
|
||||
Create gallery data structure for templates - simple and useful
|
||||
"""
|
||||
folder_path = Path(folder_path)
|
||||
images = []
|
||||
image_extensions = {'.jpg', '.jpeg', '.png', '.gif'}
|
||||
|
||||
for file_path in folder_path.iterdir():
|
||||
if file_path.suffix.lower() in image_extensions and file_path.is_file():
|
||||
optimizer = ImageOptimizer()
|
||||
info = optimizer.get_image_info(file_path)
|
||||
|
||||
if 'error' not in info:
|
||||
images.append({
|
||||
'filename': file_path.name,
|
||||
'path': str(file_path.relative_to(folder_path.parent)),
|
||||
'width': info['width'],
|
||||
'height': info['height'],
|
||||
'size_kb': info['size_kb'],
|
||||
'exif': info.get('exif', {}),
|
||||
'date_taken': info.get('exif', {}).get('DateTimeOriginal', ''),
|
||||
'camera': info.get('exif', {}).get('Model', '')
|
||||
})
|
||||
|
||||
# Sort by date taken or filename
|
||||
images.sort(key=lambda x: x.get('date_taken') or x['filename'])
|
||||
return images
|
Reference in New Issue
Block a user