improve title splitting and font adjustment logic for better text fitting in poster image generation

This commit is contained in:
mhdzumair
2025-03-13 06:39:54 +05:30
parent 72d7f695a1
commit e7335b98b0
+86 -38
View File
@@ -157,58 +157,99 @@ def load_font(font_path, font_size):
# Function to split the title into multiple lines
def split_title(title, font, draw, max_width):
lines = []
words = title.split()
# Calculate character width using a cache or default method
if not words:
return []
# Calculate character width for common characters
char_widths = {
char: draw.textbbox((0, 0), char, font=font)[2] for char in set(title)
}
average_character_width = sum(char_widths.values()) / len(char_widths)
average_character_width = (
sum(char_widths.values()) / len(char_widths) if char_widths else 10
)
lines = []
current_line = []
current_width = 0
for word in words:
# Calculate word width more accurately
word_width = draw.textbbox((0, 0), word, font=font)[2]
# If this is the first word on the line or the line is still empty
if not current_line:
current_line.append(word)
current_width = word_width
continue
# Test if adding this word exceeds the max width
space_width = char_widths.get(" ", average_character_width)
if current_width + space_width + word_width <= max_width:
current_line.append(word)
current_width += space_width + word_width
else:
# Complete current line and start a new one
lines.append(" ".join(current_line))
current_line = [word]
current_width = word_width
# Add the last line if it's not empty
if current_line:
lines.append(" ".join(current_line))
while words:
line = ""
line_width = 0
while words:
word = words[0]
word_width = sum(
char_widths.get(char, average_character_width) for char in word
)
if line_width + word_width <= max_width or not line:
line += word + " "
line_width += word_width + average_character_width
words.pop(0)
else:
break
lines.append(line.strip())
return lines
# Function to adjust font size and split title
def adjust_font_and_split(
title, font_path, max_width, max_lines, initial_font_size, draw, min_font_size=10
):
lower_bound = min_font_size
upper_bound = initial_font_size
best_fit_font = None
best_fit_lines = []
# Start with the largest font size and try to fit within max_lines
# If that doesn't work, reduce font size
while lower_bound <= upper_bound:
mid_font_size = (lower_bound + upper_bound) // 2
font = load_font(font_path, mid_font_size)
for font_size in range(initial_font_size, min_font_size - 1, -1):
font = load_font(font_path, font_size)
lines = split_title(title, font, draw, max_width)
# Check if the current font size fits the criteria
all_lines_fit = all(
draw.textbbox((0, 0), line, font=font)[2] <= max_width for line in lines
)
if len(lines) <= max_lines and all_lines_fit:
best_fit_font = font
best_fit_lines = lines
lower_bound = mid_font_size + 1 # Try a larger font size
else:
upper_bound = mid_font_size - 1 # Reduce font size and try again
# Skip empty results (shouldn't happen with improved split_title)
if not lines:
continue
return best_fit_lines, best_fit_font
# If we can fit all lines within max_lines, we're done
if len(lines) <= max_lines:
return lines, font
# If we get here, even the smallest font size couldn't fit within max_lines
# So we'll use the smallest font and truncate/adjust as necessary
font = load_font(font_path, min_font_size)
lines = split_title(title, font, draw, max_width)
# If we still have too many lines, truncate and add ellipsis to the last line
if len(lines) > max_lines:
truncated_lines = lines[
: max_lines - 1
] # Leave room for last line with ellipsis
# Handle the last line: try to add ellipsis
last_line = lines[max_lines - 1]
# Keep removing words until we can fit "..." at the end
words = last_line.split()
for i in range(len(words), 0, -1):
partial_line = " ".join(words[:i])
with_ellipsis = partial_line + "..."
if draw.textbbox((0, 0), with_ellipsis, font=font)[2] <= max_width:
truncated_lines.append(with_ellipsis)
break
# If even a single word with ellipsis doesn't fit, just use ellipsis
if len(truncated_lines) < max_lines:
truncated_lines.append("...")
lines = truncated_lines
# Guaranteed to return something useful
return lines, font
def get_average_color(image, bbox):
@@ -255,6 +296,7 @@ def add_title_to_poster(image: Image.Image, title_text: str) -> Image.Image:
initial_font_size = 50 # Starting font size which will be adjusted dynamically
min_font_size = 20 # Minimum font size to avoid tiny text
# Use our improved function that guarantees a result
lines, font = adjust_font_and_split(
title_text,
font_path,
@@ -265,7 +307,13 @@ def add_title_to_poster(image: Image.Image, title_text: str) -> Image.Image:
min_font_size,
)
# Calculate the total height of the text block using textbbox and consider spacing between lines
# If no lines or font returned (shouldn't happen with improved function), use fallback
if not lines or not font:
# Fallback to a very simple approach
font = load_font(font_path, min_font_size)
lines = [title_text[:20] + "..."] if len(title_text) > 20 else [title_text]
# Calculate the total height of the text block
line_spacing = 10 # Additional space between lines
text_block_height = sum(
draw.textbbox((0, 0), line, font=font)[3] for line in lines