- 新增 PosterSmartEngine,AI 生成文案 + 海报渲染 - 5 种布局支持文本换行和自适应字体 - 修复按钮/标签颜色显示问题 - 优化渐变遮罩和内容区域计算 - Prompt 优化:标题格式为产品名+描述
185 lines
5.4 KiB
Python
185 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
文字渲染器
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Tuple, Optional, Union
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
from ..config import get_font_path, FONT_FILES
|
|
|
|
|
|
class TextRenderer:
|
|
"""文字渲染器"""
|
|
|
|
_font_cache = {}
|
|
|
|
@classmethod
|
|
def load_font(cls, font_name: str, size: int) -> ImageFont.FreeTypeFont:
|
|
"""加载字体 (带缓存)"""
|
|
cache_key = (font_name, size)
|
|
if cache_key not in cls._font_cache:
|
|
font_path = get_font_path(font_name)
|
|
try:
|
|
cls._font_cache[cache_key] = ImageFont.truetype(str(font_path), size)
|
|
except Exception:
|
|
cls._font_cache[cache_key] = ImageFont.load_default()
|
|
return cls._font_cache[cache_key]
|
|
|
|
@staticmethod
|
|
def draw_text(
|
|
draw: ImageDraw.ImageDraw,
|
|
pos: Tuple[int, int],
|
|
text: str,
|
|
font: ImageFont.FreeTypeFont,
|
|
fill: Union[str, Tuple],
|
|
anchor: str = "lt"
|
|
) -> Tuple[int, int]:
|
|
"""
|
|
绘制文字
|
|
|
|
Returns:
|
|
(width, height) 文字尺寸
|
|
"""
|
|
draw.text(pos, text, font=font, fill=fill, anchor=anchor)
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
@staticmethod
|
|
def draw_text_with_shadow(
|
|
draw: ImageDraw.ImageDraw,
|
|
pos: Tuple[int, int],
|
|
text: str,
|
|
font: ImageFont.FreeTypeFont,
|
|
fill: Union[str, Tuple],
|
|
shadow_color: Tuple[int, int, int, int] = (0, 0, 0, 60),
|
|
offset: Tuple[int, int] = (2, 2)
|
|
) -> Tuple[int, int]:
|
|
"""绘制带阴影的文字"""
|
|
x, y = pos
|
|
# 阴影
|
|
draw.text((x + offset[0], y + offset[1]), text, font=font, fill=shadow_color)
|
|
# 主文字
|
|
draw.text((x, y), text, font=font, fill=fill)
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
@staticmethod
|
|
def draw_emoji(
|
|
draw: ImageDraw.ImageDraw,
|
|
pos: Tuple[int, int],
|
|
emoji: str,
|
|
size: int = 109
|
|
) -> Tuple[int, int]:
|
|
"""绘制 emoji"""
|
|
font = ImageFont.truetype(str(get_font_path("emoji")), size)
|
|
draw.text(pos, emoji, font=font, embedded_color=True)
|
|
return size, size
|
|
|
|
@staticmethod
|
|
def measure_text(
|
|
text: str,
|
|
font: ImageFont.FreeTypeFont
|
|
) -> Tuple[int, int]:
|
|
"""测量文字尺寸"""
|
|
temp = Image.new("RGBA", (1, 1))
|
|
draw = ImageDraw.Draw(temp)
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
@classmethod
|
|
def get_title_font(cls, size: int = 80) -> ImageFont.FreeTypeFont:
|
|
"""获取标题字体"""
|
|
return cls.load_font("title_bold", size)
|
|
|
|
@classmethod
|
|
def get_body_font(cls, size: int = 28) -> ImageFont.FreeTypeFont:
|
|
"""获取正文字体"""
|
|
return cls.load_font("body_regular", size)
|
|
|
|
@classmethod
|
|
def get_adaptive_title_font(cls, text: str, max_width: int,
|
|
base_size: int = 96, min_size: int = 48) -> ImageFont.FreeTypeFont:
|
|
"""
|
|
获取自适应大小的标题字体
|
|
|
|
根据文本长度和最大宽度,自动调整字体大小
|
|
"""
|
|
size = base_size
|
|
while size >= min_size:
|
|
font = cls.load_font("title_bold", size)
|
|
w, _ = cls.measure_text(text, font)
|
|
if w <= max_width:
|
|
return font
|
|
size -= 4
|
|
return cls.load_font("title_bold", min_size)
|
|
|
|
@classmethod
|
|
def wrap_text(cls, text: str, font: ImageFont.FreeTypeFont, max_width: int) -> list:
|
|
"""
|
|
文本自动换行
|
|
|
|
Args:
|
|
text: 原始文本
|
|
font: 字体
|
|
max_width: 最大宽度
|
|
|
|
Returns:
|
|
换行后的文本行列表
|
|
"""
|
|
if not text:
|
|
return []
|
|
|
|
lines = []
|
|
current_line = ""
|
|
|
|
for char in text:
|
|
test_line = current_line + char
|
|
w, _ = cls.measure_text(test_line, font)
|
|
|
|
if w <= max_width:
|
|
current_line = test_line
|
|
else:
|
|
if current_line:
|
|
lines.append(current_line)
|
|
current_line = char
|
|
|
|
if current_line:
|
|
lines.append(current_line)
|
|
|
|
return lines
|
|
|
|
@classmethod
|
|
def draw_wrapped_text(
|
|
cls,
|
|
draw: ImageDraw.ImageDraw,
|
|
pos: Tuple[int, int],
|
|
text: str,
|
|
font: ImageFont.FreeTypeFont,
|
|
fill: Union[str, Tuple],
|
|
max_width: int,
|
|
line_spacing: int = 8
|
|
) -> Tuple[int, int]:
|
|
"""
|
|
绘制自动换行的文本
|
|
|
|
Returns:
|
|
(总宽度, 总高度)
|
|
"""
|
|
lines = cls.wrap_text(text, font, max_width)
|
|
|
|
x, y = pos
|
|
total_height = 0
|
|
max_line_width = 0
|
|
|
|
for line in lines:
|
|
w, h = cls.measure_text(line, font)
|
|
draw.text((x, y), line, font=font, fill=fill)
|
|
y += h + line_spacing
|
|
total_height += h + line_spacing
|
|
max_line_width = max(max_line_width, w)
|
|
|
|
return max_line_width, total_height - line_spacing if lines else 0
|