nonebot-plugin-l4d2-server 0.6.6__py3-none-any.whl → 1.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_l4d2_server/__init__.py +10 -93
- nonebot_plugin_l4d2_server/__main__.py +140 -0
- nonebot_plugin_l4d2_server/config.py +35 -0
- nonebot_plugin_l4d2_server/data/font/loli.ttf +0 -0
- nonebot_plugin_l4d2_server/l4_anne/__init__.py +124 -0
- nonebot_plugin_l4d2_server/l4_anne/ranne.py +18 -0
- nonebot_plugin_l4d2_server/l4_help/Help.json +102 -0
- nonebot_plugin_l4d2_server/l4_help/__init__.py +57 -0
- nonebot_plugin_l4d2_server/l4_help/draw.py +206 -0
- nonebot_plugin_l4d2_server/l4_help/icon//344/273/213/347/273/215.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//344/273/273/345/212/241.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//344/277/241/346/201/257.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/205/254/345/221/212.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/210/200/345/211/221.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/210/207/346/215/242.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/210/240/351/231/244.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/210/267/346/226/260.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/215/241/347/273/204.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/223/252/351/207/214.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/234/260/345/233/276.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/205/245.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/207/272.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//345/275/261.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/213/274/345/233/276.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/216/242/347/264/242.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/216/250/351/200/201.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/224/266/351/233/206.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/224/273/347/225/245.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/233/264/346/226/260.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/235/220/346/226/231.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/237/245/350/257/242.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/240/241/351/252/214.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/257/217/346/234/210.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/267/261/346/270/212.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/267/273/345/212/240.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//346/270/205/351/231/244.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//347/212/266/346/200/201.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//347/255/276/345/210/260.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//347/273/221/345/256/232.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//350/241/250.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//350/241/250/346/203/205.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//350/247/222/350/211/262.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//350/256/260/345/275/225.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//351/205/215/347/275/256.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/icon//351/207/215/345/220/257.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/texture2d/badge.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/texture2d/banner.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/texture2d/bg.jpg +0 -0
- nonebot_plugin_l4d2_server/l4_help/texture2d/button.png +0 -0
- nonebot_plugin_l4d2_server/l4_help/texture2d/icon.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/__init__.py +16 -0
- nonebot_plugin_l4d2_server/l4_image/anne_pil.py +17 -0
- nonebot_plugin_l4d2_server/l4_image/convert.py +175 -0
- nonebot_plugin_l4d2_server/{l4d2_image → l4_image}/download.py +11 -35
- nonebot_plugin_l4d2_server/l4_image/html_img.py +105 -0
- nonebot_plugin_l4d2_server/l4_image/image_tools.py +468 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/back.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/anne/back1.jpg +0 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/head/head.png +0 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/header/logo.png +0 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/header/player1.jpg +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.html +299 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/HYPixel11pxU-2.ttf +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.html +341 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.html +355 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/Tutumianhuatang-Bold-2.ttf +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/bilibili.svg +1 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/github.svg +1 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/l.svg +0 -1
- nonebot_plugin_l4d2_server/l4_image/img/template/m.svg +1 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/normal.html +247 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/vac.png +0 -0
- nonebot_plugin_l4d2_server/l4_image/img/template/vac_white.png +0 -0
- nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/w.svg +1 -1
- nonebot_plugin_l4d2_server/l4_image/model.py +15 -0
- nonebot_plugin_l4d2_server/{l4d2_image → l4_image}/vtfs.py +4 -2
- nonebot_plugin_l4d2_server/l4_request/__init__.py +143 -0
- nonebot_plugin_l4d2_server/l4_request/draw_msg.py +83 -0
- nonebot_plugin_l4d2_server/utils/api/api.py +10 -0
- nonebot_plugin_l4d2_server/utils/api/models.py +137 -0
- nonebot_plugin_l4d2_server/utils/api/request.py +337 -0
- nonebot_plugin_l4d2_server/utils/database/models.py +18 -0
- nonebot_plugin_l4d2_server/{l4d2_utils → utils}/utils.py +60 -63
- {nonebot_plugin_l4d2_server-0.6.6.dist-info → nonebot_plugin_l4d2_server-1.0.0a2.dist-info}/METADATA +57 -66
- nonebot_plugin_l4d2_server-1.0.0a2.dist-info/RECORD +96 -0
- {nonebot_plugin_l4d2_server-0.6.6.dist-info → nonebot_plugin_l4d2_server-1.0.0a2.dist-info}/WHEEL +1 -1
- nonebot_plugin_l4d2_server/data/L4D2/image/template/help.html +0 -263
- nonebot_plugin_l4d2_server/data/L4D2/image/template/help_dack.html +0 -231
- nonebot_plugin_l4d2_server/data/L4D2/image/template/m.svg +0 -1
- nonebot_plugin_l4d2_server/data/img/white.png +0 -0
- nonebot_plugin_l4d2_server/l4d2_anne/__init__.py +0 -95
- nonebot_plugin_l4d2_server/l4d2_anne/analysis.py +0 -54
- nonebot_plugin_l4d2_server/l4d2_anne/anne_telecom.py +0 -79
- nonebot_plugin_l4d2_server/l4d2_anne/server.py +0 -47
- nonebot_plugin_l4d2_server/l4d2_anne/startand.py +0 -17
- nonebot_plugin_l4d2_server/l4d2_anne/utils.py +0 -294
- nonebot_plugin_l4d2_server/l4d2_data/__init__.py +0 -105
- nonebot_plugin_l4d2_server/l4d2_data/config.py +0 -18
- nonebot_plugin_l4d2_server/l4d2_data/players.py +0 -100
- nonebot_plugin_l4d2_server/l4d2_data/serverip.py +0 -40
- nonebot_plugin_l4d2_server/l4d2_file/__init__.py +0 -222
- nonebot_plugin_l4d2_server/l4d2_file/ayromote.py +0 -64
- nonebot_plugin_l4d2_server/l4d2_file/input_json.py +0 -77
- nonebot_plugin_l4d2_server/l4d2_file/remote.py +0 -86
- nonebot_plugin_l4d2_server/l4d2_file/utils.py +0 -104
- nonebot_plugin_l4d2_server/l4d2_image/__init__.py +0 -125
- nonebot_plugin_l4d2_server/l4d2_image/htmlimg.py +0 -18
- nonebot_plugin_l4d2_server/l4d2_image/images.py +0 -92
- nonebot_plugin_l4d2_server/l4d2_image/one.py +0 -44
- nonebot_plugin_l4d2_server/l4d2_image/send_image_tool.py +0 -32
- nonebot_plugin_l4d2_server/l4d2_image/steam.py +0 -63
- nonebot_plugin_l4d2_server/l4d2_push/__init__.py +0 -225
- nonebot_plugin_l4d2_server/l4d2_queries/__init__.py +0 -326
- nonebot_plugin_l4d2_server/l4d2_queries/himi.py +0 -113
- nonebot_plugin_l4d2_server/l4d2_queries/local_ip.py +0 -41
- nonebot_plugin_l4d2_server/l4d2_queries/qqgroup.py +0 -370
- nonebot_plugin_l4d2_server/l4d2_queries/send_msg.py +0 -131
- nonebot_plugin_l4d2_server/l4d2_queries/utils.py +0 -212
- nonebot_plugin_l4d2_server/l4d2_server/__init__.py +0 -118
- nonebot_plugin_l4d2_server/l4d2_server/index.py +0 -0
- nonebot_plugin_l4d2_server/l4d2_server/rcon.py +0 -53
- nonebot_plugin_l4d2_server/l4d2_server/workshop.py +0 -82
- nonebot_plugin_l4d2_server/l4d2_update/__init__.py +0 -137
- nonebot_plugin_l4d2_server/l4d2_update/draw_update_log.py +0 -45
- nonebot_plugin_l4d2_server/l4d2_update/restart.py +0 -69
- nonebot_plugin_l4d2_server/l4d2_update/update.py +0 -53
- nonebot_plugin_l4d2_server/l4d2_utils/classcal.py +0 -53
- nonebot_plugin_l4d2_server/l4d2_utils/command.py +0 -23
- nonebot_plugin_l4d2_server/l4d2_utils/config.py +0 -201
- nonebot_plugin_l4d2_server/l4d2_utils/message.py +0 -59
- nonebot_plugin_l4d2_server/l4d2_utils/rule.py +0 -35
- nonebot_plugin_l4d2_server/l4d2_utils/seach.py +0 -43
- nonebot_plugin_l4d2_server/l4d2_utils/txt_to_img.py +0 -32
- nonebot_plugin_l4d2_server/l4d2_web/web.py +0 -277
- nonebot_plugin_l4d2_server/l4d2_web/webUI.py +0 -506
- nonebot_plugin_l4d2_server/l4d2_web/webUI_s.py +0 -94
- nonebot_plugin_l4d2_server-0.6.6.dist-info/RECORD +0 -70
- /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/anne.html +0 -0
- /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/group_ip.html +0 -0
- /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/ip.html +0 -0
- /nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/fingerprint.svg +0 -0
- /nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/vue.css +0 -0
- /nonebot_plugin_l4d2_server/{l4d2_data/database.py → l4_request/utils.py} +0 -0
- {nonebot_plugin_l4d2_server-0.6.6.dist-info → nonebot_plugin_l4d2_server-1.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
import random
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
import jinja2
|
6
|
+
from nonebot.log import logger
|
7
|
+
from nonebot_plugin_htmlrender import html_to_pic
|
8
|
+
|
9
|
+
from ..config import config
|
10
|
+
from ..utils.api.models import OutServer
|
11
|
+
|
12
|
+
# from .htmlimg import dict_to_dict_img
|
13
|
+
# from ..l4d2_anne.anne_telecom import ANNE_API
|
14
|
+
|
15
|
+
template_path = Path(__file__).parent / "img/template"
|
16
|
+
|
17
|
+
env = jinja2.Environment(
|
18
|
+
loader=jinja2.FileSystemLoader(template_path),
|
19
|
+
enable_async=True,
|
20
|
+
autoescape=True,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
async def server_ip_pic(server_dict: List[OutServer]):
|
25
|
+
"""
|
26
|
+
输入一个字典列表,输出图片
|
27
|
+
msg_dict:folder/name/map_/players/max_players/Players/[Name]
|
28
|
+
"""
|
29
|
+
for server_info in server_dict:
|
30
|
+
server_info["server"].player_count = (
|
31
|
+
0
|
32
|
+
if server_info["server"].player_count is None
|
33
|
+
else server_info["server"].player_count
|
34
|
+
)
|
35
|
+
server_info["server"].max_players = (
|
36
|
+
0
|
37
|
+
if server_info["server"].max_players is None
|
38
|
+
else server_info["server"].max_players
|
39
|
+
)
|
40
|
+
|
41
|
+
max_number = config.l4_players
|
42
|
+
if server_info.get("player"):
|
43
|
+
sorted_players = sorted(
|
44
|
+
server_info["player"],
|
45
|
+
key=lambda x: x.score,
|
46
|
+
reverse=True,
|
47
|
+
)[:max_number]
|
48
|
+
print(sorted_players)
|
49
|
+
server_info["player"] = sorted_players
|
50
|
+
else:
|
51
|
+
server_info["player"] = []
|
52
|
+
|
53
|
+
# server_info["server"].server_type= f"{server_info['server'].server_type}.svg"
|
54
|
+
print(server_dict)
|
55
|
+
pic = await get_server_img(server_dict)
|
56
|
+
if pic:
|
57
|
+
logger.success("正在输出图片")
|
58
|
+
else:
|
59
|
+
logger.warning("我的图图呢")
|
60
|
+
return pic
|
61
|
+
|
62
|
+
|
63
|
+
async def get_server_img(plugins: List[OutServer]) -> Optional[bytes]:
|
64
|
+
try:
|
65
|
+
if config.l4_style == "孤独摇滚":
|
66
|
+
template = env.get_template("Bocchi_The_Rock.html")
|
67
|
+
elif config.l4_style == "电玩像素":
|
68
|
+
template = env.get_template("Pixel.html")
|
69
|
+
elif config.l4_style == "缤纷彩虹":
|
70
|
+
template = env.get_template("Rainbow.html")
|
71
|
+
elif config.l4_style == "随机":
|
72
|
+
html_files = [
|
73
|
+
str(f.name) for f in template_path.rglob("*.html") if f.is_file()
|
74
|
+
]
|
75
|
+
template = env.get_template(random.choice(html_files))
|
76
|
+
else:
|
77
|
+
template = env.get_template("normal.html")
|
78
|
+
content = await template.render_async(
|
79
|
+
plugins=plugins,
|
80
|
+
max_count=config.l4_players,
|
81
|
+
)
|
82
|
+
return await html_to_pic(
|
83
|
+
content,
|
84
|
+
wait=0,
|
85
|
+
viewport={"width": 100, "height": 100},
|
86
|
+
template_path=f"file://{template_path.absolute()}",
|
87
|
+
)
|
88
|
+
except Exception as e:
|
89
|
+
logger.warning(f"Error in get_help_img: {e}")
|
90
|
+
return None
|
91
|
+
|
92
|
+
|
93
|
+
# async def server_group_ip_pic(msg_list: List[ServerGroup]) -> bytes:
|
94
|
+
# """
|
95
|
+
# 输入一个群组字典列表,输出图片
|
96
|
+
# msg_dict:folder/name/map_/players/max_players/Players/[Name]
|
97
|
+
# """
|
98
|
+
# template = env.get_template("group_ip.html")
|
99
|
+
# html = await template.render_async(plugins=msg_list)
|
100
|
+
# return await html_to_pic(
|
101
|
+
# html=html,
|
102
|
+
# wait=0,
|
103
|
+
# viewport={"width": 1100, "height": 800},
|
104
|
+
# template_path=f"file://{template_path.absolute()}",
|
105
|
+
# )
|
@@ -0,0 +1,468 @@
|
|
1
|
+
import math
|
2
|
+
import random
|
3
|
+
from io import BytesIO
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Tuple, Union
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
from httpx import get
|
9
|
+
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
10
|
+
|
11
|
+
TEXT_PATH = Path(__file__).parent / "texture2d"
|
12
|
+
BG_PATH = Path(__file__).parents[1] / "default_bg"
|
13
|
+
|
14
|
+
|
15
|
+
def get_div():
|
16
|
+
return Image.open(TEXT_PATH / "div.png")
|
17
|
+
|
18
|
+
|
19
|
+
async def sget(url: str):
|
20
|
+
async with httpx.AsyncClient(timeout=None) as client: # noqa: S113
|
21
|
+
return await client.get(url=url)
|
22
|
+
|
23
|
+
|
24
|
+
def get_status_icon(status: Union[int, bool]) -> Image.Image:
|
25
|
+
if status:
|
26
|
+
img = Image.open(TEXT_PATH / "yes.png")
|
27
|
+
else:
|
28
|
+
img = Image.open(TEXT_PATH / "no.png")
|
29
|
+
return img
|
30
|
+
|
31
|
+
|
32
|
+
def get_v4_footer():
|
33
|
+
return Image.open(TEXT_PATH / "footer.png")
|
34
|
+
|
35
|
+
|
36
|
+
def get_v4_bg(w: int, h: int, is_dark: bool = False, is_blur: bool = False):
|
37
|
+
ci_img = CustomizeImage(BG_PATH)
|
38
|
+
img = ci_img.get_image(None, w, h)
|
39
|
+
if is_blur:
|
40
|
+
img = img.filter(ImageFilter.GaussianBlur(radius=20))
|
41
|
+
if is_dark:
|
42
|
+
black_img = Image.new("RGBA", (w, h), (0, 0, 0, 180))
|
43
|
+
img.paste(black_img, (0, 0), black_img)
|
44
|
+
return img.convert("RGBA")
|
45
|
+
|
46
|
+
|
47
|
+
async def shift_image_hue(img: Image.Image, angle: float = 30) -> Image.Image:
|
48
|
+
alpha = img.getchannel("A")
|
49
|
+
img = img.convert("HSV")
|
50
|
+
|
51
|
+
pixels = img.load()
|
52
|
+
assert pixels is not None
|
53
|
+
hue_shift = angle
|
54
|
+
|
55
|
+
for y in range(img.height):
|
56
|
+
for x in range(img.width):
|
57
|
+
h, s, v = pixels[x, y] # type: ignore
|
58
|
+
h = (h + hue_shift) % 360
|
59
|
+
pixels[x, y] = (h, s, v) # type: ignore
|
60
|
+
|
61
|
+
img = img.convert("RGBA")
|
62
|
+
img.putalpha(alpha)
|
63
|
+
return img
|
64
|
+
|
65
|
+
|
66
|
+
async def get_pic(url: str, size: Optional[Tuple[int, int]] = None) -> Image.Image:
|
67
|
+
"""
|
68
|
+
从网络获取图片, 格式化为RGBA格式的指定尺寸
|
69
|
+
"""
|
70
|
+
async with httpx.AsyncClient(timeout=None) as client: # noqa: S113
|
71
|
+
resp = await client.get(url=url)
|
72
|
+
if resp.status_code != 200:
|
73
|
+
if size is None:
|
74
|
+
size = (960, 600)
|
75
|
+
return Image.new("RGBA", size)
|
76
|
+
pic = Image.open(BytesIO(resp.read()))
|
77
|
+
pic = pic.convert("RGBA")
|
78
|
+
if size is not None:
|
79
|
+
pic = pic.resize(size)
|
80
|
+
return pic
|
81
|
+
|
82
|
+
|
83
|
+
def draw_center_text_by_line(
|
84
|
+
img: ImageDraw.ImageDraw,
|
85
|
+
pos: Tuple[int, int],
|
86
|
+
text: str,
|
87
|
+
font: ImageFont.FreeTypeFont,
|
88
|
+
fill: Union[Tuple[int, int, int, int], str],
|
89
|
+
max_length: float,
|
90
|
+
not_center: bool = False,
|
91
|
+
) -> float:
|
92
|
+
pun = "。!?;!?…"
|
93
|
+
gun = "。!?;!?」』"
|
94
|
+
x, y = pos
|
95
|
+
|
96
|
+
if hasattr(font, "getsize"):
|
97
|
+
_, h = font.getsize("X") # type: ignore
|
98
|
+
else:
|
99
|
+
bbox = font.getbbox("X")
|
100
|
+
_, h = 0, bbox[3] - bbox[1]
|
101
|
+
|
102
|
+
line = ""
|
103
|
+
lenth = 0
|
104
|
+
anchor = "la" if not_center else "mm"
|
105
|
+
for index, char in enumerate(text):
|
106
|
+
if hasattr(font, "getsize"):
|
107
|
+
# 获取当前字符的宽度
|
108
|
+
size, _ = font.getsize(char) # type: ignore
|
109
|
+
else:
|
110
|
+
bbox = font.getbbox(char)
|
111
|
+
size, _ = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
112
|
+
lenth += size
|
113
|
+
line += char
|
114
|
+
if lenth < max_length and char not in pun and char != "\n":
|
115
|
+
pass
|
116
|
+
else:
|
117
|
+
if index + 1 < len(text) and text[index + 1] in gun:
|
118
|
+
pass
|
119
|
+
else:
|
120
|
+
line = line.replace("\n", "")
|
121
|
+
img.text((x, y), line, fill, font, anchor)
|
122
|
+
line, lenth = "", 0
|
123
|
+
y += h * 2.5
|
124
|
+
else:
|
125
|
+
img.text((x, y), line, fill, font, anchor)
|
126
|
+
return y
|
127
|
+
|
128
|
+
|
129
|
+
def draw_text_by_line(
|
130
|
+
img: Image.Image,
|
131
|
+
pos: Tuple[int, int],
|
132
|
+
text: str,
|
133
|
+
font: ImageFont.FreeTypeFont,
|
134
|
+
fill: Union[Tuple[int, int, int, int], str],
|
135
|
+
max_length: float,
|
136
|
+
center=False, # noqa: ANN001
|
137
|
+
line_space: Optional[float] = None,
|
138
|
+
) -> float:
|
139
|
+
"""
|
140
|
+
在图片上写长段文字, 自动换行
|
141
|
+
max_length单行最大长度, 单位像素
|
142
|
+
line_space 行间距, 单位像素, 默认是字体高度的0.3倍
|
143
|
+
"""
|
144
|
+
x, y = pos
|
145
|
+
|
146
|
+
if hasattr(font, "getsize"):
|
147
|
+
_, h = font.getsize("X") # type: ignore
|
148
|
+
else:
|
149
|
+
bbox = font.getbbox("X")
|
150
|
+
_, h = 0, bbox[3] - bbox[1]
|
151
|
+
|
152
|
+
y_add = math.ceil(1.3 * h) if line_space is None else math.ceil(h + line_space)
|
153
|
+
draw = ImageDraw.Draw(img)
|
154
|
+
row = "" # 存储本行文字
|
155
|
+
length = 0 # 记录本行长度
|
156
|
+
for character in text:
|
157
|
+
# 获取当前字符的宽度
|
158
|
+
if hasattr(font, "getsize"):
|
159
|
+
w, h = font.getsize(character) # type: ignore
|
160
|
+
else:
|
161
|
+
bbox = font.getbbox("X")
|
162
|
+
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
163
|
+
|
164
|
+
if length + w * 2 <= max_length:
|
165
|
+
row += character
|
166
|
+
length += w
|
167
|
+
else:
|
168
|
+
row += character
|
169
|
+
if center:
|
170
|
+
if hasattr(font, "getsize"):
|
171
|
+
font_size = font.getsize(row) # type: ignore
|
172
|
+
else:
|
173
|
+
bbox = font.getbbox(character)
|
174
|
+
font_size = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
175
|
+
x = math.ceil((img.size[0] - font_size[0]) / 2)
|
176
|
+
draw.text((x, y), row, font=font, fill=fill)
|
177
|
+
row = ""
|
178
|
+
length = 0
|
179
|
+
y += y_add
|
180
|
+
if row != "":
|
181
|
+
if center:
|
182
|
+
if hasattr(font, "getsize"):
|
183
|
+
font_size = font.getsize(row) # type: ignore
|
184
|
+
else:
|
185
|
+
bbox = font.getbbox(row)
|
186
|
+
font_size = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
187
|
+
x = math.ceil((img.size[0] - font_size[0]) / 2)
|
188
|
+
draw.text((x, y), row, font=font, fill=fill)
|
189
|
+
return y
|
190
|
+
|
191
|
+
|
192
|
+
def easy_paste(
|
193
|
+
im: Image.Image,
|
194
|
+
im_paste: Image.Image,
|
195
|
+
pos=(0, 0), # noqa: ANN001
|
196
|
+
direction="lt", # noqa: ANN001
|
197
|
+
):
|
198
|
+
"""
|
199
|
+
inplace method
|
200
|
+
快速粘贴, 自动获取被粘贴图像的坐标。
|
201
|
+
pos应当是粘贴点坐标,direction指定粘贴点方位,例如lt为左上
|
202
|
+
"""
|
203
|
+
x, y = pos
|
204
|
+
size_x, size_y = im_paste.size
|
205
|
+
if "d" in direction:
|
206
|
+
y = y - size_y
|
207
|
+
if "r" in direction:
|
208
|
+
x = x - size_x
|
209
|
+
if "c" in direction:
|
210
|
+
x = x - int(0.5 * size_x)
|
211
|
+
y = y - int(0.5 * size_y)
|
212
|
+
im.paste(im_paste, (x, y, x + size_x, y + size_y), im_paste)
|
213
|
+
|
214
|
+
|
215
|
+
def easy_alpha_composite(
|
216
|
+
im: Image.Image,
|
217
|
+
im_paste: Image.Image,
|
218
|
+
pos=(0, 0), # noqa: ANN001
|
219
|
+
direction="lt", # noqa: ANN001
|
220
|
+
) -> Image.Image:
|
221
|
+
"""
|
222
|
+
透明图像快速粘贴
|
223
|
+
"""
|
224
|
+
base = Image.new("RGBA", im.size)
|
225
|
+
easy_paste(base, im_paste, pos, direction)
|
226
|
+
return Image.alpha_composite(im, base)
|
227
|
+
|
228
|
+
|
229
|
+
async def get_qq_avatar(
|
230
|
+
qid: Optional[Union[int, str]] = None,
|
231
|
+
avatar_url: Optional[str] = None,
|
232
|
+
) -> Image.Image:
|
233
|
+
if qid:
|
234
|
+
avatar_url = f"http://q1.qlogo.cn/g?b=qq&nk={qid}&s=640"
|
235
|
+
elif avatar_url is None:
|
236
|
+
avatar_url = "https://q1.qlogo.cn/g?b=qq&nk=3399214199&s=640"
|
237
|
+
return Image.open(BytesIO((await sget(avatar_url)).content)).convert(
|
238
|
+
"RGBA",
|
239
|
+
)
|
240
|
+
|
241
|
+
|
242
|
+
async def draw_pic_with_ring(
|
243
|
+
pic: Image.Image,
|
244
|
+
size: int,
|
245
|
+
bg_color: Optional[Tuple[int, int, int]] = None,
|
246
|
+
is_ring: bool = True,
|
247
|
+
):
|
248
|
+
"""
|
249
|
+
:说明:
|
250
|
+
绘制一张带白色圆环的1:1比例图片。
|
251
|
+
|
252
|
+
:参数:
|
253
|
+
* pic: `Image.Image`: 要修改的图片。
|
254
|
+
* size: `int`: 最后传出图片的大小(1:1)。
|
255
|
+
* bg_color: `Optional[Tuple[int, int, int]]`: 是否指定圆环内背景颜色。
|
256
|
+
|
257
|
+
:返回:
|
258
|
+
* img: `Image.Image`: 图片对象
|
259
|
+
"""
|
260
|
+
ring_pic = Image.open(TEXT_PATH / "ring.png")
|
261
|
+
mask_pic = Image.open(TEXT_PATH / "mask.png")
|
262
|
+
img = Image.new("RGBA", (size, size))
|
263
|
+
mask = mask_pic.resize((size, size))
|
264
|
+
resize_pic = crop_center_img(pic, size, size)
|
265
|
+
if bg_color:
|
266
|
+
img_color = Image.new("RGBA", (size, size), bg_color)
|
267
|
+
img_color.paste(resize_pic, (0, 0), resize_pic)
|
268
|
+
img.paste(img_color, (0, 0), mask)
|
269
|
+
else:
|
270
|
+
img.paste(resize_pic, (0, 0), mask)
|
271
|
+
|
272
|
+
if is_ring:
|
273
|
+
ring = ring_pic.resize((size, size))
|
274
|
+
img.paste(ring, (0, 0), ring)
|
275
|
+
|
276
|
+
return img
|
277
|
+
|
278
|
+
|
279
|
+
def crop_center_img(
|
280
|
+
img: Image.Image,
|
281
|
+
based_w: int,
|
282
|
+
based_h: int,
|
283
|
+
) -> Image.Image:
|
284
|
+
# 确定图片的长宽
|
285
|
+
based_scale = "%.3f" % (based_w / based_h)
|
286
|
+
w, h = img.size
|
287
|
+
scale_f = "%.3f" % (w / h)
|
288
|
+
new_w = math.ceil(based_h * float(scale_f))
|
289
|
+
new_h = math.ceil(based_w / float(scale_f))
|
290
|
+
if scale_f > based_scale:
|
291
|
+
resize_img = img.resize((new_w, based_h), Image.Resampling.LANCZOS)
|
292
|
+
x1 = int(new_w / 2 - based_w / 2)
|
293
|
+
y1 = 0
|
294
|
+
x2 = int(new_w / 2 + based_w / 2)
|
295
|
+
y2 = based_h
|
296
|
+
else:
|
297
|
+
resize_img = img.resize((based_w, new_h), Image.Resampling.LANCZOS)
|
298
|
+
x1 = 0
|
299
|
+
y1 = int(new_h / 2 - based_h / 2)
|
300
|
+
x2 = based_w
|
301
|
+
y2 = int(new_h / 2 + based_h / 2)
|
302
|
+
return resize_img.crop((x1, y1, x2, y2))
|
303
|
+
|
304
|
+
|
305
|
+
async def get_color_bg(
|
306
|
+
based_w: int,
|
307
|
+
based_h: int,
|
308
|
+
bg_path: Optional[Path] = None,
|
309
|
+
without_mask: bool = False,
|
310
|
+
is_full: bool = False,
|
311
|
+
color: Optional[Tuple[int, int, int]] = None,
|
312
|
+
full_opacity: int = 200,
|
313
|
+
) -> Image.Image:
|
314
|
+
ci_img = CustomizeImage(bg_path) # type: ignore
|
315
|
+
img = ci_img.get_image(None, based_w, based_h)
|
316
|
+
if color is None:
|
317
|
+
color = ci_img.get_bg_color(img)
|
318
|
+
if is_full:
|
319
|
+
color_img = Image.new("RGBA", (based_w, based_h), color)
|
320
|
+
mask = Image.new(
|
321
|
+
"RGBA",
|
322
|
+
(based_w, based_h),
|
323
|
+
(255, 255, 255, full_opacity),
|
324
|
+
)
|
325
|
+
img.paste(color_img, (0, 0), mask)
|
326
|
+
elif not without_mask:
|
327
|
+
color_mask = Image.new("RGBA", (based_w, based_h), color)
|
328
|
+
enka_mask = Image.open(TEXT_PATH / "bg_mask.png").resize(
|
329
|
+
(based_w, based_h),
|
330
|
+
)
|
331
|
+
img.paste(color_mask, (0, 0), enka_mask)
|
332
|
+
return img
|
333
|
+
|
334
|
+
|
335
|
+
class CustomizeImage:
|
336
|
+
def __init__(self, bg_path: Path) -> None:
|
337
|
+
self.bg_path = bg_path
|
338
|
+
|
339
|
+
def get_image(
|
340
|
+
self,
|
341
|
+
image: Union[str, Image.Image, None],
|
342
|
+
based_w: int,
|
343
|
+
based_h: int,
|
344
|
+
) -> Image.Image:
|
345
|
+
# 获取背景图片
|
346
|
+
if isinstance(image, Image.Image):
|
347
|
+
edit_bg = image
|
348
|
+
elif image:
|
349
|
+
edit_bg = Image.open(BytesIO(get(image).content)).convert("RGBA")
|
350
|
+
else:
|
351
|
+
_lst = list(self.bg_path.iterdir())
|
352
|
+
if _lst:
|
353
|
+
path = random.choice(list(self.bg_path.iterdir()))
|
354
|
+
else:
|
355
|
+
path = random.choice(list(BG_PATH.iterdir()))
|
356
|
+
edit_bg = Image.open(path).convert("RGBA")
|
357
|
+
|
358
|
+
# 确定图片的长宽
|
359
|
+
return crop_center_img(edit_bg, based_w, based_h)
|
360
|
+
|
361
|
+
@staticmethod
|
362
|
+
def get_dominant_color(pil_img: Image.Image) -> Tuple[int, int, int]:
|
363
|
+
img = pil_img.copy()
|
364
|
+
img = img.convert("RGBA")
|
365
|
+
img = img.resize((1, 1), resample=0)
|
366
|
+
return img.getpixel((0, 0)) # type: ignore
|
367
|
+
|
368
|
+
@staticmethod
|
369
|
+
def get_bg_color(
|
370
|
+
edit_bg: Image.Image,
|
371
|
+
is_light: Optional[bool] = False,
|
372
|
+
) -> Tuple[int, int, int]:
|
373
|
+
# 获取背景主色
|
374
|
+
color = 8
|
375
|
+
q = edit_bg.quantize(colors=color, method=Image.Quantize.FASTOCTREE)
|
376
|
+
bg_color = (0, 0, 0)
|
377
|
+
based_light = 195 if is_light else 120
|
378
|
+
temp = 9999
|
379
|
+
for i in range(color):
|
380
|
+
bg = tuple(
|
381
|
+
q.getpalette()[ # type:ignore
|
382
|
+
i * 3 : (i * 3) + 3 # noqa:E203
|
383
|
+
],
|
384
|
+
)
|
385
|
+
light_value = bg[0] * 0.3 + bg[1] * 0.6 + bg[2] * 0.1
|
386
|
+
if abs(light_value - based_light) < temp: # noqa:E203
|
387
|
+
bg_color = bg
|
388
|
+
temp = abs(light_value - based_light)
|
389
|
+
return bg_color # type:ignore
|
390
|
+
|
391
|
+
@staticmethod
|
392
|
+
def get_text_color(bg_color: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
393
|
+
# 通过背景主色(bg_color)确定文字主色
|
394
|
+
r = 125
|
395
|
+
if max(*bg_color) > 255 - r:
|
396
|
+
r *= -1
|
397
|
+
return (
|
398
|
+
math.floor(min(bg_color[0] + r, 255)),
|
399
|
+
math.floor(min(bg_color[1] + r, 255)),
|
400
|
+
math.floor(min(bg_color[2] + r, 255)),
|
401
|
+
)
|
402
|
+
|
403
|
+
@staticmethod
|
404
|
+
def get_char_color(bg_color: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
405
|
+
r = 140
|
406
|
+
if max(*bg_color) > 255 - r:
|
407
|
+
r *= -1
|
408
|
+
return (
|
409
|
+
math.floor(bg_color[0] + 5 if bg_color[0] + r <= 255 else 255),
|
410
|
+
math.floor(bg_color[1] + 5 if bg_color[1] + r <= 255 else 255),
|
411
|
+
math.floor(bg_color[2] + 5 if bg_color[2] + r <= 255 else 255),
|
412
|
+
)
|
413
|
+
|
414
|
+
@staticmethod
|
415
|
+
def get_char_high_color(
|
416
|
+
bg_color: Tuple[int, int, int],
|
417
|
+
) -> Tuple[int, int, int]:
|
418
|
+
r = 140
|
419
|
+
d = 20
|
420
|
+
if max(*bg_color) > 255 - r:
|
421
|
+
r *= -1
|
422
|
+
return (
|
423
|
+
math.floor(bg_color[0] + d if bg_color[0] + r <= 255 else 255),
|
424
|
+
math.floor(bg_color[1] + d if bg_color[1] + r <= 255 else 255),
|
425
|
+
math.floor(bg_color[2] + d if bg_color[2] + r <= 255 else 255),
|
426
|
+
)
|
427
|
+
|
428
|
+
@staticmethod
|
429
|
+
def get_bg_detail_color(
|
430
|
+
bg_color: Tuple[int, int, int],
|
431
|
+
) -> Tuple[int, int, int]:
|
432
|
+
r = 140
|
433
|
+
if max(*bg_color) > 255 - r:
|
434
|
+
r *= -1
|
435
|
+
return (
|
436
|
+
math.floor(bg_color[0] - 20 if bg_color[0] + r <= 255 else 255),
|
437
|
+
math.floor(bg_color[1] - 20 if bg_color[1] + r <= 255 else 255),
|
438
|
+
math.floor(bg_color[2] - 20 if bg_color[2] + r <= 255 else 255),
|
439
|
+
)
|
440
|
+
|
441
|
+
@staticmethod
|
442
|
+
def get_highlight_color(
|
443
|
+
color: Tuple[int, int, int],
|
444
|
+
) -> Tuple[int, int, int]:
|
445
|
+
red_color = color[0]
|
446
|
+
green_color = color[1]
|
447
|
+
blue_color = color[2]
|
448
|
+
|
449
|
+
highlight_color = {
|
450
|
+
"red": red_color - 127 if red_color > 127 else 127,
|
451
|
+
"green": green_color - 127 if green_color > 127 else 127,
|
452
|
+
"blue": blue_color - 127 if blue_color > 127 else 127,
|
453
|
+
}
|
454
|
+
|
455
|
+
max_color = max(highlight_color.values())
|
456
|
+
|
457
|
+
name = ""
|
458
|
+
for _highlight_color in highlight_color:
|
459
|
+
if highlight_color[_highlight_color] == max_color:
|
460
|
+
name = str(_highlight_color)
|
461
|
+
|
462
|
+
if name == "red":
|
463
|
+
return red_color, highlight_color["green"], highlight_color["blue"]
|
464
|
+
if name == "green":
|
465
|
+
return highlight_color["red"], green_color, highlight_color["blue"]
|
466
|
+
if name == "blue":
|
467
|
+
return highlight_color["red"], highlight_color["green"], blue_color
|
468
|
+
return 0, 0, 0 # Error
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|