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