99i commited on
Commit
768b041
·
verified ·
1 Parent(s): 35cd92c

Upload 4 files

Browse files
Files changed (5) hide show
  1. .gitattributes +3 -0
  2. TwitterColorEmoji.ttf +3 -0
  3. generate_image.py +939 -0
  4. msyh.ttc +3 -0
  5. msyhbd.ttc +3 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ msyh.ttc filter=lfs diff=lfs merge=lfs -text
37
+ msyhbd.ttc filter=lfs diff=lfs merge=lfs -text
38
+ TwitterColorEmoji.ttf filter=lfs diff=lfs merge=lfs -text
TwitterColorEmoji.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a0bceb7ee4941569eab695a785332f7287d59062ec1e9921fc2966e3a005b301
3
+ size 14477376
generate_image.py ADDED
@@ -0,0 +1,939 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utf-8
2
+ # image_generator.py
3
+ """
4
+ Advanced image card generator with markdown and emoji support
5
+ """
6
+ import math
7
+ import random
8
+ import os
9
+ from PIL import Image, ImageDraw, ImageFont, ImageOps
10
+ import emoji
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import List, Tuple, Optional, Dict
14
+ from datetime import datetime
15
+
16
+
17
+ @dataclass
18
+ class TextStyle:
19
+ """文本样式定义"""
20
+ font_name: str = 'regular' # regular, bold, emoji
21
+ font_size: int = 30 # 字体大小
22
+ indent: int = 0 # 缩进像素
23
+ line_spacing: int = 15 # 行间距
24
+ is_title: bool = False # 是否为标题
25
+ is_category: bool = False # 是否为分类标题
26
+ keep_with_next: bool = False # 是否与下一行保持在一起
27
+
28
+
29
+ @dataclass
30
+ class TextSegment:
31
+ """文本片段定义"""
32
+ text: str # 文本内容
33
+ style: TextStyle # 样式
34
+ original_text: str = '' # 原始文本(用于调试)
35
+
36
+
37
+ @dataclass
38
+ class ProcessedLine:
39
+ """处理后的行信息"""
40
+ text: str # 实际文本内容
41
+ style: TextStyle # 样式信息
42
+ height: int = 0 # 行高
43
+ line_count: int = 1 # 实际占用行数
44
+
45
+
46
+ class FontManager:
47
+ """字体管理器"""
48
+
49
+ def __init__(self, font_paths: Dict[str, str]):
50
+ self.fonts = {}
51
+ self.font_paths = font_paths
52
+ self._initialize_fonts()
53
+
54
+ def _initialize_fonts(self):
55
+ """初始化基础字体"""
56
+ sizes = [30, 35, 40] # 基础字号
57
+ for size in sizes:
58
+ self.fonts[f'regular_{size}'] = ImageFont.truetype(self.font_paths['regular'], size)
59
+ self.fonts[f'bold_{size}'] = ImageFont.truetype(self.font_paths['bold'], size)
60
+ # emoji字体
61
+ self.fonts['emoji_30'] = ImageFont.truetype(self.font_paths['emoji'], 30)
62
+
63
+ def get_font(self, style: TextStyle) -> ImageFont.FreeTypeFont:
64
+ """获取对应样式的字体"""
65
+ if style.font_name == 'emoji':
66
+ return self.fonts['emoji_30']
67
+
68
+ base_name = 'bold' if style.font_name == 'bold' or style.is_title or style.is_category else 'regular'
69
+ font_key = f'{base_name}_{style.font_size}'
70
+
71
+ if font_key not in self.fonts:
72
+ # 动态创建新字号的字体
73
+ self.fonts[font_key] = ImageFont.truetype(
74
+ self.font_paths['bold' if base_name == 'bold' else 'regular'],
75
+ style.font_size
76
+ )
77
+
78
+ return self.fonts[font_key]
79
+
80
+
81
+ def get_gradient_styles() -> List[Dict[str, tuple]]:
82
+ """
83
+ 获取精心设计的背景渐变样式
84
+ """
85
+ return [
86
+
87
+ # Mac 高级白
88
+ {
89
+ "start_color": (246, 246, 248), # 珍珠白
90
+ "end_color": (250, 250, 252) # 云雾白
91
+ },
92
+ {
93
+ "start_color": (245, 245, 247), # 奶白色
94
+ "end_color": (248, 248, 250) # 象牙白
95
+ },
96
+ # macOS Monterey 风格
97
+ {
98
+ "start_color": (191, 203, 255), # 淡蓝紫
99
+ "end_color": (255, 203, 237) # 浅粉红
100
+ },
101
+ {
102
+ "start_color": (168, 225, 255), # 天空蓝
103
+ "end_color": (203, 255, 242) # 清新薄荷
104
+ },
105
+
106
+ # 优雅渐变系列
107
+ {
108
+ "start_color": (255, 209, 209), # 珊瑚粉
109
+ "end_color": (243, 209, 255) # 淡紫色
110
+ },
111
+ {
112
+ "start_color": (255, 230, 209), # 奶橘色
113
+ "end_color": (255, 209, 247) # 粉紫色
114
+ },
115
+
116
+ # 清新通透
117
+ {
118
+ "start_color": (213, 255, 219), # 嫩绿色
119
+ "end_color": (209, 247, 255) # 浅蓝色
120
+ },
121
+ {
122
+ "start_color": (255, 236, 209), # 杏橘色
123
+ "end_color": (255, 209, 216) # 浅玫瑰
124
+ },
125
+
126
+ # 高级灰调
127
+ {
128
+ "start_color": (237, 240, 245), # 珍珠灰
129
+ "end_color": (245, 237, 245) # 薰衣草灰
130
+ },
131
+ {
132
+ "start_color": (240, 245, 255), # 云雾蓝
133
+ "end_color": (245, 240, 245) # 淡紫灰
134
+ },
135
+
136
+ # 梦幻糖果色
137
+ {
138
+ "start_color": (255, 223, 242), # 棉花糖粉
139
+ "end_color": (242, 223, 255) # 淡紫丁香
140
+ },
141
+ {
142
+ "start_color": (223, 255, 247), # 薄荷绿
143
+ "end_color": (223, 242, 255) # 天空蓝
144
+ },
145
+
146
+ # 高饱和度系列
147
+ {
148
+ "start_color": (255, 192, 203), # 粉红色
149
+ "end_color": (192, 203, 255) # 淡紫蓝
150
+ },
151
+ {
152
+ "start_color": (192, 255, 238), # 碧绿色
153
+ "end_color": (238, 192, 255) # 淡紫色
154
+ },
155
+
156
+ # 静谧系列
157
+ {
158
+ "start_color": (230, 240, 255), # 宁静蓝
159
+ "end_color": (255, 240, 245) # 柔粉色
160
+ },
161
+ {
162
+ "start_color": (245, 240, 255), # 淡紫色
163
+ "end_color": (240, 255, 240) # 清新绿
164
+ },
165
+
166
+ # 温柔渐变
167
+ {
168
+ "start_color": (255, 235, 235), # 温柔粉
169
+ "end_color": (235, 235, 255) # 淡雅紫
170
+ },
171
+ {
172
+ "start_color": (235, 255, 235), # 嫩芽绿
173
+ "end_color": (255, 235, 245) # 浅粉红
174
+ }
175
+ ]
176
+
177
+
178
+ def create_gradient_background(width: int, height: int) -> Image.Image:
179
+ """创建渐变背景 - 从左上到右下的对角线渐变"""
180
+ gradient_styles = get_gradient_styles()
181
+ style = random.choice(gradient_styles)
182
+ start_color = style["start_color"]
183
+ end_color = style["end_color"]
184
+
185
+ # 创建基础图像
186
+ base = Image.new('RGB', (width, height))
187
+ draw = ImageDraw.Draw(base)
188
+
189
+ # 计算渐变
190
+ for y in range(height):
191
+ for x in range(width):
192
+ # 计算当前位置到左上角的相对距离 (对角线渐变)
193
+ # 使用 position 在 0 到 1 之间表示渐变程度
194
+ position = (x + y) / (width + height)
195
+
196
+ # 为每个颜色通道计算渐变值
197
+ r = int(start_color[0] * (1 - position) + end_color[0] * position)
198
+ g = int(start_color[1] * (1 - position) + end_color[1] * position)
199
+ b = int(start_color[2] * (1 - position) + end_color[2] * position)
200
+
201
+ # 绘制像素
202
+ draw.point((x, y), fill=(r, g, b))
203
+
204
+ return base
205
+
206
+
207
+ def get_theme_colors() -> Tuple[tuple, str, bool]:
208
+ """获取主题颜色配置"""
209
+ current_hour = datetime.now().hour
210
+ current_minute = datetime.now().minute
211
+
212
+ if (current_hour == 8 and current_minute >= 30) or (9 <= current_hour < 19):
213
+ use_dark = random.random() < 0.1
214
+ else:
215
+ use_dark = True
216
+
217
+ if use_dark:
218
+ # 深色毛玻璃效果: 深色半透明背景(50%透明度) + 白色文字
219
+ return ((50, 50, 50, 128), "#FFFFFF", True) # alpha值调整为128实现50%透明度
220
+ else:
221
+ # 浅色毛玻璃效果: 白色半透明背景(50%透明度) + 黑色文字
222
+ return ((255, 255, 255, 128), "#000000", False) # alpha值调整为128实现50%透明度
223
+
224
+
225
+ def create_rounded_rectangle(image: Image.Image, x: int, y: int, w: int, h: int, radius: int, bg_color: tuple):
226
+ """创建圆角毛玻璃矩形"""
227
+ # 创建透明背景的矩形
228
+ rectangle = Image.new('RGBA', (int(w), int(h)), (0, 0, 0, 0))
229
+ draw = ImageDraw.Draw(rectangle)
230
+
231
+ # 绘制带透明度的圆角矩形
232
+ draw.rounded_rectangle(
233
+ [(0, 0), (int(w), int(h))],
234
+ radius,
235
+ fill=bg_color # 使用带透明度的背景色
236
+ )
237
+
238
+ # 使用alpha通道混合方式粘贴到背景上
239
+ image.paste(rectangle, (int(x), int(y)), rectangle)
240
+
241
+
242
+ def round_corner_image(image: Image.Image, radius: int) -> Image.Image:
243
+ """将图片转为圆角"""
244
+ # 创建一个带有圆角的蒙版
245
+ circle = Image.new('L', (radius * 2, radius * 2), 0)
246
+ draw = ImageDraw.Draw(circle)
247
+ draw.ellipse((0, 0, radius * 2, radius * 2), fill=255)
248
+
249
+ # 创建一个完整的蒙版
250
+ mask = Image.new('L', image.size, 255)
251
+
252
+ # 添加四个圆角
253
+ mask.paste(circle.crop((0, 0, radius, radius)), (0, 0)) # 左上
254
+ mask.paste(circle.crop((radius, 0, radius * 2, radius)), (image.width - radius, 0)) # 右上
255
+ mask.paste(circle.crop((0, radius, radius, radius * 2)), (0, image.height - radius)) # 左下
256
+ mask.paste(circle.crop((radius, radius, radius * 2, radius * 2)),
257
+ (image.width - radius, image.height - radius)) # 右下
258
+
259
+ # 创建一个空白的透明图像
260
+ output = Image.new('RGBA', image.size, (0, 0, 0, 0))
261
+
262
+ # 将原图和蒙版合并
263
+ output.paste(image, (0, 0))
264
+ output.putalpha(mask)
265
+
266
+ return output
267
+
268
+
269
+ def add_title_image(background: Image.Image, title_image_path: str, rect_x: int, rect_y: int, rect_width: int) -> int:
270
+ """添加标题图片"""
271
+ try:
272
+ with Image.open(title_image_path) as title_img:
273
+ # 如果图片不是RGBA模式,转换为RGBA
274
+ if title_img.mode != 'RGBA':
275
+ title_img = title_img.convert('RGBA')
276
+
277
+ # 设置图片宽度等于文字区域宽度
278
+ target_width = rect_width - 40 # 左右各留20像素边距
279
+
280
+ # 计算等比例缩放后的高度
281
+ aspect_ratio = title_img.height / title_img.width
282
+ target_height = int(target_width * aspect_ratio)
283
+
284
+ # 调整图片大小
285
+ resized_img = title_img.resize((int(target_width), target_height), Image.Resampling.LANCZOS)
286
+
287
+ # 添加圆角
288
+ rounded_img = round_corner_image(resized_img, radius=20) # 可以调整圆角半径
289
+
290
+ # 计算居中位置(水平方向)
291
+ x = rect_x + 20 # 左边距20像素
292
+ y = rect_y + 20 # 顶部边距20像素
293
+
294
+ # 粘贴图片(使用图片自身的alpha通道)
295
+ background.paste(rounded_img, (x, y), rounded_img)
296
+
297
+ return y + target_height + 20 # 返回图片底部位置加上20像素间距
298
+ except Exception as e:
299
+ print(f"Error loading title image: {e}")
300
+ return rect_y + 30
301
+
302
+
303
+ class MarkdownParser:
304
+ """Markdown解析器"""
305
+
306
+ def __init__(self):
307
+ self.reset()
308
+
309
+ def reset(self):
310
+ self.segments = []
311
+ self.current_section = None # 当前处理的段落类型
312
+
313
+ def parse(self, text: str) -> List[TextSegment]:
314
+ """解析整个文本"""
315
+ self.reset()
316
+ segments = []
317
+ lines = text.splitlines()
318
+
319
+ for i, line in enumerate(lines):
320
+ line = line.strip()
321
+ if not line:
322
+ # 只有当下一行有内容时才添加空行
323
+ next_has_content = False
324
+ for next_line in lines[i + 1:]:
325
+ if next_line.strip():
326
+ next_has_content = True
327
+ break
328
+ if next_has_content:
329
+ style = TextStyle(
330
+ line_spacing=20 if segments and segments[-1].style.is_title else 15
331
+ )
332
+ segments.append(TextSegment(text='', style=style))
333
+ continue
334
+
335
+ # 处理常规行
336
+ line_segments = self.parse_line(line)
337
+ segments.extend(line_segments)
338
+
339
+ # 只在确定有下一行内容时添加空行
340
+ if i < len(lines) - 1:
341
+ has_next_content = False
342
+ for next_line in lines[i + 1:]:
343
+ if next_line.strip():
344
+ has_next_content = True
345
+ break
346
+ if has_next_content:
347
+ style = line_segments[-1].style
348
+ segments.append(TextSegment(text='', style=TextStyle(line_spacing=style.line_spacing)))
349
+
350
+ # 最后添加签名,不添加任何额外空行
351
+ if segments:
352
+ signature = TextSegment(
353
+ text=" —By 99i",
354
+ style=TextStyle(font_name='regular', indent=0, line_spacing=0) # 设置 line_spacing=0
355
+ )
356
+ segments.append(signature)
357
+
358
+ return segments
359
+
360
+ def is_category_title(self, text: str) -> bool:
361
+ """判断是否为分类标题"""
362
+ return text.strip() in ['国内要闻', '国际动态']
363
+
364
+ def process_title_marks(self, text: str) -> str:
365
+ """处理标题标记"""
366
+ # 移除 ** 标记
367
+ text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
368
+ # 统一中文冒号
369
+ text = text.replace(':', ':')
370
+ return text
371
+
372
+ def split_number_and_content(self, text: str) -> Tuple[str, str]:
373
+ """分离序号和内容"""
374
+ match = re.match(r'(\d+)\.\s*(.+)', text)
375
+ if match:
376
+ return match.group(1), match.group(2)
377
+ return '', text
378
+
379
+ def split_title_and_content(self, text: str) -> Tuple[str, str]:
380
+ """分离标题和内容"""
381
+ parts = text.split(':', 1)
382
+ if len(parts) == 2:
383
+ return parts[0] + ':', parts[1].strip()
384
+ return text, ''
385
+
386
+ def parse_line(self, text: str) -> List[TextSegment]:
387
+ """解析单行文本"""
388
+ if not text.strip():
389
+ return [TextSegment(text='', style=TextStyle())]
390
+
391
+ # 处理一级标题
392
+ if text.startswith('# '):
393
+ style = TextStyle(
394
+ font_name='bold',
395
+ font_size=40,
396
+ is_title=True,
397
+ indent=0
398
+ )
399
+ return [TextSegment(text=text[2:].strip(), style=style)]
400
+
401
+ # 处理二级标题
402
+ if text.startswith('## '):
403
+ style = TextStyle(
404
+ font_name='bold',
405
+ font_size=35,
406
+ is_title=True,
407
+ line_spacing=25,
408
+ indent=0
409
+ )
410
+ self.current_section = text[3:].strip()
411
+ return [TextSegment(text=self.current_section, style=style)]
412
+
413
+ # 处理分类标题
414
+ if self.is_category_title(text):
415
+ style = TextStyle(
416
+ font_name='bold',
417
+ font_size=35,
418
+ is_category=True,
419
+ line_spacing=25,
420
+ indent=0
421
+ )
422
+ return [TextSegment(text=text.strip(), style=style)]
423
+
424
+ # 处理emoji标题格式
425
+ if text.strip() and emoji.is_emoji(text[0]):
426
+ # 移除文本中的加粗标记 **
427
+ content = text.strip()
428
+ if '**' in content:
429
+ content = content.replace('**', '')
430
+
431
+ style = TextStyle(
432
+ font_name='bold',
433
+ font_size=40, # 使用H1的���体大小
434
+ is_title=True,
435
+ line_spacing=25,
436
+ indent=0
437
+ )
438
+ return [TextSegment(text=content, style=style)]
439
+
440
+ # 处理带序号的新闻条目
441
+ number, content = self.split_number_and_content(text)
442
+ if number:
443
+ content = self.process_title_marks(content)
444
+ title, body = self.split_title_and_content(content)
445
+ segments = []
446
+
447
+ title_style = TextStyle(
448
+ font_name='bold',
449
+ indent=0,
450
+ is_title=True,
451
+ line_spacing=15 if body else 20
452
+ )
453
+ segments.append(TextSegment(
454
+ text=f"{number}. {title}",
455
+ style=title_style
456
+ ))
457
+
458
+ if body:
459
+ content_style = TextStyle(
460
+ font_name='regular',
461
+ indent=40,
462
+ line_spacing=20
463
+ )
464
+ segments.append(TextSegment(
465
+ text=body,
466
+ style=content_style
467
+ ))
468
+ return segments
469
+
470
+ # 处理破折号开头的内容
471
+ if text.strip().startswith('-'):
472
+ style = TextStyle(
473
+ font_name='regular',
474
+ indent=40,
475
+ line_spacing=15
476
+ )
477
+ return [TextSegment(text=text.strip(), style=style)]
478
+
479
+ # 处理普通文本
480
+ style = TextStyle(
481
+ font_name='regular',
482
+ indent=40 if self.current_section else 0,
483
+ line_spacing=15
484
+ )
485
+
486
+ return [TextSegment(text=text.strip(), style=style)]
487
+
488
+
489
+ def parse_line(self, text: str) -> List[TextSegment]:
490
+ """解析单行文本"""
491
+ if not text.strip():
492
+ return [TextSegment(text='', style=TextStyle())]
493
+
494
+ # 处理一级标题
495
+ if text.startswith('# '):
496
+ style = TextStyle(
497
+ font_name='bold',
498
+ font_size=40,
499
+ is_title=True,
500
+ indent=0
501
+ )
502
+ return [TextSegment(text=text[2:].strip(), style=style)]
503
+
504
+ # 处理二级标题
505
+ if text.startswith('## '):
506
+ style = TextStyle(
507
+ font_name='bold',
508
+ font_size=35,
509
+ is_title=True,
510
+ line_spacing=25,
511
+ indent=0
512
+ )
513
+ self.current_section = text[3:].strip()
514
+ return [TextSegment(text=self.current_section, style=style)]
515
+
516
+ # 处理分类标题
517
+ if self.is_category_title(text):
518
+ style = TextStyle(
519
+ font_name='bold',
520
+ font_size=35,
521
+ is_category=True,
522
+ line_spacing=25,
523
+ indent=0
524
+ )
525
+ return [TextSegment(text=text.strip(), style=style)]
526
+
527
+ # 处理emoji标题格式
528
+ # 匹配模式:emoji + 空格 + (可选的**) + 内容 + (可选的**)
529
+ if text.strip() and emoji.is_emoji(text[0]):
530
+ # 移除文本中的加粗标记 **
531
+ content = text.strip()
532
+ if '**' in content:
533
+ content = content.replace('**', '')
534
+
535
+ style = TextStyle(
536
+ font_name='bold',
537
+ font_size=40, # 使用H1的字体大小
538
+ is_title=True,
539
+ line_spacing=25,
540
+ indent=0
541
+ )
542
+ return [TextSegment(text=content, style=style)]
543
+
544
+ # 处理带序号的新闻条目
545
+ number, content = self.split_number_and_content(text)
546
+ if number:
547
+ content = self.process_title_marks(content)
548
+ title, body = self.split_title_and_content(content)
549
+ segments = []
550
+
551
+ title_style = TextStyle(
552
+ font_name='bold',
553
+ indent=0,
554
+ is_title=True,
555
+ line_spacing=15 if body else 20
556
+ )
557
+ segments.append(TextSegment(
558
+ text=f"{number}. {title}",
559
+ style=title_style
560
+ ))
561
+
562
+ if body:
563
+ content_style = TextStyle(
564
+ font_name='regular',
565
+ indent=40,
566
+ line_spacing=20
567
+ )
568
+ segments.append(TextSegment(
569
+ text=body,
570
+ style=content_style
571
+ ))
572
+ return segments
573
+
574
+ # 处理破折号开头的内容
575
+ if text.strip().startswith('-'):
576
+ style = TextStyle(
577
+ font_name='regular',
578
+ indent=40,
579
+ line_spacing=15
580
+ )
581
+ return [TextSegment(text=text.strip(), style=style)]
582
+
583
+ # 处理普通文本
584
+ style = TextStyle(
585
+ font_name='regular',
586
+ indent=40 if self.current_section else 0,
587
+ line_spacing=15
588
+ )
589
+
590
+ return [TextSegment(text=text.strip(), style=style)]
591
+
592
+ def parse(self, text: str) -> List[TextSegment]:
593
+ """解析整个文本"""
594
+ self.reset()
595
+ segments = []
596
+ lines = text.splitlines()
597
+
598
+ for i, line in enumerate(lines):
599
+ line = line.strip()
600
+ if not line:
601
+ # 只有当下一行有内容时才添加空行
602
+ next_has_content = False
603
+ for next_line in lines[i + 1:]:
604
+ if next_line.strip():
605
+ next_has_content = True
606
+ break
607
+ if next_has_content:
608
+ style = TextStyle(
609
+ line_spacing=20 if segments and segments[-1].style.is_title else 15
610
+ )
611
+ segments.append(TextSegment(text='', style=style))
612
+ continue
613
+
614
+ # 处理常规行
615
+ line_segments = self.parse_line(line)
616
+ segments.extend(line_segments)
617
+
618
+ # 只在确定有下一行内容时添加空行
619
+ if i < len(lines) - 1:
620
+ has_next_content = False
621
+ for next_line in lines[i + 1:]:
622
+ if next_line.strip():
623
+ has_next_content = True
624
+ break
625
+ if has_next_content:
626
+ style = line_segments[-1].style
627
+ segments.append(TextSegment(text='', style=TextStyle(line_spacing=style.line_spacing)))
628
+
629
+ # 最后添加签名,不添加任何额外空行
630
+ if segments:
631
+ signature = TextSegment(
632
+ text=" —By 嫣然",
633
+ style=TextStyle(font_name='regular', indent=0, line_spacing=0) # 设置 line_spacing=0
634
+ )
635
+ segments.append(signature)
636
+
637
+ return segments
638
+
639
+
640
+ class TextRenderer:
641
+ """文本渲染器"""
642
+
643
+ def __init__(self, font_manager: FontManager, max_width: int):
644
+ self.font_manager = font_manager
645
+ self.max_width = max_width
646
+ self.temp_image = Image.new('RGBA', (2000, 100))
647
+ self.temp_draw = ImageDraw.Draw(self.temp_image)
648
+
649
+ def measure_text(self, text: str, font: ImageFont.FreeTypeFont,
650
+ emoji_font: Optional[ImageFont.FreeTypeFont] = None) -> Tuple[int, int]:
651
+ """测量文本尺寸,考虑emoji"""
652
+ total_width = 0
653
+ max_height = 0
654
+
655
+ for char in text:
656
+ if emoji.is_emoji(char) and emoji_font:
657
+ bbox = self.temp_draw.textbbox((0, 0), char, font=emoji_font)
658
+ width = bbox[2] - bbox[0]
659
+ height = bbox[3] - bbox[1]
660
+ else:
661
+ bbox = self.temp_draw.textbbox((0, 0), char, font=font)
662
+ width = bbox[2] - bbox[0]
663
+ height = bbox[3] - bbox[1]
664
+
665
+ total_width += width
666
+ max_height = max(max_height, height)
667
+
668
+ return total_width, max_height
669
+
670
+ def draw_text_with_emoji(self, draw: ImageDraw.ImageDraw, pos: Tuple[int, int], text: str,
671
+ font: ImageFont.FreeTypeFont, emoji_font: ImageFont.FreeTypeFont,
672
+ fill: str = "white") -> int:
673
+ """绘制包含emoji的文本,返回绘制宽度"""
674
+ x, y = pos
675
+ total_width = 0
676
+
677
+ for char in text:
678
+ if emoji.is_emoji(char):
679
+ # 使用emoji字体
680
+ bbox = draw.textbbox((x, y), char, font=emoji_font)
681
+ draw.text((x, y), char, font=emoji_font, fill=fill)
682
+ char_width = bbox[2] - bbox[0]
683
+ else:
684
+ # 使用常规字体
685
+ bbox = draw.textbbox((x, y), char, font=font)
686
+ draw.text((x, y), char, font=font, fill=fill)
687
+ char_width = bbox[2] - bbox[0]
688
+
689
+ x += char_width
690
+ total_width += char_width
691
+
692
+ return total_width
693
+
694
+ def calculate_height(self, processed_lines: List[ProcessedLine]) -> int:
695
+ """计算总高度,确保不在最后添加额外间距"""
696
+ total_height = 0
697
+ prev_line = None
698
+
699
+ for i, line in enumerate(processed_lines):
700
+ if not line.text.strip():
701
+ # 只有当不是最后一行,且后面还有内容时才添加间距
702
+ if i < len(processed_lines) - 1 and any(l.text.strip() for l in processed_lines[i + 1:]):
703
+ if prev_line:
704
+ total_height += prev_line.style.line_spacing
705
+ continue
706
+
707
+ # 计算当前行高度
708
+ line_height = line.height * line.line_count
709
+
710
+ # 添加行间距,但不在最后一行后添加
711
+ if prev_line and i < len(processed_lines) - 1:
712
+ if prev_line.style.is_category:
713
+ total_height += 30
714
+ elif prev_line.style.is_title and not line.style.is_title:
715
+ total_height += 20
716
+ else:
717
+ total_height += line.style.line_spacing
718
+
719
+ total_height += line_height
720
+ prev_line = line
721
+
722
+ return total_height
723
+
724
+ def split_text_to_lines(self, segment: TextSegment, available_width: int) -> List[ProcessedLine]:
725
+ """将文本分割成合适宽度的行,支持emoji"""
726
+ if not segment.text.strip():
727
+ return [ProcessedLine(text='', style=segment.style, height=0, line_count=1)]
728
+
729
+ font = self.font_manager.get_font(segment.style)
730
+ emoji_font = self.font_manager.fonts['emoji_30']
731
+ words = []
732
+ current_word = ''
733
+ processed_lines = []
734
+
735
+ # 分词处理
736
+ for char in segment.text:
737
+ if emoji.is_emoji(char):
738
+ if current_word:
739
+ words.append(current_word)
740
+ current_word = ''
741
+ words.append(char) # emoji作为单独的词
742
+ elif char in [' ', ',', '。', ':', '、', '!', '?', ';']:
743
+ if current_word:
744
+ words.append(current_word)
745
+ words.append(char)
746
+ current_word = ''
747
+ else:
748
+ if ord(char) > 0x4e00: # 中文字符
749
+ if current_word:
750
+ words.append(current_word)
751
+ current_word = ''
752
+ words.append(char)
753
+ else:
754
+ current_word += char
755
+
756
+ if current_word:
757
+ words.append(current_word)
758
+
759
+ current_line = ''
760
+ line_height = 0
761
+
762
+ for word in words:
763
+ test_line = current_line + word
764
+ width, height = self.measure_text(test_line, font, emoji_font)
765
+ line_height = max(line_height, height)
766
+
767
+ if width <= available_width:
768
+ current_line = test_line
769
+ else:
770
+ if current_line:
771
+ processed_lines.append(ProcessedLine(
772
+ text=current_line,
773
+ style=segment.style,
774
+ height=line_height,
775
+ line_count=1
776
+ ))
777
+ current_line = word
778
+
779
+ if current_line:
780
+ processed_lines.append(ProcessedLine(
781
+ text=current_line,
782
+ style=segment.style,
783
+ height=line_height,
784
+ line_count=1
785
+ ))
786
+
787
+ return processed_lines
788
+
789
+
790
+ def compress_image(image_path: str, output_path: str, max_size: int = 3145728): # 3MB in bytes
791
+ """
792
+ Compress an image to ensure it's under a certain file size.
793
+
794
+ :param image_path: The path to the image to be compressed.
795
+ :param output_path: The path where the compressed image will be saved.
796
+ :param max_size: The maximum file size in bytes (default is 3MB).
797
+ """
798
+ # Open the image
799
+ with Image.open(image_path) as img:
800
+ # Convert to RGB if it's not already
801
+ if img.mode != 'RGB':
802
+ img = img.convert('RGB')
803
+
804
+ # Define the quality to start with
805
+ quality = 95 # Start with a high quality
806
+
807
+ # Save the image with different qualities until the file size is acceptable
808
+ while True:
809
+ # Save the image with the current quality
810
+ img.save(output_path, "PNG", optimize=True, compress_level=0)
811
+
812
+ # Check the file size
813
+ if os.path.getsize(output_path) <= max_size:
814
+ break # The file size is acceptable, break the loop
815
+
816
+ # If the file is still too large, decrease the quality
817
+ quality -= 5
818
+ if quality < 10: # To prevent an infinite loop, set a minimum quality
819
+ break
820
+
821
+ # If the quality is too low, you might want to handle it here
822
+ if quality < 10:
823
+ print("The image could not be compressed enough to meet the size requirements.")
824
+
825
+
826
+ def generate_image(text: str, output_path: str, title_image: Optional[str] = None):
827
+ """生成图片主函数 - 修复彩色emoji渲染"""
828
+ try:
829
+ width = 720
830
+ current_dir = os.path.dirname(os.path.abspath(__file__))
831
+ font_paths = {
832
+ 'regular': os.path.join(current_dir, "msyh.ttc"),
833
+ 'bold': os.path.join(current_dir, "msyhbd.ttc"),
834
+ 'emoji': os.path.join(current_dir, "TwitterColorEmoji.ttf") # 或其他彩色emoji字体
835
+ }
836
+
837
+ # 验证字体文件
838
+ for font_type, path in font_paths.items():
839
+ if not os.path.exists(path):
840
+ raise FileNotFoundError(f"Font file not found: {path}")
841
+
842
+ # 初始化组件
843
+ font_manager = FontManager(font_paths)
844
+ rect_width = width - 80
845
+ max_content_width = rect_width - 80
846
+ parser = MarkdownParser()
847
+ renderer = TextRenderer(font_manager, max_content_width)
848
+
849
+ # 解析文本
850
+ segments = parser.parse(text)
851
+ processed_lines = []
852
+
853
+ for segment in segments:
854
+ available_width = max_content_width - segment.style.indent
855
+ if segment.text.strip():
856
+ lines = renderer.split_text_to_lines(segment, available_width)
857
+ processed_lines.extend(lines)
858
+ else:
859
+ processed_lines.append(ProcessedLine(
860
+ text='',
861
+ style=segment.style,
862
+ height=0,
863
+ line_count=1
864
+ ))
865
+
866
+ # 计算高度
867
+ title_height = 0
868
+ if title_image:
869
+ try:
870
+ with Image.open(title_image) as img:
871
+ aspect_ratio = img.height / img.width
872
+ title_height = int((rect_width - 40) * aspect_ratio) + 40
873
+ except Exception as e:
874
+ print(f"Title image processing error: {e}")
875
+
876
+ content_height = renderer.calculate_height(processed_lines)
877
+ rect_height = content_height + title_height
878
+ rect_x = (width - rect_width) // 2
879
+ rect_y = 40
880
+ total_height = rect_height + 80
881
+
882
+ # 创建RGBA背景
883
+ background = create_gradient_background(width, total_height)
884
+ draw = ImageDraw.Draw(background)
885
+
886
+ # 获取主题颜色
887
+ background_color, text_color, is_dark_theme = get_theme_colors()
888
+ if len(background_color) == 3:
889
+ background_color = background_color + (128,) # 添加alpha通道
890
+
891
+ # 创建卡片背景
892
+ create_rounded_rectangle(
893
+ background, rect_x, rect_y, rect_width, rect_height,
894
+ radius=30, bg_color=background_color
895
+ )
896
+
897
+ # 绘制内容
898
+ current_y = rect_y + 30
899
+ if title_image:
900
+ current_y = add_title_image(background, title_image, rect_x, rect_y, rect_width)
901
+
902
+ # 逐字符绘制文本
903
+ for i, line in enumerate(processed_lines):
904
+ if not line.text.strip():
905
+ if i < len(processed_lines) - 1 and any(l.text.strip() for l in processed_lines[i + 1:]):
906
+ current_y += line.style.line_spacing
907
+ continue
908
+
909
+ x = rect_x + 40 + line.style.indent
910
+ current_x = x
911
+
912
+ # 逐字符渲染
913
+ for char in line.text:
914
+ if emoji.is_emoji(char):
915
+ # emoji字体渲染
916
+ emoji_font = font_manager.fonts['emoji_30']
917
+ bbox = draw.textbbox((current_x, current_y), char, font=emoji_font)
918
+ # 使用RGBA模式绘制emoji
919
+ draw.text((current_x, current_y), char, font=emoji_font, embedded_color=True)
920
+ current_x += bbox[2] - bbox[0]
921
+ else:
922
+ # 普通文字渲染
923
+ font = font_manager.get_font(line.style)
924
+ bbox = draw.textbbox((current_x, current_y), char, font=font)
925
+ draw.text((current_x, current_y), char, font=font, fill=text_color)
926
+ current_x += bbox[2] - bbox[0]
927
+
928
+ if i < len(processed_lines) - 1:
929
+ current_y += line.height + line.style.line_spacing
930
+ else:
931
+ current_y += line.height
932
+
933
+ # 直接保存为PNG,保持RGBA模式
934
+ background = background.convert('RGB')
935
+ background.save(output_path, "PNG", optimize=False, compress_level=0)
936
+
937
+ except Exception as e:
938
+ print(f"Error generating image: {e}")
939
+ raise
msyh.ttc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3084f1f88369af6bf9989c909024164d953d1e38d08734f05f28ef24b2f9d577
3
+ size 19701556
msyhbd.ttc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:519309b7ab0479c4dc3ace5e291de5a8702175be5586e165bc810267bd4619a5
3
+ size 16880832