#!/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