436 lines
14 KiB
Python
436 lines
14 KiB
Python
import datetime
|
||
import time
|
||
import socket
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
import os
|
||
import re
|
||
import requests
|
||
|
||
PLATFORM_GOCQ = 'gocq'
|
||
PLATFORM_QQCHAN = 'qqchan'
|
||
|
||
FG_COLORS = {
|
||
"black": "30",
|
||
"red": "31",
|
||
"green": "32",
|
||
"yellow": "33",
|
||
"blue": "34",
|
||
"purple": "35",
|
||
"cyan": "36",
|
||
"white": "37",
|
||
"default": "39",
|
||
}
|
||
|
||
BG_COLORS = {
|
||
"black": "40",
|
||
"red": "41",
|
||
"green": "42",
|
||
"yellow": "43",
|
||
"blue": "44",
|
||
"purple": "45",
|
||
"cyan": "46",
|
||
"white": "47",
|
||
"default": "49",
|
||
}
|
||
|
||
LEVEL_INFO = "INFO"
|
||
LEVEL_WARNING = "WARNING"
|
||
LEVEL_ERROR = "ERROR"
|
||
LEVEL_CRITICAL = "CRITICAL"
|
||
|
||
level_colors = {
|
||
"INFO": "green",
|
||
"WARNING": "yellow",
|
||
"ERROR": "red",
|
||
"CRITICAL": "purple",
|
||
}
|
||
|
||
def log(
|
||
msg: str,
|
||
level: str = "INFO",
|
||
tag: str = "System",
|
||
fg: str = None,
|
||
bg: str = None,
|
||
max_len: int = 100):
|
||
"""
|
||
日志记录函数
|
||
"""
|
||
if len(msg) > max_len:
|
||
msg = msg[:max_len] + "..."
|
||
now = datetime.datetime.now().strftime("%m-%d %H:%M:%S")
|
||
pre = f"[{now}] [{level}] [{tag}]: {msg}"
|
||
if level == "INFO":
|
||
if fg is None:
|
||
fg = FG_COLORS["green"]
|
||
if bg is None:
|
||
bg = BG_COLORS["default"]
|
||
elif level == "WARNING":
|
||
if fg is None:
|
||
fg = FG_COLORS["yellow"]
|
||
if bg is None:
|
||
bg = BG_COLORS["default"]
|
||
elif level == "ERROR":
|
||
if fg is None:
|
||
fg = FG_COLORS["red"]
|
||
if bg is None:
|
||
bg = BG_COLORS["default"]
|
||
elif level == "CRITICAL":
|
||
if fg is None:
|
||
fg = FG_COLORS["purple"]
|
||
if bg is None:
|
||
bg = BG_COLORS["default"]
|
||
|
||
print(f"\033[{fg};{bg}m{pre}\033[0m")
|
||
|
||
|
||
def port_checker(port: int, host: str = "localhost"):
|
||
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||
sk.settimeout(1)
|
||
try:
|
||
sk.connect((host, port))
|
||
sk.close()
|
||
return True
|
||
except Exception:
|
||
sk.close()
|
||
return False
|
||
|
||
def word2img(title: str, text: str, max_width=30, font_size=20):
|
||
if os.path.exists("resources/fonts/genshin.ttf"):
|
||
font_path = "resources/fonts/genshin.ttf"
|
||
elif os.path.exists("QQChannelChatGPT/resources/fonts/genshin.ttf"):
|
||
font_path = "QQChannelChatGPT/resources/fonts/genshin.ttf"
|
||
elif os.path.exists("C:/Windows/Fonts/simhei.ttf"):
|
||
font_path = "C:/Windows/Fonts/simhei.ttf"
|
||
elif os.path.exists("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"):
|
||
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
|
||
else:
|
||
raise Exception("找不到字体文件")
|
||
|
||
width_factor = 1.0
|
||
height_factor = 1.5
|
||
# 格式化文本宽度最大为30
|
||
lines = text.split('\n')
|
||
i = 0
|
||
length = len(lines)
|
||
for l in lines:
|
||
if len(l) > max_width:
|
||
# lines[i] = l[:max_width] + '\n' + l[max_width:]
|
||
# for
|
||
cp = l
|
||
for ii in range(len(l)):
|
||
if ii % max_width == 0:
|
||
cp = cp[:ii] + '\n' + cp[ii:]
|
||
length += 1
|
||
lines[i] = cp
|
||
i += 1
|
||
text = '\n'.join(lines)
|
||
width = int(max_width * font_size * width_factor)
|
||
height = int(length * font_size * height_factor)
|
||
image = Image.new('RGB', (width, height), (255, 255, 255))
|
||
draw = ImageDraw.Draw(image)
|
||
|
||
text_font = ImageFont.truetype(font_path, font_size)
|
||
title_font = ImageFont.truetype(font_path, font_size + 5)
|
||
# 标题居中
|
||
title_width, title_height = title_font.getsize(title)
|
||
draw.text(((width - title_width) / 2, 10), title, fill=(0, 0, 0), font=title_font)
|
||
# 文本不居中
|
||
draw.text((10, title_height+20), text, fill=(0, 0, 0), font=text_font)
|
||
|
||
return image
|
||
|
||
|
||
def word2img_markdown(markdown: str, max_width=35, font_size=25):
|
||
|
||
if os.path.exists("resources/fonts/genshin.ttf"):
|
||
font_path = "resources/fonts/genshin.ttf"
|
||
elif os.path.exists("QQChannelChatGPT/resources/fonts/genshin.ttf"):
|
||
font_path = "QQChannelChatGPT/resources/fonts/genshin.ttf"
|
||
elif os.path.exists("C:/Windows/Fonts/simhei.ttf"):
|
||
font_path = "C:/Windows/Fonts/simhei.ttf"
|
||
elif os.path.exists("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"):
|
||
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
|
||
else:
|
||
raise Exception("找不到字体文件")
|
||
|
||
# try to render markdown
|
||
|
||
|
||
|
||
def render_markdown(markdown_text, image_width=800, image_height=600, font_size=16, font_color=(0, 0, 0), bg_color=(255, 255, 255)):
|
||
|
||
if os.path.exists("resources/fonts/genshin.ttf"):
|
||
font_path = "resources/fonts/genshin.ttf"
|
||
elif os.path.exists("QQChannelChatGPT/resources/fonts/genshin.ttf"):
|
||
font_path = "QQChannelChatGPT/resources/fonts/genshin.ttf"
|
||
elif os.path.exists("C:/Windows/Fonts/simhei.ttf"):
|
||
font_path = "C:/Windows/Fonts/simhei.ttf"
|
||
elif os.path.exists("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"):
|
||
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
|
||
else:
|
||
raise Exception("找不到字体文件")
|
||
# backup
|
||
if os.path.exists("resources/fonts/simhei.ttf"):
|
||
font_path1 = "resources/fonts/simhei.ttf"
|
||
elif os.path.exists("QQChannelChatGPT/resources/fonts/simhei.ttf"):
|
||
font_path1 = "QQChannelChatGPT/resources/fonts/simhei.ttf"
|
||
else:
|
||
font_path1 = font_path
|
||
# 加载字体
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
|
||
# pre_process, get height of each line
|
||
pre_lines = markdown_text.split('\n')
|
||
height = 0
|
||
pre_in_code = False
|
||
i = -1
|
||
# pre_codes = []
|
||
for line in pre_lines:
|
||
i += 1
|
||
line = line.strip()
|
||
if pre_in_code and not line.startswith("```"):
|
||
height += font_size+2
|
||
# pre_codes.append(line)
|
||
continue
|
||
if line.startswith("#"):
|
||
header_level = line.count("#")
|
||
height += 62 - header_level * 4
|
||
elif line.startswith("-"):
|
||
height += font_size+5
|
||
elif line.startswith(">"):
|
||
height += font_size+20
|
||
elif line.startswith("```"):
|
||
if pre_in_code:
|
||
pre_in_code = False
|
||
# pre_codes = []
|
||
height += 10
|
||
else:
|
||
pre_in_code = True
|
||
height += 5
|
||
elif re.search(r"`(.*?)`", line):
|
||
height += font_size+25
|
||
else:
|
||
cnt = 1
|
||
if font.getsize(line)[0] > image_width:
|
||
cp = line
|
||
for ii in range(len(line)):
|
||
if ii % image_width == 0:
|
||
cp = cp[:ii] + '\n' + cp[ii:]
|
||
pre_lines[i] = cp
|
||
cnt+=1
|
||
height += font_size * cnt + 8
|
||
|
||
markdown_text = '\n'.join(pre_lines)
|
||
print("Pre process done, height: ", height)
|
||
image_height = height
|
||
|
||
|
||
# 创建空白图像
|
||
image = Image.new('RGB', (image_width, image_height), bg_color)
|
||
draw = ImageDraw.Draw(image)
|
||
|
||
|
||
# # get all the emojis unicode in the markdown text
|
||
# unicode_text = markdown_text.encode('unicode_escape').decode()
|
||
# # print(unicode_text)
|
||
# unicode_emojis = re.findall(r'\\U\w{8}', unicode_text)
|
||
# emoji_base_url = "https://abs.twimg.com/emoji/v1/72x72/{unicode_emoji}.png"
|
||
|
||
# 设置初始位置
|
||
x, y = 10, 10
|
||
|
||
# 解析Markdown文本
|
||
lines = markdown_text.split("\n")
|
||
# lines = pre_lines
|
||
|
||
in_code_block = False
|
||
code_block_start_y = 0
|
||
code_block_codes = []
|
||
|
||
for line in lines:
|
||
if in_code_block and not line.startswith("```"):
|
||
code_block_codes.append(line)
|
||
y += font_size + 2
|
||
continue
|
||
line = line.strip()
|
||
|
||
# y += 28 - header_level * 4 + 20
|
||
|
||
|
||
if line.startswith("#"):
|
||
# unicode_emojis = re.findall(r'\\U0001\w{4}', line)
|
||
# for unicode_emoji in unicode_emojis:
|
||
# line = line.replace(unicode_emoji, "")
|
||
# unicode_emoji = ""
|
||
# if len(unicode_emojis) > 0:
|
||
# unicode_emoji = unicode_emojis[0]
|
||
|
||
# 处理标题
|
||
header_level = line.count("#")
|
||
line = line.strip("#").strip()
|
||
font_size_header = 32 - header_level * 4
|
||
|
||
# if unicode_emoji != "":
|
||
# emoji_url = emoji_base_url.format(unicode_emoji=unicode_emoji[-5:])
|
||
# emoji = Image.open(requests.get(emoji_url, stream=True).raw)
|
||
# emoji = emoji.resize((font_size, font_size))
|
||
# image.paste(emoji, (x, y))
|
||
# x += font_size
|
||
|
||
font = ImageFont.truetype(font_path, font_size_header)
|
||
draw.text((x, y), line, font=font, fill=font_color)
|
||
|
||
# material design color: blue 500
|
||
draw.line((x, y + font_size_header + 8, image_width - 10, y + font_size_header + 8), fill=(230, 230, 230), width=3)
|
||
y += font_size_header + 30
|
||
|
||
# y += font_size + 10
|
||
elif line.startswith(">"):
|
||
# 处理引用
|
||
quote_text = line.strip(">")
|
||
# quote_width = image_width - 20 # 引用框的宽度为图像宽度减去左右边距
|
||
# quote_height = font_size + 10 # 引用框的高度为字体大小加上上下边距
|
||
# quote_box = (x, y, x + quote_width, y + quote_height)
|
||
# draw.rounded_rectangle(quote_box, radius=5, fill=(230, 230, 230), width=2) # 使用灰色填充矩形框作为引用背景
|
||
|
||
draw.line((x, y, x, y + font_size + 10), fill=(230, 230, 230), width=5)
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
draw.text((x + 5, y + 5), quote_text, font=font, fill=(180, 180, 180))
|
||
y += font_size + 25
|
||
|
||
# y += 16+5
|
||
elif line.startswith("-"):
|
||
# 处理列表
|
||
list_text = line.strip("-").strip()
|
||
font_size = 16
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
draw.text((x, y), " · " + list_text, font=font, fill=font_color)
|
||
y += font_size + 5
|
||
|
||
# y += 5+10+font_size*line.count
|
||
elif line.startswith("```"):
|
||
if not in_code_block:
|
||
code_block_start_y = y+5
|
||
in_code_block = True
|
||
else:
|
||
# print(code_block_codes)
|
||
in_code_block = False
|
||
codes = "\n".join(code_block_codes)
|
||
code_block_codes = []
|
||
|
||
draw.rounded_rectangle((x, code_block_start_y, image_width - 10, y+15), radius=5, fill=(240, 240, 240), width=2)
|
||
font = ImageFont.truetype(font_path1, 16)
|
||
draw.text((x + 10, code_block_start_y + 5), codes, font=font, fill=font_color)
|
||
|
||
y += 15
|
||
# y += font_size+10
|
||
elif re.search(r"`(.*?)`", line):
|
||
# 处理行内代码
|
||
code_regex = r"`(.*?)`"
|
||
parts_inline = re.findall(code_regex, line)
|
||
# print(parts_inline)
|
||
parts = re.split(code_regex, line)
|
||
# print(parts)
|
||
for part in parts:
|
||
# the judge has a tiny bug.
|
||
# when line is like "hi`hi`". all the parts will be in parts_inline.
|
||
if part in parts_inline:
|
||
code_text = part.strip("`")
|
||
font_size = 16
|
||
code_width = font.getsize(code_text)[0] + 10
|
||
code_height = font_size + 10
|
||
code_box = (x, y-5, x + code_width, y + code_height)
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
draw.rounded_rectangle(code_box, radius=5, fill=(230, 230, 230), width=2) # 使用灰色填充矩形框作为引用背景
|
||
draw.text((x+5, y), code_text, font=font, fill=font_color)
|
||
x += code_width
|
||
else:
|
||
font_size = 16
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
draw.text((x, y), part, font=font, fill=font_color)
|
||
x += font.getsize(part)[0]
|
||
y += font_size + 20
|
||
x = 10
|
||
else:
|
||
# 处理普通文本
|
||
font_size = 16
|
||
font = ImageFont.truetype(font_path, font_size)
|
||
draw.text((x, y), line, font=font, fill=font_color)
|
||
y += font_size + 5
|
||
|
||
return image
|
||
|
||
|
||
def save_temp_img(img: Image) -> str:
|
||
if not os.path.exists("temp"):
|
||
os.makedirs("temp")
|
||
|
||
# 获得文件创建时间,清除超过1小时的
|
||
try:
|
||
for f in os.listdir("temp"):
|
||
path = os.path.join("temp", f)
|
||
if os.path.isfile(path):
|
||
ctime = os.path.getctime(path)
|
||
if time.time() - ctime > 3600:
|
||
os.remove(path)
|
||
except Exception as e:
|
||
log(f"清除临时文件失败: {e}", level=LEVEL_WARNING, tag="GeneralUtils")
|
||
|
||
# 获得时间戳
|
||
timestamp = int(time.time())
|
||
p = f"temp/{timestamp}.png"
|
||
img.save(p)
|
||
return p
|
||
|
||
|
||
def create_text_image(title: str, text: str, max_width=30, font_size=20):
|
||
'''
|
||
文本转图片。
|
||
title: 标题
|
||
text: 文本内容
|
||
max_width: 文本宽度最大值(默认30)
|
||
font_size: 字体大小(默认20)
|
||
|
||
返回:文件路径
|
||
'''
|
||
try:
|
||
img = word2img(title, text, max_width, font_size)
|
||
p = save_temp_img(img)
|
||
return p
|
||
except Exception as e:
|
||
raise e
|
||
|
||
def create_markdown_image(text: str):
|
||
'''
|
||
markdown文本转图片。
|
||
返回:文件路径
|
||
'''
|
||
try:
|
||
img = render_markdown(text)
|
||
p = save_temp_img(img)
|
||
return p
|
||
except Exception as e:
|
||
raise e
|
||
|
||
def test_markdown():
|
||
# 示例使用
|
||
markdown_text = """
|
||
# Help Center
|
||
## 指令列表
|
||
`/help` - 显示帮助中心
|
||
`/start` - 开始使用
|
||
`/about` - 关于
|
||
`/feedback` - 反馈
|
||
|
||
## Plugins列表
|
||
`/plugins` - 显示插件列表
|
||
`/plugins enable <plugin_name>` - 启用插件
|
||
`/plugins disable <plugin_name>` - 禁用插件
|
||
|
||
> Hi, thanks for using this bot. If you have any questions, please contact me.
|
||
|
||
"""
|
||
|
||
image = render_markdown(markdown_text)
|
||
image.show() # 显示渲染后的图像
|