File: //home/arjun/projects/aigenerator/AI-LG-backend/Ai_logo_generation/utils/banner_utils.py
from io import BytesIO
import random
from PIL import Image, ImageDraw, ImageFont
def hex_to_rgb(hex_color):
return tuple(int(hex_color.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
def relative_luminance(color):
def channel_luminance(channel):
channel /= 255.0
return (
channel / 12.92
if channel <= 0.03928
else ((channel + 0.055) / 1.055) ** 2.4
)
r, g, b = color
return (
0.2126 * channel_luminance(r)
+ 0.7152 * channel_luminance(g)
+ 0.0722 * channel_luminance(b)
)
def contrast_ratio(color1, color2):
lum1, lum2 = relative_luminance(color1), relative_luminance(color2)
return (max(lum1, lum2) + 0.05) / (min(lum1, lum2) + 0.05)
def ensure_contrast(bg_color, text_color, min_ratio=4.5, max_attempts=10):
"""
Ensures the contrast ratio between background and text color is above a certain threshold.
Limits recursion to avoid infinite loops.
"""
def adjust_color(color, bg_color):
new_color = list(color)
for i in range(3):
if sum(bg_color) / 3 < 128:
new_color[i] = min(255, new_color[i] + 50)
else:
new_color[i] = max(0, new_color[i] - 50)
return tuple(new_color)
attempts = 0
while contrast_ratio(bg_color, text_color) < min_ratio and attempts < max_attempts:
text_color = adjust_color(text_color, bg_color)
attempts += 1
if contrast_ratio(bg_color, text_color) < min_ratio:
# If the color still doesn't meet the contrast ratio after max_attempts, return a safe fallback color
return (255, 255, 255) if sum(bg_color) / 3 < 128 else (0, 0, 0)
return text_color
def truncate_text(draw, text, font, max_width):
"""
Truncate a line of text with ellipses if it exceeds the max width.
Only append ellipses if truncation is needed.
"""
# Check if the text length exceeds the max width
text_width = draw.textlength(text, font=font)
# If it fits within the width, return the text as is
if text_width <= max_width:
return text
# Otherwise, truncate and append ellipses
truncated_text = text
while (
draw.textlength(truncated_text, font=font) > max_width - 3
): # 3 is the width of "..."
truncated_text = truncated_text[:-1] # Remove one character at a time
truncated_text += "..."
return truncated_text
def wrap_slogan(draw, text, font, max_width, max_words_per_line=2, max_lines=4):
"""
Wrap the text within the max width and ensure no more than max_words_per_line words per line.
Truncate with ellipses if the line exceeds the width.
"""
words, lines, line = text.split(), [], []
# Calculate 90% of max width to determine when to start a new line
ninety_percent_width = max_width * 0.9
for word in words:
# If the current line has words, check its width and decide whether to continue adding
line_text = " ".join(line)
line_width = draw.textlength(line_text, font=font)
# Check if adding the word exceeds 90% of max width, start a new line
if line_width + draw.textlength(word, font=font) <= ninety_percent_width:
line.append(word)
else:
# If the line already has words, finalize the line and start a new one with the current word
if line:
lines.append(truncate_text(draw, " ".join(line), font, max_width))
line = [word] # Start new line with the current word
else:
# If the line is empty, just add the word and truncate if needed
line.append(word)
# If the word itself exceeds the max width, truncate it and move to the next line
if draw.textlength(word, font=font) > max_width:
lines.append(truncate_text(draw, word, font, max_width))
line = [] # Start a new line after truncating the word
# Check if the current line exceeds max_words_per_line or max_width
if (
len(line) >= max_words_per_line
or draw.textlength(" ".join(line), font=font) > max_width
):
# Truncate the line if it's too long and add to the lines
truncated_line = truncate_text(draw, " ".join(line), font, max_width)
lines.append(truncated_line)
line = []
# Stop if we've reached the max number of lines
if len(lines) == max_lines:
lines.append("...") # Add ellipsis at the end of the last line
break
# If there are remaining words and we haven't hit the line limit, process them
if line and len(lines) < max_lines:
truncated_line = truncate_text(draw, " ".join(line), font, max_width)
lines.append(truncated_line)
# If we've exceeded the max_lines, truncate the last line with ellipsis
if len(lines) > max_lines:
lines = lines[: max_lines - 1] # Keep up to max_lines - 1
lines.append("...") # Add ellipsis for overflow text
return lines
def wrap_description_paragraphs(
draw, description_lines, font, max_width, max_height, max_lines=3
):
"""
Wrap each line of the description into a paragraph. If the line exceeds the `max_width`,
it will be truncated with ellipses. If the number of lines exceeds `max_lines`,
the excess lines will be truncated with ellipses.
"""
wrapped_lines = []
total_height = 0
for line in description_lines:
words = line.split() # Split the line into words
paragraph = []
for word in words:
paragraph.append(word)
paragraph_text = " ".join(paragraph)
line_width = draw.textlength(paragraph_text, font=font)
# If the line width exceeds the max_width, finalize the current line and start a new one
if line_width > max_width:
# Truncate the line if it's too long
truncated_line = truncate_text(
draw, " ".join(paragraph[:-1]), font, max_width
)
wrapped_lines.append(truncated_line)
paragraph = [word] # Start a new line with the current word
# Finalize the paragraph (add the last part of it)
if paragraph:
wrapped_lines.append(
truncate_text(draw, " ".join(paragraph), font, max_width)
)
# Track total height of the lines
total_height = sum(
[draw.textbbox((0, 0), line, font=font)[3] for line in wrapped_lines]
)
# If we've exceeded max height, truncate the paragraph
if total_height > max_height:
wrapped_lines.append("...")
break
# If the total height exceeds max_height, ensure it doesn't exceed the max_lines and truncate
if len(wrapped_lines) > max_lines:
wrapped_lines = wrapped_lines[: max_lines - 1] # Keep only up to max_lines
wrapped_lines.append(
"..."
) # Add ellipsis to the last line to indicate truncation
return wrapped_lines
def calculate_average_color(image, region=None):
cropped = image.crop(region) if region else image
reduced = cropped.resize((10, 10))
pixels = list(reduced.getdata())
return tuple(sum(p[i] for p in pixels) // len(pixels) for i in range(3))
def draw_text_with_border(
draw, position, text, font, text_color, border_color, border_thickness=2
):
x, y = position
for offset_x in range(-border_thickness, border_thickness + 1):
for offset_y in range(-border_thickness, border_thickness + 1):
if offset_x or offset_y:
draw.text(
(x + offset_x, y + offset_y), text, font=font, fill=border_color
)
draw.text((x, y), text, font=font, fill=text_color)
def create_banner(
background_bytes,
foreground_bytes,
font_bytes,
banner_title,
slogan,
description,
primary_color,
secondary_color,
):
"""
Create a portrait banner image with a background, foreground image, slogan, and description.
"""
# Initialize images (with portrait orientation)
banner_width, banner_height = 900, 1200
background_image = Image.open(BytesIO(background_bytes)).resize(
(banner_width, banner_height)
)
foreground_image = Image.open(BytesIO(foreground_bytes))
# Resize foreground image
scale_factor = min(
banner_width / foreground_image.width, banner_height / foreground_image.height
)
foreground_image = foreground_image.resize(
(
int(foreground_image.width * scale_factor),
int(foreground_image.height * scale_factor),
)
)
# Resize the image to fit within the banner dimensions
scale_factor = min(
banner_width / foreground_image.width, banner_height / foreground_image.height
)
foreground_image = foreground_image.resize(
(
int(foreground_image.width * scale_factor),
int(foreground_image.height * scale_factor),
)
)
# Position foreground image horizontally (left, right, or center)
placement = random.choice(["left", "right", "center"])
# Crop the image to 60% of its width based on placement
if placement == "left":
foreground_image = foreground_image.crop(
(
int(foreground_image.width * 0.6),
0,
foreground_image.width,
foreground_image.height,
)
)
elif placement == "right":
foreground_image = foreground_image.crop(
(0, 0, int(foreground_image.width * 0.4), foreground_image.height)
)
# Determine the x-coordinate based on the placement
if placement == "left":
fg_x = 0 # Place on the left edge
elif placement == "right":
fg_x = banner_width - foreground_image.width # Place on the right edge
else: # 'center'
fg_x = (banner_width - foreground_image.width) // 2 # Center horizontally
# Always place the foreground image at the top (fixed y-coordinate)
fg_y = 20 # Place at the top with some padding
# Merge foreground with background
transparent_image = Image.new("RGBA", (banner_width, banner_height), (0, 0, 0, 0))
transparent_image.paste(foreground_image, (fg_x, fg_y))
background_image = Image.alpha_composite(
background_image.convert("RGBA"), transparent_image
)
# Colors and contrast adjustments
bg_color = background_image.resize((1, 1)).getpixel((0, 0))[:3]
primary_color = ensure_contrast(
bg_color,
hex_to_rgb(primary_color) if isinstance(primary_color, str) else primary_color,
)
secondary_color = ensure_contrast(
bg_color,
(
hex_to_rgb(secondary_color)
if isinstance(secondary_color, str)
else secondary_color
),
)
border_color = calculate_average_color(
background_image,
(
banner_width // 4,
banner_height // 4,
banner_width * 3 // 4,
banner_height * 3 // 4,
),
)
draw = ImageDraw.Draw(background_image)
try:
title_font = ImageFont.truetype(BytesIO(font_bytes), banner_height // 15)
except Exception:
title_font = ImageFont.load_default(banner_height // 15)
title_lines = wrap_slogan(
draw, banner_title, title_font, banner_width // 1.2, max_lines=2
)
total_title_height = sum(
draw.textbbox((0, 0), line, font=title_font)[3] for line in title_lines
) + 10 * (len(title_lines) - 1)
title_y = (
(banner_height // 3 - total_title_height // 2)
if slogan or description
else (banner_height // 2 - total_title_height // 2)
)
title_x = banner_width // 2
for line in title_lines:
line_width = draw.textlength(line, font=title_font)
draw_text_with_border(
draw,
(title_x - line_width // 2, title_y),
line,
title_font,
primary_color,
border_color,
)
title_y += title_font.size + 10
# Handle slogan
if slogan:
try:
# Load a bold font if possible (replace with an actual bold font path if necessary)
slogan_font = ImageFont.truetype(BytesIO(font_bytes), banner_height // 21)
except Exception:
slogan_font = ImageFont.load_default(banner_height // 21)
# Capitalize the slogan text before wrapping it
capitalized_slogan = slogan.upper()
# Wrap the capitalized and bold slogan text
slogan_lines = wrap_slogan(
draw,
capitalized_slogan,
slogan_font,
banner_width // 1.2,
max_words_per_line=4,
max_lines=4,
)
slogan_y = title_y + 60
slogan_x = banner_width // 2 # Center horizontally
for line in slogan_lines:
line_width = draw.textlength(line, font=slogan_font)
draw_text_with_border(
draw,
(slogan_x - line_width // 2, slogan_y),
line,
slogan_font,
primary_color,
border_color,
)
slogan_y += slogan_font.size + 10
# Handle description
if description:
try:
description_font = ImageFont.truetype(
BytesIO(font_bytes), banner_height // 36
)
except Exception:
description_font = ImageFont.load_default(banner_height // 36)
description_lines = wrap_description_paragraphs(
draw,
description,
description_font,
banner_width // 1.2,
banner_height,
max_lines=5,
)
if slogan:
desc_y = slogan_y + 20
else:
desc_y = banner_height // 2
desc_x = banner_width // 2 # Center horizontally
for line in description_lines:
line_width = draw.textlength(line, font=description_font)
draw_text_with_border(
draw,
(desc_x - line_width // 2, desc_y),
line,
description_font,
secondary_color,
border_color,
)
desc_y += description_font.size + 5
return background_image