from flask import Flask, request, jsonify, send_file, render_template from PIL import Image, ImageDraw, ImageFont import io import os import requests from werkzeug.utils import secure_filename try: import pytesseract TESSERACT_AVAILABLE = True except ImportError: TESSERACT_AVAILABLE = False app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size app.config['UPLOAD_FOLDER'] = 'uploads' # Environment variables for enabling/disabling features # Both default to enabled ('true') if not set ENABLE_UI = os.getenv('ENABLE_UI', 'true').lower() in ('true', '1', 'yes', 'on') ENABLE_API = os.getenv('ENABLE_API', 'true').lower() in ('true', '1', 'yes', 'on') # Ensure upload folder exists os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def trim_transparent_borders(img): """ Trim transparent/whitespace borders from image Returns the cropped image """ if img.mode not in ('RGBA', 'LA'): # No transparency to trim return img # Get the alpha channel if img.mode == 'RGBA': alpha = img.split()[3] else: alpha = img.split()[1] # Get the bounding box of non-transparent pixels bbox = alpha.getbbox() if bbox: return img.crop(bbox) else: # Image is completely transparent, return as is return img def detect_font_from_image(img): """ Attempt to detect font characteristics from the logo using OCR Returns a font path that best matches the detected style Uses all available fonts from get_available_fonts() """ if not TESSERACT_AVAILABLE: return None try: # Get detailed OCR data including font info data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) # Get all available fonts available_fonts = get_available_fonts() if not available_fonts or available_fonts[0]['path'] == 'default': return None # Try to detect if logo uses bold/heavy text by checking confidence scores # Higher confidence often correlates with bolder, clearer text confidences = [conf for conf in data['conf'] if conf != -1] avg_confidence = sum(confidences) / len(confidences) if confidences else 0 # Categorize fonts by style - prioritize TV-appropriate fonts display_fonts = [] # Bebas, Anton, Impact - very bold display fonts black_fonts = [] # Black/Heavy weight fonts bold_fonts = [] # Bold fonts regular_fonts = [] # Regular weight fonts for font in available_fonts: font_name_lower = font['name'].lower() # TV logos often use display/condensed fonts if any(keyword in font_name_lower for keyword in ['bebas', 'anton', 'impact', 'oswald']): display_fonts.append(font['path']) # Black/Heavy fonts for strong branding elif any(keyword in font_name_lower for keyword in ['black', 'heavy', 'extrabold']): black_fonts.append(font['path']) # Bold fonts elif 'bold' in font_name_lower: bold_fonts.append(font['path']) # Regular fonts else: regular_fonts.append(font['path']) # Prioritize based on confidence - TV logos typically use bold/display fonts if avg_confidence > 70: # Very clear text - likely uses display or black fonts font_list = display_fonts + black_fonts + bold_fonts + regular_fonts elif avg_confidence > 50: # Moderately clear - likely bold font_list = black_fonts + bold_fonts + display_fonts + regular_fonts else: # Less clear - might be lighter weight font_list = bold_fonts + regular_fonts + black_fonts + display_fonts # Return first available font from the prioritized list for font_path in font_list: if os.path.exists(font_path): return font_path except Exception as e: # If OCR fails, fall back to None pass return None def get_available_fonts(): """ Get list of available fonts on the system with their paths and display names Works on Linux (Docker), macOS, and Windows """ font_list = [ # DejaVu fonts (fonts-dejavu package) - Sans-serif {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 'name': 'DejaVu Sans Bold'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 'name': 'DejaVu Sans'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf', 'name': 'DejaVu Sans ExtraLight'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf', 'name': 'DejaVu Sans Condensed Bold'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf', 'name': 'DejaVu Sans Condensed'}, # DejaVu fonts - Serif {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf', 'name': 'DejaVu Serif Bold'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf', 'name': 'DejaVu Serif'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSerifCondensed-Bold.ttf', 'name': 'DejaVu Serif Condensed Bold'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSerifCondensed.ttf', 'name': 'DejaVu Serif Condensed'}, # DejaVu fonts - Monospace {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 'name': 'DejaVu Sans Mono Bold'}, {'path': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', 'name': 'DejaVu Sans Mono'}, # Liberation fonts (fonts-liberation package) - Sans-serif {'path': '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', 'name': 'Liberation Sans Bold'}, {'path': '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', 'name': 'Liberation Sans'}, {'path': '/usr/share/fonts/truetype/liberation/LiberationSansNarrow-Bold.ttf', 'name': 'Liberation Sans Narrow Bold'}, {'path': '/usr/share/fonts/truetype/liberation/LiberationSansNarrow-Regular.ttf', 'name': 'Liberation Sans Narrow'}, # Liberation fonts - Serif {'path': '/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf', 'name': 'Liberation Serif Bold'}, {'path': '/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf', 'name': 'Liberation Serif'}, # Liberation fonts - Monospace {'path': '/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf', 'name': 'Liberation Mono Bold'}, {'path': '/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf', 'name': 'Liberation Mono'}, # Liberation2 fonts (fonts-liberation2 package) {'path': '/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf', 'name': 'Liberation Sans Bold (v2)'}, {'path': '/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf', 'name': 'Liberation Sans (v2)'}, # Noto fonts (fonts-noto package) - Sans-serif {'path': '/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf', 'name': 'Noto Sans Bold'}, {'path': '/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf', 'name': 'Noto Sans'}, {'path': '/usr/share/fonts/truetype/noto/NotoSans-ExtraBold.ttf', 'name': 'Noto Sans ExtraBold'}, {'path': '/usr/share/fonts/truetype/noto/NotoSans-Light.ttf', 'name': 'Noto Sans Light'}, # Noto fonts - Serif {'path': '/usr/share/fonts/truetype/noto/NotoSerif-Bold.ttf', 'name': 'Noto Serif Bold'}, {'path': '/usr/share/fonts/truetype/noto/NotoSerif-Regular.ttf', 'name': 'Noto Serif'}, # Noto fonts - Monospace {'path': '/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf', 'name': 'Noto Mono'}, # FreeFont (fonts-freefont-ttf package) - Sans-serif {'path': '/usr/share/fonts/truetype/freefont/FreeSansBold.ttf', 'name': 'FreeSans Bold'}, {'path': '/usr/share/fonts/truetype/freefont/FreeSans.ttf', 'name': 'FreeSans'}, # FreeFont - Serif {'path': '/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf', 'name': 'FreeSerif Bold'}, {'path': '/usr/share/fonts/truetype/freefont/FreeSerif.ttf', 'name': 'FreeSerif'}, # FreeFont - Monospace {'path': '/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf', 'name': 'FreeMono Bold'}, {'path': '/usr/share/fonts/truetype/freefont/FreeMono.ttf', 'name': 'FreeMono'}, # Google Fonts - Sans-serif (great for logos) {'path': '/usr/share/fonts/truetype/google-fonts/Roboto-Black.ttf', 'name': 'Roboto Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Roboto-Bold.ttf', 'name': 'Roboto Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Roboto-Regular.ttf', 'name': 'Roboto'}, {'path': '/usr/share/fonts/truetype/google-fonts/OpenSans-ExtraBold.ttf', 'name': 'Open Sans ExtraBold'}, {'path': '/usr/share/fonts/truetype/google-fonts/OpenSans-Bold.ttf', 'name': 'Open Sans Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/OpenSans-Regular.ttf', 'name': 'Open Sans'}, {'path': '/usr/share/fonts/truetype/google-fonts/Montserrat-Black.ttf', 'name': 'Montserrat Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Montserrat-Bold.ttf', 'name': 'Montserrat Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Montserrat-Regular.ttf', 'name': 'Montserrat'}, {'path': '/usr/share/fonts/truetype/google-fonts/Oswald-Bold.ttf', 'name': 'Oswald Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Oswald-Regular.ttf', 'name': 'Oswald'}, {'path': '/usr/share/fonts/truetype/google-fonts/Raleway-Black.ttf', 'name': 'Raleway Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Raleway-Bold.ttf', 'name': 'Raleway Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Raleway-Regular.ttf', 'name': 'Raleway'}, {'path': '/usr/share/fonts/truetype/google-fonts/Lato-Black.ttf', 'name': 'Lato Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Lato-Bold.ttf', 'name': 'Lato Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Lato-Regular.ttf', 'name': 'Lato'}, {'path': '/usr/share/fonts/truetype/google-fonts/Poppins-Black.ttf', 'name': 'Poppins Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Poppins-Bold.ttf', 'name': 'Poppins Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Poppins-Regular.ttf', 'name': 'Poppins'}, {'path': '/usr/share/fonts/truetype/google-fonts/Anton-Regular.ttf', 'name': 'Anton'}, {'path': '/usr/share/fonts/truetype/google-fonts/BebasNeue-Regular.ttf', 'name': 'Bebas Neue'}, {'path': '/usr/share/fonts/truetype/google-fonts/PTSans-Bold.ttf', 'name': 'PT Sans Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/PTSans-Regular.ttf', 'name': 'PT Sans'}, {'path': '/usr/share/fonts/truetype/google-fonts/Nunito-Black.ttf', 'name': 'Nunito Black'}, {'path': '/usr/share/fonts/truetype/google-fonts/Nunito-Bold.ttf', 'name': 'Nunito Bold'}, {'path': '/usr/share/fonts/truetype/google-fonts/Nunito-Regular.ttf', 'name': 'Nunito'}, # macOS system fonts (fallback for running outside Docker on Mac) {'path': '/System/Library/Fonts/Helvetica.ttc', 'name': 'Helvetica'}, {'path': '/System/Library/Fonts/Supplemental/Arial.ttf', 'name': 'Arial'}, {'path': '/System/Library/Fonts/Supplemental/Arial Bold.ttf', 'name': 'Arial Bold'}, {'path': '/System/Library/Fonts/Supplemental/Arial Black.ttf', 'name': 'Arial Black'}, {'path': '/System/Library/Fonts/Supplemental/Verdana.ttf', 'name': 'Verdana'}, {'path': '/System/Library/Fonts/Supplemental/Verdana Bold.ttf', 'name': 'Verdana Bold'}, {'path': '/System/Library/Fonts/Supplemental/Tahoma.ttf', 'name': 'Tahoma'}, {'path': '/System/Library/Fonts/Supplemental/Tahoma Bold.ttf', 'name': 'Tahoma Bold'}, {'path': '/System/Library/Fonts/Supplemental/Impact.ttf', 'name': 'Impact'}, {'path': '/System/Library/Fonts/Supplemental/Times New Roman.ttf', 'name': 'Times New Roman'}, {'path': '/System/Library/Fonts/Supplemental/Times New Roman Bold.ttf', 'name': 'Times New Roman Bold'}, # Windows system fonts (fallback for running outside Docker on Windows) {'path': 'C:\\Windows\\Fonts\\arial.ttf', 'name': 'Arial (Windows)'}, {'path': 'C:\\Windows\\Fonts\\arialbd.ttf', 'name': 'Arial Bold (Windows)'}, {'path': 'C:\\Windows\\Fonts\\ariblk.ttf', 'name': 'Arial Black (Windows)'}, {'path': 'C:\\Windows\\Fonts\\verdana.ttf', 'name': 'Verdana (Windows)'}, {'path': 'C:\\Windows\\Fonts\\verdanab.ttf', 'name': 'Verdana Bold (Windows)'}, {'path': 'C:\\Windows\\Fonts\\tahoma.ttf', 'name': 'Tahoma (Windows)'}, {'path': 'C:\\Windows\\Fonts\\tahomabd.ttf', 'name': 'Tahoma Bold (Windows)'}, {'path': 'C:\\Windows\\Fonts\\impact.ttf', 'name': 'Impact (Windows)'}, {'path': 'C:\\Windows\\Fonts\\times.ttf', 'name': 'Times New Roman (Windows)'}, {'path': 'C:\\Windows\\Fonts\\timesbd.ttf', 'name': 'Times New Roman Bold (Windows)'}, ] # Filter to only fonts that exist on this system available = [] seen_names = set() for font in font_list: if os.path.exists(font['path']) and font['name'] not in seen_names: available.append(font) seen_names.add(font['name']) # If no fonts found, return a placeholder that will trigger default font usage if not available: available.append({'path': 'default', 'name': 'System Default'}) return available def get_font(font_size, font_path=None, detected_font_path=None): """ Get the best available font for text rendering Falls back to system default if no fonts are available """ try: # If a specific font was requested and it exists, use it if font_path and font_path != 'auto' and font_path != 'default' and os.path.exists(font_path): return ImageFont.truetype(font_path, font_size) # If we detected a font from the logo, use it if detected_font_path and os.path.exists(detected_font_path): return ImageFont.truetype(detected_font_path, font_size) # Otherwise use the first available font available_fonts = get_available_fonts() if available_fonts and available_fonts[0]['path'] != 'default': return ImageFont.truetype(available_fonts[0]['path'], font_size) # Fall back to PIL's default font print("Warning: No TrueType fonts found, using PIL default font") return ImageFont.load_default() except Exception as e: print(f"Error loading font: {e}. Using default font.") return ImageFont.load_default() def add_text_to_image(image_path, text, position='below', font_size=None, text_color='white', bg_color=None, padding=None, font_path=None): """ Add text to an image by expanding the canvas Args: image_path: Path to the source image text: Text to add position: Where to add text ('above', 'below', 'left', 'right') font_size: Size of the text font (auto if None) text_color: Color of the text bg_color: Background color for the expanded area (transparent if None and image has alpha) padding: Padding around the text (auto if None) font_path: Specific font path to use (auto if None) Returns: PIL Image object with text added """ # Load the original image img = Image.open(image_path) # Preserve transparency has_transparency = img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) # Convert to RGBA if it has transparency, otherwise RGB if has_transparency: img = img.convert('RGBA') # Trim transparent borders first img = trim_transparent_borders(img) else: img = img.convert('RGB') orig_width, orig_height = img.size # Auto-calculate font size if not provided (based on image dimensions and aspect ratio) if font_size is None: if position in ['above', 'below']: # For horizontal text, base on width with consideration for aspect ratio # Wider logos can have larger text aspect_ratio = orig_width / orig_height if aspect_ratio > 2.5: # Very wide logo font_size = int(orig_width * 0.10) # Slightly smaller for very wide logos elif aspect_ratio > 1.5: # Wide logo font_size = int(orig_width * 0.11) else: # Square or portrait logo font_size = int(orig_width * 0.14) # Larger for more compact logos else: # left or right # For vertical text, base on height with consideration for aspect ratio aspect_ratio = orig_height / orig_width if aspect_ratio > 2.5: # Very tall logo font_size = int(orig_height * 0.15) # Smaller for very tall logos elif aspect_ratio > 1.5: # Tall logo font_size = int(orig_height * 0.18) else: # Square or landscape logo font_size = int(orig_height * 0.22) # Larger for wider logos # Clamp with smarter bounds based on image size min_size = max(20, int(min(orig_width, orig_height) * 0.08)) # At least 8% of smallest dimension max_size = min(300, int(max(orig_width, orig_height) * 0.3)) # At most 30% of largest dimension font_size = max(min_size, min(font_size, max_size)) # Auto-calculate padding if not provided (independent of font size) if padding is None: if position in ['above', 'below']: # Base padding on image width and font size # Use geometric mean for balanced scaling padding = int((orig_width * font_size) ** 0.5 * 0.12) # Ensure reasonable bounds: 15-60 pixels typically padding = max(12, min(padding, 60)) else: # left or right # Base padding on image height and font size padding = int((orig_height * font_size) ** 0.5 * 0.12) # Ensure reasonable bounds padding = max(12, min(padding, 60)) # Auto-determine background color if bg_color is None: if has_transparency: bg_color = (0, 0, 0, 0) # Transparent else: bg_color = '#1a1a1a' # Dark gray for non-transparent images # Try to detect font from the logo detected_font_path = detect_font_from_image(img) # Get the appropriate font font = get_font(font_size, font_path, detected_font_path) # Create a temporary image to measure text size temp_img = Image.new('RGB', (1, 1)) temp_draw = ImageDraw.Draw(temp_img) # Get text bounding box bbox = temp_draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] bbox_offset_x = -bbox[0] # Offset to align text properly bbox_offset_y = -bbox[1] # Offset to align text properly # Calculate new image dimensions with directional padding # More padding between logo and text, minimal on outer edges (mimic existing padding) inner_padding = padding # Padding between logo and text outer_padding = padding // 4 # Minimal padding on outer edge side_padding = padding // 4 # Minimal padding on perpendicular sides if position in ['above', 'below']: text_area_height = text_height + outer_padding + inner_padding new_width = max(orig_width, text_width + side_padding * 2) new_height = orig_height + text_area_height else: # left or right text_area_width = text_width + outer_padding + inner_padding new_width = orig_width + text_area_width new_height = max(orig_height, text_height + side_padding * 2) # Create new image with expanded canvas (preserve transparency) if has_transparency: new_img = Image.new('RGBA', (new_width, new_height), bg_color) else: new_img = Image.new('RGB', (new_width, new_height), bg_color) # Calculate positions with directional padding if position == 'below': # Paste original image at top paste_x = (new_width - orig_width) // 2 new_img.paste(img, (paste_x, 0)) # Add text below with padding between logo and text, minimal at bottom text_x = (new_width - text_width) // 2 + bbox_offset_x text_y = orig_height + inner_padding + bbox_offset_y elif position == 'above': # Paste original image at bottom paste_x = (new_width - orig_width) // 2 new_img.paste(img, (paste_x, text_area_height)) # Add text above with minimal padding at top, more near logo text_x = (new_width - text_width) // 2 + bbox_offset_x text_y = outer_padding + bbox_offset_y elif position == 'right': # Paste original image on left paste_y = (new_height - orig_height) // 2 new_img.paste(img, (0, paste_y)) # Add text on right with padding between logo and text, minimal on right edge text_x = orig_width + inner_padding + bbox_offset_x text_y = (new_height - text_height) // 2 + bbox_offset_y else: # left # Paste original image on right paste_y = (new_height - orig_height) // 2 new_img.paste(img, (text_area_width, paste_y)) # Add text on left with minimal padding on left edge, more near logo text_x = outer_padding + bbox_offset_x text_y = (new_height - text_height) // 2 + bbox_offset_y # Draw text on new image draw = ImageDraw.Draw(new_img) draw.text((text_x, text_y), text, fill=text_color, font=font) # Prepare metadata about what was used metadata = { 'font_size_used': font_size, 'padding_used': padding, 'font_name_used': None } # Try to get font name if font_path and font_path != 'auto' and os.path.exists(font_path): metadata['font_name_used'] = os.path.basename(font_path) elif detected_font_path and os.path.exists(detected_font_path): metadata['font_name_used'] = os.path.basename(detected_font_path) else: available_fonts = get_available_fonts() if available_fonts and available_fonts[0]['path'] != 'default': metadata['font_name_used'] = available_fonts[0]['name'] return new_img, metadata @app.route('/') def index(): """Serve the web interface""" if not ENABLE_UI: return jsonify({'error': 'Web UI is disabled'}), 403 return render_template('index.html') @app.route('/api/fonts', methods=['GET']) def get_fonts(): """Get list of available fonts""" if not ENABLE_API: return jsonify({'error': 'API is disabled'}), 403 fonts = get_available_fonts() return jsonify({'fonts': fonts}) @app.route('/api/tv-logos', methods=['GET']) def get_tv_logos(): """ Fetch TV logos from github.com/tv-logo/tv-logos repository Query parameters: - search: Search query (fuzzy, case-insensitive) - country: Filter by country code """ if not ENABLE_API: return jsonify({'error': 'API is disabled'}), 403 try: # Get query parameters search_query = request.args.get('search', '').lower() country_filter = request.args.get('country', '').lower() # Use GitHub API to get the repository tree recursively # Try 'main' branch (modern default) github_api_url = "https://api.github.com/repos/tv-logo/tv-logos/git/trees/main?recursive=1" headers = {'Accept': 'application/vnd.github.v3+json'} response = requests.get(github_api_url, headers=headers, timeout=30) # If 'main' doesn't exist, try 'master' if response.status_code == 404: github_api_url = "https://api.github.com/repos/tv-logo/tv-logos/git/trees/master?recursive=1" response = requests.get(github_api_url, headers=headers, timeout=30) response.raise_for_status() tree_data = response.json() # Determine which branch worked branch = 'main' if 'main' in github_api_url else 'master' logos = [] countries = set() # Process all files in the tree for item in tree_data.get('tree', []): path = item.get('path', '') # Skip if not an image file if not path.lower().endswith(('.png', '.jpg', '.jpeg', '.svg')): continue # Parse the path - expected format: countries/COUNTRY/logo.png path_parts = path.split('/') # Extract country from path like: countries/usa/logo.png country = None if len(path_parts) >= 3 and path_parts[0] == 'countries': country = path_parts[1] countries.add(country) elif len(path_parts) >= 2: # Fallback for other structures - use first directory first_dir = path_parts[0] if first_dir not in ['.github', 'docs', 'scripts', 'paypal-donate', 'README.md']: country = first_dir countries.add(country) # Skip if no country identified if not country: continue # Get filename filename = path_parts[-1] logo_name = os.path.splitext(filename)[0] # Apply country filter if country_filter and country.lower() != country_filter: continue # Apply search filter (filename only) if search_query and search_query not in logo_name.lower(): continue # Construct the raw GitHub URL raw_url = f"https://raw.githubusercontent.com/tv-logo/tv-logos/{branch}/{path}" logos.append({ 'name': logo_name, 'country': country, 'url': raw_url, 'thumbnail': raw_url, 'path': path }) # Sort logos by name logos.sort(key=lambda x: x['name'].lower()) # Return ALL logos without limits - frontend will handle pagination return jsonify({ 'logos': logos, 'countries': sorted(list(countries)), 'total': len(logos) }) except Exception as e: return jsonify({'error': f'Failed to fetch logos: {str(e)}'}), 500 @app.route('/api/process', methods=['POST']) def process_image(): """ API endpoint to process an image Expected form data: - image: Image file - text: Text to add - position: Where to add text (above/below/left/right) - font_size: Font size (optional, default 60) - text_color: Text color (optional, default white) - bg_color: Background color (optional, default #1a1a1a) - padding: Padding around text (optional, default 20) """ if not ENABLE_API: return jsonify({'error': 'API is disabled'}), 403 # Check if image file is present if 'image' not in request.files: return jsonify({'error': 'No image file provided'}), 400 file = request.files['image'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if not allowed_file(file.filename): return jsonify({'error': 'Invalid file type. Allowed: PNG, JPG, JPEG, GIF, WEBP'}), 400 # Get parameters text = request.form.get('text', '') if not text: return jsonify({'error': 'No text provided'}), 400 position = request.form.get('position', 'below').lower() if position not in ['above', 'below', 'left', 'right']: return jsonify({'error': 'Invalid position. Use: above, below, left, or right'}), 400 # Handle font size - allow auto/empty for automatic sizing font_size_input = request.form.get('font_size', 'auto') if font_size_input == '' or font_size_input == 'auto': font_size = None else: try: font_size = int(font_size_input) except ValueError: return jsonify({'error': 'Font size must be a number or "auto"'}), 400 # Handle padding - allow auto/empty for automatic padding padding_input = request.form.get('padding', 'auto') if padding_input == '' or padding_input == 'auto': padding = None else: try: padding = int(padding_input) except ValueError: return jsonify({'error': 'Padding must be a number or "auto"'}), 400 text_color = request.form.get('text_color', 'white') bg_color = request.form.get('bg_color', 'transparent') # Allow transparent background if bg_color == '' or bg_color == 'transparent' or bg_color == 'auto': bg_color = None # Get font path if specified font_path = request.form.get('font_path', None) if font_path == '' or font_path == 'auto': font_path = None # Save uploaded file filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) try: # Process the image result_img, metadata = add_text_to_image( filepath, text, position, font_size, text_color, bg_color, padding, font_path ) # Save to bytes buffer img_io = io.BytesIO() # Determine output format (preserve transparency for PNG) output_format = 'PNG' if filename.lower().endswith(('.jpg', '.jpeg')): output_format = 'JPEG' # Convert RGBA to RGB for JPEG if result_img.mode == 'RGBA': rgb_img = Image.new('RGB', result_img.size, (255, 255, 255)) rgb_img.paste(result_img, mask=result_img.split()[3]) result_img = rgb_img elif filename.lower().endswith('.gif'): output_format = 'GIF' elif filename.lower().endswith('.webp'): output_format = 'WEBP' # Save with appropriate settings if output_format == 'PNG': result_img.save(img_io, output_format, optimize=True) elif output_format == 'JPEG': result_img.save(img_io, output_format, quality=95) else: result_img.save(img_io, output_format, quality=95) img_io.seek(0) # Clean up uploaded file os.remove(filepath) # Determine mimetype mimetype_map = { 'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', 'WEBP': 'image/webp' } response = send_file( img_io, mimetype=mimetype_map.get(output_format, 'image/png'), as_attachment=True, download_name=f'processed_{filename}' ) response.headers['X-Font-Size-Used'] = str(metadata['font_size_used']) response.headers['X-Padding-Used'] = str(metadata['padding_used']) if metadata['font_name_used']: response.headers['X-Font-Name-Used'] = metadata['font_name_used'] return response except Exception as e: # Clean up on error if os.path.exists(filepath): os.remove(filepath) return jsonify({'error': f'Error processing image: {str(e)}'}), 500 @app.route('/api/health', methods=['GET']) def health(): """Health check endpoint""" return jsonify({'status': 'ok'}) @app.route('/api/calculate-auto-values', methods=['GET']) def calculate_auto_values(): """ Calculate auto font size and padding for given image dimensions Query parameters: - width: Original image width (required) - height: Original image height (required) - position: Text position (optional, default: below) - font_size: Font size or 'auto' (optional, default: auto) - padding: Padding or 'auto' (optional, default: auto) Returns: JSON with calculated font_size and padding values """ if not ENABLE_API: return jsonify({'error': 'API is disabled'}), 403 try: width = int(request.args.get('width')) height = int(request.args.get('height')) except (TypeError, ValueError): return jsonify({'error': 'width and height must be provided as integers'}), 400 position = request.args.get('position', 'below').lower() if position not in ['above', 'below', 'left', 'right']: return jsonify({'error': 'Invalid position. Use: above, below, left, or right'}), 400 # Handle font size font_size_input = request.args.get('font_size', 'auto') if font_size_input == '' or font_size_input == 'auto': # Calculate auto font size using same logic as add_text_to_image if position in ['above', 'below']: aspect_ratio = width / height if aspect_ratio > 2.5: font_size = int(width * 0.10) elif aspect_ratio > 1.5: font_size = int(width * 0.11) else: font_size = int(width * 0.14) else: # left or right aspect_ratio = height / width if aspect_ratio > 2.5: font_size = int(height * 0.15) elif aspect_ratio > 1.5: font_size = int(height * 0.18) else: font_size = int(height * 0.22) min_size = max(20, int(min(width, height) * 0.08)) max_size = min(300, int(max(width, height) * 0.3)) font_size = max(min_size, min(font_size, max_size)) else: try: font_size = int(font_size_input) except ValueError: return jsonify({'error': 'Font size must be a number or "auto"'}), 400 # Handle padding padding_input = request.args.get('padding', 'auto') if padding_input == '' or padding_input == 'auto': # Calculate auto padding using same logic as add_text_to_image if position in ['above', 'below']: padding = int((width * font_size) ** 0.5 * 0.12) padding = max(12, min(padding, 60)) else: # left or right padding = int((height * font_size) ** 0.5 * 0.12) padding = max(12, min(padding, 60)) else: try: padding = int(padding_input) except ValueError: return jsonify({'error': 'Padding must be a number or "auto"'}), 400 return jsonify({ 'font_size': font_size, 'padding': padding }) @app.route('/api/image', methods=['GET']) def process_image_url(): """ API endpoint to process an image from URL and return the image directly Query parameters: - url: Image URL (required) - text: Text to add (required) - position: Where to add text (optional, default: below) - font_size: Font size or 'auto' (optional, default: auto) - text_color: Text color (optional, default: white) - bg_color: Background color or 'transparent' (optional, default: transparent) - padding: Padding or 'auto' (optional, default: auto) Example: /api/image?url=https://example.com/logo.png&text=Breaking%20News&position=below """ if not ENABLE_API: return jsonify({'error': 'API is disabled'}), 403 # Get required parameters image_url = request.args.get('url') text = request.args.get('text') if not image_url: return jsonify({'error': 'Missing required parameter: url'}), 400 if not text: return jsonify({'error': 'Missing required parameter: text'}), 400 # Get optional parameters with defaults position = request.args.get('position', 'below').lower() if position not in ['above', 'below', 'left', 'right']: return jsonify({'error': 'Invalid position. Use: above, below, left, or right'}), 400 # Handle font size font_size_input = request.args.get('font_size', 'auto') if font_size_input == '' or font_size_input == 'auto': font_size = None else: try: font_size = int(font_size_input) except ValueError: return jsonify({'error': 'Font size must be a number or "auto"'}), 400 # Handle padding padding_input = request.args.get('padding', 'auto') if padding_input == '' or padding_input == 'auto': padding = None else: try: padding = int(padding_input) except ValueError: return jsonify({'error': 'Padding must be a number or "auto"'}), 400 text_color = request.args.get('text_color', 'white') bg_color = request.args.get('bg_color', 'transparent') # Allow transparent background if bg_color == '' or bg_color == 'transparent' or bg_color == 'auto': bg_color = None # Get font path if specified font_path = request.args.get('font_path', None) if font_path == '' or font_path == 'auto': font_path = None try: # Download the image response = requests.get(image_url, timeout=10, headers={'User-Agent': 'LogoTextAdder/1.0'}) response.raise_for_status() # Save to temporary file img_bytes = io.BytesIO(response.content) # Determine format from content-type or URL content_type = response.headers.get('content-type', '') if 'png' in content_type or image_url.lower().endswith('.png'): output_format = 'PNG' mimetype = 'image/png' elif 'jpeg' in content_type or 'jpg' in content_type or image_url.lower().endswith(('.jpg', '.jpeg')): output_format = 'JPEG' mimetype = 'image/jpeg' elif 'gif' in content_type or image_url.lower().endswith('.gif'): output_format = 'GIF' mimetype = 'image/gif' elif 'webp' in content_type or image_url.lower().endswith('.webp'): output_format = 'WEBP' mimetype = 'image/webp' else: # Default to PNG output_format = 'PNG' mimetype = 'image/png' # Create temp file to process temp_filename = f"temp_{os.urandom(8).hex()}.{output_format.lower()}" temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename) with open(temp_filepath, 'wb') as f: f.write(response.content) # Process the image result_img, metadata = add_text_to_image( temp_filepath, text, position, font_size, text_color, bg_color, padding, font_path ) # Save to bytes buffer img_io = io.BytesIO() # Convert RGBA to RGB for JPEG if output_format == 'JPEG' and result_img.mode == 'RGBA': rgb_img = Image.new('RGB', result_img.size, (255, 255, 255)) rgb_img.paste(result_img, mask=result_img.split()[3]) result_img = rgb_img # Save with appropriate settings if output_format == 'PNG': result_img.save(img_io, output_format, optimize=True) elif output_format == 'JPEG': result_img.save(img_io, output_format, quality=95) else: result_img.save(img_io, output_format, quality=95) img_io.seek(0) # Clean up temp file os.remove(temp_filepath) # Return image directly response = send_file( img_io, mimetype=mimetype, as_attachment=False, download_name=f'logo_{text[:20].replace(" ", "_")}.{output_format.lower()}' ) response.headers['X-Font-Size-Used'] = str(metadata['font_size_used']) response.headers['X-Padding-Used'] = str(metadata['padding_used']) if metadata['font_name_used']: response.headers['X-Font-Name-Used'] = metadata['font_name_used'] return response except requests.RequestException as e: return jsonify({'error': f'Failed to download image: {str(e)}'}), 400 except Exception as e: # Clean up on error if 'temp_filepath' in locals() and os.path.exists(temp_filepath): os.remove(temp_filepath) return jsonify({'error': f'Error processing image: {str(e)}'}), 500 if __name__ == '__main__': app.run(debug=False, host='0.0.0.0', port=5001)