Files
chenpipi0807-PIP_ArtisticWords/core/text_renderer.py
T
2025-03-09 09:09:17 +08:00

273 lines
11 KiB
Python

import os
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
import random
class TextRenderer:
"""Handles text rendering with various effects."""
def __init__(self, font_path, font_size=100, **kwargs):
"""
Initialize the text renderer.
Args:
font_path: Path to the font file
font_size: Font size
"""
self.font_path = font_path
self.font_size = font_size
# 添加错误处理和详细日志
try:
print(f"[TextRenderer] 尝试加载字体: {font_path}")
# 检查字体文件是否存在
if not os.path.exists(font_path):
print(f"[TextRenderer] 错误: 字体文件不存在: {font_path}")
# 尝试使用默认字体
self.font = ImageFont.load_default()
print("[TextRenderer] 已加载默认字体作为替代")
else:
self.font = ImageFont.truetype(font_path, font_size)
print(f"[TextRenderer] 字体加载成功: {font_path}")
except Exception as e:
print(f"[TextRenderer] 加载字体时出错: {e}")
# 使用默认字体作为备选
self.font = ImageFont.load_default()
print("[TextRenderer] 已加载默认字体作为替代")
def apply_smart_line_breaks(self, text):
"""
Apply smart line breaks based on number of words.
Args:
text: Text to process
Returns:
Processed text with line breaks
"""
# Split text into words
words = text.split()
word_count = len(words)
# Apply smart line breaks based on word count
if word_count <= 2:
# 1-2 words: No line breaks
return text
elif word_count <= 4:
# 3-4 words: Split into 2 lines
mid = word_count // 2
return ' '.join(words[:mid]) + '\n' + ' '.join(words[mid:])
elif word_count <= 6:
# 5-6 words: Split into 3 lines
line_words = word_count // 3
return ' '.join(words[:line_words]) + '\n' + ' '.join(words[line_words:2*line_words]) + '\n' + ' '.join(words[2*line_words:])
else:
# 7+ words: Split into 4 lines max
lines = []
line_count = min(4, (word_count + 1) // 2)
words_per_line = word_count // line_count
for i in range(line_count - 1):
start_idx = i * words_per_line
end_idx = (i + 1) * words_per_line
lines.append(' '.join(words[start_idx:end_idx]))
# Add remaining words to the last line
lines.append(' '.join(words[(line_count - 1) * words_per_line:]))
return '\n'.join(lines)
def find_optimal_font_size(self, text, max_width, max_height, start_size=10, max_size=300):
"""
Find the optimal font size to fit text within constraints.
Args:
text: Text to measure
max_width: Maximum allowed width
max_height: Maximum allowed height
start_size: Starting font size
max_size: Maximum font size
Returns:
Optimal font size
"""
low, high = start_size, max_size
optimal_size = start_size
# 检查font_path是否有效,如果无效使用默认字体
use_default_font = False
if not self.font_path or not os.path.exists(self.font_path):
print(f"[TextRenderer] 警告: 在find_optimal_font_size中找不到字体 {self.font_path},将使用默认字体")
use_default_font = True
while low <= high:
mid = (low + high) // 2
try:
# 根据字体可用情况选择字体
if use_default_font:
font = ImageFont.load_default()
else:
font = ImageFont.truetype(self.font_path, mid)
left, top, right, bottom = font.getbbox(text)
width, height = right - left, bottom - top
# Consider multiline text
if '\n' in text:
lines = text.split('\n')
height = 0
width = 0
line_spacing = mid * 0.2 # 20% of font size for line spacing
for line in lines:
left, top, right, bottom = font.getbbox(line)
line_width, line_height = right - left, bottom - top
width = max(width, line_width)
height += line_height
# Add line spacing
height += line_spacing * (len(lines) - 1)
if width <= max_width and height <= max_height:
optimal_size = mid
low = mid + 1
else:
high = mid - 1
except Exception as e:
print(f"[TextRenderer] 在字体大小调整过程中出错: {e}")
# 出错时降低尝试字号
high = mid - 1
return optimal_size
def create_base_text_image(self, text, style, width, height, fit_text=True, safe_area=None):
"""
Create a base text image with the given style.
Args:
text: Text to render
style: Style dictionary
width: Image width
height: Image height
fit_text: Whether to fit text to safe area
safe_area: Safe area bounds (x0, y0, x1, y1) or None
Returns:
PIL.Image with rendered text
"""
# 添加调试信息
print(f"[TextRenderer] 创建文本图像: 尺寸={width}x{height}, 安全区域={safe_area}")
# Create a transparent base image
base_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(base_img)
# Apply smart line breaks
processed_text = self.apply_smart_line_breaks(text)
# Calculate font size if fitting to safe area
if fit_text and safe_area:
x0, y0, x1, y1 = safe_area
safe_width = abs(x1 - x0)
safe_height = abs(y1 - y0)
print(f"[TextRenderer] 安全区域大小: {safe_width}x{safe_height}")
optimal_size = self.find_optimal_font_size(processed_text, safe_width, safe_height)
print(f"[TextRenderer] 计算的最佳字体大小: {optimal_size}")
self.font_size = optimal_size
# 当字体大小改变后,需要重新加载字体
try:
if not self.font_path or not os.path.exists(self.font_path):
# 如果找不到字体文件,使用默认字体
self.font = ImageFont.load_default()
else:
self.font = ImageFont.truetype(self.font_path, self.font_size)
print(f"[TextRenderer] 重新加载字体,大小为 {self.font_size}")
except Exception as e:
print(f"[TextRenderer] 重新加载字体时出错: {e}")
self.font = ImageFont.load_default()
# Get base color from style
text_color = (255, 255, 255, 255) # Default white
if style and 'text_color' in style:
text_color = self.hex_to_rgba(style['text_color'])
# 使用fontbbox信息获取文本位置
# Calculate text position to center it
try:
# 获取文本尺寸 - 使用适合多行文本的方法
if '\n' in processed_text:
lines = processed_text.split('\n')
text_width = 0
text_height = 0
# 计算所有行的总高度和最大宽度
for line in lines:
bbox = self.font.getbbox(line)
line_width = bbox[2] - bbox[0]
line_height = bbox[3] - bbox[1]
text_width = max(text_width, line_width)
text_height += line_height
# 添加行间距
line_spacing = int(self.font_size * 0.2) # 20% of font size
text_height += line_spacing * (len(lines) - 1)
else:
bbox = self.font.getbbox(processed_text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
print(f"[TextRenderer] 文本尺寸: {text_width}x{text_height}")
# Center text in safe area or whole image
if safe_area:
x0, y0, x1, y1 = safe_area
x = x0 + (safe_width - text_width) // 2
y = y0 + (safe_height - text_height) // 2
print(f"[TextRenderer] 文本位置: ({x}, {y}) (在安全区域内)")
else:
x = (width - text_width) // 2
y = (height - text_height) // 2
print(f"[TextRenderer] 文本位置: ({x}, {y}) (全图居中)")
# 绘制多行文本
if '\n' in processed_text:
y_pos = y
for line in lines:
bbox = self.font.getbbox(line)
line_width = bbox[2] - bbox[0]
line_height = bbox[3] - bbox[1]
# 计算行的X坐标(居中对齐)
line_x = x + (text_width - line_width) // 2
# 绘制当前行
draw.text((line_x, y_pos), line, fill=text_color, font=self.font)
# 更新Y坐标
y_pos += line_height + line_spacing
else:
# 绘制单行文本
draw.text((x, y), processed_text, fill=text_color, font=self.font)
except Exception as e:
print(f"[TextRenderer] 绘制文本时出错: {e}")
# 出错时使用简单方法绘制
draw.text((width//4, height//4), processed_text, fill=text_color, font=self.font)
return base_img
def hex_to_rgba(self, hex_color, alpha=255):
"""Convert hex color to RGBA tuple."""
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
return (r, g, b, alpha)
elif len(hex_color) == 8:
r, g, b, a = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4, 6))
return (r, g, b, a)
return (255, 255, 255, alpha) # Default white if invalid