""" Dynamic Product Image Generator Generates product images with configurable colors, hardware positions, and other options. Uses PIL/Pillow for image manipulation. """ import os import json import hashlib from io import BytesIO from PIL import Image, ImageDraw, ImageEnhance, ImageColor from pathlib import Path class ProductImageGenerator: """Handles dynamic generation of product images based on configuration.""" def __init__(self, config_file='data/image_configs.json'): """Initialize with configuration file.""" self.config_file = config_file self.configs = self._load_configs() self.cache_dir = Path('cache/product_images') self.cache_dir.mkdir(parents=True, exist_ok=True) def _load_configs(self): """Load image configuration from JSON file.""" try: with open(self.config_file, 'r') as f: return json.load(f) except Exception as e: print(f"Error loading image configs: {e}") return {} def _find_image(self, image_path): """ Find image file, trying different extensions if needed. Args: image_path: Path to image (e.g., 'images/cobrai.jpg') Returns: Actual path to image file, or None if not found """ # Try the exact path first if os.path.exists(image_path): return image_path # Get base path without extension base_path = os.path.splitext(image_path)[0] # Try common image extensions for ext in ['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG']: test_path = base_path + ext if os.path.exists(test_path): return test_path return None def generate_product_image(self, product_code, color='white', hinge='right', material='aluminum', use_cache=True): """ Generate a product image with specified configuration. Args: product_code: Product identifier (e.g., 'cobrai') color: Color name (e.g., 'white', 'black', 'bronze') hinge: Hinge side ('right' or 'left') material: Material type (for future use) use_cache: Whether to use cached images Returns: PIL Image object """ product_code = product_code.lower() # Check if product config exists if product_code not in self.configs: raise ValueError(f"Product {product_code} not found in configuration") config = self.configs[product_code] # Check cache first if use_cache: cache_key = self._get_cache_key(product_code, color, hinge, material) cached_image = self._get_cached_image(cache_key) if cached_image: return cached_image # Load base image base_image_path = config['sourceImage'] # Try to find the image with different extensions if needed image_path = self._find_image(base_image_path) if not image_path: raise FileNotFoundError(f"Could not load base image: {base_image_path}. Tried .jpg, .png, .jpeg") try: base_image = Image.open(image_path).convert('RGBA') except Exception as e: raise FileNotFoundError(f"Could not load base image: {image_path}. Error: {e}") # Apply color to regions if color in config['availableColors']: color_rgb = tuple(config['availableColors'][color]['rgb']) base_image = self._apply_color_to_regions( base_image, config['colorableRegions'], color_rgb ) # Apply hardware (handles, locks) if hinge in config.get('hardwarePositions', {}): base_image = self._apply_hardware( base_image, config['hardwarePositions'], hinge ) # Flip image if left hinge if hinge == 'left': base_image = base_image.transpose(Image.FLIP_LEFT_RIGHT) # Cache the result if use_cache: self._cache_image(cache_key, base_image) return base_image def _apply_color_to_regions(self, image, regions, target_color): """ Apply color to specific regions of the image. This method attempts to recolor the frame/panel areas while preserving shadows, highlights, and texture. """ img_copy = image.copy() pixels = img_copy.load() width, height = img_copy.size # Get the average brightness of the target color target_brightness = sum(target_color) / 3 for region in regions: x1, y1 = region['topLeft'] x2, y2 = region['bottomRight'] # Ensure coordinates are within bounds x1 = max(0, min(x1, width - 1)) x2 = max(0, min(x2, width)) y1 = max(0, min(y1, height - 1)) y2 = max(0, min(y2, height)) # Apply color to region while preserving luminosity for y in range(y1, y2): for x in range(x1, x2): try: r, g, b, a = pixels[x, y] # Calculate original brightness original_brightness = (r + g + b) / 3 # Preserve relative brightness if original_brightness > 0: brightness_factor = original_brightness / 255.0 # Apply target color with brightness preservation new_r = int(target_color[0] * brightness_factor) new_g = int(target_color[1] * brightness_factor) new_b = int(target_color[2] * brightness_factor) # Ensure values are in valid range new_r = max(0, min(255, new_r)) new_g = max(0, min(255, new_g)) new_b = max(0, min(255, new_b)) pixels[x, y] = (new_r, new_g, new_b, a) except IndexError: continue return img_copy def _apply_hardware(self, image, hardware_positions, hinge_side): """Apply hardware (handles, locks) to the image.""" img_copy = image.copy() hardware_key = f"handle_{hinge_side}" if hardware_key in hardware_positions: hardware_config = hardware_positions[hardware_key] hardware_path = hardware_config.get('image') if hardware_path: # Try to find the hardware image with different extensions actual_path = self._find_image(hardware_path) if actual_path: try: hardware_img = Image.open(actual_path).convert('RGBA') # Flip if needed if hardware_config.get('flip', False): hardware_img = hardware_img.transpose(Image.FLIP_LEFT_RIGHT) # Paste hardware at specified position x, y = hardware_config['x'], hardware_config['y'] img_copy.paste(hardware_img, (x, y), hardware_img) except Exception as e: print(f"Could not apply hardware: {e}") return img_copy def _get_cache_key(self, product_code, color, hinge, material): """Generate a unique cache key for the configuration.""" key_string = f"{product_code}_{color}_{hinge}_{material}" return hashlib.md5(key_string.encode()).hexdigest() def _get_cached_image(self, cache_key): """Retrieve cached image if available.""" cache_path = self.cache_dir / f"{cache_key}.png" if cache_path.exists(): try: return Image.open(cache_path) except Exception: return None return None def _cache_image(self, cache_key, image): """Save image to cache.""" cache_path = self.cache_dir / f"{cache_key}.png" try: image.save(cache_path, 'PNG', optimize=True) except Exception as e: print(f"Could not cache image: {e}") def get_product_config(self, product_code): """Get configuration for a specific product.""" return self.configs.get(product_code.lower()) def clear_cache(self, product_code=None): """Clear cached images. If product_code provided, only clear that product's cache.""" if product_code: # Clear specific product cache (would need to track cache keys) pass else: # Clear all cache for cache_file in self.cache_dir.glob('*.png'): try: cache_file.unlink() except Exception: pass def recolor_image_advanced(image, source_color_range, target_color): """ Advanced recoloring that replaces pixels within a color range. Args: image: PIL Image source_color_range: Dict with 'min' and 'max' RGB tuples target_color: Target RGB tuple """ img_copy = image.copy() pixels = img_copy.load() width, height = img_copy.size min_r, min_g, min_b = source_color_range['min'] max_r, max_g, max_b = source_color_range['max'] for y in range(height): for x in range(width): r, g, b, a = pixels[x, y] # Check if pixel is within source color range if (min_r <= r <= max_r and min_g <= g <= max_g and min_b <= b <= max_b): # Calculate brightness factor brightness = (r + g + b) / 3 / 255.0 # Apply target color with brightness new_r = int(target_color[0] * brightness) new_g = int(target_color[1] * brightness) new_b = int(target_color[2] * brightness) pixels[x, y] = (new_r, new_g, new_b, a) return img_copy def image_to_base64(image, format='PNG'): """Convert PIL Image to base64 string.""" import base64 buffered = BytesIO() image.save(buffered, format=format) img_str = base64.b64encode(buffered.getvalue()).decode() return f"data:image/{format.lower()};base64,{img_str}"