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:
79
README.md
79
README.md
@@ -40,6 +40,11 @@ If you prefer to run the application without Docker:
|
||||
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
196
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
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
@@ -3,3 +3,4 @@ Pillow==11.0.0
|
||||
Werkzeug==3.1.3
|
||||
pytesseract==0.3.13
|
||||
requests==2.32.3
|
||||
gunicorn==21.2.0
|
||||
|
||||
@@ -161,10 +161,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
@@ -679,30 +675,6 @@
|
||||
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;
|
||||
padding: 20px;
|
||||
@@ -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`;
|
||||
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;
|
||||
// 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;
|
||||
// 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
|
||||
}
|
||||
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 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;
|
||||
// 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;';
|
||||
// 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]
|
||||
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`;
|
||||
// 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';
|
||||
// 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`;
|
||||
// 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';
|
||||
// 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]
|
||||
} 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`;
|
||||
// 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';
|
||||
// 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`;
|
||||
// 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';
|
||||
// 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]
|
||||
} 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`;
|
||||
// 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';
|
||||
// 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`;
|
||||
// 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';
|
||||
// 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]
|
||||
} 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`;
|
||||
// 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';
|
||||
// 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`;
|
||||
// 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';
|
||||
}
|
||||
// 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>
|
||||
|
||||
Reference in New Issue
Block a user