Skip to content

Instantly share code, notes, and snippets.

@thefotes
Created December 9, 2025 20:14
Show Gist options
  • Select an option

  • Save thefotes/2291278f2fd68640d0b238418100e35f to your computer and use it in GitHub Desktop.

Select an option

Save thefotes/2291278f2fd68640d0b238418100e35f to your computer and use it in GitHub Desktop.
Professional Placeholder Image Generator - Python script to generate placeholder images with profile photos, names, titles, and company logos

Placeholder Image Generator

Generate professional placeholder images with profile photos, names, titles, and company logos.

Installation

pip install -r requirements.txt

Usage

Basic Examples

Using a local image file:

python generate_placeholder.py \
  --image photo.jpg \
  --name "Seth Familiar" \
  --title "RVP, Global Advisory Services" \
  --output placeholder.png

Using a URL (e.g., Slack profile):

python generate_placeholder.py \
  --url "https://example.com/profile.jpg" \
  --name "John Doe" \
  --title "Senior Software Engineer" \
  --output output.png

With custom logo:

python generate_placeholder.py \
  --image photo.jpg \
  --name "Jane Smith" \
  --title "CTO" \
  --logo "company-logo.png" \
  --output placeholder.png

Customization Options

Custom colors:

python generate_placeholder.py \
  --image photo.jpg \
  --name "Bob Wilson" \
  --title "Product Manager" \
  --bg-color "#1A1A1A" \
  --text-color "#00FF00" \
  --border-color "#FFD700" \
  --output custom.png

Custom dimensions:

python generate_placeholder.py \
  --image photo.jpg \
  --name "Alice Brown" \
  --title "Designer" \
  --width 800 \
  --height 1000 \
  --output large.png

Options

Required Arguments

  • --image or --url: Profile image (file path or URL)
  • --name: Name to display
  • --title: Title/position to display
  • --output: Output file path

Optional Arguments

  • --logo: Company logo path (default: "Grubhub Logo.png" if exists)
  • --bg-color: Background color (default: #2C2C2C)
  • --text-color: Text color (default: #FFFFFF)
  • --border-color: Profile border color (default: #FFFFFF)
  • --border-width: Profile border width in pixels (default: 1)
  • --separator-color: Separator line color (default: #FFFFFF)
  • --separator-width: Separator line width in pixels (default: 200)
  • --width: Canvas width in pixels (default: 540)
  • --height: Canvas height in pixels (default: 680)

Default Settings

  • Canvas Size: 540x680 pixels
  • Background: Dark gray (#2C2C2C)
  • Text Color: White (#FFFFFF)
  • Border Color: White (#FFFFFF)
  • Border Width: 1px
  • Profile Size: 280px circular image
  • Separator: 200px wide, 1px height white line
  • Logo Size: Max 250x60 pixels
  • Logo: Automatically uses "Grubhub Logo.png" if present

Features

  • Downloads images from URLs (perfect for Slack profiles)
  • Creates circular profile photos with configurable borders (1px default)
  • Automatic text wrapping for long titles
  • Horizontal separator line between title and logo
  • Transparent logo support (PNG with alpha channel)
  • Customizable colors, dimensions, and spacing
  • Cross-platform font support (macOS, Linux, Windows)

Examples

See the included sample for reference output styling.

#!/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()
Pillow>=10.0.0
requests>=2.31.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment