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 @@
-
-
@@ -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}
`;
- // 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}
`;
- // 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);
+ });
+ });