main
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3import string
  4from pathlib import Path
  5
  6from loguru import logger
  7from PIL import Image, ImageDraw
  8from PIL.ImageFont import FreeTypeFont
  9from pyrogram.client import Client
 10from pyuegc import EGC
 11
 12from config import DOWNLOAD_DIR
 13from quotly.fonts import EMOJI_CMAP, MATH_CMAP, SYMBOL_CMAP, TEXT_CMAP
 14
 15
 16async def download_avatar(client: Client, avatar_id: str) -> str:
 17    save_path = Path(DOWNLOAD_DIR) / f"avatars/{avatar_id}.jpg"
 18    save_path.parent.mkdir(exist_ok=True, parents=True)
 19    if not save_path.exists():
 20        avatar_path: str = await client.download_media(avatar_id, save_path.as_posix())  # type: ignore
 21    else:
 22        avatar_path = save_path.as_posix()
 23    return avatar_path
 24
 25
 26def get_name_color(uid: int = 0) -> str:
 27    color = [
 28        "#FF8E86",  #  red
 29        "#FFA357",  #  orange
 30        "#B18FFF",  #  purple
 31        "#4DD6BF",  #  green
 32        "#45E8D1",  #  sea
 33        "#7AC9FF",  #  blue
 34        "#FF7FD5",  #  pink
 35    ]
 36    index = abs(uid) % 7
 37    return color[index]
 38
 39
 40def split_lines(
 41    text: str,
 42    max_width: int,
 43    text_font: FreeTypeFont,
 44    emoji_font: FreeTypeFont,
 45    math_font: FreeTypeFont,
 46    symbol_font: FreeTypeFont,
 47) -> list[str]:
 48    """Split text into lines with max width.
 49
 50    This code was originally generated by gemini-2.5-pro
 51    """
 52    # 最终所有处理好的行将存放在这里
 53    final_lines = []
 54
 55    # 步骤1: 首先处理原始文本中的强制换行符
 56    paragraphs = text.split("\n")
 57
 58    for i, paragraph in enumerate(paragraphs):
 59        if not paragraph:  # 段落为空, 说明是连续的换行符,保留一个空行
 60            # 在前一个段落非空的情况下, 才添加空行, 避免开头就有空行
 61            if i > 0 and paragraphs[i - 1]:
 62                final_lines.append("")
 63            continue
 64
 65        # 步骤2: 智能分词, 先使用EGC将文本按Unicode码点分组
 66        # 然后将连续的英文/数字/下划线/连字符视为一个整体(一个单词),
 67        # 而其他所有字符(如中文、空格、标点)都视为单个单位。
 68        tokens = []
 69        unicode_list = EGC(paragraph)  # 按照Unicode码点分组
 70        cur = ""
 71        for unicode in unicode_list:
 72            if unicode in string.ascii_letters + string.digits + "-_":
 73                cur += unicode
 74            else:
 75                if cur:
 76                    tokens.append(cur)
 77                    cur = ""
 78                tokens.append(unicode)
 79        if cur:
 80            tokens.append(cur)
 81
 82        # 步骤3: 贪心算法, 遍历所有“原子单位”
 83        current_line = ""
 84        current_width = 0
 85        for token in tokens:
 86            token_width = get_string_width(token, text_font, emoji_font, math_font, symbol_font)
 87
 88            # 如果当前行是空的, 但第一个token就超过了最大宽度
 89            # 这种极端情况,我们只能强制放入这一行, 让它溢出
 90            if not current_line:
 91                current_line = token
 92                current_width = token_width
 93                continue
 94
 95            # 步骤4: 判断是否需要换行
 96            if current_width + token_width > max_width:
 97                # 当前行满了,需要换行
 98                final_lines.append(current_line)
 99
100                # 新的一行以当前token开始
101                # 注意: 如果token是空格, 新的一行可以从非空格开始
102                if token.isspace():
103                    current_line = ""
104                    current_width = 0
105                else:
106                    current_line = token
107                    current_width = token_width
108            else:
109                # 不需要换行,将token追加到当前行
110                current_line += token
111                current_width += token_width
112
113        # 不要忘记处理最后一行
114        if current_line:
115            final_lines.append(current_line)
116
117    # 步骤5: 合并所有行,返回最终结果
118    if not final_lines:
119        final_lines = [""]
120    return final_lines
121
122
123def get_string_width(
124    text: str,
125    text_font: FreeTypeFont,
126    emoji_font: FreeTypeFont,
127    math_font: FreeTypeFont,
128    symbol_font: FreeTypeFont,
129) -> int:
130    """计算整个字符串的总显示宽度."""
131    width = 0
132    board = ImageDraw.Draw(Image.new("RGB", (1, 1)))
133
134    segments = get_text_segments(text)
135    for font, segment in segments:
136        if font == "text":
137            width += board.textlength(segment, font=text_font)
138        elif font == "symbol":
139            width += board.textlength(segment, font=symbol_font)
140        elif font == "math":
141            width += board.textlength(segment, font=math_font)
142        else:  # Emoji segment
143            for char in segment:
144                emoji_bbox = emoji_font.getbbox(char)
145                emoji_w, emoji_h = emoji_bbox[2] - emoji_bbox[0], emoji_bbox[3] - emoji_bbox[1]
146                if emoji_h > 0:
147                    width += emoji_w * 0.25
148    return int(width)
149
150
151def get_text_segments(text: str) -> list[tuple[str, str]]:
152    """将文本根据类型分割成 (字体, 文本切片) 的列表.
153
154    Example:
155        >>> get_text_segments("Hello, world! 😊")
156        [('text', 'Hello, world'), ('emoji', '😊')]
157    """
158    segments = []
159    current_segment = ""
160    if not text:
161        return []
162    unicode_list = EGC(text)
163    logger.trace(f"Split text into: {unicode_list}")
164    # 确定第一个字符的字体
165    current_font = get_char_type(unicode_list[0])
166
167    for unicode in unicode_list:
168        # 判断当前字符应该用哪个字体
169        char_font = get_char_type(unicode)
170        if char_font == current_font:
171            # 字体未变, 追加到当前片段
172            current_segment += unicode
173        else:
174            # 字体改变, 保存前一个片段, 并开启新片段
175            if current_segment:
176                segments.append((current_font, current_segment))
177            current_font = char_font
178            current_segment = unicode
179
180    # 添加最后一个片段
181    if current_segment:
182        segments.append((current_font, current_segment))
183    logger.trace(f"Text segments: {segments}")
184    return segments
185
186
187def get_char_type(char: str) -> str:
188    """获取字符的字体类型."""
189    char_type = None
190    if len(char) > 1:  # 可能是emoji, 检查 变体选择器-16 (U+FE0F) 是否存在
191        ords = [ord(c) for c in char]
192        if int("FE0F", 16) in ords:
193            char_type = "emoji"
194    elif ord(char) in TEXT_CMAP:
195        char_type = "text"
196    elif ord(char) in SYMBOL_CMAP:
197        char_type = "symbol"
198    elif ord(char) in EMOJI_CMAP:
199        char_type = "emoji"
200    elif ord(char) in MATH_CMAP:
201        char_type = "math"
202    if char_type is None:
203        logger.warning(f"unknown char: {char}")
204        return "text"
205    logger.trace(f"{char_type} char: {char}")
206    return char_type