nonebot-plugin-osubot 6.22.1__py3-none-any.whl → 6.26.4__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_osubot/api.py +13 -7
- nonebot_plugin_osubot/config.py +10 -10
- nonebot_plugin_osubot/draw/bmap.py +20 -22
- nonebot_plugin_osubot/draw/bp.py +3 -13
- nonebot_plugin_osubot/draw/catch_preview.py +2 -16
- nonebot_plugin_osubot/draw/echarts.py +8 -1
- nonebot_plugin_osubot/draw/info.py +59 -207
- nonebot_plugin_osubot/draw/info_templates/index.html +507 -0
- nonebot_plugin_osubot/draw/info_templates/output.css +2 -0
- nonebot_plugin_osubot/draw/map.py +9 -11
- nonebot_plugin_osubot/draw/osu_preview.py +50 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/css/style.css +258 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/README.md +109 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js +3 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js.map +1 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js +3 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js.map +1 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/beatmap.js +211 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/hitobject.js +29 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/point.js +55 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/scroll.js +45 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/timingpoint.js +35 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/LegacyRandom.js +81 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/PalpableCatchHitObject.js +53 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/bananashower.js +33 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/catch.js +211 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/fruit.js +21 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/juicestream.js +176 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/hitnote.js +21 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/holdnote.js +37 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/mania.js +164 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/preview.js +61 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/bezier2.js +33 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/catmullcurve.js +34 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/centripetalcatmullrom.js +30 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/circumstancedcircle.js +47 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curve.js +25 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curvetype.js +17 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/equaldistancemulticurve.js +70 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/linearbezier.js +40 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/hitcircle.js +85 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/slider.js +120 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/spinner.js +56 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/standard.js +170 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/donkat.js +40 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/drumroll.js +34 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/shaker.js +58 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/taiko.js +120 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/util.js +61 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/pic.html +110 -0
- nonebot_plugin_osubot/draw/rating.py +6 -3
- nonebot_plugin_osubot/draw/score.py +23 -81
- nonebot_plugin_osubot/draw/taiko_preview.py +14 -13
- nonebot_plugin_osubot/draw/templates/bpa_chart.html +1 -1
- nonebot_plugin_osubot/draw/utils.py +162 -16
- nonebot_plugin_osubot/file.py +184 -31
- nonebot_plugin_osubot/mania/__init__.py +9 -10
- nonebot_plugin_osubot/matcher/__init__.py +2 -0
- nonebot_plugin_osubot/matcher/bp_analyze.py +14 -9
- nonebot_plugin_osubot/matcher/guess.py +250 -294
- nonebot_plugin_osubot/matcher/map_convert.py +21 -13
- nonebot_plugin_osubot/matcher/medal.py +1 -1
- nonebot_plugin_osubot/matcher/osudl.py +5 -4
- nonebot_plugin_osubot/matcher/pr.py +0 -4
- nonebot_plugin_osubot/matcher/preview.py +10 -3
- nonebot_plugin_osubot/matcher/recommend.py +7 -12
- nonebot_plugin_osubot/mods.py +62 -61
- nonebot_plugin_osubot/network/first_response.py +1 -1
- nonebot_plugin_osubot/osufile/mods/AP.png +0 -0
- nonebot_plugin_osubot/osufile/mods/CL.png +0 -0
- nonebot_plugin_osubot/osufile/mods/DT.png +0 -0
- nonebot_plugin_osubot/osufile/mods/EZ.png +0 -0
- nonebot_plugin_osubot/osufile/mods/FI.png +0 -0
- nonebot_plugin_osubot/osufile/mods/FL.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HR.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HT.png +0 -0
- nonebot_plugin_osubot/osufile/mods/MR.png +0 -0
- nonebot_plugin_osubot/osufile/mods/NC.png +0 -0
- nonebot_plugin_osubot/osufile/mods/NF.png +0 -0
- nonebot_plugin_osubot/osufile/mods/PF.png +0 -0
- nonebot_plugin_osubot/osufile/mods/RX.png +0 -0
- nonebot_plugin_osubot/osufile/mods/SD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/SO.png +0 -0
- nonebot_plugin_osubot/osufile/mods/TD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/V2.png +0 -0
- nonebot_plugin_osubot/pp.py +7 -0
- nonebot_plugin_osubot/schema/__init__.py +0 -2
- nonebot_plugin_osubot/schema/beatmapsets.py +42 -0
- nonebot_plugin_osubot/schema/draw_info.py +54 -0
- nonebot_plugin_osubot/schema/score.py +2 -0
- nonebot_plugin_osubot/schema/user.py +1 -0
- {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/METADATA +18 -17
- {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/RECORD +95 -52
- nonebot_plugin_osubot-6.26.4.dist-info/WHEEL +4 -0
- nonebot_plugin_osubot/schema/sayo_beatmap.py +0 -59
- nonebot_plugin_osubot-6.22.1.dist-info/WHEEL +0 -4
|
@@ -3,7 +3,7 @@ from io import BytesIO
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
|
|
6
|
-
from PIL import ImageDraw, ImageFilter, ImageEnhance
|
|
6
|
+
from PIL import ImageDraw, ImageFilter, ImageEnhance
|
|
7
7
|
|
|
8
8
|
from ..info import get_bg
|
|
9
9
|
from ..mods import get_mods_list
|
|
@@ -14,7 +14,7 @@ from ..beatmap_stats_moder import with_mods
|
|
|
14
14
|
from ..pp import cal_pp, get_ss_pp, get_if_pp_ss_pp
|
|
15
15
|
from ..schema.score import Mod, UnifiedScore, NewStatistics
|
|
16
16
|
from ..api import osu_api, get_user_scores, get_user_info_data, get_ppysb_map_scores
|
|
17
|
-
from ..file import map_path, download_osu,
|
|
17
|
+
from ..file import map_path, download_osu, user_cache_path
|
|
18
18
|
from .utils import (
|
|
19
19
|
crop_bg,
|
|
20
20
|
draw_acc,
|
|
@@ -29,6 +29,9 @@ from .utils import (
|
|
|
29
29
|
filter_scores_with_regex,
|
|
30
30
|
trim_text_with_ellipsis,
|
|
31
31
|
draw_text_with_outline,
|
|
32
|
+
handle_team_image,
|
|
33
|
+
process_user_avatar_with_gif,
|
|
34
|
+
get_map_difficulty_arrays,
|
|
32
35
|
)
|
|
33
36
|
from .static import (
|
|
34
37
|
Image,
|
|
@@ -79,8 +82,6 @@ async def draw_score(
|
|
|
79
82
|
scores = await get_user_scores(uid, mode, "recent", source=source, legacy_only=not is_lazer, limit=best)
|
|
80
83
|
if not scores:
|
|
81
84
|
raise NetworkError("未查询到游玩记录")
|
|
82
|
-
if not is_lazer:
|
|
83
|
-
scores = [i for i in scores if Mod(acronym="CL") in i.mods]
|
|
84
85
|
if project in ("recent", "pr"):
|
|
85
86
|
if len(scores) < best:
|
|
86
87
|
raise NetworkError("未查询到游玩记录")
|
|
@@ -96,20 +97,18 @@ async def draw_score(
|
|
|
96
97
|
raise Exception("Project Error")
|
|
97
98
|
# 从官网获取信息
|
|
98
99
|
path = map_path / str(score.beatmap.set_id)
|
|
99
|
-
|
|
100
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
101
101
|
osu = path / f"{score.beatmap.id}.osu"
|
|
102
102
|
task2 = asyncio.create_task(osu_api("map", map_id=score.beatmap.id))
|
|
103
103
|
if not osu.exists():
|
|
104
104
|
await download_osu(score.beatmap.set_id, score.beatmap.id)
|
|
105
105
|
info = await task1
|
|
106
106
|
user_path = user_cache_path / str(info.id)
|
|
107
|
-
|
|
108
|
-
user_path.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
user_path.mkdir(parents=True, exist_ok=True)
|
|
109
108
|
map_json = await task2
|
|
110
109
|
# 判断是否开启lazer模式
|
|
111
110
|
if source == "osu":
|
|
112
|
-
score = cal_score_info(is_lazer, score)
|
|
111
|
+
score = cal_score_info(is_lazer, score, source)
|
|
113
112
|
return await draw_score_pic(score, info, map_json, "", is_lazer, source)
|
|
114
113
|
|
|
115
114
|
|
|
@@ -149,10 +148,6 @@ async def get_score_data(
|
|
|
149
148
|
]
|
|
150
149
|
else:
|
|
151
150
|
score_ls = await get_ppysb_map_scores(map_json["checksum"], uid, mode)
|
|
152
|
-
if not is_lazer:
|
|
153
|
-
score_ls = [i for i in score_ls if Mod(acronym="CL") in i.mods]
|
|
154
|
-
for i in score_ls:
|
|
155
|
-
i.mods.remove(Mod(acronym="CL"))
|
|
156
151
|
if not score_ls:
|
|
157
152
|
raise NetworkError("未查询到游玩记录")
|
|
158
153
|
if mods:
|
|
@@ -175,18 +170,16 @@ async def get_score_data(
|
|
|
175
170
|
score_ls.sort(key=lambda x: x.total_score, reverse=True)
|
|
176
171
|
score = score_ls[0]
|
|
177
172
|
path = map_path / str(map_json["beatmapset_id"])
|
|
178
|
-
|
|
179
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
180
174
|
osu = path / f"{mapid}.osu"
|
|
181
175
|
if not osu.exists():
|
|
182
176
|
await download_osu(map_json["beatmapset_id"], mapid)
|
|
183
177
|
info = await task
|
|
184
178
|
user_path = user_cache_path / str(info.id)
|
|
185
|
-
|
|
186
|
-
user_path.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
user_path.mkdir(parents=True, exist_ok=True)
|
|
187
180
|
# 判断是否开启lazer模式
|
|
188
181
|
if source == "osu":
|
|
189
|
-
score = cal_score_info(is_lazer, score)
|
|
182
|
+
score = cal_score_info(is_lazer, score, source)
|
|
190
183
|
return await draw_score_pic(score, info, map_json, grank, is_lazer, source)
|
|
191
184
|
|
|
192
185
|
|
|
@@ -195,8 +188,7 @@ async def draw_score_pic(score_info: UnifiedScore, info: UnifiedUser, map_json,
|
|
|
195
188
|
original_mapinfo = mapinfo.copy()
|
|
196
189
|
mapinfo = with_mods(mapinfo, score_info, score_info.mods)
|
|
197
190
|
path = map_path / str(mapinfo.beatmapset_id)
|
|
198
|
-
|
|
199
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
200
192
|
# pp
|
|
201
193
|
osu = path / f"{mapinfo.id}.osu"
|
|
202
194
|
pp_info = cal_pp(score_info, str(osu.absolute()), is_lazer)
|
|
@@ -265,8 +257,9 @@ async def draw_score_pic(score_info: UnifiedScore, info: UnifiedUser, map_json,
|
|
|
265
257
|
for mods_num, s_mods in enumerate(score_info.mods):
|
|
266
258
|
mods_bg = osufile / "mods" / f"{s_mods.acronym}.png"
|
|
267
259
|
try:
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
with Image.open(mods_bg) as mods_img:
|
|
261
|
+
mods_img = mods_img.convert("RGBA")
|
|
262
|
+
im.alpha_composite(mods_img, (880 + 50 * mods_num, 100))
|
|
270
263
|
except FileNotFoundError:
|
|
271
264
|
pass
|
|
272
265
|
# 成绩S-F
|
|
@@ -289,21 +282,10 @@ async def draw_score_pic(score_info: UnifiedScore, info: UnifiedUser, map_json,
|
|
|
289
282
|
im = draw_acc(im, score_info.accuracy, score_info.ruleset_id)
|
|
290
283
|
# 地区
|
|
291
284
|
country = osufile / "flags" / f"{info.country_code}.png"
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if not team_path.exists():
|
|
297
|
-
team_img = await get_projectimg(info.team.flag_url)
|
|
298
|
-
team_img = Image.open(team_img).convert("RGBA")
|
|
299
|
-
team_img.save(team_path)
|
|
300
|
-
try:
|
|
301
|
-
team_img = Image.open(team_path).convert("RGBA").resize((80, 40))
|
|
302
|
-
im.alpha_composite(team_img, (208, 660))
|
|
303
|
-
except UnidentifiedImageError:
|
|
304
|
-
team_path.unlink()
|
|
305
|
-
raise NetworkError("team 图片下载错误,请重试!")
|
|
306
|
-
draw.text((297, 675), info.team.name, font=Torus_Regular_20, anchor="lt")
|
|
285
|
+
with Image.open(country) as country_img:
|
|
286
|
+
country_bg = country_img.convert("RGBA").resize((66, 45))
|
|
287
|
+
im.alpha_composite(country_bg, (208, 597))
|
|
288
|
+
await handle_team_image(im, draw, info, (208, 660), (80, 40), (297, 675), Torus_Regular_20)
|
|
307
289
|
# supporter
|
|
308
290
|
# if info.is_supporter:
|
|
309
291
|
# im.alpha_composite(SupporterBg.resize((40, 40)), (250, 640))
|
|
@@ -326,13 +308,7 @@ async def draw_score_pic(score_info: UnifiedScore, info: UnifiedUser, map_json,
|
|
|
326
308
|
temp_accuracy += 1
|
|
327
309
|
mapinfo.cs = max(4.0, min(temp_accuracy, 7.0))
|
|
328
310
|
# cs, ar, od, hp
|
|
329
|
-
mapdiff =
|
|
330
|
-
original_mapdiff = [
|
|
331
|
-
original_mapinfo.cs,
|
|
332
|
-
original_mapinfo.drain,
|
|
333
|
-
original_mapinfo.accuracy,
|
|
334
|
-
original_mapinfo.ar,
|
|
335
|
-
]
|
|
311
|
+
mapdiff, original_mapdiff = get_map_difficulty_arrays(mapinfo, original_mapinfo)
|
|
336
312
|
|
|
337
313
|
for num, (orig, new) in enumerate(zip(original_mapdiff, mapdiff)):
|
|
338
314
|
orig_difflen = int(400 * max(0, orig) / 10) if orig <= 10 else 400
|
|
@@ -667,39 +643,7 @@ async def draw_score_pic(score_info: UnifiedScore, info: UnifiedUser, map_json,
|
|
|
667
643
|
user_icon = await open_user_icon(info, source)
|
|
668
644
|
_ = asyncio.create_task(update_map(mapinfo.beatmapset_id, mapinfo.id))
|
|
669
645
|
_ = asyncio.create_task(update_icon(info))
|
|
670
|
-
|
|
671
|
-
if not getattr(user_icon, "is_animated", False):
|
|
672
|
-
icon_bg = user_icon.convert("RGBA").resize((170, 170))
|
|
673
|
-
icon_img = draw_fillet(icon_bg, 15)
|
|
674
|
-
im.alpha_composite(icon_img, (27, 532))
|
|
675
|
-
byt = BytesIO()
|
|
676
|
-
im.convert("RGB").save(byt, "jpeg")
|
|
677
|
-
im.close()
|
|
678
|
-
user_icon.close()
|
|
679
|
-
return byt
|
|
680
|
-
for gif_frame in ImageSequence.Iterator(user_icon):
|
|
681
|
-
# 将 GIF 图片中的每一帧转换为 RGBA 模式
|
|
682
|
-
gif_frame = gif_frame.convert("RGBA").resize((170, 170))
|
|
683
|
-
gif_frame = draw_fillet(gif_frame, 15)
|
|
684
|
-
# 创建一个新的 RGBA 图片,将 PNG 图片作为背景,将当前帧添加到背景上
|
|
685
|
-
rgba_frame = Image.new("RGBA", im.size, (0, 0, 0, 0))
|
|
686
|
-
rgba_frame.paste(im, (0, 0), im)
|
|
687
|
-
rgba_frame.paste(gif_frame, (27, 532), gif_frame)
|
|
688
|
-
# 将 RGBA 图片转换为 RGB 模式,并添加到 GIF 图片中
|
|
689
|
-
gif_frames.append(rgba_frame)
|
|
690
|
-
gif_bytes = BytesIO()
|
|
691
|
-
# 保存 GIF 图片
|
|
692
|
-
gif_frames[0].save(
|
|
693
|
-
gif_bytes,
|
|
694
|
-
format="gif",
|
|
695
|
-
save_all=True,
|
|
696
|
-
append_images=gif_frames[1:],
|
|
697
|
-
duration=user_icon.info["duration"],
|
|
698
|
-
)
|
|
699
|
-
# 输出
|
|
700
|
-
gif_frames[0].close()
|
|
701
|
-
user_icon.close()
|
|
702
|
-
return gif_bytes
|
|
646
|
+
return await process_user_avatar_with_gif(im, user_icon, (27, 532), (170, 170), 15)
|
|
703
647
|
|
|
704
648
|
|
|
705
649
|
def cal_legacy_acc(statistics: NewStatistics) -> float:
|
|
@@ -801,12 +745,10 @@ def cal_legacy_rank(score_info: UnifiedScore, is_hidden: bool):
|
|
|
801
745
|
return "N/A"
|
|
802
746
|
|
|
803
747
|
|
|
804
|
-
def cal_score_info(is_lazer: bool, score_info: UnifiedScore) -> UnifiedScore:
|
|
748
|
+
def cal_score_info(is_lazer: bool, score_info: UnifiedScore, source: str = "osu") -> UnifiedScore:
|
|
805
749
|
if is_lazer:
|
|
806
750
|
score_info.legacy_total_score = score_info.total_score
|
|
807
|
-
if not is_lazer and
|
|
808
|
-
score_info.mods = [i for i in score_info.mods if i.acronym != "CL"]
|
|
809
|
-
if score_info.ruleset_id == 3 and not is_lazer:
|
|
751
|
+
if score_info.ruleset_id == 3 and not is_lazer and source != "ppysb":
|
|
810
752
|
score_info.accuracy = cal_legacy_acc(score_info.statistics)
|
|
811
753
|
if not is_lazer:
|
|
812
754
|
is_hidden = any(i in score_info.mods for i in (Mod(acronym="HD"), Mod(acronym="FL"), Mod(acronym="FI")))
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from PIL import Image, ImageDraw
|
|
6
6
|
|
|
7
|
-
from .static import Torus_Regular_8, Torus_Regular_15
|
|
7
|
+
from .static import Torus_Regular_8, Torus_Regular_15
|
|
8
8
|
|
|
9
9
|
HIT_DON = 0x00
|
|
10
10
|
HIT_KAT = 0x01
|
|
@@ -35,20 +35,21 @@ def map_to_image(map_data) -> BytesIO:
|
|
|
35
35
|
img = Image.new(mode="RGB", size=(3000, 30000), color=0x121212)
|
|
36
36
|
draw = ImageDraw.Draw(img)
|
|
37
37
|
|
|
38
|
-
title_font = Torus_Regular_30
|
|
39
|
-
semi_font = Torus_Regular_25
|
|
40
|
-
reg_font = Torus_Regular_20
|
|
38
|
+
# title_font = Torus_Regular_30
|
|
39
|
+
# semi_font = Torus_Regular_25
|
|
40
|
+
# reg_font = Torus_Regular_20
|
|
41
41
|
small_font = Torus_Regular_15
|
|
42
42
|
tiny_font = Torus_Regular_8
|
|
43
43
|
|
|
44
|
-
draw.text(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
draw.text((LEFT_MARGIN, 90), "by " + map_data.creator, font=semi_font, fill="#CCC")
|
|
51
|
-
draw.text((LEFT_MARGIN, 135), "HP = " + str(map_data.hp) +
|
|
44
|
+
# draw.text(
|
|
45
|
+
# (LEFT_MARGIN, 40),
|
|
46
|
+
# map_data.artist + " - " + map_data.title + " [" + map_data.diff + "]",
|
|
47
|
+
# font=title_font,
|
|
48
|
+
# fill="#FFF",
|
|
49
|
+
# )
|
|
50
|
+
# draw.text((LEFT_MARGIN, 90), "by " + map_data.creator, font=semi_font, fill="#CCC")
|
|
51
|
+
# draw.text((LEFT_MARGIN, 135), "HP = " + str(map_data.hp) +
|
|
52
|
+
# " OD = " + str(map_data.od), font=reg_font, fill="#AAA")
|
|
52
53
|
|
|
53
54
|
max_meter = 0
|
|
54
55
|
timing_sections = [] # (start, beat_ms, meter, [list of hit_object])
|
|
@@ -67,7 +68,7 @@ def map_to_image(map_data) -> BytesIO:
|
|
|
67
68
|
(map_data.timing_points[i][0], map_data.timing_points[i][1], map_data.timing_points[i][2], current_list)
|
|
68
69
|
)
|
|
69
70
|
|
|
70
|
-
y_start =
|
|
71
|
+
y_start = 40
|
|
71
72
|
max_x = 0
|
|
72
73
|
max_y = 0
|
|
73
74
|
bar_number = 1
|
|
@@ -8,7 +8,7 @@ from difflib import SequenceMatcher
|
|
|
8
8
|
|
|
9
9
|
from PIL.ImageFile import ImageFile
|
|
10
10
|
from matplotlib.figure import Figure
|
|
11
|
-
from PIL import ImageDraw, ImageFilter, ImageEnhance, UnidentifiedImageError
|
|
11
|
+
from PIL import ImageDraw, ImageFilter, ImageEnhance, UnidentifiedImageError, ImageSequence
|
|
12
12
|
|
|
13
13
|
from ..schema.user import UnifiedUser
|
|
14
14
|
from ..schema import SeasonalBackgrounds
|
|
@@ -130,14 +130,10 @@ async def crop_bg(size: tuple[int, int], path: Union[str, Path, BytesIO, Image.I
|
|
|
130
130
|
if not isinstance(path, Image.Image):
|
|
131
131
|
try:
|
|
132
132
|
bg = Image.open(path).convert("RGBA")
|
|
133
|
-
except UnidentifiedImageError:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
url = random.choice(pic.backgrounds).url
|
|
138
|
-
res = await safe_async_get(url)
|
|
139
|
-
bg = Image.open(BytesIO(res.content)).convert("RGBA")
|
|
140
|
-
except FileNotFoundError:
|
|
133
|
+
except (UnidentifiedImageError, FileNotFoundError):
|
|
134
|
+
# Refactored: combine duplicate exception handling
|
|
135
|
+
if isinstance(path, (str, Path)) and Path(path).exists():
|
|
136
|
+
os.remove(path)
|
|
141
137
|
data = await get_seasonal_bg()
|
|
142
138
|
pic = SeasonalBackgrounds(**data)
|
|
143
139
|
url = random.choice(pic.backgrounds).url
|
|
@@ -145,8 +141,8 @@ async def crop_bg(size: tuple[int, int], path: Union[str, Path, BytesIO, Image.I
|
|
|
145
141
|
bg = Image.open(BytesIO(res.content)).convert("RGBA")
|
|
146
142
|
else:
|
|
147
143
|
bg = path
|
|
148
|
-
bg_w, bg_h = bg.size
|
|
149
|
-
fix_w, fix_h = size
|
|
144
|
+
bg_w, bg_h = bg.size
|
|
145
|
+
fix_w, fix_h = size
|
|
150
146
|
# 固定比例
|
|
151
147
|
fix_scale = fix_h / fix_w
|
|
152
148
|
# 图片比例
|
|
@@ -229,8 +225,7 @@ def calc_songlen(length: int) -> str:
|
|
|
229
225
|
|
|
230
226
|
async def open_user_icon(info: UnifiedUser, source) -> Image:
|
|
231
227
|
path = user_cache_path / str(info.id)
|
|
232
|
-
|
|
233
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
234
229
|
for file_path in path.glob("icon*.*"):
|
|
235
230
|
# 检查文件是否为图片格式
|
|
236
231
|
if file_path.suffix.lower() in [".jpg", ".png", ".jpeg", ".gif", ".bmp"]:
|
|
@@ -249,9 +244,7 @@ async def open_user_icon(info: UnifiedUser, source) -> Image:
|
|
|
249
244
|
|
|
250
245
|
|
|
251
246
|
def is_close(n1, n2) -> bool:
|
|
252
|
-
|
|
253
|
-
return True
|
|
254
|
-
return False
|
|
247
|
+
return abs(n1 - n2) < 0.01
|
|
255
248
|
|
|
256
249
|
|
|
257
250
|
async def update_icon(info: UnifiedUser):
|
|
@@ -454,3 +447,156 @@ def draw_text_with_outline(draw, position, text, font, anchor, fill):
|
|
|
454
447
|
fill=(0, 0, 0, 255),
|
|
455
448
|
)
|
|
456
449
|
draw.text(position, text, font=font, anchor=anchor, fill=fill)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
async def process_user_avatar_with_gif(
|
|
453
|
+
base_image: Image.Image,
|
|
454
|
+
user_icon: Image.Image,
|
|
455
|
+
position: tuple[int, int],
|
|
456
|
+
size: tuple[int, int],
|
|
457
|
+
corner_radius: int,
|
|
458
|
+
) -> BytesIO:
|
|
459
|
+
"""
|
|
460
|
+
Process user avatar (static or animated) and composite it onto the base image.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
base_image: The base image to composite the avatar onto
|
|
464
|
+
user_icon: The user avatar image (can be animated GIF or static)
|
|
465
|
+
position: (x, y) position to place the avatar on the base image
|
|
466
|
+
size: (width, height) to resize the avatar to
|
|
467
|
+
corner_radius: Radius for rounded corners
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
BytesIO containing the final image (JPEG for static, GIF for animated)
|
|
471
|
+
"""
|
|
472
|
+
if not getattr(user_icon, "is_animated", False):
|
|
473
|
+
icon_bg = user_icon.convert("RGBA").resize(size)
|
|
474
|
+
icon_img = draw_fillet(icon_bg, corner_radius)
|
|
475
|
+
base_image.alpha_composite(icon_img, position)
|
|
476
|
+
byt = BytesIO()
|
|
477
|
+
base_image.convert("RGB").save(byt, "jpeg")
|
|
478
|
+
base_image.close()
|
|
479
|
+
user_icon.close()
|
|
480
|
+
return byt
|
|
481
|
+
|
|
482
|
+
gif_frames = []
|
|
483
|
+
for gif_frame in ImageSequence.Iterator(user_icon):
|
|
484
|
+
# 将 GIF 图片中的每一帧转换为 RGBA 模式
|
|
485
|
+
gif_frame = gif_frame.convert("RGBA").resize(size)
|
|
486
|
+
gif_frame = draw_fillet(gif_frame, corner_radius)
|
|
487
|
+
# 创建一个新的 RGBA 图片,将 PNG 图片作为背景,将当前帧添加到背景上
|
|
488
|
+
rgba_frame = Image.new("RGBA", base_image.size, (0, 0, 0, 0))
|
|
489
|
+
rgba_frame.paste(base_image, (0, 0), base_image)
|
|
490
|
+
rgba_frame.paste(gif_frame, position, gif_frame)
|
|
491
|
+
# 将 RGBA 图片转换为 RGB 模式,并添加到 GIF 图片中
|
|
492
|
+
gif_frames.append(rgba_frame)
|
|
493
|
+
gif_bytes = BytesIO()
|
|
494
|
+
# 保存 GIF 图片
|
|
495
|
+
gif_frames[0].save(
|
|
496
|
+
gif_bytes,
|
|
497
|
+
format="gif",
|
|
498
|
+
save_all=True,
|
|
499
|
+
append_images=gif_frames[1:],
|
|
500
|
+
duration=user_icon.info["duration"],
|
|
501
|
+
)
|
|
502
|
+
# 输出
|
|
503
|
+
gif_frames[0].close()
|
|
504
|
+
user_icon.close()
|
|
505
|
+
return gif_bytes
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
async def handle_team_image(
|
|
509
|
+
base_image: Image.Image,
|
|
510
|
+
draw_context: Optional[ImageDraw.Draw],
|
|
511
|
+
info,
|
|
512
|
+
position: tuple[int, int],
|
|
513
|
+
size: tuple[int, int],
|
|
514
|
+
text_position: Optional[tuple[int, int]] = None,
|
|
515
|
+
text_font: Optional[object] = None,
|
|
516
|
+
) -> None:
|
|
517
|
+
"""
|
|
518
|
+
Download and composite team flag image onto the base image, optionally drawing team name.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
base_image: The base image to composite the team flag onto
|
|
522
|
+
draw_context: PIL ImageDraw context for drawing text (None if no text needed)
|
|
523
|
+
info: User info object containing team information
|
|
524
|
+
position: (x, y) position to place the team flag on the base image
|
|
525
|
+
size: (width, height) to resize the team flag to
|
|
526
|
+
text_position: Optional (x, y) position to draw team name text
|
|
527
|
+
text_font: Optional font for team name text
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
NetworkError: If team image download fails
|
|
531
|
+
"""
|
|
532
|
+
from ..exceptions import NetworkError
|
|
533
|
+
|
|
534
|
+
if info.team and info.team.flag_url:
|
|
535
|
+
team_path = team_cache_path / f"{info.team.id}.png"
|
|
536
|
+
if not team_path.exists():
|
|
537
|
+
team_img = await get_projectimg(info.team.flag_url)
|
|
538
|
+
with Image.open(team_img) as team_image:
|
|
539
|
+
team_image = team_image.convert("RGBA")
|
|
540
|
+
team_image.save(team_path)
|
|
541
|
+
try:
|
|
542
|
+
with Image.open(team_path) as team_img:
|
|
543
|
+
team_img = team_img.convert("RGBA").resize(size)
|
|
544
|
+
base_image.alpha_composite(team_img, position)
|
|
545
|
+
except UnidentifiedImageError:
|
|
546
|
+
team_path.unlink()
|
|
547
|
+
raise NetworkError("team 图片下载错误,请重试!")
|
|
548
|
+
|
|
549
|
+
# Draw team name if text parameters are provided
|
|
550
|
+
if draw_context and text_position and text_font and info.team.name:
|
|
551
|
+
draw_context.text(text_position, info.team.name, font=text_font, anchor="lt")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def load_osu_file_and_setup_template(template_path: str, beatmap_id: int, beatmapset_id: int):
|
|
555
|
+
"""
|
|
556
|
+
Load OSU file and setup Jinja2 template environment.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
template_path: Path to template directory
|
|
560
|
+
beatmap_id: Beatmap ID
|
|
561
|
+
beatmapset_id: Beatmapset ID
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Tuple of (osu_file_content, template_object)
|
|
565
|
+
"""
|
|
566
|
+
import jinja2
|
|
567
|
+
|
|
568
|
+
path = map_path / str(beatmapset_id)
|
|
569
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
570
|
+
osu = path / f"{beatmap_id}.osu"
|
|
571
|
+
if not osu.exists():
|
|
572
|
+
await download_osu(beatmapset_id, beatmap_id)
|
|
573
|
+
with open(osu, encoding="utf-8-sig") as f:
|
|
574
|
+
osu_file = f.read()
|
|
575
|
+
template_name = "pic.html"
|
|
576
|
+
template_env = jinja2.Environment( # noqa: S701
|
|
577
|
+
loader=jinja2.FileSystemLoader(template_path),
|
|
578
|
+
enable_async=True,
|
|
579
|
+
)
|
|
580
|
+
template = template_env.get_template(template_name)
|
|
581
|
+
return osu_file, template
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_map_difficulty_arrays(mapinfo, original_mapinfo):
|
|
585
|
+
"""
|
|
586
|
+
Extract map difficulty arrays for comparison.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
mapinfo: Current map info with difficulty stats
|
|
590
|
+
original_mapinfo: Original map info with difficulty stats
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Tuple of (mapdiff, original_mapdiff) arrays with [cs, drain, accuracy, ar]
|
|
594
|
+
"""
|
|
595
|
+
mapdiff = [mapinfo.cs, mapinfo.drain, mapinfo.accuracy, mapinfo.ar]
|
|
596
|
+
original_mapdiff = [
|
|
597
|
+
original_mapinfo.cs,
|
|
598
|
+
original_mapinfo.drain,
|
|
599
|
+
original_mapinfo.accuracy,
|
|
600
|
+
original_mapinfo.ar,
|
|
601
|
+
]
|
|
602
|
+
return mapdiff, original_mapdiff
|