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.
Files changed (97) hide show
  1. nonebot_plugin_osubot/api.py +13 -7
  2. nonebot_plugin_osubot/config.py +10 -10
  3. nonebot_plugin_osubot/draw/bmap.py +20 -22
  4. nonebot_plugin_osubot/draw/bp.py +3 -13
  5. nonebot_plugin_osubot/draw/catch_preview.py +2 -16
  6. nonebot_plugin_osubot/draw/echarts.py +8 -1
  7. nonebot_plugin_osubot/draw/info.py +59 -207
  8. nonebot_plugin_osubot/draw/info_templates/index.html +507 -0
  9. nonebot_plugin_osubot/draw/info_templates/output.css +2 -0
  10. nonebot_plugin_osubot/draw/map.py +9 -11
  11. nonebot_plugin_osubot/draw/osu_preview.py +50 -0
  12. nonebot_plugin_osubot/draw/osu_preview_templates/css/style.css +258 -0
  13. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/README.md +109 -0
  14. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js +3 -0
  15. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js.map +1 -0
  16. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js +3 -0
  17. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js.map +1 -0
  18. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/beatmap.js +211 -0
  19. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/hitobject.js +29 -0
  20. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/point.js +55 -0
  21. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/scroll.js +45 -0
  22. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/timingpoint.js +35 -0
  23. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/LegacyRandom.js +81 -0
  24. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/PalpableCatchHitObject.js +53 -0
  25. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/bananashower.js +33 -0
  26. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/catch.js +211 -0
  27. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/fruit.js +21 -0
  28. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/juicestream.js +176 -0
  29. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/hitnote.js +21 -0
  30. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/holdnote.js +37 -0
  31. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/mania.js +164 -0
  32. nonebot_plugin_osubot/draw/osu_preview_templates/js/preview.js +61 -0
  33. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/bezier2.js +33 -0
  34. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/catmullcurve.js +34 -0
  35. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/centripetalcatmullrom.js +30 -0
  36. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/circumstancedcircle.js +47 -0
  37. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curve.js +25 -0
  38. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curvetype.js +17 -0
  39. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/equaldistancemulticurve.js +70 -0
  40. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/linearbezier.js +40 -0
  41. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/hitcircle.js +85 -0
  42. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/slider.js +120 -0
  43. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/spinner.js +56 -0
  44. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/standard.js +170 -0
  45. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/donkat.js +40 -0
  46. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/drumroll.js +34 -0
  47. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/shaker.js +58 -0
  48. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/taiko.js +120 -0
  49. nonebot_plugin_osubot/draw/osu_preview_templates/js/util.js +61 -0
  50. nonebot_plugin_osubot/draw/osu_preview_templates/pic.html +110 -0
  51. nonebot_plugin_osubot/draw/rating.py +6 -3
  52. nonebot_plugin_osubot/draw/score.py +23 -81
  53. nonebot_plugin_osubot/draw/taiko_preview.py +14 -13
  54. nonebot_plugin_osubot/draw/templates/bpa_chart.html +1 -1
  55. nonebot_plugin_osubot/draw/utils.py +162 -16
  56. nonebot_plugin_osubot/file.py +184 -31
  57. nonebot_plugin_osubot/mania/__init__.py +9 -10
  58. nonebot_plugin_osubot/matcher/__init__.py +2 -0
  59. nonebot_plugin_osubot/matcher/bp_analyze.py +14 -9
  60. nonebot_plugin_osubot/matcher/guess.py +250 -294
  61. nonebot_plugin_osubot/matcher/map_convert.py +21 -13
  62. nonebot_plugin_osubot/matcher/medal.py +1 -1
  63. nonebot_plugin_osubot/matcher/osudl.py +5 -4
  64. nonebot_plugin_osubot/matcher/pr.py +0 -4
  65. nonebot_plugin_osubot/matcher/preview.py +10 -3
  66. nonebot_plugin_osubot/matcher/recommend.py +7 -12
  67. nonebot_plugin_osubot/mods.py +62 -61
  68. nonebot_plugin_osubot/network/first_response.py +1 -1
  69. nonebot_plugin_osubot/osufile/mods/AP.png +0 -0
  70. nonebot_plugin_osubot/osufile/mods/CL.png +0 -0
  71. nonebot_plugin_osubot/osufile/mods/DT.png +0 -0
  72. nonebot_plugin_osubot/osufile/mods/EZ.png +0 -0
  73. nonebot_plugin_osubot/osufile/mods/FI.png +0 -0
  74. nonebot_plugin_osubot/osufile/mods/FL.png +0 -0
  75. nonebot_plugin_osubot/osufile/mods/HD.png +0 -0
  76. nonebot_plugin_osubot/osufile/mods/HR.png +0 -0
  77. nonebot_plugin_osubot/osufile/mods/HT.png +0 -0
  78. nonebot_plugin_osubot/osufile/mods/MR.png +0 -0
  79. nonebot_plugin_osubot/osufile/mods/NC.png +0 -0
  80. nonebot_plugin_osubot/osufile/mods/NF.png +0 -0
  81. nonebot_plugin_osubot/osufile/mods/PF.png +0 -0
  82. nonebot_plugin_osubot/osufile/mods/RX.png +0 -0
  83. nonebot_plugin_osubot/osufile/mods/SD.png +0 -0
  84. nonebot_plugin_osubot/osufile/mods/SO.png +0 -0
  85. nonebot_plugin_osubot/osufile/mods/TD.png +0 -0
  86. nonebot_plugin_osubot/osufile/mods/V2.png +0 -0
  87. nonebot_plugin_osubot/pp.py +7 -0
  88. nonebot_plugin_osubot/schema/__init__.py +0 -2
  89. nonebot_plugin_osubot/schema/beatmapsets.py +42 -0
  90. nonebot_plugin_osubot/schema/draw_info.py +54 -0
  91. nonebot_plugin_osubot/schema/score.py +2 -0
  92. nonebot_plugin_osubot/schema/user.py +1 -0
  93. {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/METADATA +18 -17
  94. {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/RECORD +95 -52
  95. nonebot_plugin_osubot-6.26.4.dist-info/WHEEL +4 -0
  96. nonebot_plugin_osubot/schema/sayo_beatmap.py +0 -59
  97. 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, ImageSequence, UnidentifiedImageError
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, get_projectimg, team_cache_path, user_cache_path
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
- if not path.exists():
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
- if not user_path.exists():
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
- if not path.exists():
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
- if not user_path.exists():
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
- if not path.exists():
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
- mods_img = Image.open(mods_bg).convert("RGBA")
269
- im.alpha_composite(mods_img, (880 + 50 * mods_num, 100))
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
- country_bg = Image.open(country).convert("RGBA").resize((66, 45))
293
- im.alpha_composite(country_bg, (208, 597))
294
- if info.team and info.team.flag_url:
295
- team_path = team_cache_path / f"{info.team.id}.png"
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 = [mapinfo.cs, mapinfo.drain, mapinfo.accuracy, mapinfo.ar]
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
- gif_frames = []
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 Mod(acronym="CL") in score_info.mods:
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, Torus_Regular_20, Torus_Regular_25, Torus_Regular_30
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
- (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) + " OD = " + str(map_data.od), font=reg_font, fill="#AAA")
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 = 200
71
+ y_start = 40
71
72
  max_x = 0
72
73
  max_y = 0
73
74
  bar_number = 1
@@ -43,7 +43,7 @@
43
43
  xAxis: [
44
44
  {
45
45
  type: 'category',
46
- data: Array.from({ length: 200 }, (_, i) => i),
46
+ data: Array.from({ length: {{length}} }, (_, i) => i),
47
47
  axisLabel: {
48
48
  interval: 9
49
49
  }
@@ -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
- os.remove(path)
135
- data = await get_seasonal_bg()
136
- pic = SeasonalBackgrounds(**data)
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[0], bg.size[1]
149
- fix_w, fix_h = size[0], size[1]
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
- if not path.exists():
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
- if abs(n1 - n2) < 0.01:
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