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

@@ -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

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
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)

View File

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

View File

@@ -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 @@
</head>
<body>
<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="form-container">
<div class="content">
@@ -893,6 +860,11 @@
Show dimensions on preview
</label>
<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>
@@ -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 = `
<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>
`;
// 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 = `
<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>
`;
// 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);
});
});
</script>
</body>
</html>