commit 04cffb964ecbc62ee3c44d440496530c4acde2d0 Author: Seth Van Niekerk Date: Sun Jan 11 10:27:06 2026 -0500 Initial Commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5b760d7 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61dc51 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81e1b55 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d855a0 --- /dev/null +++ b/README.md @@ -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. diff --git a/app.py b/app.py new file mode 100644 index 0000000..94cc003 --- /dev/null +++ b/app.py @@ -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) diff --git a/docker-build-push.sh b/docker-build-push.sh new file mode 100755 index 0000000..aa8dd6a --- /dev/null +++ b/docker-build-push.sh @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a5d8771 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + logo-txt: + image: ghcr.io/sethwv/logo-txt:latest + ports: + - "5001:5001" + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..327db73 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask==3.1.0 +Pillow==11.0.0 +Werkzeug==3.1.3 +pytesseract==0.3.13 +requests==2.32.3 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..bd17ac2 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,619 @@ + + + + + + Logo Text Adder + + + +
+
+

📺 Logo Text Adder

+

Add custom text to your TV station and network logos

+
+ +
+
+ +
+
+ +
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

⚙️ Advanced Options

+ +
+
+ + + Leave as "auto" for automatic sizing +
+ +
+ + + Leave as "auto" for automatic padding +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+ Use "transparent" to preserve logo transparency +
+
+ + +
+ + +
+
+ + + +