chore: sync code and project files
This commit is contained in:
@@ -90,6 +90,79 @@ class TextRenderer:
|
||||
return color
|
||||
return (0, 0, 0, 255)
|
||||
|
||||
def _wrap_text_to_width(self, text: str, font: ImageFont.FreeTypeFont, max_width: int) -> str:
|
||||
"""
|
||||
将文本按最大宽度自动换行(支持中英文混排)。
|
||||
- 保留原始换行符为段落边界
|
||||
- 英文优先按空格断词;中文按字符贪心换行
|
||||
"""
|
||||
try:
|
||||
mw = int(max_width or 0)
|
||||
except Exception:
|
||||
mw = 0
|
||||
if mw <= 0:
|
||||
return text
|
||||
|
||||
# 兼容:去掉末尾多余空行
|
||||
raw_paras = (text or "").split("\n")
|
||||
out_lines: List[str] = []
|
||||
|
||||
# 1x1 dummy draw 用于测量
|
||||
dummy_draw = ImageDraw.Draw(Image.new("RGBA", (1, 1)))
|
||||
|
||||
def text_w(s: str) -> float:
|
||||
try:
|
||||
return float(dummy_draw.textlength(s, font=font))
|
||||
except Exception:
|
||||
bbox = dummy_draw.textbbox((0, 0), s, font=font)
|
||||
return float((bbox[2] - bbox[0]) if bbox else 0)
|
||||
|
||||
for para in raw_paras:
|
||||
p = (para or "").rstrip()
|
||||
if not p:
|
||||
out_lines.append("")
|
||||
continue
|
||||
|
||||
# 英文/混排:尝试按空格分词,否则按字符
|
||||
use_words = (" " in p)
|
||||
tokens = p.split(" ") if use_words else list(p)
|
||||
|
||||
cur = ""
|
||||
for tok in tokens:
|
||||
cand = (cur + (" " if (use_words and cur) else "") + tok) if cur else tok
|
||||
if text_w(cand) <= mw:
|
||||
cur = cand
|
||||
continue
|
||||
|
||||
# 当前行放不下:先落一行
|
||||
if cur:
|
||||
out_lines.append(cur)
|
||||
cur = tok
|
||||
else:
|
||||
# 单 token 超宽:强制按字符拆
|
||||
if use_words:
|
||||
chars = list(tok)
|
||||
else:
|
||||
chars = [tok]
|
||||
buf = ""
|
||||
for ch in chars:
|
||||
cand2 = buf + ch
|
||||
if text_w(cand2) <= mw or not buf:
|
||||
buf = cand2
|
||||
else:
|
||||
out_lines.append(buf)
|
||||
buf = ch
|
||||
cur = buf
|
||||
|
||||
if cur:
|
||||
out_lines.append(cur)
|
||||
|
||||
# 去掉尾部空行(保持中间空行)
|
||||
while out_lines and out_lines[-1] == "":
|
||||
out_lines.pop()
|
||||
|
||||
return "\n".join(out_lines)
|
||||
|
||||
def render(self, text: str, style: Union[Dict[str, Any], str], cache: bool = True) -> str:
|
||||
"""
|
||||
渲染文本并返回图片路径
|
||||
@@ -122,14 +195,29 @@ class TextRenderer:
|
||||
font_size = style.get("font_size", 60)
|
||||
font = self._get_font(font_path, font_size)
|
||||
font_color = self._parse_color(style.get("font_color", "#FFFFFF"))
|
||||
bold = bool(style.get("bold", False))
|
||||
italic = bool(style.get("italic", False))
|
||||
underline = bool(style.get("underline", False))
|
||||
|
||||
# 3. 测量文本尺寸
|
||||
# 3. 自动换行(可选)
|
||||
max_width = style.get("max_width") or style.get("maxWidth") or style.get("text_box_width")
|
||||
try:
|
||||
max_width = int(max_width) if max_width is not None else 0
|
||||
except Exception:
|
||||
max_width = 0
|
||||
if max_width > 0:
|
||||
text = self._wrap_text_to_width(text, font, max_width)
|
||||
|
||||
# 4. 测量文本尺寸(支持多行)
|
||||
dummy_draw = ImageDraw.Draw(Image.new("RGBA", (1, 1)))
|
||||
bbox = dummy_draw.textbbox((0, 0), text, font=font)
|
||||
text_w = bbox[2] - bbox[0]
|
||||
text_h = bbox[3] - bbox[1]
|
||||
try:
|
||||
bbox = dummy_draw.multiline_textbbox((0, 0), text, font=font, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
bbox = dummy_draw.textbbox((0, 0), text, font=font)
|
||||
text_w = (bbox[2] - bbox[0]) if bbox else 0
|
||||
text_h = (bbox[3] - bbox[1]) if bbox else 0
|
||||
|
||||
# 4. 计算总尺寸 (包含 padding, stroke, shadow)
|
||||
# 5. 计算总尺寸 (包含 padding, stroke, shadow)
|
||||
strokes = style.get("stroke", [])
|
||||
if isinstance(strokes, dict): strokes = [strokes] # 兼容旧格式
|
||||
|
||||
@@ -156,7 +244,7 @@ class TextRenderer:
|
||||
canvas_w = content_w + extra_margin * 2
|
||||
canvas_h = content_h + extra_margin * 2
|
||||
|
||||
# 5. 创建画布
|
||||
# 6. 创建画布
|
||||
img = Image.new("RGBA", (int(canvas_w), int(canvas_h)), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
@@ -164,7 +252,7 @@ class TextRenderer:
|
||||
center_x = canvas_w // 2
|
||||
center_y = canvas_h // 2
|
||||
|
||||
# 6. 绘制顺序: 阴影 -> 背景 -> 描边 -> 文本
|
||||
# 7. 绘制顺序: 阴影 -> 背景 -> 描边 -> 文本
|
||||
|
||||
# --- 绘制阴影 (针对整个块) ---
|
||||
if shadow:
|
||||
@@ -183,7 +271,11 @@ class TextRenderer:
|
||||
# 文字阴影
|
||||
txt_x = center_x - text_w / 2
|
||||
txt_y = center_y - text_h / 2
|
||||
shadow_draw.text((txt_x, txt_y), text, font=font, fill=shadow_color)
|
||||
# 多行阴影
|
||||
try:
|
||||
shadow_draw.multiline_text((txt_x, txt_y), text, font=font, fill=shadow_color, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
shadow_draw.text((txt_x, txt_y), text, font=font, fill=shadow_color)
|
||||
# 描边阴影
|
||||
for s in strokes:
|
||||
width = s.get("width", 0)
|
||||
@@ -217,10 +309,55 @@ class TextRenderer:
|
||||
width = s.get("width", 0)
|
||||
if width > 0:
|
||||
# 通过偏移模拟描边 (Pillow stroke_width 效果一般,但这里先用原生参数)
|
||||
draw.text((txt_x, txt_y), text, font=font, fill=color, stroke_width=width, stroke_fill=color)
|
||||
try:
|
||||
draw.multiline_text((txt_x, txt_y), text, font=font, fill=color, spacing=int(font_size * 0.25), align="center", stroke_width=width, stroke_fill=color)
|
||||
except Exception:
|
||||
draw.text((txt_x, txt_y), text, font=font, fill=color, stroke_width=width, stroke_fill=color)
|
||||
|
||||
# --- 绘制文字 ---
|
||||
draw.text((txt_x, txt_y), text, font=font, fill=font_color)
|
||||
# italic:通过仿射变换做简单斜体(先绘制到单独图层,再 shear)
|
||||
# bold:通过多次微小偏移叠加模拟加粗(比改 stroke 更接近“字重”)
|
||||
if italic:
|
||||
text_layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
text_draw = ImageDraw.Draw(text_layer)
|
||||
if bold:
|
||||
for dx in (0, 1):
|
||||
try:
|
||||
text_draw.multiline_text((txt_x + dx, txt_y), text, font=font, fill=font_color, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
text_draw.text((txt_x + dx, txt_y), text, font=font, fill=font_color)
|
||||
else:
|
||||
try:
|
||||
text_draw.multiline_text((txt_x, txt_y), text, font=font, fill=font_color, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
text_draw.text((txt_x, txt_y), text, font=font, fill=font_color)
|
||||
shear = 0.22 # 经验值:适中倾斜
|
||||
text_layer = text_layer.transform(
|
||||
text_layer.size,
|
||||
Image.AFFINE,
|
||||
(1, shear, 0, 0, 1, 0),
|
||||
resample=Image.BICUBIC
|
||||
)
|
||||
img = Image.alpha_composite(img, text_layer)
|
||||
draw = ImageDraw.Draw(img)
|
||||
else:
|
||||
if bold:
|
||||
for dx in (0, 1):
|
||||
try:
|
||||
draw.multiline_text((txt_x + dx, txt_y), text, font=font, fill=font_color, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
draw.text((txt_x + dx, txt_y), text, font=font, fill=font_color)
|
||||
else:
|
||||
try:
|
||||
draw.multiline_text((txt_x, txt_y), text, font=font, fill=font_color, spacing=int(font_size * 0.25), align="center")
|
||||
except Exception:
|
||||
draw.text((txt_x, txt_y), text, font=font, fill=font_color)
|
||||
|
||||
# underline:在文本底部画线(与字号相关)
|
||||
if underline:
|
||||
line_y = txt_y + text_h + max(2, int(font_size * 0.08))
|
||||
line_th = max(2, int(font_size * 0.06))
|
||||
draw.rectangle([txt_x, line_y, txt_x + text_w, line_y + line_th], fill=font_color)
|
||||
|
||||
# 7. 裁剪多余透明区域
|
||||
bbox = img.getbbox()
|
||||
|
||||
Reference in New Issue
Block a user