From e7335b98b04c4b8462440a1fac26cef6ebcc0f65 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Thu, 13 Mar 2025 06:39:54 +0530 Subject: [PATCH] improve title splitting and font adjustment logic for better text fitting in poster image generation --- utils/poster.py | 124 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/utils/poster.py b/utils/poster.py index 482b9a7..71b5bc9 100644 --- a/utils/poster.py +++ b/utils/poster.py @@ -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