feat: add environment controls, API URL display, and improved auto-sizing

Features:
- Add ENABLE_UI and ENABLE_API environment variables for deployment flexibility
- Add API URL display textbox with copy button in web interface
- Create /api/calculate-auto-values endpoint to centralize sizing logic
- Dimension overlay now calls API for accurate calculations instead of duplicating logic

Improvements:
- Enhance auto-sizing algorithm with aspect-ratio awareness
- Improve padding calculation using geometric mean formula
- Font detection now uses all available fonts from get_available_fonts()
- API URL includes all parameters (font_path, font_size, padding, etc.)

UI/UX:
- Remove hidden header section and logo copy buttons
- API URL textarea wraps and grows vertically to show full URL
- Clean up unused code and debug console.log statements

Dependencies:
- Add gunicorn==21.2.0 to requirements.txt

Documentation:
- Document environment variables with Docker and Python examples
- Add production deployment guidance
- Update API documentation with font_path parameter
- Add API URL feature to usage instructions

Bug fixes:
- Change debug=False for production
- Remove unused 're' import from app.py
This commit is contained in:
2026-01-17 13:48:15 -05:00
parent 954cd87a20
commit d26f619901
4 changed files with 515 additions and 251 deletions

View File

@@ -40,6 +40,11 @@ If you prefer to run the application without Docker:
python app.py 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:** 4. **Access the application:**
Open your browser and navigate to `http://localhost:5001` 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 3. Enter the text you want to add
4. Choose the position (above, below, left, or right) 4. Choose the position (above, below, left, or right)
5. Optionally adjust advanced settings: 5. Optionally adjust advanced settings:
- Font (default: auto - automatically detects best match)
- Font size (default: auto - scales based on image size) - Font size (default: auto - scales based on image size)
- Padding (default: auto - scales based on font size) - Padding (default: auto - scales based on font size)
- Text color (default: white) - Text color (default: white)
- Background color (default: transparent) - Background color (default: transparent)
6. Click "Generate Logo" 6. Click "Generate Logo"
7. Preview and download your enhanced 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 ### 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 - `text` (string, required): Text to add to the image
- `position` (string, optional): Where to place the text (`above`, `below`, `left`, or `right`) - default: `below` - `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_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` - `text_color` (string, optional): Text color - default: `white`
- `bg_color` (string, optional): Background color or "transparent" - default: `transparent` - `bg_color` (string, optional): Background color or "transparent" - default: `transparent`
- `padding` (integer or "auto", optional): Padding in pixels or "auto" - default: `auto` - `padding` (integer or "auto", optional): Padding in pixels or "auto" - default: `auto`
@@ -215,6 +223,71 @@ logo-txt/
## Configuration ## 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 ### Docker Configuration
The application runs on port 5001 by default. To change the port, modify the `docker-compose.yml` file: 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') - `UPLOAD_FOLDER`: Temporary folder for uploads (default: 'uploads')
- Server host and port in the `if __name__ == '__main__'` block - 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 ## Troubleshooting
### Port Already in Use ### Port Already in Use

196
app.py
View File

@@ -2,7 +2,6 @@ from flask import Flask, request, jsonify, send_file, render_template
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import io import io
import os import os
import re
import requests import requests
from werkzeug.utils import secure_filename 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['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
app.config['UPLOAD_FOLDER'] = 'uploads' 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 # Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) 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 Attempt to detect font characteristics from the logo using OCR
Returns a font path that best matches the detected style Returns a font path that best matches the detected style
Uses all available fonts from get_available_fonts()
""" """
if not TESSERACT_AVAILABLE: if not TESSERACT_AVAILABLE:
return None return None
@@ -60,35 +65,49 @@ def detect_font_from_image(img):
# Get detailed OCR data including font info # Get detailed OCR data including font info
data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)
# Look for font characteristics in detected text # Get all available fonts
# Common bold/heavy fonts used in TV logos available_fonts = get_available_fonts()
bold_fonts = [ if not available_fonts or available_fonts[0]['path'] == 'default':
'/System/Library/Fonts/Supplemental/Arial Black.ttf', return None
'/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',
]
# Try to detect if logo uses bold/heavy text by checking confidence scores # Try to detect if logo uses bold/heavy text by checking confidence scores
# Higher confidence often correlates with bolder, clearer text # Higher confidence often correlates with bolder, clearer text
confidences = [conf for conf in data['conf'] if conf != -1] confidences = [conf for conf in data['conf'] if conf != -1]
avg_confidence = sum(confidences) / len(confidences) if confidences else 0 avg_confidence = sum(confidences) / len(confidences) if confidences else 0
# If high confidence detected text, likely uses bold fonts # Categorize fonts by style - prioritize TV-appropriate fonts
font_list = bold_fonts if avg_confidence > 60 else regular_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: for font_path in font_list:
if os.path.exists(font_path): if os.path.exists(font_path):
return 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 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 font_size is None:
if position in ['above', 'below']: 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 else: # left or right
font_size = int(orig_height * 0.20) # 20% of image height (larger for vertical text) # For vertical text, base on height with consideration for aspect ratio
font_size = max(30, min(font_size, 250)) # Clamp between 30 and 250 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
# Auto-calculate padding if not provided # 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 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 # Auto-determine background color
if bg_color is None: if bg_color is None:
@@ -395,11 +443,15 @@ def add_text_to_image(image_path, text, position='below', font_size=None,
@app.route('/') @app.route('/')
def index(): def index():
"""Serve the web interface""" """Serve the web interface"""
if not ENABLE_UI:
return jsonify({'error': 'Web UI is disabled'}), 403
return render_template('index.html') return render_template('index.html')
@app.route('/api/fonts', methods=['GET']) @app.route('/api/fonts', methods=['GET'])
def get_fonts(): def get_fonts():
"""Get list of available fonts""" """Get list of available fonts"""
if not ENABLE_API:
return jsonify({'error': 'API is disabled'}), 403
fonts = get_available_fonts() fonts = get_available_fonts()
return jsonify({'fonts': fonts}) return jsonify({'fonts': fonts})
@@ -412,6 +464,8 @@ def get_tv_logos():
- search: Search query (fuzzy, case-insensitive) - search: Search query (fuzzy, case-insensitive)
- country: Filter by country code - country: Filter by country code
""" """
if not ENABLE_API:
return jsonify({'error': 'API is disabled'}), 403
try: try:
# Get query parameters # Get query parameters
search_query = request.args.get('search', '').lower() search_query = request.args.get('search', '').lower()
@@ -516,6 +570,8 @@ def process_image():
- bg_color: Background color (optional, default #1a1a1a) - bg_color: Background color (optional, default #1a1a1a)
- padding: Padding around text (optional, default 20) - padding: Padding around text (optional, default 20)
""" """
if not ENABLE_API:
return jsonify({'error': 'API is disabled'}), 403
# Check if image file is present # Check if image file is present
if 'image' not in request.files: if 'image' not in request.files:
return jsonify({'error': 'No image file provided'}), 400 return jsonify({'error': 'No image file provided'}), 400
@@ -647,6 +703,84 @@ def health():
"""Health check endpoint""" """Health check endpoint"""
return jsonify({'status': 'ok'}) 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']) @app.route('/api/image', methods=['GET'])
def process_image_url(): def process_image_url():
""" """
@@ -664,6 +798,8 @@ def process_image_url():
Example: Example:
/api/image?url=https://example.com/logo.png&text=Breaking%20News&position=below /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 # Get required parameters
image_url = request.args.get('url') image_url = request.args.get('url')
text = request.args.get('text') text = request.args.get('text')
@@ -802,4 +938,4 @@ def process_image_url():
if __name__ == '__main__': 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)

View File

@@ -3,3 +3,4 @@ Pillow==11.0.0
Werkzeug==3.1.3 Werkzeug==3.1.3
pytesseract==0.3.13 pytesseract==0.3.13
requests==2.32.3 requests==2.32.3
gunicorn==21.2.0

View File

@@ -161,10 +161,6 @@
} }
} }
.header {
display: none;
}
.content { .content {
padding: 40px; padding: 40px;
} }
@@ -679,30 +675,6 @@
padding: 0 6px; 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 { .loading-spinner {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
@@ -726,11 +698,6 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header">
<h1>📺 Logo Text Adder</h1>
<p>Add custom text to your TV station and network logos</p>
</div>
<div class="main-layout"> <div class="main-layout">
<div class="form-container"> <div class="form-container">
<div class="content"> <div class="content">
@@ -893,6 +860,11 @@
Show dimensions on preview Show dimensions on preview
</label> </label>
<div class="preview-download-hint">Click image to download</div> <div class="preview-download-hint">Click image to download</div>
<div style="margin-top: 20px; max-width: 800px; margin-left: auto; margin-right: auto;">
<label for="apiUrl" style="display: block; margin-bottom: 8px; color: #e0e0e0; font-weight: 600; font-size: 0.95em;">API Call URL:</label>
<textarea id="apiUrl" readonly style="width: 100%; padding: 12px 15px; border: 2px solid #333; border-radius: 8px; font-size: 14px; background: #2a2a2a; color: #e0e0e0; font-family: monospace; cursor: text; user-select: all; resize: vertical; min-height: 44px; line-height: 1.5; overflow-y: auto; white-space: pre-wrap; word-break: break-all;" onclick="this.select()"></textarea>
<small style="display: block; margin-top: 6px; color: #666; font-size: 0.85em;">Use this URL to programmatically generate the same logo via the API</small>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -966,6 +938,8 @@
// Trigger live preview when font changes // Trigger live preview when font changes
triggerLivePreview(); triggerLivePreview();
// Update API URL when font changes
updateApiUrl();
} }
// Load available fonts // Load available fonts
@@ -1071,7 +1045,6 @@
} }
allLogos = data.logos; allLogos = data.logos;
console.log(`Loaded ${data.total || allLogos.length} total logos`);
// Populate country filter if not already done // Populate country filter if not already done
if (allCountries.length === 0 && data.countries) { if (allCountries.length === 0 && data.countries) {
@@ -1114,24 +1087,9 @@
logoItem.className = 'logo-item'; logoItem.className = 'logo-item';
logoItem.innerHTML = ` logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy"> <img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-copy-icon" data-url="${logo.url}" title="Copy direct link to image">📋</div>
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div> <div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`; `;
// 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', () => { logoItem.addEventListener('click', () => {
// Remove selected class from all items // Remove selected class from all items
document.querySelectorAll('.logo-item').forEach(item => { document.querySelectorAll('.logo-item').forEach(item => {
@@ -1184,24 +1142,9 @@
logoItem.className = 'logo-item'; logoItem.className = 'logo-item';
logoItem.innerHTML = ` logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy"> <img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-copy-icon" data-url="${logo.url}" title="Copy direct link to image">📋</div>
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div> <div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`; `;
// 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', () => { logoItem.addEventListener('click', () => {
document.querySelectorAll('.logo-item').forEach(item => { document.querySelectorAll('.logo-item').forEach(item => {
item.classList.remove('selected'); item.classList.remove('selected');
@@ -1424,166 +1367,185 @@
const naturalHeight = previewImage.naturalHeight; const naturalHeight = previewImage.naturalHeight;
const scale = imgWidth / naturalWidth; const scale = imgWidth / naturalWidth;
// Parse actual values (could be 'auto' or a number) // Use API to get calculated values if auto, otherwise use provided values
// For left/right: auto font size is 20% of height, for above/below: 12% of width const updateVisualization = async () => {
let fontSizeNum; let fontSizeNum, paddingNum;
if (fontSize === 'auto') {
if (position === 'left' || position === 'right') { if (fontSize === 'auto' || padding === 'auto') {
fontSizeNum = Math.round(originalHeight * 0.20); 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 { } 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`; fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`;
paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`; paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`;
// Backend uses inner_padding (full padding) and outer_padding (padding/4) // Backend uses inner_padding (full padding) and outer_padding (padding/4)
const innerPadding = paddingNum * scale; const innerPadding = paddingNum * scale;
const outerPadding = (paddingNum / 4) * scale; const outerPadding = (paddingNum / 4) * scale;
// For horizontal text (left/right positions), we need to estimate actual text width // For horizontal text (left/right positions), we need to estimate actual text width
// Use canvas to measure text width more accurately // Use canvas to measure text width more accurately
const text = document.getElementById('text').value; const text = document.getElementById('text').value;
let textWidth, textHeight; let textWidth, textHeight;
if (position === 'left' || position === 'right') { if (position === 'left' || position === 'right') {
// For left/right: use actual text width and actual bounding box height // For left/right: use actual text width and actual bounding box height
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.font = `${fontSizeNum}px Arial`; ctx.font = `${fontSizeNum}px Arial`;
const metrics = ctx.measureText(text); const metrics = ctx.measureText(text);
textWidth = metrics.width * scale; textWidth = metrics.width * scale;
// Use actual text bounding box height (tighter than font size) // Use actual text bounding box height (tighter than font size)
textHeight = fontSizeNum * 0.75 * scale; textHeight = fontSizeNum * 0.75 * scale;
} else { } else {
// For above/below: text height is tighter bounding box // For above/below: text height is tighter bounding box
textHeight = fontSizeNum * 0.75 * scale; textHeight = fontSizeNum * 0.75 * scale;
textWidth = fontSizeNum * scale; // Not used in above/below textWidth = fontSizeNum * scale; // Not used in above/below
} }
// Calculate text_area dimensions (matches backend exactly) // Calculate text_area dimensions (matches backend exactly)
let textAreaHeight, textAreaWidth; let textAreaHeight, textAreaWidth;
if (position === 'below' || position === 'above') { if (position === 'below' || position === 'above') {
textAreaHeight = textHeight + outerPadding + innerPadding; textAreaHeight = textHeight + outerPadding + innerPadding;
} else { } else {
textAreaWidth = textWidth + outerPadding + innerPadding; textAreaWidth = textWidth + outerPadding + innerPadding;
} }
// Calculate where the original logo is positioned in the final image // Calculate where the original logo is positioned in the final image
const scaledOriginalWidth = originalWidth * scale; const scaledOriginalWidth = originalWidth * scale;
const scaledOriginalHeight = originalHeight * scale; const scaledOriginalHeight = originalHeight * scale;
// Reset all styles completely // Reset all styles completely
fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;'; 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;'; 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;'; 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;'; paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
if (position === 'below') { if (position === 'below') {
// Layout: [logo at y=0] [innerPadding] [text] [outerPadding] // Layout: [logo at y=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text bounding box - FULL WIDTH // Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px'; fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px'; fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`; fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`; fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to BOTTOM edge of text box // Font label on the LEFT side, anchored to BOTTOM edge of text box
fontSizeLabel.style.right = '100%'; fontSizeLabel.style.right = '100%';
fontSizeLabel.style.bottom = '0px'; fontSizeLabel.style.bottom = '0px';
fontSizeLabel.style.marginRight = '8px'; fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH // Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px'; paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px'; paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${scaledOriginalHeight}px`; paddingIndicator.style.top = `${scaledOriginalHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`; paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to BOTTOM edge of padding box // Padding label on the RIGHT side, anchored to BOTTOM edge of padding box
paddingLabel.style.left = '100%'; paddingLabel.style.left = '100%';
paddingLabel.style.bottom = '0px'; paddingLabel.style.bottom = '0px';
paddingLabel.style.marginLeft = '8px'; paddingLabel.style.marginLeft = '8px';
} else if (position === 'above') { } else if (position === 'above') {
// Layout: [outerPadding] [text] [innerPadding] [logo] // Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text bounding box - FULL WIDTH // Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px'; fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px'; fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${outerPadding}px`; fontSizeIndicator.style.top = `${outerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`; fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to TOP edge of text box // Font label on the LEFT side, anchored to TOP edge of text box
fontSizeLabel.style.right = '100%'; fontSizeLabel.style.right = '100%';
fontSizeLabel.style.top = '0px'; fontSizeLabel.style.top = '0px';
fontSizeLabel.style.marginRight = '8px'; fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH // Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px'; paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px'; paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${outerPadding + textHeight}px`; paddingIndicator.style.top = `${outerPadding + textHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`; paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to TOP edge of padding box // Padding label on the RIGHT side, anchored to TOP edge of padding box
paddingLabel.style.left = '100%'; paddingLabel.style.left = '100%';
paddingLabel.style.top = '0px'; paddingLabel.style.top = '0px';
paddingLabel.style.marginLeft = '8px'; paddingLabel.style.marginLeft = '8px';
} else if (position === 'right') { } else if (position === 'right') {
// Layout: [logo at x=0] [innerPadding] [text] [outerPadding] // Layout: [logo at x=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text width - FULL HEIGHT // Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`; fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`;
fontSizeIndicator.style.top = '0px'; fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px'; fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`; fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to RIGHT edge of text box // Font label ABOVE the box, anchored to RIGHT edge of text box
fontSizeLabel.style.right = '0px'; fontSizeLabel.style.right = '0px';
fontSizeLabel.style.bottom = '100%'; fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px'; fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT // Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${scaledOriginalWidth}px`; paddingIndicator.style.left = `${scaledOriginalWidth}px`;
paddingIndicator.style.top = '0px'; paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px'; paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`; paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to RIGHT edge of padding box // Padding label BELOW the box, anchored to RIGHT edge of padding box
paddingLabel.style.right = '0px'; paddingLabel.style.right = '0px';
paddingLabel.style.top = '100%'; paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px'; paddingLabel.style.marginTop = '8px';
} else { } else {
// position === 'left' // position === 'left'
// Layout: [outerPadding] [text] [innerPadding] [logo] // Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text width - FULL HEIGHT // Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${outerPadding}px`; fontSizeIndicator.style.left = `${outerPadding}px`;
fontSizeIndicator.style.top = '0px'; fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px'; fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`; fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to LEFT edge of text box // Font label ABOVE the box, anchored to LEFT edge of text box
fontSizeLabel.style.left = '0px'; fontSizeLabel.style.left = '0px';
fontSizeLabel.style.bottom = '100%'; fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px'; fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT // Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${outerPadding + textWidth}px`; paddingIndicator.style.left = `${outerPadding + textWidth}px`;
paddingIndicator.style.top = '0px'; paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px'; paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`; paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to LEFT edge of padding box // Padding label BELOW the box, anchored to LEFT edge of padding box
paddingLabel.style.left = '0px'; paddingLabel.style.left = '0px';
paddingLabel.style.top = '100%'; paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px'; paddingLabel.style.marginTop = '8px';
} }
};
updateVisualization();
} }
showDimensions.addEventListener('change', updateDimensionOverlay); showDimensions.addEventListener('change', updateDimensionOverlay);
@@ -1893,6 +1855,92 @@
imageFile.addEventListener('change', () => { imageFile.addEventListener('change', () => {
setTimeout(triggerLivePreview, 100); 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);
});
});
</script> </script>
</body> </body>
</html> </html>