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
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
This commit is contained in:
parent
d507065184
commit
130fbc015b
2 changed files with 118 additions and 214 deletions
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue