Editor Support

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Jason
2026-05-05 15:42:30 -05:00
parent 9f7c078ccc
commit 29154bd651
32 changed files with 3698 additions and 17 deletions
+216
View File
@@ -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'
}
})