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.
Files changed (147) hide show
  1. nonebot_plugin_l4d2_server/__init__.py +10 -93
  2. nonebot_plugin_l4d2_server/__main__.py +140 -0
  3. nonebot_plugin_l4d2_server/config.py +35 -0
  4. nonebot_plugin_l4d2_server/data/font/loli.ttf +0 -0
  5. nonebot_plugin_l4d2_server/l4_anne/__init__.py +124 -0
  6. nonebot_plugin_l4d2_server/l4_anne/ranne.py +18 -0
  7. nonebot_plugin_l4d2_server/l4_help/Help.json +102 -0
  8. nonebot_plugin_l4d2_server/l4_help/__init__.py +57 -0
  9. nonebot_plugin_l4d2_server/l4_help/draw.py +206 -0
  10. nonebot_plugin_l4d2_server/l4_help/icon//344/273/213/347/273/215.png +0 -0
  11. nonebot_plugin_l4d2_server/l4_help/icon//344/273/273/345/212/241.png +0 -0
  12. nonebot_plugin_l4d2_server/l4_help/icon//344/277/241/346/201/257.png +0 -0
  13. nonebot_plugin_l4d2_server/l4_help/icon//345/205/254/345/221/212.png +0 -0
  14. nonebot_plugin_l4d2_server/l4_help/icon//345/210/200/345/211/221.png +0 -0
  15. nonebot_plugin_l4d2_server/l4_help/icon//345/210/207/346/215/242.png +0 -0
  16. nonebot_plugin_l4d2_server/l4_help/icon//345/210/240/351/231/244.png +0 -0
  17. nonebot_plugin_l4d2_server/l4_help/icon//345/210/267/346/226/260.png +0 -0
  18. nonebot_plugin_l4d2_server/l4_help/icon//345/215/241/347/273/204.png +0 -0
  19. nonebot_plugin_l4d2_server/l4_help/icon//345/223/252/351/207/214.png +0 -0
  20. nonebot_plugin_l4d2_server/l4_help/icon//345/234/260/345/233/276.png +0 -0
  21. nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/205/245.png +0 -0
  22. nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/207/272.png +0 -0
  23. nonebot_plugin_l4d2_server/l4_help/icon//345/275/261.png +0 -0
  24. nonebot_plugin_l4d2_server/l4_help/icon//346/213/274/345/233/276.png +0 -0
  25. nonebot_plugin_l4d2_server/l4_help/icon//346/216/242/347/264/242.png +0 -0
  26. nonebot_plugin_l4d2_server/l4_help/icon//346/216/250/351/200/201.png +0 -0
  27. nonebot_plugin_l4d2_server/l4_help/icon//346/224/266/351/233/206.png +0 -0
  28. nonebot_plugin_l4d2_server/l4_help/icon//346/224/273/347/225/245.png +0 -0
  29. nonebot_plugin_l4d2_server/l4_help/icon//346/233/264/346/226/260.png +0 -0
  30. nonebot_plugin_l4d2_server/l4_help/icon//346/235/220/346/226/231.png +0 -0
  31. nonebot_plugin_l4d2_server/l4_help/icon//346/237/245/350/257/242.png +0 -0
  32. nonebot_plugin_l4d2_server/l4_help/icon//346/240/241/351/252/214.png +0 -0
  33. nonebot_plugin_l4d2_server/l4_help/icon//346/257/217/346/234/210.png +0 -0
  34. nonebot_plugin_l4d2_server/l4_help/icon//346/267/261/346/270/212.png +0 -0
  35. nonebot_plugin_l4d2_server/l4_help/icon//346/267/273/345/212/240.png +0 -0
  36. nonebot_plugin_l4d2_server/l4_help/icon//346/270/205/351/231/244.png +0 -0
  37. nonebot_plugin_l4d2_server/l4_help/icon//347/212/266/346/200/201.png +0 -0
  38. nonebot_plugin_l4d2_server/l4_help/icon//347/255/276/345/210/260.png +0 -0
  39. nonebot_plugin_l4d2_server/l4_help/icon//347/273/221/345/256/232.png +0 -0
  40. nonebot_plugin_l4d2_server/l4_help/icon//350/241/250.png +0 -0
  41. nonebot_plugin_l4d2_server/l4_help/icon//350/241/250/346/203/205.png +0 -0
  42. nonebot_plugin_l4d2_server/l4_help/icon//350/247/222/350/211/262.png +0 -0
  43. nonebot_plugin_l4d2_server/l4_help/icon//350/256/260/345/275/225.png +0 -0
  44. nonebot_plugin_l4d2_server/l4_help/icon//351/205/215/347/275/256.png +0 -0
  45. nonebot_plugin_l4d2_server/l4_help/icon//351/207/215/345/220/257.png +0 -0
  46. nonebot_plugin_l4d2_server/l4_help/texture2d/badge.png +0 -0
  47. nonebot_plugin_l4d2_server/l4_help/texture2d/banner.png +0 -0
  48. nonebot_plugin_l4d2_server/l4_help/texture2d/bg.jpg +0 -0
  49. nonebot_plugin_l4d2_server/l4_help/texture2d/button.png +0 -0
  50. nonebot_plugin_l4d2_server/l4_help/texture2d/icon.png +0 -0
  51. nonebot_plugin_l4d2_server/l4_image/__init__.py +16 -0
  52. nonebot_plugin_l4d2_server/l4_image/anne_pil.py +17 -0
  53. nonebot_plugin_l4d2_server/l4_image/convert.py +175 -0
  54. nonebot_plugin_l4d2_server/{l4d2_image → l4_image}/download.py +11 -35
  55. nonebot_plugin_l4d2_server/l4_image/html_img.py +105 -0
  56. nonebot_plugin_l4d2_server/l4_image/image_tools.py +468 -0
  57. nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/back.png +0 -0
  58. nonebot_plugin_l4d2_server/l4_image/img/anne/back1.jpg +0 -0
  59. nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/head/head.png +0 -0
  60. nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/header/logo.png +0 -0
  61. nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/header/player1.jpg +0 -0
  62. nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.html +299 -0
  63. nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.png +0 -0
  64. nonebot_plugin_l4d2_server/l4_image/img/template/HYPixel11pxU-2.ttf +0 -0
  65. nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.html +341 -0
  66. nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.png +0 -0
  67. nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.html +355 -0
  68. nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.png +0 -0
  69. nonebot_plugin_l4d2_server/l4_image/img/template/Tutumianhuatang-Bold-2.ttf +0 -0
  70. nonebot_plugin_l4d2_server/l4_image/img/template/bilibili.svg +1 -0
  71. nonebot_plugin_l4d2_server/l4_image/img/template/github.svg +1 -0
  72. nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/l.svg +0 -1
  73. nonebot_plugin_l4d2_server/l4_image/img/template/m.svg +1 -0
  74. nonebot_plugin_l4d2_server/l4_image/img/template/normal.html +247 -0
  75. nonebot_plugin_l4d2_server/l4_image/img/template/vac.png +0 -0
  76. nonebot_plugin_l4d2_server/l4_image/img/template/vac_white.png +0 -0
  77. nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/w.svg +1 -1
  78. nonebot_plugin_l4d2_server/l4_image/model.py +15 -0
  79. nonebot_plugin_l4d2_server/{l4d2_image → l4_image}/vtfs.py +4 -2
  80. nonebot_plugin_l4d2_server/l4_request/__init__.py +143 -0
  81. nonebot_plugin_l4d2_server/l4_request/draw_msg.py +83 -0
  82. nonebot_plugin_l4d2_server/utils/api/api.py +10 -0
  83. nonebot_plugin_l4d2_server/utils/api/models.py +137 -0
  84. nonebot_plugin_l4d2_server/utils/api/request.py +337 -0
  85. nonebot_plugin_l4d2_server/utils/database/models.py +18 -0
  86. nonebot_plugin_l4d2_server/{l4d2_utils → utils}/utils.py +60 -63
  87. {nonebot_plugin_l4d2_server-0.6.6.dist-info → nonebot_plugin_l4d2_server-1.0.0a2.dist-info}/METADATA +57 -66
  88. nonebot_plugin_l4d2_server-1.0.0a2.dist-info/RECORD +96 -0
  89. {nonebot_plugin_l4d2_server-0.6.6.dist-info → nonebot_plugin_l4d2_server-1.0.0a2.dist-info}/WHEEL +1 -1
  90. nonebot_plugin_l4d2_server/data/L4D2/image/template/help.html +0 -263
  91. nonebot_plugin_l4d2_server/data/L4D2/image/template/help_dack.html +0 -231
  92. nonebot_plugin_l4d2_server/data/L4D2/image/template/m.svg +0 -1
  93. nonebot_plugin_l4d2_server/data/img/white.png +0 -0
  94. nonebot_plugin_l4d2_server/l4d2_anne/__init__.py +0 -95
  95. nonebot_plugin_l4d2_server/l4d2_anne/analysis.py +0 -54
  96. nonebot_plugin_l4d2_server/l4d2_anne/anne_telecom.py +0 -79
  97. nonebot_plugin_l4d2_server/l4d2_anne/server.py +0 -47
  98. nonebot_plugin_l4d2_server/l4d2_anne/startand.py +0 -17
  99. nonebot_plugin_l4d2_server/l4d2_anne/utils.py +0 -294
  100. nonebot_plugin_l4d2_server/l4d2_data/__init__.py +0 -105
  101. nonebot_plugin_l4d2_server/l4d2_data/config.py +0 -18
  102. nonebot_plugin_l4d2_server/l4d2_data/players.py +0 -100
  103. nonebot_plugin_l4d2_server/l4d2_data/serverip.py +0 -40
  104. nonebot_plugin_l4d2_server/l4d2_file/__init__.py +0 -222
  105. nonebot_plugin_l4d2_server/l4d2_file/ayromote.py +0 -64
  106. nonebot_plugin_l4d2_server/l4d2_file/input_json.py +0 -77
  107. nonebot_plugin_l4d2_server/l4d2_file/remote.py +0 -86
  108. nonebot_plugin_l4d2_server/l4d2_file/utils.py +0 -104
  109. nonebot_plugin_l4d2_server/l4d2_image/__init__.py +0 -125
  110. nonebot_plugin_l4d2_server/l4d2_image/htmlimg.py +0 -18
  111. nonebot_plugin_l4d2_server/l4d2_image/images.py +0 -92
  112. nonebot_plugin_l4d2_server/l4d2_image/one.py +0 -44
  113. nonebot_plugin_l4d2_server/l4d2_image/send_image_tool.py +0 -32
  114. nonebot_plugin_l4d2_server/l4d2_image/steam.py +0 -63
  115. nonebot_plugin_l4d2_server/l4d2_push/__init__.py +0 -225
  116. nonebot_plugin_l4d2_server/l4d2_queries/__init__.py +0 -326
  117. nonebot_plugin_l4d2_server/l4d2_queries/himi.py +0 -113
  118. nonebot_plugin_l4d2_server/l4d2_queries/local_ip.py +0 -41
  119. nonebot_plugin_l4d2_server/l4d2_queries/qqgroup.py +0 -370
  120. nonebot_plugin_l4d2_server/l4d2_queries/send_msg.py +0 -131
  121. nonebot_plugin_l4d2_server/l4d2_queries/utils.py +0 -212
  122. nonebot_plugin_l4d2_server/l4d2_server/__init__.py +0 -118
  123. nonebot_plugin_l4d2_server/l4d2_server/index.py +0 -0
  124. nonebot_plugin_l4d2_server/l4d2_server/rcon.py +0 -53
  125. nonebot_plugin_l4d2_server/l4d2_server/workshop.py +0 -82
  126. nonebot_plugin_l4d2_server/l4d2_update/__init__.py +0 -137
  127. nonebot_plugin_l4d2_server/l4d2_update/draw_update_log.py +0 -45
  128. nonebot_plugin_l4d2_server/l4d2_update/restart.py +0 -69
  129. nonebot_plugin_l4d2_server/l4d2_update/update.py +0 -53
  130. nonebot_plugin_l4d2_server/l4d2_utils/classcal.py +0 -53
  131. nonebot_plugin_l4d2_server/l4d2_utils/command.py +0 -23
  132. nonebot_plugin_l4d2_server/l4d2_utils/config.py +0 -201
  133. nonebot_plugin_l4d2_server/l4d2_utils/message.py +0 -59
  134. nonebot_plugin_l4d2_server/l4d2_utils/rule.py +0 -35
  135. nonebot_plugin_l4d2_server/l4d2_utils/seach.py +0 -43
  136. nonebot_plugin_l4d2_server/l4d2_utils/txt_to_img.py +0 -32
  137. nonebot_plugin_l4d2_server/l4d2_web/web.py +0 -277
  138. nonebot_plugin_l4d2_server/l4d2_web/webUI.py +0 -506
  139. nonebot_plugin_l4d2_server/l4d2_web/webUI_s.py +0 -94
  140. nonebot_plugin_l4d2_server-0.6.6.dist-info/RECORD +0 -70
  141. /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/anne.html +0 -0
  142. /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/group_ip.html +0 -0
  143. /nonebot_plugin_l4d2_server/{data/L4D2/image/template → l4_image/img/anne}/ip.html +0 -0
  144. /nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/fingerprint.svg +0 -0
  145. /nonebot_plugin_l4d2_server/{data/L4D2/image → l4_image/img}/template/vue.css +0 -0
  146. /nonebot_plugin_l4d2_server/{l4d2_data/database.py → l4_request/utils.py} +0 -0
  147. {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