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
|
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
196
app.py
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,19 +1367,35 @@
|
|||||||
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 {
|
||||||
} else {
|
const params = new URLSearchParams({
|
||||||
fontSizeNum = Math.round(originalWidth * 0.12);
|
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 = parseInt(fontSize);
|
fontSizeNum = parseInt(fontSize);
|
||||||
|
paddingNum = parseInt(padding);
|
||||||
}
|
}
|
||||||
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`;
|
||||||
@@ -1584,6 +1543,9 @@
|
|||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user