""" Linux BenchTools - Image Processor Handles image compression, resizing and thumbnail generation """ import os from pathlib import Path from typing import Tuple, Optional from PIL import Image import hashlib from datetime import datetime from app.core.config import settings from app.utils.image_config_loader import image_compression_config class ImageProcessor: """Image processing utilities""" @staticmethod def process_image_with_level( image_path: str, output_dir: str, compression_level: Optional[str] = None, output_format: Optional[str] = None, save_original: bool = True ) -> Tuple[str, int, Optional[str]]: """ Process an image using configured compression level Saves original in original/ subdirectory and resized in main directory Args: image_path: Path to source image output_dir: Directory for output compression_level: Compression level (high, medium, low, minimal) If None, uses default from config output_format: Output format (None = PNG from config) save_original: Save original file in original/ subdirectory Returns: Tuple of (output_path, file_size_bytes, original_path) """ # Get compression settings and folders config level_config = image_compression_config.get_level(compression_level) folders = image_compression_config.get_folders() if output_format is None: output_format = image_compression_config.get_output_format() # Create subdirectories original_dir = os.path.join(output_dir, folders.get("original", "original")) os.makedirs(original_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) # Save original if requested original_path = None if save_original and image_compression_config.should_keep_original(): import shutil original_filename = os.path.basename(image_path) original_path = os.path.join(original_dir, original_filename) shutil.copy2(image_path, original_path) # Process and resize image resized_path, file_size = ImageProcessor.process_image( image_path=image_path, output_dir=output_dir, max_width=level_config.get("max_width"), max_height=level_config.get("max_height"), quality=level_config.get("quality"), output_format=output_format ) return resized_path, file_size, original_path @staticmethod def create_thumbnail_with_level( image_path: str, output_dir: str, compression_level: Optional[str] = None, output_format: Optional[str] = None ) -> Tuple[str, int]: """ Create thumbnail using configured compression level Saves in thumbnail/ subdirectory Args: image_path: Path to source image output_dir: Directory for output compression_level: Compression level (high, medium, low, minimal) output_format: Output format (None = PNG from config) Returns: Tuple of (output_path, file_size_bytes) """ # Get compression settings and folders config level_config = image_compression_config.get_level(compression_level) folders = image_compression_config.get_folders() if output_format is None: output_format = image_compression_config.get_output_format() # Create thumbnail subdirectory thumbnail_dir = os.path.join(output_dir, folders.get("thumbnail", "thumbnail")) os.makedirs(thumbnail_dir, exist_ok=True) return ImageProcessor.create_thumbnail( image_path=image_path, output_dir=thumbnail_dir, size=level_config.get("thumbnail_size"), quality=level_config.get("thumbnail_quality"), output_format=output_format ) @staticmethod def process_image( image_path: str, output_dir: str, max_width: Optional[int] = None, max_height: Optional[int] = None, quality: Optional[int] = None, output_format: str = "webp" ) -> Tuple[str, int]: """ Process an image: resize and compress Args: image_path: Path to source image output_dir: Directory for output max_width: Maximum width (None = no limit) max_height: Maximum height (None = no limit) quality: Compression quality 1-100 (None = use settings) output_format: Output format (webp, jpeg, png) Returns: Tuple of (output_path, file_size_bytes) """ # Use settings if not provided if max_width is None: max_width = settings.IMAGE_MAX_WIDTH if max_height is None: max_height = settings.IMAGE_MAX_HEIGHT if quality is None: quality = settings.IMAGE_COMPRESSION_QUALITY # Open image img = Image.open(image_path) # Convert RGBA to RGB for JPEG/WebP if img.mode == 'RGBA' and output_format.lower() in ['jpeg', 'jpg', 'webp']: # Create white background background = Image.new('RGB', img.size, (255, 255, 255)) background.paste(img, mask=img.split()[3]) # Use alpha channel as mask img = background # Resize if needed original_width, original_height = img.size if max_width and original_width > max_width or max_height and original_height > max_height: img.thumbnail((max_width or original_width, max_height or original_height), Image.Resampling.LANCZOS) # Generate unique filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") original_name = Path(image_path).stem output_filename = f"{original_name}_{timestamp}.{output_format}" output_path = os.path.join(output_dir, output_filename) # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) # Save with compression save_kwargs = {'quality': quality, 'optimize': True} if output_format.lower() == 'webp': save_kwargs['method'] = 6 # Better compression elif output_format.lower() in ['jpeg', 'jpg']: save_kwargs['progressive'] = True img.save(output_path, format=output_format.upper(), **save_kwargs) # Get file size file_size = os.path.getsize(output_path) return output_path, file_size @staticmethod def create_thumbnail( image_path: str, output_dir: str, size: Optional[int] = None, quality: Optional[int] = None, output_format: Optional[str] = None ) -> Tuple[str, int]: """ Create a thumbnail Args: image_path: Path to source image output_dir: Directory for output size: Thumbnail size (square, None = use settings) quality: Compression quality (None = use settings) output_format: Output format (None = use settings) Returns: Tuple of (output_path, file_size_bytes) """ # Use settings if not provided if size is None: size = settings.THUMBNAIL_SIZE if quality is None: quality = settings.THUMBNAIL_QUALITY if output_format is None: output_format = settings.THUMBNAIL_FORMAT # Open image img = Image.open(image_path) # Convert RGBA to RGB for JPEG/WebP if img.mode == 'RGBA' and output_format.lower() in ['jpeg', 'jpg', 'webp']: background = Image.new('RGB', img.size, (255, 255, 255)) background.paste(img, mask=img.split()[3]) img = background # Resize keeping aspect ratio (width-based) # size parameter represents the target width width, height = img.size aspect_ratio = height / width new_width = size new_height = int(size * aspect_ratio) # Use thumbnail method to preserve aspect ratio img.thumbnail((new_width, new_height), Image.Resampling.LANCZOS) # Generate filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") original_name = Path(image_path).stem output_filename = f"{original_name}_thumb_{timestamp}.{output_format}" output_path = os.path.join(output_dir, output_filename) # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) # Save save_kwargs = {'quality': quality, 'optimize': True} if output_format.lower() == 'webp': save_kwargs['method'] = 6 elif output_format.lower() in ['jpeg', 'jpg']: save_kwargs['progressive'] = True img.save(output_path, format=output_format.upper(), **save_kwargs) # Get file size file_size = os.path.getsize(output_path) return output_path, file_size @staticmethod def get_image_hash(image_path: str) -> str: """ Calculate SHA256 hash of image file Args: image_path: Path to image Returns: SHA256 hash as hex string """ sha256_hash = hashlib.sha256() with open(image_path, "rb") as f: # Read in chunks for large files for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() @staticmethod def get_image_info(image_path: str) -> dict: """ Get image information Args: image_path: Path to image Returns: Dictionary with image info """ img = Image.open(image_path) return { "width": img.width, "height": img.height, "format": img.format, "mode": img.mode, "size_bytes": os.path.getsize(image_path), "hash": ImageProcessor.get_image_hash(image_path) } @staticmethod def is_valid_image(file_path: str) -> bool: """ Check if file is a valid image Args: file_path: Path to file Returns: True if valid image, False otherwise """ try: img = Image.open(file_path) img.verify() return True except Exception: return False @staticmethod def get_mime_type(file_path: str) -> Optional[str]: """ Get MIME type from image file Args: file_path: Path to image Returns: MIME type string or None """ try: img = Image.open(file_path) format_to_mime = { 'JPEG': 'image/jpeg', 'PNG': 'image/png', 'GIF': 'image/gif', 'BMP': 'image/bmp', 'WEBP': 'image/webp', 'TIFF': 'image/tiff' } return format_to_mime.get(img.format, f'image/{img.format.lower()}') except Exception: return None