Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 35 additions & 247 deletions utils/image_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,247 +693,7 @@ def draw_box(
fill=self.TEXT_COLOR,
font=self.font_small,
)

return img

@staticmethod
def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img):
mask = Image.new("L", (avatar_size * 4, avatar_size * 4), 0)
draw_mask = ImageDraw.Draw(mask)
draw_mask.ellipse((0, 0, avatar_size * 4, avatar_size * 4), fill=255)
mask = mask.resize((avatar_size, avatar_size), Image.Resampling.LANCZOS)
avatar_img.putalpha(mask)
img.paste(avatar_img, (avatar_x, avatar_y), avatar_img)


class ServerStatGenerator:
WIDTH: int = 800
HEIGHT: int = 710
HEADER_HEIGHT: int = 150

BG_COLOR = (25, 25, 25)
TEXT_COLOR = (255, 255, 255)
HEADER_BG_COLOR = (50, 50, 50)
BOX_COLOR = (35, 35, 35)
TITLE_COLOR = (200, 200, 200)
SUBTEXT_COLOR = (170, 170, 170)

def __init__(self):
self.base_path = pathlib.Path(__file__).parent.resolve() / ".." / "ressources"
self._load_fonts()

def _load_fonts(self):
font_file = self.base_path / "font" / "outfit.ttf"
try:
self.font_title = ImageFont.truetype(str(font_file), 45)
self.font_large = ImageFont.truetype(str(font_file), 38)
self.font_regular = ImageFont.truetype(str(font_file), 28)
self.font_small = ImageFont.truetype(str(font_file), 22)
self.font_tiny = ImageFont.truetype(str(font_file), 18)
except IOError:
self.font_title = ImageFont.load_default()
self.font_large = ImageFont.load_default()
self.font_regular = ImageFont.load_default()
self.font_small = ImageFont.load_default()
self.font_tiny = ImageFont.load_default()

async def generate_server_stat(self, guild: discord.Guild, stats: dict):
img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.BG_COLOR)
draw = ImageDraw.Draw(img)

draw.rectangle([0, 0, self.WIDTH, self.HEADER_HEIGHT], fill=self.HEADER_BG_COLOR)

icon_x, icon_y, icon_size = 40, 25, 100
try:
if guild.icon:
icon_data = await guild.icon.read()
icon_img = (
Image.open(BytesIO(icon_data)).resize((icon_size, icon_size)).convert("RGBA")
)
else:
raise Exception()
self.create_avatar_mask(icon_img, icon_size, icon_x, icon_y, img)
except Exception:
default_icon = Image.new("RGBA", (icon_size, icon_size), (120, 120, 120, 255))
self.create_avatar_mask(default_icon, icon_size, icon_x, icon_y, img)

draw.text((170, 45), "RNGdle - Server Stats", fill=self.TEXT_COLOR, font=self.font_title)

async def fetch_avatar(member, size):
if member:
try:
data = await member.display_avatar.read()
return Image.open(BytesIO(data)).resize((size, size)).convert("RGBA")
except:
pass
return Image.new("RGBA", (size, size), (120, 120, 120, 255))

best_avatar = await fetch_avatar(stats["best_roll"].get("member"), 32)
worst_avatar = await fetch_avatar(stats["worst_roll"].get("member"), 32)

tier_avatars = {}
for t, members_list in stats.get("tier_members", {}).items():
tier_avatars[t] = []
for member in members_list:
if member:
avatar = await fetch_avatar(member, 24)
tier_avatars[t].append(avatar)

def draw_box(
x,
y,
w,
h,
title,
value,
subtext=None,
value_color=None,
outline_color=None,
score_suffix=None,
avatar=None,
):
if value_color is None:
value_color = self.TEXT_COLOR

if outline_color:
draw.rounded_rectangle(
[x, y, x + w, y + h],
radius=12,
fill=self.BOX_COLOR,
outline=outline_color,
width=2,
)
else:
draw.rounded_rectangle([x, y, x + w, y + h], radius=12, fill=self.BOX_COLOR)

draw.text((x + 20, y + 15), title, fill=self.TITLE_COLOR, font=self.font_small)

if avatar:
av_x = int(x + w - avatar.width - 15)
av_y = int(y + 10)
self.create_avatar_mask(avatar, avatar.width, av_x, av_y, img)

draw.text((x + 20, y + 45), value, fill=value_color, font=self.font_regular)

if score_suffix:
val_width = draw.textlength(value + " ", font=self.font_regular)
draw.text(
(x + 20 + val_width, y + 45),
score_suffix,
fill=(215, 215, 215),
font=self.font_regular,
)

if subtext:
draw.text((x + 20, y + 80), subtext, fill=self.SUBTEXT_COLOR, font=self.font_tiny)

best_color = get_tier_color(get_score_tier(stats["best_roll"]["score"]))
worst_color = get_tier_color(get_score_tier(stats["worst_roll"]["score"]))

best_number_str = f"{stats['best_roll']['number']:,}".replace(",", " ")
best_score_str = f"({stats['best_roll']['score']:,} EP)".replace(",", " ")

worst_number_str = f"{stats['worst_roll']['number']:,}".replace(",", " ")
worst_score_str = f"({stats['worst_roll']['score']:,} EP)".replace(",", " ")

draw_box(
40,
180,
345,
110,
"Best Roll OAT",
best_number_str,
f"by {stats['best_roll']['user']}",
value_color=best_color,
outline_color=best_color,
score_suffix=best_score_str,
avatar=best_avatar,
)
draw_box(
415,
180,
345,
110,
"Worst Roll OAT",
worst_number_str,
f"by {stats['worst_roll']['user']}",
value_color=worst_color,
outline_color=worst_color,
score_suffix=worst_score_str,
avatar=worst_avatar,
)

box_w = 226
avg_color = get_tier_color(get_score_tier(stats["avg_score"]))

draw_box(40, 310, box_w, 110, "Total Rolls", f"{stats['total_rolls']:,}".replace(",", " "))
draw_box(
286,
310,
box_w,
110,
"Average Score",
f"{stats['avg_score']:,} EP".replace(",", " "),
value_color=avg_color,
outline_color=avg_color,
)
draw_box(
532,
310,
box_w,
110,
"Overall Score",
f"{stats['overall_score']:,} EP".replace(",", " "),
)

draw.rounded_rectangle([40, 440, 760, 680], radius=12, fill=self.BOX_COLOR)
draw.text((60, 455), "Tier Breakdown", fill=self.TITLE_COLOR, font=self.font_small)

tiers = ["MYTHIC", "ANOMALY", "EPIC", "RARE", "UNCOMMON", "COMMON", "TRASH"]
col_x = [80, 440]
start_y = 500

for i, tier in enumerate(tiers):
count = stats["rarities"].get(tier, 0)
color = get_tier_color(Tier[tier])

col_index = 0 if i < 4 else 1
row_index = i if i < 4 else i - 4

x_pos = col_x[col_index]
y_pos = start_y + (45 * row_index)

draw.text((x_pos, y_pos), f"{tier}:", fill=color, font=self.font_small)

count_str = f"{count:,}".replace(",", " ")
count_width = draw.textlength(count_str, font=self.font_small)
draw.text(
(x_pos + 220 - count_width, y_pos),
count_str,
fill=self.TEXT_COLOR,
font=self.font_small,
)

if tier in tier_avatars:
avatars_to_draw = tier_avatars[tier][:3]

# On dessine la liste à l'envers pour que le #1 (index 0) soit dessiné en dernier
# et apparaisse donc visuellement "par-dessus" les autres
for idx, avatar in reversed(list(enumerate(avatars_to_draw))):
# Un écart de 14 pixels pour des images de 24px crée un chevauchement de 10px
avatar_x = int(x_pos + 235 + (idx * 14))
avatar_y = int(y_pos - 1)

# On dessine un cercle gris foncé (BOX_COLOR) derrière l'avatar pour créer
# une petite bordure qui "coupe" l'avatar en dessous et les détache proprement
draw.ellipse(
[avatar_x - 2, avatar_y - 2, avatar_x + 24 + 2, avatar_y + 24 + 2],
fill=self.BOX_COLOR,
)

self.create_avatar_mask(avatar, 24, avatar_x, avatar_y, img)

return img
return img

@staticmethod
def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img):
Expand Down Expand Up @@ -1133,6 +893,17 @@ def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img):
avatar_img.putalpha(mask)
img.paste(avatar_img, (avatar_x, avatar_y), avatar_img)

return img

@staticmethod
def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img):
mask = Image.new("L", (avatar_size * 4, avatar_size * 4), 0)
draw_mask = ImageDraw.Draw(mask)
draw_mask.ellipse((0, 0, avatar_size * 4, avatar_size * 4), fill=255)
mask = mask.resize((avatar_size, avatar_size), Image.Resampling.LANCZOS)
avatar_img.putalpha(mask)
img.paste(avatar_img, (avatar_x, avatar_y), avatar_img)


class ServerStatGenerator:
WIDTH: int = 800
Expand Down Expand Up @@ -1200,9 +971,12 @@ async def fetch_avatar(member, size):
worst_avatar = await fetch_avatar(stats["worst_roll"].get("member"), 32)

tier_avatars = {}
for t, member in stats.get("tier_members", {}).items():
if member:
tier_avatars[t] = await fetch_avatar(member, 24)
for t, members_list in stats.get("tier_members", {}).items():
tier_avatars[t] = []
for member in members_list:
if member:
avatar = await fetch_avatar(member, 24)
tier_avatars[t].append(avatar)

def draw_box(
x,
Expand Down Expand Up @@ -1340,9 +1114,23 @@ def draw_box(
)

if tier in tier_avatars:
self.create_avatar_mask(
tier_avatars[tier], 24, int(x_pos + 235), int(y_pos - 1), img
)
avatars_to_draw = tier_avatars[tier][:3]

# We draw the list in reverse order so that #1 (index 0) is drawn last
# and thus appears visually "on top" of the others
for idx, avatar in reversed(list(enumerate(avatars_to_draw))):
# A 14-pixel difference for 24-pixel images results in a 10-pixel overlap
avatar_x = int(x_pos + 235 + (idx * 14))
avatar_y = int(y_pos - 1)

# Draw a dark gray circle (BOX_COLOR) behind the avatar to create
# a small border that "cuts off" the bottom of the avatar and separates them neatly
draw.ellipse(
[avatar_x - 2, avatar_y - 2, avatar_x + 24 + 2, avatar_y + 24 + 2],
fill=self.BOX_COLOR,
)

self.create_avatar_mask(avatar, 24, avatar_x, avatar_y, img)

return img

Expand Down
Loading