""" Image mapping utilities for product catalog. This module provides functions to map product image filenames to their actual file paths in the product_samples directory structure, with fallback handling for missing images and caching for performance. """ import os from pathlib import Path from typing import Dict, Optional from functools import lru_cache # Base path for product images (relative to project root) PRODUCT_SAMPLES_BASE = "search_personalization/product_samples" PLACEHOLDER_IMAGE = "search_personalization/product_samples/product_image_coming_soon.png" THUMBNAIL_DIR = Path("search_personalization/product_samples/.thumbnails") # In-memory cache for image path existence checks _image_cache: Dict[str, str] = {} def get_thumbnail_path(product: Dict) -> str: """ Get a small thumbnail for fast UI rendering (~200px wide, ~70% quality). Generates on first access, then serves from disk cache. Args: product: Product dictionary containing 'category' and 'image' fields Returns: Path to the thumbnail image, or placeholder if original doesn't exist. """ original = get_image_path(product) if original == PLACEHOLDER_IMAGE: return PLACEHOLDER_IMAGE rel = Path(original).relative_to(PRODUCT_SAMPLES_BASE) thumb = THUMBNAIL_DIR / rel if thumb.exists(): return str(thumb) # Generate thumbnail try: from PIL import Image thumb.parent.mkdir(parents=True, exist_ok=True) img = Image.open(original) img.thumbnail((200, 200)) img.save(thumb, "JPEG", quality=70) return str(thumb) except Exception: return original def get_image_path(product: Dict, use_cache: bool = True) -> str: """ Map product image filename to actual file path in product_samples directory. This function constructs the full path to a product image based on the product's category and image filename. If the image file doesn't exist, it returns the path to a placeholder image. Args: product: Product dictionary containing 'category' and 'image' fields use_cache: Whether to use cached image path results (default: True) Returns: String path to the image file, either the actual product image or the placeholder image if the product image is not found Examples: >>> product = {"category": "accessories", "image": "abc123.jpg"} >>> get_image_path(product) 'product_samples/accessories/abc123.jpg' >>> product = {"category": "apparel", "image": "missing.jpg"} >>> get_image_path(product) # If missing.jpg doesn't exist 'product_samples/product_image_coming_soon.png' Requirements: 9.4 """ # Extract category and image from product category = product.get('category', '') image_filename = product.get('image', '') # If no image filename provided, return placeholder if not image_filename: return PLACEHOLDER_IMAGE # If no category provided, return placeholder if not category: return PLACEHOLDER_IMAGE # Create cache key cache_key = f"{category}/{image_filename}" # Check cache first if enabled if use_cache and cache_key in _image_cache: return _image_cache[cache_key] # Construct the expected image path image_path = os.path.join(PRODUCT_SAMPLES_BASE, category, image_filename) # Check if the image file exists if os.path.exists(image_path) and os.path.isfile(image_path): # Cache the result if use_cache: _image_cache[cache_key] = image_path return image_path # Image doesn't exist, return placeholder if use_cache: _image_cache[cache_key] = PLACEHOLDER_IMAGE return PLACEHOLDER_IMAGE @lru_cache(maxsize=1000) def get_image_path_cached(category: str, image_filename: str) -> str: """ Cached version of image path lookup using functools.lru_cache. This function provides an alternative caching mechanism using Python's built-in LRU cache decorator. It's more efficient for repeated lookups of the same category/image combinations. Args: category: Product category (e.g., 'accessories', 'apparel', 'footwear') image_filename: Image filename (e.g., 'abc123.jpg') Returns: String path to the image file or placeholder Note: This function uses @lru_cache which caches up to 1000 unique category/filename combinations. The cache persists for the lifetime of the application. """ # If no image filename provided, return placeholder if not image_filename: return PLACEHOLDER_IMAGE # If no category provided, return placeholder if not category: return PLACEHOLDER_IMAGE # Construct the expected image path image_path = os.path.join(PRODUCT_SAMPLES_BASE, category, image_filename) # Check if the image file exists if os.path.exists(image_path) and os.path.isfile(image_path): return image_path # Image doesn't exist, return placeholder return PLACEHOLDER_IMAGE def clear_image_cache() -> None: """ Clear the in-memory image path cache. This function clears both the manual cache dictionary and the LRU cache used by get_image_path_cached. Useful for testing or when the file system changes. """ global _image_cache _image_cache.clear() get_image_path_cached.cache_clear() def get_cache_info() -> Dict[str, any]: """ Get information about the current cache state. Returns: Dictionary containing cache statistics: - manual_cache_size: Number of entries in the manual cache - lru_cache_info: Statistics from the LRU cache """ return { 'manual_cache_size': len(_image_cache), 'lru_cache_info': get_image_path_cached.cache_info()._asdict() } def preload_image_cache(products: list) -> int: """ Preload the image cache with paths for a list of products. This function can be used to warm up the cache when loading products from OpenSearch, improving performance for subsequent image path lookups. Args: products: List of product dictionaries Returns: Number of image paths successfully cached Example: >>> products = [ ... {"category": "accessories", "image": "img1.jpg"}, ... {"category": "apparel", "image": "img2.jpg"} ... ] >>> count = preload_image_cache(products) >>> print(f"Cached {count} image paths") """ cached_count = 0 for product in products: category = product.get('category', '') image_filename = product.get('image', '') if category and image_filename: # Use the cached version to populate the LRU cache get_image_path_cached(category, image_filename) cached_count += 1 return cached_count def validate_placeholder_exists() -> bool: """ Check if the placeholder image file exists. Returns: True if placeholder image exists, False otherwise """ return os.path.exists(PLACEHOLDER_IMAGE) and os.path.isfile(PLACEHOLDER_IMAGE)