@@ -0,0 +1,216 @@
|
||||
"""
|
||||
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/<product_code>/<layer>?color=<color>&material=<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/<type>/<subtype>/<code>/<layer>-<color>.<ext>
|
||||
2. /images/<type>/<subtype>/<code>/<layer>.<ext>
|
||||
3. /images/<type>/<subtype>/layers/<layer>-<color>.<ext>
|
||||
4. /images/<type>/<subtype>/layers/<layer>.<ext>
|
||||
5. /images/<type>/layers/<layer>.<ext>
|
||||
6. /images/layers/<layer>.<ext>
|
||||
|
||||
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/<product_code>/<layer>')
|
||||
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/<product_code>/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/<product_code>/<layer>?color=<color>',
|
||||
'get_info': '/api/canvas/<product_code>/info',
|
||||
'test': '/api/canvas/test'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user