204 lines
7.4 KiB
Python
204 lines
7.4 KiB
Python
"""
|
|
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 |