diff --git a/utils/image_generator.py b/utils/image_generator.py index b310524..86657ec 100644 --- a/utils/image_generator.py +++ b/utils/image_generator.py @@ -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): @@ -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 @@ -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, @@ -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