Add glass effect to profile stats layer and improve layout. Introduce create_glass_effect function for modern styling, update stats drawing with icons, and enhance XP progress bar. Refactor profile image handling for better visual presentation.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

This commit is contained in:
Valerie 2025-05-24 07:00:13 -04:00
parent d507065184
commit 130fbc015b
2 changed files with 118 additions and 214 deletions

View file

@ -388,5 +388,22 @@ def get_avg_duration(image: Image.Image) -> int:
return 0
def create_glass_effect(image: Image.Image, opacity: float = 0.3, blur_radius: int = 10) -> Image.Image:
"""Create a modern glass effect with blur and transparency."""
# Create a copy of the image to blur
glass = image.copy()
# Apply gaussian blur
glass = glass.filter(ImageFilter.GaussianBlur(blur_radius))
# Create a semi-transparent white overlay
overlay = Image.new('RGBA', image.size, (255, 255, 255, int(255 * opacity)))
# Composite the blurred image with the overlay
glass = Image.alpha_composite(glass.convert('RGBA'), overlay)
return glass
if __name__ == "__main__":
print(calc_aspect_ratio(200, 70))

View file

@ -237,220 +237,107 @@ def generate_default_profile(
star_icon_x = 850 # Left bound of star icon
star_icon_y = 35 # Top bound of star icon
# Establish layer for all text and accents
stats = Image.new("RGBA", desired_card_size, (0, 0, 0, 0))
# Create the stats layer with glass effect
stats_layer = Image.new("RGBA", desired_card_size, (0, 0, 0, 0))
stats_area = (
stats_layer.width // 4, # x1
stats_layer.height // 3, # y1
stats_layer.width - (stats_layer.width // 4), # x2
stats_layer.height - (stats_layer.height // 4), # y2
)
# Create glass effect for stats area
stats_bg = card.crop(stats_area)
glass_bg = imgtools.create_glass_effect(stats_bg, opacity=0.2, blur_radius=15)
stats_layer.paste(glass_bg, stats_area)
# Add rounded corners to the glass effect
stats_mask = imgtools.get_rounded_corner_mask(glass_bg, radius=20)
stats_layer.putalpha(stats_mask)
# Setup progress bar
# Draw stats with improved styling
draw = ImageDraw.Draw(stats_layer)
font_size = 24
font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), font_size)
# Add stats with icons and modern layout
icon_size = font_size + 4
spacing = icon_size + 10
# Messages stat with icon
msg_icon = "💬"
draw.text(
(stats_area[0] + 10, stats_area[1] + 10),
msg_icon,
font=font,
fill=stat_color,
)
draw.text(
(stats_area[0] + 10 + spacing, stats_area[1] + 10),
f"{humanize_number(messages)}",
font=font,
fill=stat_color,
)
# Voice time stat with icon
voice_icon = "🎤"
draw.text(
(stats_area[0] + 10, stats_area[1] + 10 + spacing),
voice_icon,
font=font,
fill=stat_color,
)
draw.text(
(stats_area[0] + 10 + spacing, stats_area[1] + 10 + spacing),
imgtools.abbreviate_time(voicetime),
font=font,
fill=stat_color,
)
# Stars stat with icon
star_icon = ""
draw.text(
(stats_area[0] + 10, stats_area[1] + 10 + spacing * 2),
star_icon,
font=font,
fill=stat_color,
)
draw.text(
(stats_area[0] + 10 + spacing, stats_area[1] + 10 + spacing * 2),
f"{humanize_number(stars)}",
font=font,
fill=stat_color,
)
# Add XP progress bar with glass effect
progress = (current_xp - previous_xp) / (next_xp - previous_xp)
level_bar = imgtools.make_progress_bar(
bar_width = stats_area[2] - stats_area[0] - 20
bar_height = 20
progress_bar = imgtools.make_progress_bar(
bar_width,
bar_height,
progress,
level_bar_color,
color=level_bar_color,
background_color=(100, 100, 100, 128)
)
stats.paste(level_bar, (bar_start, bar_top), level_bar)
stats_layer.paste(progress_bar, (stats_area[0] + 10, stats_area[3] - 30), progress_bar)
# Establish font
font_path = font_path or imgtools.DEFAULT_FONT
if isinstance(font_path, str):
font_path = Path(font_path)
if not font_path.exists():
# Hosted api on another server? Check if we have it
if (imgtools.DEFAULT_FONTS / font_path.name).exists():
font_path = imgtools.DEFAULT_FONTS / font_path.name
else:
font_path = imgtools.DEFAULT_FONT
# Convert back to string
font_path = str(font_path)
# Add level text with modern styling
level_text = f"LEVEL {level}"
if prestige > 0:
level_text = f"P{prestige}{level_text}"
level_font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), 32)
draw.text(
(stats_area[0] + 10, stats_area[1] - 40),
level_text,
font=level_font,
fill=user_color,
stroke_width=2,
stroke_fill=(0, 0, 0)
)
draw = ImageDraw.Draw(stats)
# ---------------- Username text ----------------
fontsize = 60
font = ImageFont.truetype(font_path, fontsize)
with Pilmoji(stats) as pilmoji:
# Ensure text doesnt pass star_icon_x
while pilmoji.getsize(username, font)[0] + stat_start > star_icon_x - 10:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
pilmoji.text(
xy=(stat_start, name_y),
text=username,
fill=user_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Prestige text ----------------
if prestige:
text = _("(Prestige {})").format(f"{humanize_number(prestige)}")
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass stat_end
while font.getlength(text) + stat_start > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_start, name_y + 70),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
if prestige_emoji:
prestige_icon = Image.open(BytesIO(prestige_emoji)).resize((50, 50), Image.Resampling.LANCZOS)
if prestige_icon.mode != "RGBA":
prestige_icon = prestige_icon.convert("RGBA")
placement = (round(stat_start + font.getlength(text) + 10), name_y + 65)
stats.paste(prestige_icon, placement, prestige_icon)
# ---------------- Stars text ----------------
text = humanize_number(stars)
fontsize = 60
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass stat_end
while font.getlength(text) + star_text_x > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(star_text_x, star_text_y),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
stats.paste(imgtools.STAR, (star_icon_x, star_icon_y), imgtools.STAR)
# ---------------- Rank text ----------------
text = _("Rank: {}").format(f"#{humanize_number(position)}")
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass stat_split point
while font.getlength(text) + stat_start > stat_split - 5:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_start, stats_y),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Level text ----------------
text = _("Level: {}").format(humanize_number(level))
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass the stat_split point
while font.getlength(text) + stat_start > stat_split - 5:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_start, stats_y + stat_offset),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Messages text ----------------
text = _("Messages: {}").format(humanize_number(messages))
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass the stat_end
while font.getlength(text) + stat_split > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_split, stats_y),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Voice text ----------------
text = _("Voice: {}").format(imgtools.abbreviate_time(voicetime))
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass the stat_end
while font.getlength(text) + stat_split > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_split, stats_y + stat_offset),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Balance text ----------------
if balance:
text = _("Balance: {}").format(f"{humanize_number(balance)} {currency_name}")
font = ImageFont.truetype(font_path, 40)
with Pilmoji(stats) as pilmoji:
# Ensure text doesnt pass the stat_end
while pilmoji.getsize(text, font)[0] + stat_start > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
placement = (stat_start, stat_bottom - stat_offset * 2)
pilmoji.text(
xy=placement,
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Experience text ----------------
current = current_xp - previous_xp
goal = next_xp - previous_xp
text = _("Exp: {} ({} total)").format(
f"{humanize_number(current)}/{humanize_number(goal)}", humanize_number(current_xp)
)
fontsize = 40
font = ImageFont.truetype(font_path, fontsize)
# Ensure text doesnt pass the stat_end
while font.getlength(text) + stat_start > stat_end:
fontsize -= 1
font = ImageFont.truetype(font_path, fontsize)
draw.text(
xy=(stat_start, stat_bottom - stat_offset),
text=text,
fill=stat_color,
stroke_width=stroke_width,
stroke_fill=default_fill,
font=font,
)
# ---------------- Profile Accents ----------------
# Draw a circle outline around where the avatar is
# Calculate the circle outline's placement around the avatar
circle = imgtools.make_circle_outline(thickness=5, color=user_color)
outline_size = (380, 380)
circle = circle.resize(outline_size, Image.Resampling.LANCZOS)
placement = (circle_x - 25, circle_y - 25)
stats.paste(circle, placement, circle)
# Place status icon
status_icon = imgtools.STATUS[status].resize((75, 75), Image.Resampling.LANCZOS)
stats.paste(status_icon, (circle_x + 260, circle_y + 260), status_icon)
# Paste role icon on top left of profile circle
if role_icon_bytes:
try:
role_icon_img = Image.open(BytesIO(role_icon_bytes)).resize((70, 70), Image.Resampling.LANCZOS)
stats.paste(role_icon_img, (10, 10), role_icon_img)
except ValueError as e:
if reraise:
raise e
err = (
f"Failed to paste role icon image for {username}"
if isinstance(role_icon, bytes)
else f"Failed to paste role icon image for {username}: {role_icon}"
)
log.error(err, exc_info=e)
# ---------------- Start finalizing the image ----------------
# Resize the profile image
desired_pfp_size = (330, 330)
# Composite the layers
if not render_gif or (not pfp_animated and not bg_animated):
if card.mode != "RGBA":
log.debug(f"Converting card mode '{card.mode}' to RGBA")
@ -464,11 +351,11 @@ def generate_default_profile(
# Paste onto the stats
card.paste(blur_section, (blur_edge, 0), blur_section)
card = imgtools.round_image_corners(card, 45)
pfp = pfp.resize(desired_pfp_size, Image.Resampling.LANCZOS)
pfp = pfp.resize(desired_card_size, Image.Resampling.LANCZOS)
# Crop the profile image into a circle
pfp = imgtools.make_profile_circle(pfp)
# Paste the items onto the card
card.paste(stats, (0, 0), stats)
card.paste(stats_layer, (0, 0), stats_layer)
card.paste(pfp, (circle_x, circle_y), pfp)
if debug:
card.show()
@ -487,7 +374,7 @@ def generate_default_profile(
# Paste onto the stats
card.paste(blur_section, (blur_edge, 0), blur_section)
card.paste(stats, (0, 0), stats)
card.paste(stats_layer, (0, 0), stats_layer)
avg_duration = imgtools.get_avg_duration(pfp)
log.debug(f"Rendering pfp as gif with avg duration of {avg_duration}ms")
@ -500,7 +387,7 @@ def generate_default_profile(
if pfp_frame.mode != "RGBA":
pfp_frame = pfp_frame.convert("RGBA")
# Resize the profile image for each frame
pfp_frame = pfp_frame.resize(desired_pfp_size, Image.Resampling.NEAREST)
pfp_frame = pfp_frame.resize(desired_card_size, Image.Resampling.NEAREST)
# Crop the profile image into a circle
pfp_frame = imgtools.make_profile_circle(pfp_frame, method=Image.Resampling.NEAREST)
# Paste the profile image onto the card
@ -530,7 +417,7 @@ def generate_default_profile(
if pfp.mode != "RGBA":
log.debug(f"Converting pfp mode '{pfp.mode}' to RGBA")
pfp = pfp.convert("RGBA")
pfp = pfp.resize(desired_pfp_size, Image.Resampling.LANCZOS)
pfp = pfp.resize(desired_card_size, Image.Resampling.LANCZOS)
# Crop the profile image into a circle
pfp = imgtools.make_profile_circle(pfp)
for frame in range(card.n_frames):
@ -547,7 +434,7 @@ def generate_default_profile(
card_frame.paste(blur_section, (blur_edge, 0), blur_section)
card_frame.paste(pfp, (circle_x, circle_y), pfp)
card_frame.paste(stats, (0, 0), stats)
card_frame.paste(stats_layer, (0, 0), stats_layer)
frames.append(card_frame)
@ -622,11 +509,11 @@ def generate_default_profile(
if pfp_frame.mode != "RGBA":
pfp_frame = pfp_frame.convert("RGBA")
pfp_frame = pfp_frame.resize(desired_pfp_size, Image.Resampling.NEAREST)
pfp_frame = pfp_frame.resize(desired_card_size, Image.Resampling.NEAREST)
pfp_frame = imgtools.make_profile_circle(pfp_frame, method=Image.Resampling.NEAREST)
card_frame.paste(pfp_frame, (circle_x, circle_y), pfp_frame)
card_frame.paste(stats, (0, 0), stats)
card_frame.paste(stats_layer, (0, 0), stats_layer)
combined_frames.append(card_frame)