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