""" Canvas Image API - Hierarchical Image Fallback System This module provides intelligent image serving with fallback logic: 1. Product-specific images 2. Subtype-level fallbacks 3. Type-level fallbacks 4. Global fallbacks URL Pattern: /api/canvas//?color=&material= """ from flask import Blueprint, send_file, request, jsonify, current_app import os from pathlib import Path import json canvas_bp = Blueprint('canvas', __name__) # Load product data to get type/subtype information def get_product_data(): """Load product data from JSON file""" data_path = os.path.join(current_app.root_path, 'data', 'products.json') try: with open(data_path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: current_app.logger.error(f"Error loading product data: {e}") return [] def get_product_info(product_code): """Get product type, subtype, and other metadata""" products = get_product_data() for product in products: if product.get('productCode') == product_code or product.get('id') == product_code: base_type = product.get('baseType', '').lower() sub_type = None # Extract subtype subtype_obj = product.get('subType', {}) if isinstance(subtype_obj, dict): if subtype_obj.get('door'): sub_type = subtype_obj['door'].lower().replace(' ', '-') elif subtype_obj.get('window'): sub_type = subtype_obj['window'].lower().replace(' ', '-') return { 'code': product_code, 'type': base_type, 'subtype': sub_type, 'colors': product.get('colors', []), 'materials': product.get('materials', []) } return None def find_image_with_fallback(product_code, layer, color=None, material=None): """ Find image using hierarchical fallback system. Priority order: 1. /images////-. 2. /images////. 3. /images///layers/-. 4. /images///layers/. 5. /images//layers/. 6. /images/layers/. Returns: (file_path, mime_type) or (None, None) """ product_info = get_product_info(product_code) if not product_info: current_app.logger.warning(f"Product not found: {product_code}") return None, None images_dir = os.path.join(current_app.root_path, 'static', 'images') base_type = product_info['type'] sub_type = product_info['subtype'] # Supported extensions extensions = ['png', 'jpg', 'jpeg', 'webp'] # Build search paths in priority order search_paths = [] # 1. Product-specific with color if color and base_type and sub_type: for ext in extensions: search_paths.append(os.path.join(images_dir, base_type, sub_type, product_code, f"{layer}-{color.lower()}.{ext}")) # 2. Product-specific without color if base_type and sub_type: for ext in extensions: search_paths.append(os.path.join(images_dir, base_type, sub_type, product_code, f"{layer}.{ext}")) # 3. Subtype fallback with color if color and base_type and sub_type: for ext in extensions: search_paths.append(os.path.join(images_dir, base_type, sub_type, 'layers', f"{layer}-{color.lower()}.{ext}")) # 4. Subtype fallback without color if base_type and sub_type: for ext in extensions: search_paths.append(os.path.join(images_dir, base_type, sub_type, 'layers', f"{layer}.{ext}")) # 5. Type fallback if base_type: for ext in extensions: search_paths.append(os.path.join(images_dir, base_type, 'layers', f"{layer}.{ext}")) # 6. Global fallback for ext in extensions: search_paths.append(os.path.join(images_dir, 'layers', f"{layer}.{ext}")) # Search for first existing file for path in search_paths: if os.path.exists(path): # Determine MIME type ext = path.split('.')[-1].lower() mime_types = { 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'webp': 'image/webp' } mime_type = mime_types.get(ext, 'image/png') current_app.logger.info(f"Found image: {path}") return path, mime_type current_app.logger.warning(f"No image found for {product_code}/{layer} (color: {color})") return None, None @canvas_bp.route('/api/canvas//') def get_layer_image(product_code, layer): """ Serve a specific layer image for a product with hierarchical fallback. Query parameters: - color: Optional color variant - material: Optional material (for future use) Returns: Image file or 404 """ color = request.args.get('color') material = request.args.get('material') # Validate layer name (prevent directory traversal) valid_layers = ['base', 'door', 'hardware', 'overlay', 'foreground'] if layer not in valid_layers: return jsonify({'error': 'Invalid layer name'}), 400 # Find image with fallback image_path, mime_type = find_image_with_fallback(product_code, layer, color, material) if image_path: return send_file(image_path, mimetype=mime_type) else: # Return placeholder or 404 return jsonify({ 'error': 'Image not found', 'product': product_code, 'layer': layer, 'color': color }), 404 @canvas_bp.route('/api/canvas//info') def get_canvas_info(product_code): """ Get information about available layers for a product. Useful for frontend to know which layers exist. """ product_info = get_product_info(product_code) if not product_info: return jsonify({'error': 'Product not found'}), 404 # Check which layers are available available_layers = {} layers_to_check = ['base', 'door', 'hardware', 'overlay', 'foreground'] for layer in layers_to_check: # Check if layer exists (with or without color) path, _ = find_image_with_fallback(product_code, layer) if path: available_layers[layer] = True # For door layer, check which colors are available if layer == 'door' and product_info['colors']: available_colors = [] for color in product_info['colors']: color_path, _ = find_image_with_fallback(product_code, layer, color) if color_path: available_colors.append(color) available_layers['door_colors'] = available_colors return jsonify({ 'product': product_code, 'type': product_info['type'], 'subtype': product_info['subtype'], 'availableLayers': available_layers, 'colors': product_info['colors'], 'materials': product_info['materials'] }) @canvas_bp.route('/api/canvas/test') def test_canvas(): """Test endpoint to verify canvas API is working""" return jsonify({ 'status': 'ok', 'message': 'Canvas API is running', 'endpoints': { 'get_layer': '/api/canvas//?color=', 'get_info': '/api/canvas//info', 'test': '/api/canvas/test' } })