| from flask import Flask, request, send_file, jsonify |
| import json |
| import requests |
| from PIL import Image, ImageDraw, ImageFont |
| import io |
| import os |
| import uuid |
| import tempfile |
| import threading |
| from concurrent.futures import ThreadPoolExecutor |
| from urllib.parse import urlparse |
|
|
| app = Flask(__name__) |
|
|
| |
| local_data = threading.local() |
|
|
| |
| POSITIONS_CONFIG = { |
| "images": [ |
| {"path": "lwf.png", "x": 300, "y": 40}, |
| {"path": "rb.png", "x": 836, "y": 370}, |
| {"path": "cb2.png", "x": 706, "y": 420}, |
| {"path": "rwf.png", "x": 836, "y": 40}, |
| {"path": "cf.png", "x": 568, "y": 15}, |
| {"path": "lb.png", "x": 300, "y": 370}, |
| {"path": "cb1.png", "x": 435, "y": 420}, |
| {"path": "amf1.png", "x": 430, "y": 170}, |
| {"path": "dmf.png", "x": 568, "y": 300}, |
| {"path": "amf2.png", "x": 706, "y": 170}, |
| {"path": "gk.png", "x": 570, "y": 460} |
| ] |
| } |
|
|
| |
| SCALE_FACTOR = 0.40 |
| CORNER_RADIUS = 14 |
| BORDER_WIDTH = 3 |
| BORDER_COLOR = (173, 216, 230, 100) |
| TEXT_COLOR = (255, 255, 0, 255) |
| FONT_SIZE = 46 |
| TEXT_X = 1040 |
| TEXT_Y = 123 |
|
|
| def download_image(url, request_id): |
| """Download image from URL and return PIL Image object""" |
| try: |
| |
| headers = { |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' |
| } |
| |
| response = requests.get(url, timeout=15, headers=headers) |
| response.raise_for_status() |
| |
| |
| image_data = response.content |
| |
| |
| img = Image.open(io.BytesIO(image_data)) |
| |
| |
| original_format = img.format or "Unknown" |
| print(f"[{request_id}] Downloaded {original_format} image from {url}") |
| |
| |
| if img.mode == 'RGBA': |
| |
| return img |
| elif img.mode == 'RGB': |
| |
| return img.convert('RGBA') |
| elif img.mode == 'P': |
| |
| if 'transparency' in img.info: |
| |
| return img.convert('RGBA') |
| else: |
| |
| return img.convert('RGB').convert('RGBA') |
| elif img.mode == 'L': |
| |
| return img.convert('RGB').convert('RGBA') |
| elif img.mode == 'LA': |
| |
| return img.convert('RGBA') |
| elif img.mode == '1': |
| |
| return img.convert('RGB').convert('RGBA') |
| elif img.mode == 'CMYK': |
| |
| return img.convert('RGB').convert('RGBA') |
| else: |
| |
| print(f"[{request_id}] Unknown image mode: {img.mode}, attempting conversion") |
| return img.convert('RGBA') |
| |
| except requests.exceptions.RequestException as e: |
| print(f"[{request_id}] Network error downloading image from {url}: {e}") |
| return None |
| except Image.UnidentifiedImageError as e: |
| print(f"[{request_id}] Invalid image format from {url}: {e}") |
| return None |
| except Exception as e: |
| print(f"[{request_id}] Unexpected error downloading image from {url}: {e}") |
| return None |
|
|
| def validate_image_url(url): |
| """Validate if URL points to a supported image format""" |
| try: |
| parsed_url = urlparse(url) |
| if not parsed_url.scheme in ['http', 'https']: |
| return False |
| |
| |
| supported_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.ico'} |
| path = parsed_url.path.lower() |
| |
| |
| if '.' in path: |
| ext = '.' + path.split('.')[-1] |
| return ext in supported_extensions |
| |
| |
| |
| return True |
| |
| except Exception: |
| return False |
| """Download multiple images in parallel""" |
| downloaded_images = {} |
| |
| def download_single(key_url_pair): |
| key, url = key_url_pair |
| print(f"[{request_id}] Downloading {key} from {url}") |
| img = download_image(url, request_id) |
| return key, img |
| |
| |
| with ThreadPoolExecutor(max_workers=5) as executor: |
| results = executor.map(download_single, url_params.items()) |
| |
| for key, img in results: |
| if img is not None: |
| downloaded_images[key] = img |
| |
| return downloaded_images |
|
|
| def process_image(img, x, y, request_id): |
| """Process individual image with scaling, cropping, and styling""" |
| try: |
| |
| new_size = (int(img.width * SCALE_FACTOR), int(img.height * SCALE_FACTOR)) |
| img = img.resize(new_size, resample=Image.LANCZOS) |
| |
| |
| width, height = img.size |
| |
| |
| square_height = min(width, height) |
| cropped_img = img.crop((0, 0, width, square_height)) |
| |
| |
| decorated_size = (width + BORDER_WIDTH * 2, square_height + BORDER_WIDTH * 2) |
| decorated_img = Image.new("RGBA", decorated_size, (0, 0, 0, 0)) |
| |
| |
| border_draw = ImageDraw.Draw(decorated_img) |
| border_draw.rounded_rectangle( |
| [0, 0, decorated_size[0], decorated_size[1]], |
| radius=CORNER_RADIUS + BORDER_WIDTH, |
| fill=BORDER_COLOR |
| ) |
| |
| |
| mask = Image.new("L", cropped_img.size, 0) |
| mask_draw = ImageDraw.Draw(mask) |
| mask_draw.rounded_rectangle( |
| [0, 0, width, square_height], |
| radius=CORNER_RADIUS, |
| fill=255 |
| ) |
| |
| |
| decorated_img.paste(cropped_img, (BORDER_WIDTH, BORDER_WIDTH), mask=mask) |
| |
| return decorated_img |
| except Exception as e: |
| print(f"[{request_id}] Error processing image: {e}") |
| return None |
|
|
| @app.route('/generate', methods=['GET']) |
| def generate_image(): |
| |
| request_id = str(uuid.uuid4())[:8] |
| |
| try: |
| print(f"[{request_id}] Starting image generation request") |
| |
| |
| if not os.path.exists("image.png"): |
| return jsonify({"error": "Background image 'image.png' not found"}), 404 |
| |
| bg = Image.open("image.png").convert("RGBA").copy() |
| |
| |
| url_params = {} |
| invalid_params = [] |
| |
| for key, value in request.args.items(): |
| if key.startswith(('ss', 'amf', 'cf', 'dmf', 'gk', 'lb', 'rb', 'cb', 'lwf', 'rwf')): |
| if validate_image_url(value): |
| url_params[key] = value |
| else: |
| invalid_params.append(f"{key}={value}") |
| |
| if invalid_params: |
| print(f"[{request_id}] Found invalid image URLs: {', '.join(invalid_params)}") |
| |
| |
| text = request.args.get('text', '3126') |
| |
| print(f"[{request_id}] Found {len(url_params)} image URLs to download") |
| |
| |
| downloaded_images = download_images_parallel(url_params, request_id) |
| |
| print(f"[{request_id}] Successfully downloaded {len(downloaded_images)} images") |
| |
| |
| processed_count = 0 |
| for item in POSITIONS_CONFIG["images"]: |
| path = item["path"] |
| x = item["x"] |
| y = item["y"] |
| |
| |
| key = os.path.splitext(path)[0] |
| |
| img = None |
| |
| |
| if key in downloaded_images: |
| img = downloaded_images[key] |
| print(f"[{request_id}] Using downloaded image for {key}") |
| else: |
| |
| if os.path.exists(path): |
| img = Image.open(path).convert("RGBA").copy() |
| print(f"[{request_id}] Using local fallback image for {key}") |
| else: |
| print(f"[{request_id}] No image found for position {key}") |
| continue |
| |
| |
| decorated_img = process_image(img, x, y, request_id) |
| if decorated_img: |
| bg.paste(decorated_img, (x, y), decorated_img) |
| processed_count += 1 |
| |
| print(f"[{request_id}] Processed {processed_count} images") |
| |
| |
| try: |
| font = ImageFont.truetype("arial.ttf", FONT_SIZE) |
| except: |
| try: |
| font = ImageFont.truetype("arial.otf", FONT_SIZE) |
| except: |
| font = ImageFont.load_default() |
| |
| draw = ImageDraw.Draw(bg) |
| draw.text((TEXT_X, TEXT_Y), str(text), font=font, fill=TEXT_COLOR) |
| |
| |
| img_buffer = io.BytesIO() |
| bg.save(img_buffer, format='PNG') |
| img_buffer.seek(0) |
| |
| print(f"[{request_id}] Image generation completed successfully") |
| |
| return send_file( |
| img_buffer, |
| mimetype='image/png', |
| as_attachment=False, |
| download_name=f'generated_image_{request_id}.png' |
| ) |
| |
| except Exception as e: |
| print(f"[{request_id}] Error occurred: {str(e)}") |
| return jsonify({"error": f"An error occurred: {str(e)}", "request_id": request_id}), 500 |
|
|
| @app.route('/health', methods=['GET']) |
| def health_check(): |
| return jsonify({"status": "healthy", "message": "API is running"}) |
|
|
| @app.route('/', methods=['GET']) |
| def info(): |
| return jsonify({ |
| "message": "Dynamic Image Generator API", |
| "usage": "/generate?ss=<url>&amf1=<url>&text=<text>", |
| "example": "/generate?ss=https://files.catbox.moe/heheh.png&amf1=https://files.catbox.moe/hbvey.jpg&text=3167", |
| "supported_positions": [item["path"].replace('.png', '') for item in POSITIONS_CONFIG["images"]], |
| "supported_formats": ["JPG/JPEG", "PNG", "GIF", "BMP", "TIFF", "WebP", "ICO"], |
| "notes": [ |
| "Images are automatically converted to RGBA format", |
| "Transparency is preserved where supported", |
| "Invalid URLs are skipped with fallback to local images" |
| ] |
| }) |
|
|
| if __name__ == '__main__': |
| app.run(debug=True, host='0.0.0.0', port=7860) |