|
#!/usr/bin/env python3 |
|
""" |
|
Placeholder Image Generator |
|
Generates professional placeholder images with profile photo, name, title, and company logo. |
|
""" |
|
|
|
import argparse |
|
import os |
|
import sys |
|
from io import BytesIO |
|
from pathlib import Path |
|
|
|
import requests |
|
from PIL import Image, ImageDraw, ImageFont |
|
|
|
|
|
class PlaceholderGenerator: |
|
"""Generate placeholder images with profile photo, name, title, and logo.""" |
|
|
|
# Default configuration |
|
DEFAULTS = { |
|
'width': 540, |
|
'height': 680, |
|
'bg_color': '#2C2C2C', |
|
'text_color': '#FFFFFF', |
|
'border_color': '#FFFFFF', |
|
'separator_color': '#FFFFFF', |
|
'profile_size': 280, |
|
'border_width': 1, |
|
'name_font_size': 48, |
|
'title_font_size': 28, |
|
'logo_max_width': 250, |
|
'logo_max_height': 60, |
|
'separator_width': 200, |
|
'separator_height': 1, |
|
} |
|
|
|
def __init__(self, **kwargs): |
|
"""Initialize generator with custom or default settings.""" |
|
self.config = {**self.DEFAULTS, **kwargs} |
|
|
|
def download_image(self, url): |
|
"""Download image from URL.""" |
|
try: |
|
response = requests.get(url, timeout=10) |
|
response.raise_for_status() |
|
return Image.open(BytesIO(response.content)) |
|
except Exception as e: |
|
raise ValueError(f"Failed to download image from URL: {e}") |
|
|
|
def load_image(self, image_input): |
|
"""Load image from file path or URL.""" |
|
if isinstance(image_input, str): |
|
if image_input.startswith(('http://', 'https://')): |
|
return self.download_image(image_input) |
|
else: |
|
return Image.open(image_input) |
|
return image_input |
|
|
|
def create_circular_image(self, image, size): |
|
"""Convert image to circular shape with border.""" |
|
# Resize image to square |
|
image = image.convert('RGB') |
|
image = image.resize((size, size), Image.Resampling.LANCZOS) |
|
|
|
# Create circular mask |
|
mask = Image.new('L', (size, size), 0) |
|
draw = ImageDraw.Draw(mask) |
|
draw.ellipse((0, 0, size, size), fill=255) |
|
|
|
# Apply mask |
|
circular = Image.new('RGB', (size, size), self.config['bg_color']) |
|
circular.paste(image, (0, 0), mask) |
|
|
|
# Add border |
|
border_width = self.config['border_width'] |
|
bordered_size = size + border_width * 2 |
|
bordered = Image.new('RGB', (bordered_size, bordered_size), self.config['border_color']) |
|
|
|
# Create border mask |
|
border_mask = Image.new('L', (bordered_size, bordered_size), 0) |
|
draw = ImageDraw.Draw(border_mask) |
|
draw.ellipse((0, 0, bordered_size, bordered_size), fill=255) |
|
|
|
# Paste circular image in center |
|
bg_for_border = Image.new('RGB', (bordered_size, bordered_size), self.config['bg_color']) |
|
bg_for_border.paste(circular, (border_width, border_width)) |
|
|
|
final = Image.new('RGB', (bordered_size, bordered_size), self.config['bg_color']) |
|
final.paste(bg_for_border, (0, 0), border_mask) |
|
|
|
return final |
|
|
|
def get_font(self, size, bold=False): |
|
"""Get font with fallback options.""" |
|
font_options = [ |
|
# macOS fonts |
|
'/System/Library/Fonts/Supplemental/Arial.ttf', |
|
'/System/Library/Fonts/Supplemental/Arial Bold.ttf' if bold else '/System/Library/Fonts/Supplemental/Arial.ttf', |
|
'/Library/Fonts/Arial.ttf', |
|
# Linux fonts |
|
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf' if bold else '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', |
|
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf' if bold else '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', |
|
# Windows fonts |
|
'C:\\Windows\\Fonts\\arialbd.ttf' if bold else 'C:\\Windows\\Fonts\\arial.ttf', |
|
] |
|
|
|
for font_path in font_options: |
|
if os.path.exists(font_path): |
|
try: |
|
return ImageFont.truetype(font_path, size) |
|
except Exception: |
|
continue |
|
|
|
# Fallback to default font |
|
return ImageFont.load_default() |
|
|
|
def wrap_text(self, text, font, max_width, draw): |
|
"""Wrap text to fit within max_width.""" |
|
words = text.split() |
|
lines = [] |
|
current_line = [] |
|
|
|
for word in words: |
|
test_line = ' '.join(current_line + [word]) |
|
bbox = draw.textbbox((0, 0), test_line, font=font) |
|
width = bbox[2] - bbox[0] |
|
|
|
if width <= max_width: |
|
current_line.append(word) |
|
else: |
|
if current_line: |
|
lines.append(' '.join(current_line)) |
|
current_line = [word] |
|
|
|
if current_line: |
|
lines.append(' '.join(current_line)) |
|
|
|
return lines |
|
|
|
def resize_logo(self, logo): |
|
"""Resize logo to fit within max dimensions while maintaining aspect ratio.""" |
|
max_width = self.config['logo_max_width'] |
|
max_height = self.config['logo_max_height'] |
|
|
|
# Calculate scaling factor |
|
width_ratio = max_width / logo.width |
|
height_ratio = max_height / logo.height |
|
scale = min(width_ratio, height_ratio) |
|
|
|
new_width = int(logo.width * scale) |
|
new_height = int(logo.height * scale) |
|
|
|
return logo.resize((new_width, new_height), Image.Resampling.LANCZOS) |
|
|
|
def generate(self, profile_image, name, title, logo_path, output_path): |
|
"""Generate the placeholder image.""" |
|
# Load and process profile image |
|
profile = self.load_image(profile_image) |
|
circular_profile = self.create_circular_image(profile, self.config['profile_size']) |
|
|
|
# Create canvas |
|
canvas = Image.new('RGB', (self.config['width'], self.config['height']), self.config['bg_color']) |
|
draw = ImageDraw.Draw(canvas) |
|
|
|
# Paste profile image |
|
profile_x = (self.config['width'] - circular_profile.width) // 2 |
|
profile_y = 40 |
|
canvas.paste(circular_profile, (profile_x, profile_y)) |
|
|
|
# Get fonts |
|
name_font = self.get_font(self.config['name_font_size'], bold=True) |
|
title_font = self.get_font(self.config['title_font_size'], bold=False) |
|
|
|
# Calculate positions |
|
current_y = profile_y + circular_profile.height + 30 |
|
|
|
# Draw name |
|
name_bbox = draw.textbbox((0, 0), name, font=name_font) |
|
name_width = name_bbox[2] - name_bbox[0] |
|
name_x = (self.config['width'] - name_width) // 2 |
|
draw.text((name_x, current_y), name, fill=self.config['text_color'], font=name_font) |
|
current_y += (name_bbox[3] - name_bbox[1]) + 20 |
|
|
|
# Draw title (with wrapping if needed) |
|
title_lines = self.wrap_text(title, title_font, self.config['width'] - 60, draw) |
|
for line in title_lines: |
|
line_bbox = draw.textbbox((0, 0), line, font=title_font) |
|
line_width = line_bbox[2] - line_bbox[0] |
|
line_x = (self.config['width'] - line_width) // 2 |
|
draw.text((line_x, current_y), line, fill=self.config['text_color'], font=title_font) |
|
current_y += (line_bbox[3] - line_bbox[1]) + 5 |
|
|
|
# Add separator line |
|
current_y += 30 # Space before separator |
|
separator_width = self.config['separator_width'] |
|
separator_height = self.config['separator_height'] |
|
separator_x1 = (self.config['width'] - separator_width) // 2 |
|
separator_x2 = separator_x1 + separator_width |
|
draw.rectangle( |
|
[separator_x1, current_y, separator_x2, current_y + separator_height], |
|
fill=self.config['separator_color'] |
|
) |
|
current_y += separator_height + 30 # Space after separator |
|
|
|
# Add logo at bottom if provided |
|
if logo_path and os.path.exists(logo_path): |
|
logo = Image.open(logo_path) |
|
if logo.mode != 'RGBA': |
|
logo = logo.convert('RGBA') |
|
|
|
logo = self.resize_logo(logo) |
|
logo_x = (self.config['width'] - logo.width) // 2 |
|
logo_y = current_y |
|
|
|
# Paste with transparency |
|
canvas.paste(logo, (logo_x, logo_y), logo) |
|
|
|
# Save output |
|
canvas.save(output_path, quality=95) |
|
print(f"✓ Placeholder image generated: {output_path}") |
|
|
|
|
|
def main(): |
|
"""Command-line interface for placeholder generator.""" |
|
parser = argparse.ArgumentParser( |
|
description='Generate professional placeholder images with profile photo, name, title, and company logo.', |
|
formatter_class=argparse.RawDescriptionHelpFormatter, |
|
epilog=""" |
|
Examples: |
|
%(prog)s --image photo.jpg --name "John Doe" --title "CEO" --output output.png |
|
%(prog)s --url "https://example.com/photo.jpg" --name "Jane Smith" --title "CTO" --logo logo.png --output output.png |
|
%(prog)s --image photo.jpg --name "Bob" --title "Engineer" --bg-color "#1A1A1A" --text-color "#00FF00" --output output.png |
|
""" |
|
) |
|
|
|
# Required arguments |
|
input_group = parser.add_mutually_exclusive_group(required=True) |
|
input_group.add_argument('--image', help='Path to profile image file') |
|
input_group.add_argument('--url', help='URL to download profile image from') |
|
|
|
parser.add_argument('--name', required=True, help='Name to display') |
|
parser.add_argument('--title', required=True, help='Title/position to display') |
|
parser.add_argument('--output', required=True, help='Output file path') |
|
|
|
# Optional arguments |
|
parser.add_argument('--logo', help='Path to company logo (default: Grubhub Logo.png if exists)') |
|
parser.add_argument('--bg-color', help=f'Background color (default: {PlaceholderGenerator.DEFAULTS["bg_color"]})') |
|
parser.add_argument('--text-color', help=f'Text color (default: {PlaceholderGenerator.DEFAULTS["text_color"]})') |
|
parser.add_argument('--border-color', help=f'Profile border color (default: {PlaceholderGenerator.DEFAULTS["border_color"]})') |
|
parser.add_argument('--border-width', type=int, help=f'Profile border width in pixels (default: {PlaceholderGenerator.DEFAULTS["border_width"]})') |
|
parser.add_argument('--separator-color', help=f'Separator line color (default: {PlaceholderGenerator.DEFAULTS["separator_color"]})') |
|
parser.add_argument('--separator-width', type=int, help=f'Separator line width in pixels (default: {PlaceholderGenerator.DEFAULTS["separator_width"]})') |
|
parser.add_argument('--width', type=int, help=f'Canvas width (default: {PlaceholderGenerator.DEFAULTS["width"]})') |
|
parser.add_argument('--height', type=int, help=f'Canvas height (default: {PlaceholderGenerator.DEFAULTS["height"]})') |
|
|
|
args = parser.parse_args() |
|
|
|
# Prepare configuration |
|
config = {} |
|
if args.bg_color: |
|
config['bg_color'] = args.bg_color |
|
if args.text_color: |
|
config['text_color'] = args.text_color |
|
if args.border_color: |
|
config['border_color'] = args.border_color |
|
if args.border_width: |
|
config['border_width'] = args.border_width |
|
if args.separator_color: |
|
config['separator_color'] = args.separator_color |
|
if args.separator_width: |
|
config['separator_width'] = args.separator_width |
|
if args.width: |
|
config['width'] = args.width |
|
if args.height: |
|
config['height'] = args.height |
|
|
|
# Determine logo path |
|
logo_path = args.logo |
|
if not logo_path: |
|
default_logo = 'Grubhub Logo.png' |
|
if os.path.exists(default_logo): |
|
logo_path = default_logo |
|
|
|
# Generate placeholder |
|
try: |
|
generator = PlaceholderGenerator(**config) |
|
profile_input = args.url if args.url else args.image |
|
generator.generate(profile_input, args.name, args.title, logo_path, args.output) |
|
except Exception as e: |
|
print(f"Error: {e}", file=sys.stderr) |
|
sys.exit(1) |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |