Initial Commit
This commit is contained in:
42
.dockerignore
Normal file
42
.dockerignore
Normal file
@@ -0,0 +1,42 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
*.egg
|
||||
|
||||
# Project specific
|
||||
uploads/
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.webp
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Uploads folder
|
||||
uploads/
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.webp
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Install system dependencies for Pillow and Tesseract
|
||||
RUN apt-get update && apt-get install -y \
|
||||
tesseract-ocr \
|
||||
libtesseract-dev \
|
||||
libfreetype6-dev \
|
||||
libjpeg-dev \
|
||||
libpng-dev \
|
||||
fonts-dejavu \
|
||||
fonts-dejavu-core \
|
||||
fonts-dejavu-extra \
|
||||
fonts-liberation \
|
||||
fonts-liberation2 \
|
||||
fontconfig \
|
||||
&& fc-cache -f -v \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application files
|
||||
COPY app.py .
|
||||
COPY templates/ templates/
|
||||
|
||||
# Create uploads directory
|
||||
RUN mkdir -p uploads
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5001
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "app.py"]
|
||||
234
README.md
Normal file
234
README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 📺 Logo Text Adder
|
||||
|
||||
A Python web application for adding custom text to TV station and network logos. Upload a logo, specify your text and positioning preferences, and download the enhanced image with expanded canvas.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎨 **Web Interface**: User-friendly web UI for easy logo processing
|
||||
- 🔌 **REST API**: Programmatic access for automation
|
||||
- 📐 **Flexible Positioning**: Add text above, below, left, or right of logos
|
||||
- 🎨 **Customization**: Adjust font size, text color, background color, and padding
|
||||
- 📦 **Multiple Formats**: Supports PNG, JPG, JPEG, GIF, and WEBP
|
||||
- ⚡ **Instant Preview**: See results immediately in the browser
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
1. **Create a `docker-compose.yml` file:**
|
||||
```yaml
|
||||
services:
|
||||
logo-txt:
|
||||
image: ghcr.io/sethwv/logo-txt:latest
|
||||
ports:
|
||||
- "5001:5001"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
2. **Start the application:**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. **Access the application:**
|
||||
Open your browser and navigate to `http://localhost:5001`
|
||||
|
||||
4. **Stop the application:**
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Using Docker Run
|
||||
|
||||
1. **Pull and run the container:**
|
||||
```bash
|
||||
docker run -d -p 5001:5001 --name logo-txt ghcr.io/sethwv/logo-txt:latest
|
||||
```
|
||||
|
||||
2. **Access the application:**
|
||||
Open your browser and navigate to `http://localhost:5001`
|
||||
|
||||
3. **Stop the container:**
|
||||
```bash
|
||||
docker stop logo-txt
|
||||
docker rm logo-txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Web Interface
|
||||
|
||||
1. Open your browser and navigate to `http://localhost:5001`
|
||||
2. Upload your logo image or enter an image URL
|
||||
3. Enter the text you want to add
|
||||
4. Choose the position (above, below, left, or right)
|
||||
5. Optionally adjust advanced settings:
|
||||
- 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
|
||||
|
||||
### API Usage
|
||||
|
||||
The application provides a REST API endpoint for programmatic access.
|
||||
|
||||
#### Process Image from URL
|
||||
|
||||
`GET /api/image`
|
||||
|
||||
Process an image from a URL using query parameters. The image is returned directly.
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
- `url` (string, required): URL of the image to process
|
||||
- `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`
|
||||
- `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`
|
||||
|
||||
##### Example URLs
|
||||
|
||||
**Basic usage:**
|
||||
```
|
||||
http://localhost:5001/api/image?url=https://example.com/logo.png&text=Breaking%20News
|
||||
```
|
||||
|
||||
**With all parameters:**
|
||||
```
|
||||
http://localhost:5001/api/image?url=https://example.com/logo.png&text=Breaking%20News&position=below&font_size=auto&text_color=white&bg_color=transparent&padding=auto
|
||||
```
|
||||
|
||||
##### Example with cURL
|
||||
|
||||
```bash
|
||||
curl "http://localhost:5001/api/image?url=https://example.com/logo.png&text=Breaking%20News&position=below" \
|
||||
--output processed_logo.png
|
||||
```
|
||||
|
||||
##### Example with Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "http://localhost:5001/api/image"
|
||||
params = {
|
||||
"url": "https://example.com/logo.png",
|
||||
"text": "Breaking News",
|
||||
"position": "below",
|
||||
"font_size": "auto",
|
||||
"text_color": "white",
|
||||
"bg_color": "transparent"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
with open("processed_logo.png", "wb") as f:
|
||||
f.write(response.content)
|
||||
print("Image processed successfully!")
|
||||
else:
|
||||
print(f"Error: {response.json()}")
|
||||
```
|
||||
|
||||
##### Example with JavaScript
|
||||
|
||||
```javascript
|
||||
const params = new URLSearchParams({
|
||||
url: 'https://example.com/logo.png',
|
||||
text: 'Breaking News',
|
||||
position: 'below',
|
||||
font_size: 'auto',
|
||||
text_color: 'white',
|
||||
bg_color: 'transparent'
|
||||
});
|
||||
|
||||
const response = await fetch(`http://localhost:5001/api/image?${params}`);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
// Use the URL to display or download the image
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Error:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
logo-txt/
|
||||
├── app.py # Flask application and image processing logic
|
||||
├── templates/
|
||||
│ └── index.html # Web interface
|
||||
├── uploads/ # Temporary storage for uploaded images (auto-created)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Docker image configuration
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Image Upload**: User uploads a logo through the web interface or API
|
||||
2. **Canvas Expansion**: The application calculates the required canvas size based on text dimensions and position
|
||||
3. **Text Rendering**: Text is rendered with the specified styling on the expanded canvas
|
||||
4. **Image Composition**: The original logo and text are composited onto the new canvas
|
||||
5. **Output**: The processed image is returned to the user for download
|
||||
|
||||
## Configuration
|
||||
|
||||
### Docker Configuration
|
||||
|
||||
The application runs on port 5001 by default. To change the port, modify the `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "8080:5001" # Maps host port 8080 to container port 5001
|
||||
```
|
||||
|
||||
### Application Settings
|
||||
|
||||
You can modify these settings in `app.py`:
|
||||
|
||||
- `MAX_CONTENT_LENGTH`: Maximum upload file size (default: 16MB)
|
||||
- `UPLOAD_FOLDER`: Temporary folder for uploads (default: 'uploads')
|
||||
- Server host and port in the `if __name__ == '__main__'` block
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 5001 is already in use, change the port mapping in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "8080:5001" # Use port 8080 instead
|
||||
```
|
||||
|
||||
Then access the application at `http://localhost:8080`
|
||||
|
||||
### Font Issues
|
||||
|
||||
The Docker image includes DejaVu and Liberation fonts by default. If you need additional fonts, you can:
|
||||
|
||||
1. Modify the Dockerfile to install additional font packages
|
||||
2. Mount a font directory as a volume in `docker-compose.yml`
|
||||
|
||||
### View Container Logs
|
||||
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is provided as-is for personal and commercial use.
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit issues, fork the repository, and create pull requests for any improvements.
|
||||
548
app.py
Normal file
548
app.py
Normal file
@@ -0,0 +1,548 @@
|
||||
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
|
||||
|
||||
try:
|
||||
import pytesseract
|
||||
TESSERACT_AVAILABLE = True
|
||||
except ImportError:
|
||||
TESSERACT_AVAILABLE = False
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
||||
|
||||
# Ensure upload folder exists
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def trim_transparent_borders(img):
|
||||
"""
|
||||
Trim transparent/whitespace borders from image
|
||||
Returns the cropped image
|
||||
"""
|
||||
if img.mode not in ('RGBA', 'LA'):
|
||||
# No transparency to trim
|
||||
return img
|
||||
|
||||
# Get the alpha channel
|
||||
if img.mode == 'RGBA':
|
||||
alpha = img.split()[3]
|
||||
else:
|
||||
alpha = img.split()[1]
|
||||
|
||||
# Get the bounding box of non-transparent pixels
|
||||
bbox = alpha.getbbox()
|
||||
|
||||
if bbox:
|
||||
return img.crop(bbox)
|
||||
else:
|
||||
# Image is completely transparent, return as is
|
||||
return img
|
||||
|
||||
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
|
||||
"""
|
||||
if not TESSERACT_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 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',
|
||||
]
|
||||
|
||||
# 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
|
||||
|
||||
# Return first available font
|
||||
for font_path in font_list:
|
||||
if os.path.exists(font_path):
|
||||
return font_path
|
||||
|
||||
except Exception as e:
|
||||
# If OCR fails, fall back to None
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_font(font_size, detected_font_path=None):
|
||||
"""
|
||||
Get the best available font for text rendering
|
||||
"""
|
||||
try:
|
||||
# If we detected a font from the logo, use it
|
||||
if detected_font_path and os.path.exists(detected_font_path):
|
||||
return ImageFont.truetype(detected_font_path, font_size)
|
||||
|
||||
# Otherwise try common fonts (prioritize bold/heavy fonts)
|
||||
font_paths = [
|
||||
# macOS paths
|
||||
'/System/Library/Fonts/Supplemental/Arial Black.ttf',
|
||||
'/System/Library/Fonts/Supplemental/Impact.ttf',
|
||||
'/System/Library/Fonts/Supplemental/Arial Bold.ttf',
|
||||
'/System/Library/Fonts/Helvetica.ttc',
|
||||
# Linux paths (DejaVu)
|
||||
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
||||
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
||||
# Linux paths (Liberation - free alternative to Arial)
|
||||
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
|
||||
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
|
||||
# Windows paths
|
||||
'C:\\Windows\\Fonts\\ariblk.ttf',
|
||||
'C:\\Windows\\Fonts\\impact.ttf',
|
||||
'C:\\Windows\\Fonts\\arialbd.ttf',
|
||||
]
|
||||
|
||||
for font_path in font_paths:
|
||||
if os.path.exists(font_path):
|
||||
return ImageFont.truetype(font_path, font_size)
|
||||
|
||||
return ImageFont.load_default()
|
||||
except:
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def add_text_to_image(image_path, text, position='below', font_size=None,
|
||||
text_color='white', bg_color=None, padding=None):
|
||||
"""
|
||||
Add text to an image by expanding the canvas
|
||||
|
||||
Args:
|
||||
image_path: Path to the source image
|
||||
text: Text to add
|
||||
position: Where to add text ('above', 'below', 'left', 'right')
|
||||
font_size: Size of the text font (auto if None)
|
||||
text_color: Color of the text
|
||||
bg_color: Background color for the expanded area (transparent if None and image has alpha)
|
||||
padding: Padding around the text (auto if None)
|
||||
|
||||
Returns:
|
||||
PIL Image object with text added
|
||||
"""
|
||||
# Load the original image
|
||||
img = Image.open(image_path)
|
||||
|
||||
# Preserve transparency
|
||||
has_transparency = img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info)
|
||||
|
||||
# Convert to RGBA if it has transparency, otherwise RGB
|
||||
if has_transparency:
|
||||
img = img.convert('RGBA')
|
||||
# Trim transparent borders first
|
||||
img = trim_transparent_borders(img)
|
||||
else:
|
||||
img = img.convert('RGB')
|
||||
|
||||
orig_width, orig_height = img.size
|
||||
|
||||
# Auto-calculate font size if not provided (based on image dimensions)
|
||||
if font_size is None:
|
||||
if position in ['above', 'below']:
|
||||
font_size = int(orig_width * 0.12) # 12% of image width
|
||||
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
|
||||
|
||||
# Auto-calculate padding if not provided
|
||||
if padding is None:
|
||||
padding = int(font_size * 0.25) # 25% of font size
|
||||
|
||||
# Auto-determine background color
|
||||
if bg_color is None:
|
||||
if has_transparency:
|
||||
bg_color = (0, 0, 0, 0) # Transparent
|
||||
else:
|
||||
bg_color = '#1a1a1a' # Dark gray for non-transparent images
|
||||
|
||||
# Try to detect font from the logo
|
||||
detected_font_path = detect_font_from_image(img)
|
||||
|
||||
# Get the appropriate font
|
||||
font = get_font(font_size, detected_font_path)
|
||||
|
||||
# Create a temporary image to measure text size
|
||||
temp_img = Image.new('RGB', (1, 1))
|
||||
temp_draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Get text bounding box
|
||||
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
bbox_offset_x = -bbox[0] # Offset to align text properly
|
||||
bbox_offset_y = -bbox[1] # Offset to align text properly
|
||||
|
||||
# Calculate new image dimensions with directional padding
|
||||
# More padding between logo and text, minimal on outer edges (mimic existing padding)
|
||||
inner_padding = padding # Padding between logo and text
|
||||
outer_padding = padding // 4 # Minimal padding on outer edge
|
||||
side_padding = padding // 4 # Minimal padding on perpendicular sides
|
||||
|
||||
if position in ['above', 'below']:
|
||||
text_area_height = text_height + outer_padding + inner_padding
|
||||
new_width = max(orig_width, text_width + side_padding * 2)
|
||||
new_height = orig_height + text_area_height
|
||||
else: # left or right
|
||||
text_area_width = text_width + outer_padding + inner_padding
|
||||
new_width = orig_width + text_area_width
|
||||
new_height = max(orig_height, text_height + side_padding * 2)
|
||||
|
||||
# Create new image with expanded canvas (preserve transparency)
|
||||
if has_transparency:
|
||||
new_img = Image.new('RGBA', (new_width, new_height), bg_color)
|
||||
else:
|
||||
new_img = Image.new('RGB', (new_width, new_height), bg_color)
|
||||
|
||||
# Calculate positions with directional padding
|
||||
if position == 'below':
|
||||
# Paste original image at top
|
||||
paste_x = (new_width - orig_width) // 2
|
||||
new_img.paste(img, (paste_x, 0))
|
||||
# Add text below with padding between logo and text, minimal at bottom
|
||||
text_x = (new_width - text_width) // 2 + bbox_offset_x
|
||||
text_y = orig_height + inner_padding + bbox_offset_y
|
||||
elif position == 'above':
|
||||
# Paste original image at bottom
|
||||
paste_x = (new_width - orig_width) // 2
|
||||
new_img.paste(img, (paste_x, text_area_height))
|
||||
# Add text above with minimal padding at top, more near logo
|
||||
text_x = (new_width - text_width) // 2 + bbox_offset_x
|
||||
text_y = outer_padding + bbox_offset_y
|
||||
elif position == 'right':
|
||||
# Paste original image on left
|
||||
paste_y = (new_height - orig_height) // 2
|
||||
new_img.paste(img, (0, paste_y))
|
||||
# Add text on right with padding between logo and text, minimal on right edge
|
||||
text_x = orig_width + inner_padding + bbox_offset_x
|
||||
text_y = (new_height - text_height) // 2 + bbox_offset_y
|
||||
else: # left
|
||||
# Paste original image on right
|
||||
paste_y = (new_height - orig_height) // 2
|
||||
new_img.paste(img, (text_area_width, paste_y))
|
||||
# Add text on left with minimal padding on left edge, more near logo
|
||||
text_x = outer_padding + bbox_offset_x
|
||||
text_y = (new_height - text_height) // 2 + bbox_offset_y
|
||||
|
||||
# Draw text on new image
|
||||
draw = ImageDraw.Draw(new_img)
|
||||
draw.text((text_x, text_y), text, fill=text_color, font=font)
|
||||
|
||||
return new_img
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the web interface"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/process', methods=['POST'])
|
||||
def process_image():
|
||||
"""
|
||||
API endpoint to process an image
|
||||
|
||||
Expected form data:
|
||||
- image: Image file
|
||||
- text: Text to add
|
||||
- position: Where to add text (above/below/left/right)
|
||||
- font_size: Font size (optional, default 60)
|
||||
- text_color: Text color (optional, default white)
|
||||
- bg_color: Background color (optional, default #1a1a1a)
|
||||
- padding: Padding around text (optional, default 20)
|
||||
"""
|
||||
# Check if image file is present
|
||||
if 'image' not in request.files:
|
||||
return jsonify({'error': 'No image file provided'}), 400
|
||||
|
||||
file = request.files['image']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'No file selected'}), 400
|
||||
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({'error': 'Invalid file type. Allowed: PNG, JPG, JPEG, GIF, WEBP'}), 400
|
||||
|
||||
# Get parameters
|
||||
text = request.form.get('text', '')
|
||||
if not text:
|
||||
return jsonify({'error': 'No text provided'}), 400
|
||||
|
||||
position = request.form.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 - allow auto/empty for automatic sizing
|
||||
font_size_input = request.form.get('font_size', 'auto')
|
||||
if font_size_input == '' or font_size_input == 'auto':
|
||||
font_size = None
|
||||
else:
|
||||
try:
|
||||
font_size = int(font_size_input)
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Font size must be a number or "auto"'}), 400
|
||||
|
||||
# Handle padding - allow auto/empty for automatic padding
|
||||
padding_input = request.form.get('padding', 'auto')
|
||||
if padding_input == '' or padding_input == 'auto':
|
||||
padding = None
|
||||
else:
|
||||
try:
|
||||
padding = int(padding_input)
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Padding must be a number or "auto"'}), 400
|
||||
|
||||
text_color = request.form.get('text_color', 'white')
|
||||
bg_color = request.form.get('bg_color', 'transparent')
|
||||
|
||||
# Allow transparent background
|
||||
if bg_color == '' or bg_color == 'transparent' or bg_color == 'auto':
|
||||
bg_color = None
|
||||
|
||||
# Save uploaded file
|
||||
filename = secure_filename(file.filename)
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
file.save(filepath)
|
||||
|
||||
try:
|
||||
# Process the image
|
||||
result_img = add_text_to_image(
|
||||
filepath,
|
||||
text,
|
||||
position,
|
||||
font_size,
|
||||
text_color,
|
||||
bg_color,
|
||||
padding
|
||||
)
|
||||
|
||||
# Save to bytes buffer
|
||||
img_io = io.BytesIO()
|
||||
|
||||
# Determine output format (preserve transparency for PNG)
|
||||
output_format = 'PNG'
|
||||
if filename.lower().endswith(('.jpg', '.jpeg')):
|
||||
output_format = 'JPEG'
|
||||
# Convert RGBA to RGB for JPEG
|
||||
if result_img.mode == 'RGBA':
|
||||
rgb_img = Image.new('RGB', result_img.size, (255, 255, 255))
|
||||
rgb_img.paste(result_img, mask=result_img.split()[3])
|
||||
result_img = rgb_img
|
||||
elif filename.lower().endswith('.gif'):
|
||||
output_format = 'GIF'
|
||||
elif filename.lower().endswith('.webp'):
|
||||
output_format = 'WEBP'
|
||||
|
||||
# Save with appropriate settings
|
||||
if output_format == 'PNG':
|
||||
result_img.save(img_io, output_format, optimize=True)
|
||||
elif output_format == 'JPEG':
|
||||
result_img.save(img_io, output_format, quality=95)
|
||||
else:
|
||||
result_img.save(img_io, output_format, quality=95)
|
||||
img_io.seek(0)
|
||||
|
||||
# Clean up uploaded file
|
||||
os.remove(filepath)
|
||||
|
||||
# Determine mimetype
|
||||
mimetype_map = {
|
||||
'PNG': 'image/png',
|
||||
'JPEG': 'image/jpeg',
|
||||
'GIF': 'image/gif',
|
||||
'WEBP': 'image/webp'
|
||||
}
|
||||
|
||||
return send_file(
|
||||
img_io,
|
||||
mimetype=mimetype_map.get(output_format, 'image/png'),
|
||||
as_attachment=True,
|
||||
download_name=f'processed_{filename}'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Clean up on error
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
return jsonify({'error': f'Error processing image: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
@app.route('/api/image', methods=['GET'])
|
||||
def process_image_url():
|
||||
"""
|
||||
API endpoint to process an image from URL and return the image directly
|
||||
|
||||
Query parameters:
|
||||
- url: Image URL (required)
|
||||
- text: Text to add (required)
|
||||
- position: Where to add text (optional, default: below)
|
||||
- font_size: Font size or 'auto' (optional, default: auto)
|
||||
- text_color: Text color (optional, default: white)
|
||||
- bg_color: Background color or 'transparent' (optional, default: transparent)
|
||||
- padding: Padding or 'auto' (optional, default: auto)
|
||||
|
||||
Example:
|
||||
/api/image?url=https://example.com/logo.png&text=Breaking%20News&position=below
|
||||
"""
|
||||
# Get required parameters
|
||||
image_url = request.args.get('url')
|
||||
text = request.args.get('text')
|
||||
|
||||
if not image_url:
|
||||
return jsonify({'error': 'Missing required parameter: url'}), 400
|
||||
|
||||
if not text:
|
||||
return jsonify({'error': 'Missing required parameter: text'}), 400
|
||||
|
||||
# Get optional parameters with defaults
|
||||
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':
|
||||
font_size = None
|
||||
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':
|
||||
padding = None
|
||||
else:
|
||||
try:
|
||||
padding = int(padding_input)
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Padding must be a number or "auto"'}), 400
|
||||
|
||||
text_color = request.args.get('text_color', 'white')
|
||||
bg_color = request.args.get('bg_color', 'transparent')
|
||||
|
||||
# Allow transparent background
|
||||
if bg_color == '' or bg_color == 'transparent' or bg_color == 'auto':
|
||||
bg_color = None
|
||||
|
||||
try:
|
||||
# Download the image
|
||||
response = requests.get(image_url, timeout=10, headers={'User-Agent': 'LogoTextAdder/1.0'})
|
||||
response.raise_for_status()
|
||||
|
||||
# Save to temporary file
|
||||
img_bytes = io.BytesIO(response.content)
|
||||
|
||||
# Determine format from content-type or URL
|
||||
content_type = response.headers.get('content-type', '')
|
||||
if 'png' in content_type or image_url.lower().endswith('.png'):
|
||||
output_format = 'PNG'
|
||||
mimetype = 'image/png'
|
||||
elif 'jpeg' in content_type or 'jpg' in content_type or image_url.lower().endswith(('.jpg', '.jpeg')):
|
||||
output_format = 'JPEG'
|
||||
mimetype = 'image/jpeg'
|
||||
elif 'gif' in content_type or image_url.lower().endswith('.gif'):
|
||||
output_format = 'GIF'
|
||||
mimetype = 'image/gif'
|
||||
elif 'webp' in content_type or image_url.lower().endswith('.webp'):
|
||||
output_format = 'WEBP'
|
||||
mimetype = 'image/webp'
|
||||
else:
|
||||
# Default to PNG
|
||||
output_format = 'PNG'
|
||||
mimetype = 'image/png'
|
||||
|
||||
# Create temp file to process
|
||||
temp_filename = f"temp_{os.urandom(8).hex()}.{output_format.lower()}"
|
||||
temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
|
||||
|
||||
with open(temp_filepath, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Process the image
|
||||
result_img = add_text_to_image(
|
||||
temp_filepath,
|
||||
text,
|
||||
position,
|
||||
font_size,
|
||||
text_color,
|
||||
bg_color,
|
||||
padding
|
||||
)
|
||||
|
||||
# Save to bytes buffer
|
||||
img_io = io.BytesIO()
|
||||
|
||||
# Convert RGBA to RGB for JPEG
|
||||
if output_format == 'JPEG' and result_img.mode == 'RGBA':
|
||||
rgb_img = Image.new('RGB', result_img.size, (255, 255, 255))
|
||||
rgb_img.paste(result_img, mask=result_img.split()[3])
|
||||
result_img = rgb_img
|
||||
|
||||
# Save with appropriate settings
|
||||
if output_format == 'PNG':
|
||||
result_img.save(img_io, output_format, optimize=True)
|
||||
elif output_format == 'JPEG':
|
||||
result_img.save(img_io, output_format, quality=95)
|
||||
else:
|
||||
result_img.save(img_io, output_format, quality=95)
|
||||
|
||||
img_io.seek(0)
|
||||
|
||||
# Clean up temp file
|
||||
os.remove(temp_filepath)
|
||||
|
||||
# Return image directly
|
||||
return send_file(
|
||||
img_io,
|
||||
mimetype=mimetype,
|
||||
as_attachment=False,
|
||||
download_name=f'logo_{text[:20].replace(" ", "_")}.{output_format.lower()}'
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return jsonify({'error': f'Failed to download image: {str(e)}'}), 400
|
||||
except Exception as e:
|
||||
# Clean up on error
|
||||
if 'temp_filepath' in locals() and os.path.exists(temp_filepath):
|
||||
os.remove(temp_filepath)
|
||||
return jsonify({'error': f'Error processing image: {str(e)}'}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5001)
|
||||
29
docker-build-push.sh
Executable file
29
docker-build-push.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
# IMAGE_NAME="git.seth.services/seth/logo-txt"
|
||||
IMAGE_NAME="ghcr.io/sethwv/logo-txt"
|
||||
VERSION="${1:-latest}"
|
||||
|
||||
echo "Building Docker image for amd64 architecture..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-t ${IMAGE_NAME}:${VERSION} \
|
||||
-t ${IMAGE_NAME}:latest \
|
||||
--load \
|
||||
.
|
||||
|
||||
echo "Testing the image..."
|
||||
docker run --rm ${IMAGE_NAME}:${VERSION} python -c "import flask; import PIL; print('Image OK')"
|
||||
|
||||
echo "Pushing image to Container Registry..."
|
||||
docker push ${IMAGE_NAME}:${VERSION}
|
||||
docker push ${IMAGE_NAME}:latest
|
||||
|
||||
echo "Done! Image pushed to:"
|
||||
echo " ${IMAGE_NAME}:${VERSION}"
|
||||
echo " ${IMAGE_NAME}:latest"
|
||||
echo ""
|
||||
echo "To run the container:"
|
||||
echo " docker run -p 5001:5001 ${IMAGE_NAME}:latest"
|
||||
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
logo-txt:
|
||||
image: ghcr.io/sethwv/logo-txt:latest
|
||||
ports:
|
||||
- "5001:5001"
|
||||
restart: unless-stopped
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Flask==3.1.0
|
||||
Pillow==11.0.0
|
||||
Werkzeug==3.1.3
|
||||
pytesseract==0.3.13
|
||||
requests==2.32.3
|
||||
619
templates/index.html
Normal file
619
templates/index.html
Normal file
@@ -0,0 +1,619 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logo Text Adder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #667eea;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
background: #e8eaf6;
|
||||
border-color: #764ba2;
|
||||
}
|
||||
|
||||
.file-input-label.has-file {
|
||||
background: #e8f5e9;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.color-input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-input-group input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.position-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.position-option {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.position-option input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.position-option label {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.position-option input[type="radio"]:checked + label {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.position-option label:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.preview-section h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
background-image:
|
||||
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #c62828;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #2e7d32;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
padding: 12px 25px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #45a049;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.advanced-options {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.advanced-options h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #555;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.input-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.two-column, .position-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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="content">
|
||||
<div id="message"></div>
|
||||
|
||||
<form id="logoForm">
|
||||
<div class="form-group">
|
||||
<label>Logo Image</label>
|
||||
<div class="input-tabs">
|
||||
<button type="button" class="tab-btn active" data-tab="file">📁 Upload File</button>
|
||||
<button type="button" class="tab-btn" data-tab="url">🔗 Image URL</button>
|
||||
</div>
|
||||
<div id="fileTab" class="tab-content active">
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="imageFile" accept="image/*">
|
||||
<label for="imageFile" class="file-input-label" id="fileLabel">
|
||||
<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="urlTab" class="tab-content">
|
||||
<input type="text" id="imageUrl" placeholder="https://example.com/logo.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="text">Text to Add</label>
|
||||
<input type="text" id="text" placeholder="Enter your text here..." required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Text Position</label>
|
||||
<div class="position-grid">
|
||||
<div class="position-option">
|
||||
<input type="radio" id="above" name="position" value="above">
|
||||
<label for="above">⬆️ Above</label>
|
||||
</div>
|
||||
<div class="position-option">
|
||||
<input type="radio" id="below" name="position" value="below" checked>
|
||||
<label for="below">⬇️ Below</label>
|
||||
</div>
|
||||
<div class="position-option">
|
||||
<input type="radio" id="left" name="position" value="left">
|
||||
<label for="left">⬅️ Left</label>
|
||||
</div>
|
||||
<div class="position-option">
|
||||
<input type="radio" id="right" name="position" value="right">
|
||||
<label for="right">➡️ Right</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="advanced-options">
|
||||
<h3>⚙️ Advanced Options</h3>
|
||||
|
||||
<div class="two-column">
|
||||
<div class="form-group">
|
||||
<label for="fontSize">Font Size</label>
|
||||
<input type="text" id="fontSize" value="auto" placeholder="auto or number">
|
||||
<small style="color: #666; font-size: 0.85em;">Leave as "auto" for automatic sizing</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="text" id="padding" value="auto" placeholder="auto or number">
|
||||
<small style="color: #666; font-size: 0.85em;">Leave as "auto" for automatic padding</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="textColor">Text Color</label>
|
||||
<div class="color-input-group">
|
||||
<input type="text" id="textColor" value="white" placeholder="white or #ffffff">
|
||||
<input type="color" id="textColorPicker" value="#ffffff">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bgColor">Background Color</label>
|
||||
<div class="color-input-group">
|
||||
<input type="text" id="bgColor" value="transparent" placeholder="transparent or #1a1a1a">
|
||||
<input type="color" id="bgColorPicker" value="#1a1a1a">
|
||||
</div>
|
||||
<small style="color: #666; font-size: 0.85em;">Use "transparent" to preserve logo transparency</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="submitBtn">
|
||||
✨ Generate Logo
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="preview" class="preview-section" style="display: none;">
|
||||
<h2>Preview</h2>
|
||||
<img id="previewImage" class="preview-image" alt="Processed logo">
|
||||
<br>
|
||||
<a href="#" id="downloadBtn" class="download-btn" download>⬇️ Download Image</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('logoForm');
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
const imageUrl = document.getElementById('imageUrl');
|
||||
const fileLabel = document.getElementById('fileLabel');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const messageDiv = document.getElementById('message');
|
||||
const preview = document.getElementById('preview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const fileTab = document.getElementById('fileTab');
|
||||
const urlTab = document.getElementById('urlTab');
|
||||
let activeInputMode = 'file';
|
||||
|
||||
// Tab switching
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.tab;
|
||||
activeInputMode = tab;
|
||||
|
||||
// Update button states
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
fileTab.classList.remove('active');
|
||||
urlTab.classList.remove('active');
|
||||
|
||||
if (tab === 'file') {
|
||||
fileTab.classList.add('active');
|
||||
} else {
|
||||
urlTab.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Color picker sync
|
||||
const textColorInput = document.getElementById('textColor');
|
||||
const textColorPicker = document.getElementById('textColorPicker');
|
||||
const bgColorInput = document.getElementById('bgColor');
|
||||
const bgColorPicker = document.getElementById('bgColorPicker');
|
||||
|
||||
textColorPicker.addEventListener('input', (e) => {
|
||||
textColorInput.value = e.target.value;
|
||||
});
|
||||
|
||||
textColorInput.addEventListener('input', (e) => {
|
||||
if (e.target.value.startsWith('#')) {
|
||||
textColorPicker.value = e.target.value;
|
||||
}
|
||||
});
|
||||
|
||||
bgColorPicker.addEventListener('input', (e) => {
|
||||
bgColorInput.value = e.target.value;
|
||||
});
|
||||
|
||||
bgColorInput.addEventListener('input', (e) => {
|
||||
if (e.target.value.startsWith('#') && e.target.value.length === 7) {
|
||||
bgColorPicker.value = e.target.value;
|
||||
}
|
||||
});
|
||||
|
||||
// File input handling
|
||||
imageFile.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
fileLabel.classList.add('has-file');
|
||||
fileLabel.innerHTML = `<span>✅ ${file.name}</span>`;
|
||||
} else {
|
||||
fileLabel.classList.remove('has-file');
|
||||
fileLabel.innerHTML = '<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>';
|
||||
}
|
||||
});
|
||||
|
||||
function showMessage(message, type) {
|
||||
messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
|
||||
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function clearMessage() {
|
||||
messageDiv.innerHTML = '';
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
clearMessage();
|
||||
|
||||
const text = document.getElementById('text').value;
|
||||
if (!text.trim()) {
|
||||
showMessage('Please enter some text', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const position = document.querySelector('input[name="position"]:checked').value;
|
||||
const fontSize = document.getElementById('fontSize').value;
|
||||
const padding = document.getElementById('padding').value;
|
||||
const textColor = textColorInput.value;
|
||||
const bgColor = bgColorInput.value;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Processing...';
|
||||
preview.style.display = 'none';
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (activeInputMode === 'url') {
|
||||
// Use URL-based API
|
||||
const url = imageUrl.value.trim();
|
||||
if (!url) {
|
||||
showMessage('Please enter an image URL', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '✨ Generate Logo';
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
url: url,
|
||||
text: text,
|
||||
position: position,
|
||||
font_size: fontSize,
|
||||
padding: padding,
|
||||
text_color: textColor,
|
||||
bg_color: bgColor
|
||||
});
|
||||
|
||||
response = await fetch(`/api/image?${params}`);
|
||||
} else {
|
||||
// Use file upload API
|
||||
const file = imageFile.files[0];
|
||||
if (!file) {
|
||||
showMessage('Please select an image file', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '✨ Generate Logo';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('text', text);
|
||||
formData.append('position', position);
|
||||
formData.append('font_size', fontSize);
|
||||
formData.append('padding', padding);
|
||||
formData.append('text_color', textColor);
|
||||
formData.append('bg_color', bgColor);
|
||||
|
||||
response = await fetch('/api/process', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to process image');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
previewImage.src = url;
|
||||
downloadBtn.href = url;
|
||||
downloadBtn.download = `logo_${text.replace(/\s+/g, '_')}.png`;
|
||||
preview.style.display = 'block';
|
||||
|
||||
showMessage('✅ Logo generated successfully!', 'success');
|
||||
preview.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
} catch (error) {
|
||||
showMessage(`❌ Error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '✨ Generate Logo';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user