- Split app.py into route blueprints (files, layers, images, polygon, mask, krita) - Create services layer (polygon_storage, comfyui, file_browser) - Extract config constants to config.py - Split templates into Jinja partials (base, components, modals) - Add browse dialog for visual file navigation - Add /api/browse endpoint for directory listing
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
"""Image serving routes for ORA Editor."""
|
|
|
|
import io
|
|
import zipfile
|
|
from flask import Blueprint, request, jsonify, send_file, Response
|
|
from pathlib import Path
|
|
from PIL import Image, ImageDraw
|
|
|
|
from ora_editor.ora_ops import parse_stack_xml
|
|
from ora_editor.config import PROJECT_ROOT
|
|
|
|
images_bp = Blueprint('images', __name__)
|
|
|
|
|
|
@images_bp.route('/api/image/<layer_name>')
|
|
def api_image(layer_name):
|
|
"""Serve a specific layer image."""
|
|
ora_path = request.args.get('ora_path')
|
|
|
|
if not ora_path:
|
|
return jsonify({'error': 'Missing ora_path'}), 400
|
|
|
|
try:
|
|
with zipfile.ZipFile(ora_path, 'r') as zf:
|
|
img_data = zf.read('mergedimage.png')
|
|
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
|
except Exception as e:
|
|
return Response(f"Error loading image: {e}", status=500)
|
|
|
|
|
|
@images_bp.route('/api/image/base')
|
|
def api_image_base():
|
|
"""Serve the base/merged image."""
|
|
ora_path = request.args.get('ora_path')
|
|
|
|
if not ora_path:
|
|
return jsonify({'error': 'Missing ora_path'}), 400
|
|
|
|
try:
|
|
with zipfile.ZipFile(ora_path, 'r') as zf:
|
|
img_data = zf.read('mergedimage.png')
|
|
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
|
except Exception as e:
|
|
return Response(f"Error loading image: {e}", status=500)
|
|
|
|
|
|
@images_bp.route('/api/image/layer/<layer_name>')
|
|
def api_image_layer(layer_name):
|
|
"""Serve a specific layer as image."""
|
|
ora_path = request.args.get('ora_path')
|
|
|
|
if not ora_path:
|
|
return jsonify({'error': 'Missing ora_path'}), 400
|
|
|
|
try:
|
|
from xml.etree import ElementTree as ET
|
|
|
|
with zipfile.ZipFile(ora_path, 'r') as zf:
|
|
stack = ET.fromstring(zf.read('stack.xml'))
|
|
|
|
for child in stack.find('stack'):
|
|
if child.tag == 'layer' and child.get('name') == layer_name:
|
|
src = child.get('src')
|
|
img_data = zf.read(src)
|
|
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
|
|
|
elif child.tag == 'stack':
|
|
for layer in child:
|
|
if layer.tag == 'layer' and layer.get('name') == layer_name:
|
|
src = layer.get('src')
|
|
img_data = zf.read(src)
|
|
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
|
|
|
return jsonify({'error': 'Layer not found'}), 404
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return Response(f"Error loading layer: {e}", status=500)
|
|
|
|
|
|
@images_bp.route('/api/image/polygon')
|
|
def api_image_polygon():
|
|
"""Serve polygon overlay if stored."""
|
|
from ora_editor.services import polygon_storage
|
|
|
|
ora_path = request.args.get('ora_path')
|
|
|
|
if not ora_path:
|
|
return Response("Missing ora_path", status=400)
|
|
|
|
poly_data = polygon_storage.get(ora_path)
|
|
if not poly_data:
|
|
return Response("No polygon data", status=404)
|
|
|
|
try:
|
|
with zipfile.ZipFile(ora_path, 'r') as zf:
|
|
base_img = Image.open(zf.open('mergedimage.png')).convert('RGBA')
|
|
|
|
points = poly_data.get('points', [])
|
|
color = poly_data.get('color', '#FF0000')
|
|
width = poly_data.get('width', 2)
|
|
|
|
if len(points) < 3:
|
|
return Response("Not enough points", status=404)
|
|
|
|
w, h = base_img.size
|
|
pixel_points = [(int(p['x'] * w), int(p['y'] * h)) for p in points]
|
|
|
|
draw = ImageDraw.Draw(base_img)
|
|
hex_color = color if len(color) == 7 else color + 'FF'
|
|
draw.polygon(pixel_points, outline=hex_color, width=width)
|
|
|
|
img_io = io.BytesIO()
|
|
base_img.save(img_io, format='PNG')
|
|
img_io.seek(0)
|
|
|
|
return send_file(img_io, mimetype='image/png')
|
|
except Exception as e:
|
|
return Response(f"Error drawing polygon: {e}", status=500)
|
|
|
|
|
|
@images_bp.route('/api/image/masked')
|
|
def api_image_masked():
|
|
"""Serve the base image with mask applied as alpha channel."""
|
|
ora_path = request.args.get('ora_path')
|
|
mask_path = request.args.get('mask_path')
|
|
|
|
if not ora_path or not mask_path:
|
|
return jsonify({'error': 'Missing ora_path or mask_path'}), 400
|
|
|
|
try:
|
|
with zipfile.ZipFile(ora_path, 'r') as zf:
|
|
base_img = Image.open(zf.open('mergedimage.png')).convert('RGBA')
|
|
|
|
mask_img = Image.open(mask_path).convert('L')
|
|
|
|
if mask_img.size != base_img.size:
|
|
mask_img = mask_img.resize(base_img.size, Image.Resampling.BILINEAR)
|
|
|
|
r, g, b, _ = base_img.split()
|
|
result = Image.merge('RGBA', (r, g, b, mask_img))
|
|
|
|
img_io = io.BytesIO()
|
|
result.save(img_io, format='PNG')
|
|
img_io.seek(0)
|
|
|
|
return send_file(img_io, mimetype='image/png')
|
|
except Exception as e:
|
|
return Response(f"Error creating masked image: {e}", status=500)
|
|
|
|
|
|
@images_bp.route('/api/file/mask')
|
|
def api_file_mask():
|
|
"""Serve a mask file from temp directory."""
|
|
path = request.args.get('path')
|
|
|
|
if not path:
|
|
return jsonify({'error': 'Missing path'}), 400
|
|
|
|
full_path = Path(path)
|
|
|
|
if not full_path.exists():
|
|
return jsonify({'error': 'File not found'}), 404
|
|
|
|
return send_file(full_path, mimetype='image/png')
|