File size: 11,610 Bytes
59ca888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aded8a8
 
 
 
 
 
59ca888
aded8a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59ca888
aded8a8
59ca888
 
aded8a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59ca888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aded8a8
59ca888
aded8a8
 
59ca888
 
aded8a8
 
 
 
 
 
 
59ca888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aded8a8
 
 
 
 
 
 
 
59ca888
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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__)

# Thread-local storage for request isolation
local_data = threading.local()

# Configuration
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}
    ]
}

# Styling Settings
SCALE_FACTOR = 0.40
CORNER_RADIUS = 14
BORDER_WIDTH = 3
BORDER_COLOR = (173, 216, 230, 100)  # Light blue with alpha
TEXT_COLOR = (255, 255, 0, 255)  # Yellow
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:
        # Add headers to mimic a real browser request
        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()
        
        # Get the image data
        image_data = response.content
        
        # Try to open the image
        img = Image.open(io.BytesIO(image_data))
        
        # Get original format for logging
        original_format = img.format or "Unknown"
        print(f"[{request_id}] Downloaded {original_format} image from {url}")
        
        # Handle different image modes
        if img.mode == 'RGBA':
            # Already has transparency, keep as is
            return img
        elif img.mode == 'RGB':
            # Convert RGB to RGBA (add alpha channel)
            return img.convert('RGBA')
        elif img.mode == 'P':
            # Palette mode - check if it has transparency
            if 'transparency' in img.info:
                # Convert palette with transparency to RGBA
                return img.convert('RGBA')
            else:
                # Convert palette without transparency to RGBA
                return img.convert('RGB').convert('RGBA')
        elif img.mode == 'L':
            # Grayscale - convert to RGBA
            return img.convert('RGB').convert('RGBA')
        elif img.mode == 'LA':
            # Grayscale with alpha - convert to RGBA
            return img.convert('RGBA')
        elif img.mode == '1':
            # 1-bit pixels - convert to RGBA
            return img.convert('RGB').convert('RGBA')
        elif img.mode == 'CMYK':
            # CMYK mode - convert to RGB then RGBA
            return img.convert('RGB').convert('RGBA')
        else:
            # Any other mode - try to convert to RGBA
            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
        
        # Check file extension (not foolproof but helps filter obvious non-images)
        supported_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.ico'}
        path = parsed_url.path.lower()
        
        # If there's an extension, check if it's supported
        if '.' in path:
            ext = '.' + path.split('.')[-1]
            return ext in supported_extensions
        
        # If no extension, we'll let the download attempt proceed
        # (some URLs don't have extensions but serve images)
        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
    
    # Use ThreadPoolExecutor for parallel downloads
    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:
        # Resize image
        new_size = (int(img.width * SCALE_FACTOR), int(img.height * SCALE_FACTOR))
        img = img.resize(new_size, resample=Image.LANCZOS)
        
        # Get dimensions
        width, height = img.size
        
        # Crop top square
        square_height = min(width, height)
        cropped_img = img.crop((0, 0, width, square_height))
        
        # Create transparent base with space for border
        decorated_size = (width + BORDER_WIDTH * 2, square_height + BORDER_WIDTH * 2)
        decorated_img = Image.new("RGBA", decorated_size, (0, 0, 0, 0))
        
        # Draw light blue rounded border
        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
        )
        
        # Create mask for rounded corners
        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
        )
        
        # Paste cropped image onto border
        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():
    # Generate unique request ID for this request
    request_id = str(uuid.uuid4())[:8]
    
    try:
        print(f"[{request_id}] Starting image generation request")
        
        # Load base background image (create a copy for this 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()
        
        # Get URL parameters with validation
        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)}")
        
        # Get text parameter
        text = request.args.get('text', '3126')
        
        print(f"[{request_id}] Found {len(url_params)} image URLs to download")
        
        # Download all images in parallel
        downloaded_images = download_images_parallel(url_params, request_id)
        
        print(f"[{request_id}] Successfully downloaded {len(downloaded_images)} images")
        
        # Process each image from the positions config
        processed_count = 0
        for item in POSITIONS_CONFIG["images"]:
            path = item["path"]
            x = item["x"]
            y = item["y"]
            
            # Extract the key from the filename (remove .png extension)
            key = os.path.splitext(path)[0]
            
            img = None
            
            # Check if we have a downloaded image for this position
            if key in downloaded_images:
                img = downloaded_images[key]
                print(f"[{request_id}] Using downloaded image for {key}")
            else:
                # Try to load local image as fallback
                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  # Skip if no URL provided and no local file
            
            # Process and paste the image
            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")
        
        # Add text
        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)
        
        # Save to memory buffer
        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)