Spaces:
No application file
No application file
| from flask import Flask, send_file, request | |
| from PIL import Image, ImageDraw, ImageFont | |
| import requests | |
| import io | |
| import os | |
| from urllib.request import urlopen | |
| app = Flask(__name__) | |
| def create_gradient_vertical(width, height, color_top, color_bottom): | |
| """Create a smooth vertical gradient""" | |
| base = Image.new('RGB', (width, height), color_top) | |
| for y in range(height): | |
| r = int(color_top[0] + (color_bottom[0] - color_top[0]) * y / height) | |
| g = int(color_top[1] + (color_bottom[1] - color_top[1]) * y / height) | |
| b = int(color_top[2] + (color_bottom[2] - color_top[2]) * y / height) | |
| for x in range(width): | |
| base.putpixel((x, y), (r, g, b)) | |
| return base | |
| def get_font(size): | |
| """Try to load Comic Sans, fallback to others""" | |
| font_paths = [ | |
| "comic.ttf", | |
| "C:/Windows/Fonts/comic.ttf", | |
| "/System/Library/Fonts/Supplemental/Comic Sans MS.ttf", | |
| "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", | |
| "arial.ttf", | |
| "C:/Windows/Fonts/arial.ttf" | |
| ] | |
| for path in font_paths: | |
| try: | |
| return ImageFont.truetype(path, size) | |
| except: | |
| continue | |
| return ImageFont.load_default() | |
| def generate_card(): | |
| player_id = request.args.get('id', '89133456301399') | |
| # Fetch player data | |
| try: | |
| response = requests.get(f'https://efootdb.vercel.app/api/playerinfo?id={player_id}') | |
| data = response.json() | |
| if not data.get('success'): | |
| return "Player not found", 404 | |
| info = data['info'] | |
| except Exception as e: | |
| return f"Error fetching data: {str(e)}", 500 | |
| # Card dimensions (16:9 ratio) | |
| W, H = 1920, 1080 | |
| # Dark blue theme colors | |
| DARK_NAVY = (10, 15, 35) | |
| DEEP_BLUE = (15, 25, 55) | |
| MEDIUM_BLUE = (25, 45, 85) | |
| ACCENT_BLUE = (60, 100, 180) | |
| LIGHT_BLUE = (100, 150, 220) | |
| ELECTRIC_BLUE = (80, 180, 255) | |
| WHITE = (255, 255, 255) | |
| LIGHT_GRAY = (200, 210, 230) | |
| GOLD = (255, 215, 0) | |
| GREEN = (80, 220, 120) | |
| RED = (255, 80, 80) | |
| ORANGE = (255, 160, 60) | |
| # Create base with gradient | |
| img = create_gradient_vertical(W, H, DARK_NAVY, DEEP_BLUE) | |
| # Try to load background if exists | |
| try: | |
| if os.path.exists('background.png'): | |
| bg = Image.open('background.png').convert('RGB') | |
| bg = bg.resize((W, H)) | |
| # Blend background with gradient (50% opacity) | |
| img = Image.blend(img, bg, 0.3) | |
| except: | |
| pass | |
| draw = ImageDraw.Draw(img) | |
| # Load fonts | |
| font_title = get_font(70) | |
| font_name = get_font(90) | |
| font_header = get_font(50) | |
| font_large = get_font(42) | |
| font_medium = get_font(36) | |
| font_small = get_font(28) | |
| font_tiny = get_font(24) | |
| # === LEFT PANEL: Player Image & Basic Info === | |
| left_panel_x = 60 | |
| left_panel_y = 80 | |
| left_panel_w = 520 | |
| left_panel_h = 920 | |
| # Panel background | |
| draw.rounded_rectangle( | |
| [left_panel_x, left_panel_y, left_panel_x + left_panel_w, left_panel_y + left_panel_h], | |
| radius=20, | |
| fill=MEDIUM_BLUE | |
| ) | |
| # Player image container | |
| img_container_y = left_panel_y + 30 | |
| img_container_h = 480 | |
| draw.rounded_rectangle( | |
| [left_panel_x + 30, img_container_y, left_panel_x + left_panel_w - 30, img_container_y + img_container_h], | |
| radius=15, | |
| fill=DEEP_BLUE | |
| ) | |
| # Load and display player image | |
| try: | |
| player_img_response = urlopen(info['imageFront']) | |
| player_img = Image.open(player_img_response).convert('RGBA') | |
| # Calculate aspect ratio fit | |
| max_w = left_panel_w - 80 | |
| max_h = img_container_h - 40 | |
| ratio = min(max_w / player_img.width, max_h / player_img.height) | |
| new_w = int(player_img.width * ratio) | |
| new_h = int(player_img.height * ratio) | |
| player_img = player_img.resize((new_w, new_h), Image.Resampling.LANCZOS) | |
| # Center the image | |
| paste_x = left_panel_x + (left_panel_w - new_w) // 2 | |
| paste_y = img_container_y + (img_container_h - new_h) // 2 | |
| # Convert img to RGBA for pasting | |
| img_rgba = img.convert('RGBA') | |
| img_rgba.paste(player_img, (paste_x, paste_y), player_img) | |
| img = img_rgba.convert('RGB') | |
| draw = ImageDraw.Draw(img) | |
| except: | |
| pass | |
| # Rating badge (circular) | |
| badge_y = img_container_y + img_container_h + 30 | |
| badge_center_x = left_panel_x + left_panel_w // 2 | |
| badge_radius = 70 | |
| draw.ellipse( | |
| [badge_center_x - badge_radius, badge_y - badge_radius, | |
| badge_center_x + badge_radius, badge_y + badge_radius], | |
| fill=RED, | |
| outline=GOLD, | |
| width=6 | |
| ) | |
| # Rating text | |
| rating = info['attributes'][0]['value'] | |
| bbox = draw.textbbox((0, 0), rating, font=font_name) | |
| text_w = bbox[2] - bbox[0] | |
| text_h = bbox[3] - bbox[1] | |
| draw.text( | |
| (badge_center_x - text_w // 2, badge_y - text_h // 2 - 5), | |
| rating, | |
| font=font_name, | |
| fill=WHITE | |
| ) | |
| # Position and rarity | |
| info_y = badge_y + badge_radius + 40 | |
| position_text = f"{info['position']} • {info['rarity']}" | |
| bbox = draw.textbbox((0, 0), position_text, font=font_large) | |
| text_w = bbox[2] - bbox[0] | |
| draw.text( | |
| (badge_center_x - text_w // 2, info_y), | |
| position_text, | |
| font=font_large, | |
| fill=GOLD | |
| ) | |
| # Playing style | |
| style_y = info_y + 50 | |
| style_text = info['playingStyle'] | |
| bbox = draw.textbbox((0, 0), style_text, font=font_medium) | |
| text_w = bbox[2] - bbox[0] | |
| draw.text( | |
| (badge_center_x - text_w // 2, style_y), | |
| style_text, | |
| font=font_medium, | |
| fill=LIGHT_BLUE | |
| ) | |
| # Physical stats | |
| phys_y = style_y + 70 | |
| phys_stats = [ | |
| f"Height: {info['height']}cm", | |
| f"Weight: {info['weight']}kg", | |
| f"Age: {info['age']}", | |
| f"Foot: {info['foot']}" | |
| ] | |
| for stat in phys_stats: | |
| bbox = draw.textbbox((0, 0), stat, font=font_small) | |
| text_w = bbox[2] - bbox[0] | |
| draw.text( | |
| (badge_center_x - text_w // 2, phys_y), | |
| stat, | |
| font=font_small, | |
| fill=LIGHT_GRAY | |
| ) | |
| phys_y += 38 | |
| # === TOP RIGHT: Header Section === | |
| header_x = left_panel_x + left_panel_w + 60 | |
| header_y = 80 | |
| header_w = W - header_x - 60 | |
| header_h = 180 | |
| draw.rounded_rectangle( | |
| [header_x, header_y, header_x + header_w, header_y + header_h], | |
| radius=20, | |
| fill=MEDIUM_BLUE | |
| ) | |
| # Title | |
| draw.text((header_x + 40, header_y + 25), "PLAYER PROFILE", font=font_title, fill=ELECTRIC_BLUE) | |
| # Player name | |
| draw.text((header_x + 40, header_y + 100), info['playername'], font=font_name, fill=WHITE) | |
| # === MIDDLE RIGHT: Team & Info === | |
| team_y = header_y + header_h + 30 | |
| team_h = 160 | |
| draw.rounded_rectangle( | |
| [header_x, team_y, header_x + header_w, team_y + team_h], | |
| radius=20, | |
| fill=MEDIUM_BLUE | |
| ) | |
| # Team info | |
| team_info_y = team_y + 30 | |
| team_items = [ | |
| ("Team:", info['teamname']), | |
| ("League:", info['league']), | |
| ("Nation:", f"{info['nationality']} ({info['region']})") | |
| ] | |
| for label, value in team_items: | |
| draw.text((header_x + 40, team_info_y), label, font=font_medium, fill=LIGHT_BLUE) | |
| draw.text((header_x + 230, team_info_y), value, font=font_medium, fill=WHITE) | |
| team_info_y += 42 | |
| # Squad number badge | |
| squad_badge_x = header_x + header_w - 120 | |
| squad_badge_y = team_y + 30 | |
| squad_size = 100 | |
| draw.rounded_rectangle( | |
| [squad_badge_x, squad_badge_y, squad_badge_x + squad_size, squad_badge_y + squad_size], | |
| radius=15, | |
| fill=DEEP_BLUE, | |
| outline=GOLD, | |
| width=4 | |
| ) | |
| squad_num = info['squadnumber'] | |
| bbox = draw.textbbox((0, 0), squad_num, font=font_name) | |
| text_w = bbox[2] - bbox[0] | |
| text_h = bbox[3] - bbox[1] | |
| draw.text( | |
| (squad_badge_x + (squad_size - text_w) // 2, squad_badge_y + (squad_size - text_h) // 2 - 5), | |
| squad_num, | |
| font=font_name, | |
| fill=GOLD | |
| ) | |
| # === BOTTOM RIGHT: Stats Section === | |
| stats_y = team_y + team_h + 30 | |
| stats_h = H - stats_y - 80 | |
| draw.rounded_rectangle( | |
| [header_x, stats_y, header_x + header_w, stats_y + stats_h], | |
| radius=20, | |
| fill=MEDIUM_BLUE | |
| ) | |
| # Stats header | |
| draw.text((header_x + 40, stats_y + 25), "KEY ATTRIBUTES", font=font_header, fill=ELECTRIC_BLUE) | |
| # Key stats to display (top performance stats) | |
| key_stats = [ | |
| ('Offensive Awareness', info['attributes'][1]['value'], info['attributes'][1].get('booster')), | |
| ('Dribbling', info['attributes'][3]['value'], info['attributes'][3].get('booster')), | |
| ('Tight Possession', info['attributes'][4]['value'], info['attributes'][4].get('booster')), | |
| ('Ball Control', info['attributes'][2]['value'], info['attributes'][2].get('booster')), | |
| ('Acceleration', info['attributes'][21]['value'], info['attributes'][21].get('booster')), | |
| ('Speed', info['attributes'][20]['value'], info['attributes'][20].get('booster')), | |
| ('Finishing', info['attributes'][7]['value'], info['attributes'][7].get('booster')), | |
| ('Balance', info['attributes'][25]['value'], info['attributes'][25].get('booster')), | |
| ] | |
| stat_y = stats_y + 90 | |
| for stat_name, value, booster in key_stats: | |
| # Stat name | |
| draw.text((header_x + 40, stat_y), stat_name, font=font_medium, fill=LIGHT_GRAY) | |
| # Progress bar background | |
| bar_x = header_x + 380 | |
| bar_w = 500 | |
| bar_h = 28 | |
| draw.rounded_rectangle( | |
| [bar_x, stat_y + 5, bar_x + bar_w, stat_y + 5 + bar_h], | |
| radius=14, | |
| fill=DEEP_BLUE | |
| ) | |
| # Progress bar fill | |
| try: | |
| val_int = int(value) | |
| filled_w = int((val_int / 99) * bar_w) | |
| # Color based on value | |
| if val_int >= 90: | |
| bar_color = GREEN | |
| elif val_int >= 80: | |
| bar_color = LIGHT_BLUE | |
| elif val_int >= 70: | |
| bar_color = ORANGE | |
| else: | |
| bar_color = ACCENT_BLUE | |
| if filled_w > 0: | |
| draw.rounded_rectangle( | |
| [bar_x, stat_y + 5, bar_x + filled_w, stat_y + 5 + bar_h], | |
| radius=14, | |
| fill=bar_color | |
| ) | |
| except: | |
| pass | |
| # Value text | |
| value_text = value | |
| if booster: | |
| value_text = f"{value} {booster}" | |
| draw.text((bar_x + bar_w + 25, stat_y + 2), value_text, font=font_large, fill=WHITE) | |
| stat_y += 55 | |
| # Skills section | |
| skills_x = header_x + 40 | |
| skills_y = stat_y + 20 | |
| draw.text((skills_x, skills_y), "Special Skills:", font=font_medium, fill=LIGHT_BLUE) | |
| skills_y += 40 | |
| skills = info.get('playerSkills', []) | |
| skill_text = " • ".join(skills[:6]) # Top 6 skills | |
| # Word wrap for skills | |
| words = skill_text.split() | |
| lines = [] | |
| current_line = [] | |
| for word in words: | |
| test_line = ' '.join(current_line + [word]) | |
| bbox = draw.textbbox((0, 0), test_line, font=font_small) | |
| if bbox[2] - bbox[0] <= header_w - 100: | |
| current_line.append(word) | |
| else: | |
| if current_line: | |
| lines.append(' '.join(current_line)) | |
| current_line = [word] | |
| if current_line: | |
| lines.append(' '.join(current_line)) | |
| for line in lines[:2]: # Max 2 lines | |
| draw.text((skills_x, skills_y), line, font=font_small, fill=LIGHT_GRAY) | |
| skills_y += 32 | |
| # === FOOTER === | |
| footer_y = H - 50 | |
| draw.text((60, footer_y), f"ID: {player_id}", font=font_tiny, fill=LIGHT_GRAY) | |
| draw.text((W - 350, footer_y), f"Max Level: {info['maximumlevel']} • Form: {info['attributes'][28]['value']}", font=font_tiny, fill=LIGHT_GRAY) | |
| # Save to bytes | |
| img_io = io.BytesIO() | |
| img.save(img_io, 'PNG', quality=95) | |
| img_io.seek(0) | |
| return send_file(img_io, mimetype='image/png') | |
| if __name__ == '__main__': | |
| app.run(debug=True, host='0.0.0.0', port=5000) |