diff --git a/README.md b/README.md index 3824262..16a89a0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,11 @@ If you prefer to run the application without Docker: ```bash python app.py ``` + + **For production deployment**, use Gunicorn instead: + ```bash + gunicorn --bind 0.0.0.0:5001 --workers 4 app:app + ``` 4. **Access the application:** Open your browser and navigate to `http://localhost:5001` @@ -96,12 +101,14 @@ If you prefer to run the application without Docker: 3. Enter the text you want to add 4. Choose the position (above, below, left, or right) 5. Optionally adjust advanced settings: + - Font (default: auto - automatically detects best match) - Font size (default: auto - scales based on image size) - Padding (default: auto - scales based on font size) - Text color (default: white) - Background color (default: transparent) 6. Click "Generate Logo" 7. Preview and download your enhanced logo +8. Copy the API URL shown below the preview to use the same settings programmatically ### API Usage @@ -119,6 +126,7 @@ Process an image from a URL using query parameters. The image is returned direct - `text` (string, required): Text to add to the image - `position` (string, optional): Where to place the text (`above`, `below`, `left`, or `right`) - default: `below` - `font_size` (integer or "auto", optional): Font size in pixels, or "auto" for automatic sizing - default: `auto` +- `font_path` (string, optional): Path to a specific font file to use, or "auto" for automatic detection - default: `auto` - `text_color` (string, optional): Text color - default: `white` - `bg_color` (string, optional): Background color or "transparent" - default: `transparent` - `padding` (integer or "auto", optional): Padding in pixels or "auto" - default: `auto` @@ -215,6 +223,71 @@ logo-txt/ ## Configuration +### Environment Variables + +The application supports the following environment variables for controlling functionality: + +- **`ENABLE_UI`** (default: `true`) + - Set to `false` to disable the web interface + - Useful for API-only deployments + - Accepted values: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off` + - Example: `ENABLE_UI=false` + +- **`ENABLE_API`** (default: `true`) + - Set to `false` to disable all API endpoints + - Useful for UI-only deployments or security requirements + - Accepted values: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off` + - Example: `ENABLE_API=false` + +#### Using with Docker Compose + +Add environment variables to your `docker-compose.yml`: + +```yaml +services: + logo-txt: + image: ghcr.io/sethwv/logo-txt:latest + ports: + - "5001:5001" + environment: + - ENABLE_UI=true + - ENABLE_API=true + restart: unless-stopped +``` + +#### Using with Docker Run + +Pass environment variables with the `-e` flag: + +```bash +docker run -d -p 5001:5001 \ + -e ENABLE_UI=true \ + -e ENABLE_API=false \ + --name logo-txt \ + ghcr.io/sethwv/logo-txt:latest +``` + +#### Using with Python Directly + +Set environment variables before running the application: + +```bash +# Linux/macOS +export ENABLE_UI=true +export ENABLE_API=true +python app.py + +# Windows (Command Prompt) +set ENABLE_UI=true +set ENABLE_API=true +python app.py + +# Windows (PowerShell) +$env:ENABLE_UI="true" +$env:ENABLE_API="true" +python app.py +``` + ### Docker Configuration The application runs on port 5001 by default. To change the port, modify the `docker-compose.yml` file: @@ -232,6 +305,12 @@ You can modify these settings in `app.py`: - `UPLOAD_FOLDER`: Temporary folder for uploads (default: 'uploads') - Server host and port in the `if __name__ == '__main__'` block +**Note**: The built-in Flask development server (`python app.py`) is not suitable for production. For production deployments: +- **Docker** (recommended): Uses Gunicorn automatically with 4 workers +- **Manual deployment**: Use `gunicorn --bind 0.0.0.0:5001 --workers 4 app:app` +- Adjust `--workers` based on available CPU cores (recommended: 2-4 × CPU cores) +- Increase `--timeout` for processing large images (default in Docker: 120 seconds) + ## Troubleshooting ### Port Already in Use diff --git a/app.py b/app.py index 3f5da73..3e07bb3 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,6 @@ from flask import Flask, request, jsonify, send_file, render_template from PIL import Image, ImageDraw, ImageFont import io import os -import re import requests from werkzeug.utils import secure_filename @@ -16,6 +15,11 @@ 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) @@ -52,6 +56,7 @@ 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 @@ -60,35 +65,49 @@ def detect_font_from_image(img): # Get detailed OCR data including font info data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) - # Look for font characteristics in detected text - # Common bold/heavy fonts used in TV logos - bold_fonts = [ - '/System/Library/Fonts/Supplemental/Arial Black.ttf', - '/System/Library/Fonts/Supplemental/Impact.ttf', - '/System/Library/Fonts/Supplemental/Arial Bold.ttf', - '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', - 'C:\\Windows\\Fonts\\ariblk.ttf', - 'C:\\Windows\\Fonts\\impact.ttf', - 'C:\\Windows\\Fonts\\arialbd.ttf', - ] - - # Default to clean sans-serif fonts - regular_fonts = [ - '/System/Library/Fonts/Helvetica.ttc', - '/System/Library/Fonts/Supplemental/Arial.ttf', - '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', - 'C:\\Windows\\Fonts\\arial.ttf', - ] + # 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 - # If high confidence detected text, likely uses bold fonts - font_list = bold_fonts if avg_confidence > 60 else regular_fonts + # 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 - # Return first available font + 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 @@ -282,17 +301,46 @@ def add_text_to_image(image_path, text, position='below', font_size=None, orig_width, orig_height = img.size - # Auto-calculate font size if not provided (based on image dimensions) + # Auto-calculate font size if not provided (based on image dimensions and aspect ratio) if font_size is None: if position in ['above', 'below']: - font_size = int(orig_width * 0.12) # 12% of image width + # 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 - font_size = int(orig_height * 0.20) # 20% of image height (larger for vertical text) - font_size = max(30, min(font_size, 250)) # Clamp between 30 and 250 + # 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 + # Auto-calculate padding if not provided (independent of font size) if padding is None: - padding = int(font_size * 0.25) # 25% of font size + 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: @@ -395,11 +443,15 @@ def add_text_to_image(image_path, text, position='below', font_size=None, @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}) @@ -412,6 +464,8 @@ def get_tv_logos(): - 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() @@ -516,6 +570,8 @@ def process_image(): - 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 @@ -647,6 +703,84 @@ 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(): """ @@ -664,6 +798,8 @@ def process_image_url(): 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') @@ -802,4 +938,4 @@ def process_image_url(): if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5001) + app.run(debug=False, host='0.0.0.0', port=5001) diff --git a/requirements.txt b/requirements.txt index 327db73..c0503e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ Pillow==11.0.0 Werkzeug==3.1.3 pytesseract==0.3.13 requests==2.32.3 +gunicorn==21.2.0 diff --git a/templates/index.html b/templates/index.html index f4ecc5e..43bbce6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -161,10 +161,6 @@ } } - .header { - display: none; - } - .content { padding: 40px; } @@ -678,30 +674,6 @@ text-overflow: ellipsis; padding: 0 6px; } - - .logo-copy-icon { - position: absolute; - bottom: 34px; - right: 6px; - width: 28px; - height: 28px; - background: rgba(42, 42, 42, 0.9); - border: 1px solid #444; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 14px; - transition: all 0.2s; - z-index: 10; - } - - .logo-copy-icon:hover { - background: rgba(102, 126, 234, 0.95); - border-color: #667eea; - transform: scale(1.1); - } .loading-spinner { text-align: center; @@ -726,11 +698,6 @@
-
-

📺 Logo Text Adder

-

Add custom text to your TV station and network logos

-
-
@@ -893,6 +860,11 @@ Show dimensions on preview
Click image to download
+
+ + + Use this URL to programmatically generate the same logo via the API +
@@ -966,6 +938,8 @@ // Trigger live preview when font changes triggerLivePreview(); + // Update API URL when font changes + updateApiUrl(); } // Load available fonts @@ -1071,7 +1045,6 @@ } allLogos = data.logos; - console.log(`Loaded ${data.total || allLogos.length} total logos`); // Populate country filter if not already done if (allCountries.length === 0 && data.countries) { @@ -1114,24 +1087,9 @@ logoItem.className = 'logo-item'; logoItem.innerHTML = ` ${logo.name} -
📋
${logo.name}
`; - // Add click handler for copy icon - const copyIcon = logoItem.querySelector('.logo-copy-icon'); - copyIcon.addEventListener('click', (e) => { - e.stopPropagation(); - const url = e.target.dataset.url; - navigator.clipboard.writeText(url).then(() => { - const originalText = e.target.textContent; - e.target.textContent = '✅'; - setTimeout(() => { - e.target.textContent = originalText; - }, 2000); - }); - }); - logoItem.addEventListener('click', () => { // Remove selected class from all items document.querySelectorAll('.logo-item').forEach(item => { @@ -1184,24 +1142,9 @@ logoItem.className = 'logo-item'; logoItem.innerHTML = ` ${logo.name} -
📋
${logo.name}
`; - // Add click handler for copy icon - const copyIcon = logoItem.querySelector('.logo-copy-icon'); - copyIcon.addEventListener('click', (e) => { - e.stopPropagation(); - const url = e.target.dataset.url; - navigator.clipboard.writeText(url).then(() => { - const originalText = e.target.textContent; - e.target.textContent = '✅'; - setTimeout(() => { - e.target.textContent = originalText; - }, 2000); - }); - }); - logoItem.addEventListener('click', () => { document.querySelectorAll('.logo-item').forEach(item => { item.classList.remove('selected'); @@ -1424,166 +1367,185 @@ const naturalHeight = previewImage.naturalHeight; const scale = imgWidth / naturalWidth; - // Parse actual values (could be 'auto' or a number) - // For left/right: auto font size is 20% of height, for above/below: 12% of width - let fontSizeNum; - if (fontSize === 'auto') { - if (position === 'left' || position === 'right') { - fontSizeNum = Math.round(originalHeight * 0.20); + // Use API to get calculated values if auto, otherwise use provided values + const updateVisualization = async () => { + let fontSizeNum, paddingNum; + + if (fontSize === 'auto' || padding === 'auto') { + try { + const params = new URLSearchParams({ + width: originalWidth, + height: originalHeight, + position: position, + font_size: fontSize, + padding: padding + }); + + const response = await fetch(`/api/calculate-auto-values?${params}`); + const data = await response.json(); + + fontSizeNum = fontSize === 'auto' ? data.font_size : parseInt(fontSize); + paddingNum = padding === 'auto' ? data.padding : parseInt(padding); + } catch (error) { + console.error('Failed to calculate auto values:', error); + // Fallback to manual values if API fails + fontSizeNum = parseInt(fontSize) || 60; + paddingNum = parseInt(padding) || 20; + } } else { - fontSizeNum = Math.round(originalWidth * 0.12); + fontSizeNum = parseInt(fontSize); + paddingNum = parseInt(padding); } - } else { - fontSizeNum = parseInt(fontSize); - } - const paddingNum = padding === 'auto' ? Math.round(fontSizeNum * 0.25) : parseInt(padding); + + fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`; + paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`; + + // Backend uses inner_padding (full padding) and outer_padding (padding/4) + const innerPadding = paddingNum * scale; + const outerPadding = (paddingNum / 4) * scale; + + // For horizontal text (left/right positions), we need to estimate actual text width + // Use canvas to measure text width more accurately + const text = document.getElementById('text').value; + let textWidth, textHeight; + + if (position === 'left' || position === 'right') { + // For left/right: use actual text width and actual bounding box height + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = `${fontSizeNum}px Arial`; + const metrics = ctx.measureText(text); + textWidth = metrics.width * scale; + // Use actual text bounding box height (tighter than font size) + textHeight = fontSizeNum * 0.75 * scale; + } else { + // For above/below: text height is tighter bounding box + textHeight = fontSizeNum * 0.75 * scale; + textWidth = fontSizeNum * scale; // Not used in above/below + } + + // Calculate text_area dimensions (matches backend exactly) + let textAreaHeight, textAreaWidth; + if (position === 'below' || position === 'above') { + textAreaHeight = textHeight + outerPadding + innerPadding; + } else { + textAreaWidth = textWidth + outerPadding + innerPadding; + } + + // Calculate where the original logo is positioned in the final image + const scaledOriginalWidth = originalWidth * scale; + const scaledOriginalHeight = originalHeight * scale; + + // Reset all styles completely + fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;'; + paddingIndicator.style.cssText = 'position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: block;'; + fontSizeLabel.style.cssText = 'position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;'; + paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;'; + + if (position === 'below') { + // Layout: [logo at y=0] [innerPadding] [text] [outerPadding] + + // Blue box: shows ONLY the text bounding box - FULL WIDTH + fontSizeIndicator.style.left = '0px'; + fontSizeIndicator.style.right = '0px'; + fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`; + fontSizeIndicator.style.height = `${textHeight}px`; + + // Font label on the LEFT side, anchored to BOTTOM edge of text box + fontSizeLabel.style.right = '100%'; + fontSizeLabel.style.bottom = '0px'; + fontSizeLabel.style.marginRight = '8px'; + + // Green box: shows ONLY innerPadding - FULL WIDTH + paddingIndicator.style.left = '0px'; + paddingIndicator.style.right = '0px'; + paddingIndicator.style.top = `${scaledOriginalHeight}px`; + paddingIndicator.style.height = `${innerPadding}px`; + + // Padding label on the RIGHT side, anchored to BOTTOM edge of padding box + paddingLabel.style.left = '100%'; + paddingLabel.style.bottom = '0px'; + paddingLabel.style.marginLeft = '8px'; + + } else if (position === 'above') { + // Layout: [outerPadding] [text] [innerPadding] [logo] + + // Blue box: shows ONLY the text bounding box - FULL WIDTH + fontSizeIndicator.style.left = '0px'; + fontSizeIndicator.style.right = '0px'; + fontSizeIndicator.style.top = `${outerPadding}px`; + fontSizeIndicator.style.height = `${textHeight}px`; + + // Font label on the LEFT side, anchored to TOP edge of text box + fontSizeLabel.style.right = '100%'; + fontSizeLabel.style.top = '0px'; + fontSizeLabel.style.marginRight = '8px'; + + // Green box: shows ONLY innerPadding - FULL WIDTH + paddingIndicator.style.left = '0px'; + paddingIndicator.style.right = '0px'; + paddingIndicator.style.top = `${outerPadding + textHeight}px`; + paddingIndicator.style.height = `${innerPadding}px`; + + // Padding label on the RIGHT side, anchored to TOP edge of padding box + paddingLabel.style.left = '100%'; + paddingLabel.style.top = '0px'; + paddingLabel.style.marginLeft = '8px'; + + } else if (position === 'right') { + // Layout: [logo at x=0] [innerPadding] [text] [outerPadding] + + // Blue box: shows ONLY the text width - FULL HEIGHT + fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`; + fontSizeIndicator.style.top = '0px'; + fontSizeIndicator.style.bottom = '0px'; + fontSizeIndicator.style.width = `${textWidth}px`; + + // Font label ABOVE the box, anchored to RIGHT edge of text box + fontSizeLabel.style.right = '0px'; + fontSizeLabel.style.bottom = '100%'; + fontSizeLabel.style.marginBottom = '8px'; + + // Green box: shows ONLY innerPadding - FULL HEIGHT + paddingIndicator.style.left = `${scaledOriginalWidth}px`; + paddingIndicator.style.top = '0px'; + paddingIndicator.style.bottom = '0px'; + paddingIndicator.style.width = `${innerPadding}px`; + + // Padding label BELOW the box, anchored to RIGHT edge of padding box + paddingLabel.style.right = '0px'; + paddingLabel.style.top = '100%'; + paddingLabel.style.marginTop = '8px'; + + } else { + // position === 'left' + // Layout: [outerPadding] [text] [innerPadding] [logo] + + // Blue box: shows ONLY the text width - FULL HEIGHT + fontSizeIndicator.style.left = `${outerPadding}px`; + fontSizeIndicator.style.top = '0px'; + fontSizeIndicator.style.bottom = '0px'; + fontSizeIndicator.style.width = `${textWidth}px`; + + // Font label ABOVE the box, anchored to LEFT edge of text box + fontSizeLabel.style.left = '0px'; + fontSizeLabel.style.bottom = '100%'; + fontSizeLabel.style.marginBottom = '8px'; + + // Green box: shows ONLY innerPadding - FULL HEIGHT + paddingIndicator.style.left = `${outerPadding + textWidth}px`; + paddingIndicator.style.top = '0px'; + paddingIndicator.style.bottom = '0px'; + paddingIndicator.style.width = `${innerPadding}px`; + + // Padding label BELOW the box, anchored to LEFT edge of padding box + paddingLabel.style.left = '0px'; + paddingLabel.style.top = '100%'; + paddingLabel.style.marginTop = '8px'; + } + }; - fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`; - paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`; - - // Backend uses inner_padding (full padding) and outer_padding (padding/4) - const innerPadding = paddingNum * scale; - const outerPadding = (paddingNum / 4) * scale; - - // For horizontal text (left/right positions), we need to estimate actual text width - // Use canvas to measure text width more accurately - const text = document.getElementById('text').value; - let textWidth, textHeight; - - if (position === 'left' || position === 'right') { - // For left/right: use actual text width and actual bounding box height - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - ctx.font = `${fontSizeNum}px Arial`; - const metrics = ctx.measureText(text); - textWidth = metrics.width * scale; - // Use actual text bounding box height (tighter than font size) - textHeight = fontSizeNum * 0.75 * scale; - } else { - // For above/below: text height is tighter bounding box - textHeight = fontSizeNum * 0.75 * scale; - textWidth = fontSizeNum * scale; // Not used in above/below - } - - // Calculate text_area dimensions (matches backend exactly) - let textAreaHeight, textAreaWidth; - if (position === 'below' || position === 'above') { - textAreaHeight = textHeight + outerPadding + innerPadding; - } else { - textAreaWidth = textWidth + outerPadding + innerPadding; - } - - // Calculate where the original logo is positioned in the final image - const scaledOriginalWidth = originalWidth * scale; - const scaledOriginalHeight = originalHeight * scale; - - // Reset all styles completely - fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;'; - paddingIndicator.style.cssText = 'position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: block;'; - fontSizeLabel.style.cssText = 'position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;'; - paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;'; - - if (position === 'below') { - // Layout: [logo at y=0] [innerPadding] [text] [outerPadding] - - // Blue box: shows ONLY the text bounding box - FULL WIDTH - fontSizeIndicator.style.left = '0px'; - fontSizeIndicator.style.right = '0px'; - fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`; - fontSizeIndicator.style.height = `${textHeight}px`; - - // Font label on the LEFT side, anchored to BOTTOM edge of text box - fontSizeLabel.style.right = '100%'; - fontSizeLabel.style.bottom = '0px'; - fontSizeLabel.style.marginRight = '8px'; - - // Green box: shows ONLY innerPadding - FULL WIDTH - paddingIndicator.style.left = '0px'; - paddingIndicator.style.right = '0px'; - paddingIndicator.style.top = `${scaledOriginalHeight}px`; - paddingIndicator.style.height = `${innerPadding}px`; - - // Padding label on the RIGHT side, anchored to BOTTOM edge of padding box - paddingLabel.style.left = '100%'; - paddingLabel.style.bottom = '0px'; - paddingLabel.style.marginLeft = '8px'; - - } else if (position === 'above') { - // Layout: [outerPadding] [text] [innerPadding] [logo] - - // Blue box: shows ONLY the text bounding box - FULL WIDTH - fontSizeIndicator.style.left = '0px'; - fontSizeIndicator.style.right = '0px'; - fontSizeIndicator.style.top = `${outerPadding}px`; - fontSizeIndicator.style.height = `${textHeight}px`; - - // Font label on the LEFT side, anchored to TOP edge of text box - fontSizeLabel.style.right = '100%'; - fontSizeLabel.style.top = '0px'; - fontSizeLabel.style.marginRight = '8px'; - - // Green box: shows ONLY innerPadding - FULL WIDTH - paddingIndicator.style.left = '0px'; - paddingIndicator.style.right = '0px'; - paddingIndicator.style.top = `${outerPadding + textHeight}px`; - paddingIndicator.style.height = `${innerPadding}px`; - - // Padding label on the RIGHT side, anchored to TOP edge of padding box - paddingLabel.style.left = '100%'; - paddingLabel.style.top = '0px'; - paddingLabel.style.marginLeft = '8px'; - - } else if (position === 'right') { - // Layout: [logo at x=0] [innerPadding] [text] [outerPadding] - - // Blue box: shows ONLY the text width - FULL HEIGHT - fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`; - fontSizeIndicator.style.top = '0px'; - fontSizeIndicator.style.bottom = '0px'; - fontSizeIndicator.style.width = `${textWidth}px`; - - // Font label ABOVE the box, anchored to RIGHT edge of text box - fontSizeLabel.style.right = '0px'; - fontSizeLabel.style.bottom = '100%'; - fontSizeLabel.style.marginBottom = '8px'; - - // Green box: shows ONLY innerPadding - FULL HEIGHT - paddingIndicator.style.left = `${scaledOriginalWidth}px`; - paddingIndicator.style.top = '0px'; - paddingIndicator.style.bottom = '0px'; - paddingIndicator.style.width = `${innerPadding}px`; - - // Padding label BELOW the box, anchored to RIGHT edge of padding box - paddingLabel.style.right = '0px'; - paddingLabel.style.top = '100%'; - paddingLabel.style.marginTop = '8px'; - - } else { - // position === 'left' - // Layout: [outerPadding] [text] [innerPadding] [logo] - - // Blue box: shows ONLY the text width - FULL HEIGHT - fontSizeIndicator.style.left = `${outerPadding}px`; - fontSizeIndicator.style.top = '0px'; - fontSizeIndicator.style.bottom = '0px'; - fontSizeIndicator.style.width = `${textWidth}px`; - - // Font label ABOVE the box, anchored to LEFT edge of text box - fontSizeLabel.style.left = '0px'; - fontSizeLabel.style.bottom = '100%'; - fontSizeLabel.style.marginBottom = '8px'; - - // Green box: shows ONLY innerPadding - FULL HEIGHT - paddingIndicator.style.left = `${outerPadding + textWidth}px`; - paddingIndicator.style.top = '0px'; - paddingIndicator.style.bottom = '0px'; - paddingIndicator.style.width = `${innerPadding}px`; - - // Padding label BELOW the box, anchored to LEFT edge of padding box - paddingLabel.style.left = '0px'; - paddingLabel.style.top = '100%'; - paddingLabel.style.marginTop = '8px'; - } + updateVisualization(); } showDimensions.addEventListener('change', updateDimensionOverlay); @@ -1893,6 +1855,92 @@ imageFile.addEventListener('change', () => { setTimeout(triggerLivePreview, 100); }); + + // API URL generation and updating + const apiUrlInput = document.getElementById('apiUrl'); + + function updateApiUrl() { + const text = document.getElementById('text').value; + + // Don't show API URL if no text or no image source + if (!text.trim()) { + apiUrlInput.value = ''; + return; + } + + let imageUrl = ''; + + // Get the image URL based on active input mode + if (activeInputMode === 'browse') { + imageUrl = selectedLogoUrl.value.trim(); + } else if (activeInputMode === 'url') { + imageUrl = document.getElementById('imageUrl').value.trim(); + } else { + // File upload mode - can't generate API URL + apiUrlInput.value = 'API URL only available for URL/Browse modes (not file upload)'; + return; + } + + if (!imageUrl) { + apiUrlInput.value = ''; + return; + } + + // Build API URL + const position = document.querySelector('input[name="position"]:checked').value; + const fontSizeAutoChecked = document.getElementById('fontSizeAuto').checked; + const fontSize = fontSizeAutoChecked ? 'auto' : document.getElementById('fontSize').value; + const paddingAutoChecked = document.getElementById('paddingAuto').checked; + const padding = paddingAutoChecked ? 'auto' : document.getElementById('padding').value; + const textColor = textColorInput.value; + const bgColor = bgColorInput.value; + const fontPath = fontSelect.value; + + const params = new URLSearchParams({ + url: imageUrl, + text: text, + position: position, + font_size: fontSize, + padding: padding, + text_color: textColor, + bg_color: bgColor + }); + + // Only include font_path if it's not 'auto' (since auto is the default) + if (fontPath && fontPath !== 'auto') { + params.append('font_path', fontPath); + } + + // Build full URL (use current origin) + const baseUrl = window.location.origin; + apiUrlInput.value = `${baseUrl}/api/image?${params.toString()}`; + } + + // Update API URL whenever relevant inputs change + document.getElementById('text').addEventListener('input', updateApiUrl); + document.querySelectorAll('input[name="position"]').forEach(radio => { + radio.addEventListener('change', updateApiUrl); + }); + fontSizeInput.addEventListener('input', updateApiUrl); + fontSizeSlider.addEventListener('input', updateApiUrl); + fontSizeAuto.addEventListener('change', updateApiUrl); + paddingInput.addEventListener('input', updateApiUrl); + paddingSlider.addEventListener('input', updateApiUrl); + paddingAuto.addEventListener('change', updateApiUrl); + textColorInput.addEventListener('input', updateApiUrl); + textColorPicker.addEventListener('input', updateApiUrl); + bgColorInput.addEventListener('input', updateApiUrl); + bgColorPicker.addEventListener('input', updateApiUrl); + fontSelect.addEventListener('change', updateApiUrl); + imageUrl.addEventListener('input', updateApiUrl); + selectedLogoUrl.addEventListener('change', updateApiUrl); + + // Update when tab changes + tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + setTimeout(updateApiUrl, 100); + }); + });