Update info.json to reflect new leveling system details, including author, description, and installation message. Modify pyproject.toml to remove Wand dependency. Enhance modern.py to generate SVG profile cards with improved stats presentation and avatar handling, streamlining profile generation process.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

This commit is contained in:
Valerie 2025-05-26 21:09:13 -04:00
parent 653b16cb5e
commit 58b7e586d6
3 changed files with 248 additions and 103 deletions

View file

@ -1,16 +1,42 @@
{
"author": [
"Vertyco",
"Valerie"
],
"install_msg": "Thanks for adding Ruby Cogs! Join our Discord @ https://discord.gg/5CA8sewarU",
"name": "Ruby Cogs",
"short": "Ruby Cogs, for our fork of Red-DiscordBot (Ruby-DiscordBot)",
"description": "Ruby Cogs, for our fork of Red-DiscordBot (Ruby-DiscordBot)",
"description": "Your friendly neighborhood leveling system",
"disabled": false,
"end_user_data_statement": "This cog stores Discord IDs, counts of user messages, and their time spent in voice channels. No private info is stored about users.",
"hidden": false,
"install_msg": "Thank you for installing LevelUp! To enable leveling in this server type `[p]lset toggle`.\n\nDOCUMENTATION: https://github.com/vertyco/vrt-cogs/blob/main/levelup/README.md",
"min_bot_version": "3.5.0",
"min_python_version": [
3,
9,
0
],
"permissions": [
"read_messages",
"send_messages",
"manage_roles",
"attach_files",
"embed_links"
],
"required_cogs": {},
"requirements": [
"pillow",
"svgwrite>=1.4.3",
"wand>=0.6.11"
],
"short": "Discord leveling system",
"tags": [
"rubycogs",
"pokemon",
"activity",
"level",
"leveler",
"leveling",
"levelup",
"rank",
"utility",
"information",
"various"
]
"vert"
],
"type": "COG"
}

View file

@ -1,7 +1,8 @@
import typing as t
from pathlib import Path
from io import BytesIO
from io import BytesIO, StringIO
import logging
import svgwrite
from wand.image import Image
from wand.drawing import Drawing
from wand.color import Color
@ -14,6 +15,166 @@ except ImportError:
log = logging.getLogger("red.vrt.levelup.generator.styles.modern")
def create_vector_profile(
width: int = 1050,
height: int = 320,
avatar_size: int = 200,
username: str = "",
level: int = 1,
messages: int = 0,
voicetime: int = 0,
stars: int = 0,
prestige: int = 0,
balance: t.Optional[int] = None,
currency_name: str = "",
progress: float = 0,
current_xp: int = 0,
next_xp: int = 0,
position: int = 0,
user_color: t.Tuple[int, int, int] = (88, 101, 242),
stat_color: t.Tuple[int, int, int] = (255, 255, 255),
level_bar_color: t.Tuple[int, int, int] = (88, 101, 242),
) -> str:
"""Generate SVG profile card"""
# Create SVG document
dwg = svgwrite.Drawing(size=(width, height))
# Define gradients
# Main background gradient
grad = dwg.defs.add(dwgwrite.gradients.LinearGradient(id="background"))
grad.add_stop_color(0, "rgb(20, 20, 30)")
grad.add_stop_color(1, "rgb(40, 40, 60)")
# Progress bar gradient
prog_grad = dwg.defs.add(dwgwrite.gradients.LinearGradient(id="progress", x1="0%", y1="0%", x2="0%", y2="100%"))
r, g, b = level_bar_color
prog_grad.add_stop_color(0, f"rgb({r}, {g}, {b})")
prog_grad.add_stop_color(0.5, f"rgb({min(r+20, 255)}, {min(g+20, 255)}, {min(b+20, 255)})")
prog_grad.add_stop_color(1, f"rgb({r}, {g}, {b})")
# Add background
dwg.add(dwg.rect(insert=(0, 0), size=(width, height), fill="url(#background)"))
# Add glass panel with blur effect (using SVG filters)
blur = dwg.defs.add(dwg.filter(id="blur"))
blur.feGaussianBlur(in_="SourceGraphic", stdDeviation=8)
glass = dwg.add(dwg.rect(
insert=(15, 15),
size=(width-30, height-30),
rx=25, ry=25,
fill="rgba(255, 255, 255, 0.1)",
filter="url(#blur)",
stroke="rgba(255, 255, 255, 0.2)",
stroke_width=1
))
# Add stats container
stats = dwg.add(dwg.g(id="stats", transform=f"translate({avatar_size + 80}, 40)"))
# Add level text with glow effect
glow = dwg.defs.add(dwg.filter(id="glow"))
glow.feGaussianBlur(in_="SourceAlpha", stdDeviation=2)
glow.feOffset(dx=0, dy=0)
glow.feComponentTransfer().feFuncA(type="linear", slope=0.5)
glow.feBlend(in2="SourceGraphic", mode="normal")
level_text = f"LEVEL {level}"
if prestige > 0:
level_text = f"P{prestige}{level_text}"
r, g, b = user_color
stats.add(dwg.text(
level_text,
insert=(0, 0),
fill=f"rgb({r}, {g}, {b})",
filter="url(#glow)",
style="font-size:56px; font-weight:bold; font-family:Arial"
))
# Add stats with vector icons
def add_stat(x: int, y: int, icon: str, label: str, value: str):
group = dwg.g(transform=f"translate({x}, {y})")
# Add icon
group.add(dwg.text(
icon,
insert=(0, 0),
fill="white",
style="font-size:28px; font-family:Arial"
))
# Add label
group.add(dwg.text(
f"{label}:",
insert=(40, 0),
fill="rgb(220, 220, 220)",
style="font-size:28px; font-family:Arial"
))
# Add value with glow
group.add(dwg.text(
value,
insert=(140, 0),
fill="white",
filter="url(#glow)",
style="font-size:32px; font-weight:bold; font-family:Arial"
))
return group
# Left column stats
stats.add(add_stat(0, 80, "💬", "Messages", humanize_number(messages)))
stats.add(add_stat(0, 130, "🎤", "Voice", imgtools.abbreviate_time(voicetime)))
stats.add(add_stat(0, 180, "", "Stars", humanize_number(stars)))
# Right column stats
if balance is not None:
stats.add(add_stat(320, 80, "💰", "Balance", f"{humanize_number(balance)} {currency_name}"))
stats.add(add_stat(320, 130, "🏆", "Rank", f"#{humanize_number(position)}"))
# Progress bar
bar_width = 620
bar_height = 30
bar_x = 0
bar_y = 220
# Progress bar background
stats.add(dwg.rect(
insert=(bar_x, bar_y),
size=(bar_width, bar_height),
rx=bar_height//2,
ry=bar_height//2,
fill="rgba(0, 0, 0, 0.3)"
))
# Progress bar fill
progress_width = int(bar_width * progress)
if progress_width > 0:
stats.add(dwg.rect(
insert=(bar_x, bar_y),
size=(progress_width, bar_height),
rx=bar_height//2,
ry=bar_height//2,
fill="url(#progress)"
))
# XP Text
xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}"
stats.add(dwg.text(
xp_text,
insert=(bar_x + (bar_width - len(xp_text) * 10) / 2, bar_y - 15),
fill="white",
style="font-size:20px; font-family:Arial"
))
# Progress percentage
percent_text = f"{int(progress * 100)}%"
stats.add(dwg.text(
percent_text,
insert=(bar_x + progress_width - 30, bar_y + 20),
fill="white",
style="font-size:24px; font-weight:bold; font-family:Arial"
))
# Return SVG as string
return dwg.tostring()
def generate_modern_profile(
background_bytes: t.Optional[t.Union[bytes, str]] = None,
avatar_bytes: t.Optional[t.Union[bytes, str]] = None,
@ -42,16 +203,38 @@ def generate_modern_profile(
debug: bool = False,
**kwargs,
) -> t.Tuple[bytes, bool]:
"""Generate a modern, sleek profile card using Wand/ImageMagick"""
"""Generate a modern, sleek profile card using vector graphics"""
# Set default colors for modern theme
user_color = user_color or (88, 101, 242) # Discord Blurple
stat_color = stat_color or (255, 255, 255) # White
level_bar_color = level_bar_color or (88, 101, 242) # Discord Blurple
# Create base canvas (1050x320)
# Calculate progress
progress = (current_xp - previous_xp) / (next_xp - previous_xp)
# Generate SVG
svg_data = create_vector_profile(
username=username,
level=level,
messages=messages,
voicetime=voicetime,
stars=stars,
prestige=prestige,
balance=balance,
currency_name=currency_name,
progress=progress,
current_xp=current_xp,
next_xp=next_xp,
position=position,
user_color=user_color,
stat_color=stat_color,
level_bar_color=level_bar_color,
)
# Convert SVG to image
with Image(width=1050, height=320) as card:
# Set up background
# Set up background if provided
if background_bytes:
with Image(blob=background_bytes) as bg:
bg.transform(resize='1050x320^')
@ -60,98 +243,35 @@ def generate_modern_profile(
bg.gaussian_blur(sigma=15)
card.composite(bg, 0, 0)
else:
# Create gradient background
# Use gradient background
card.gradient('linear-gradient', 'navy-darkblue')
# Add frosted glass effect overlay
with Image(width=1050, height=320, background=Color('rgba(255, 255, 255, 0.1)')) as overlay:
overlay.gaussian_blur(sigma=50)
card.composite(overlay, 0, 0)
# Create circular avatar mask
with Drawing() as draw:
# Avatar circle (left side)
avatar_size = 200
avatar_x = 60
avatar_y = 60
if avatar_bytes:
with Image(blob=avatar_bytes) as avatar:
# Resize avatar
avatar.resize(avatar_size, avatar_size)
# Create circular mask
with Image(width=avatar_size, height=avatar_size, background=Color('transparent')) as mask:
draw.circle((avatar_size/2, avatar_size/2), (0, avatar_size/2))
draw.draw(mask)
# Apply mask to avatar
avatar.composite_channel('alpha', mask, 'copy_alpha', 0, 0)
# Add avatar to card
card.composite(avatar, avatar_x, avatar_y)
# Add modern stats panel
with Image(width=700, height=280, background=Color('rgba(0, 0, 0, 0.5)')) as stats_panel:
stats_panel.gaussian_blur(sigma=2)
stats_panel.border_color = Color('rgba(255, 255, 255, 0.1)')
stats_panel.border_width = 1
card.composite(stats_panel, 310, 20)
# Add text elements
draw.font = str(font_path) if font_path else '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
draw.font_size = 40
draw.fill_color = Color('rgb({}, {}, {})'.format(*user_color))
# Level text
level_text = f"LEVEL {level}"
if prestige > 0:
level_text = f"P{prestige}{level_text}"
draw.text(340, 70, level_text)
# Stats
draw.font_size = 25
draw.fill_color = Color('white')
y_pos = 120
# Left column
draw.text(340, y_pos, f"Messages: {humanize_number(messages)}")
draw.text(340, y_pos + 40, f"Voice Time: {imgtools.abbreviate_time(voicetime)}")
draw.text(340, y_pos + 80, f"Stars: {humanize_number(stars)}")
# Right column
if balance is not None:
draw.text(600, y_pos, f"Balance: {humanize_number(balance)} {currency_name}")
draw.text(600, y_pos + 40, f"Rank: #{humanize_number(position)}")
# Progress bar
progress = (current_xp - previous_xp) / (next_xp - previous_xp)
bar_width = 620
bar_height = 30
bar_x = 340
bar_y = 240
# Background bar
draw.fill_color = Color('rgba(0, 0, 0, 0.3)')
draw.rectangle(bar_x, bar_y, bar_x + bar_width, bar_y + bar_height)
# Progress bar
progress_width = int(bar_width * progress)
draw.fill_color = Color('rgb({}, {}, {})'.format(*level_bar_color))
if progress_width > 0:
draw.rectangle(bar_x, bar_y, bar_x + progress_width, bar_y + bar_height)
# XP Text
draw.font_size = 20
draw.fill_color = Color('white')
xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}"
draw.text(bar_x + (bar_width - len(xp_text) * 10) / 2, bar_y - 15, xp_text)
# Progress percentage
percent_text = f"{int(progress * 100)}%"
draw.text(bar_x + progress_width - 30, bar_y + 5, percent_text)
# Apply all drawings
draw.draw(card)
# Add SVG elements
with Image(blob=svg_data.encode(), format='svg') as svg:
card.composite(svg, 0, 0)
# Save the result
# Add avatar if provided
if avatar_bytes:
with Image(blob=avatar_bytes) as avatar:
# Create circular avatar
avatar.resize(200, 200)
avatar.virtual_pixel = 'transparent'
avatar.format = 'png'
avatar.alpha_channel = True
# Create circular mask
with Image(width=200, height=200, background=Color('transparent')) as mask:
mask.format = 'png'
with Drawing() as draw:
draw.fill_color = Color('white')
draw.circle((100, 100), (0, 100))
draw.draw(mask)
avatar.composite_channel('alpha', mask, 'copy_alpha', 0, 0)
# Add avatar to card
card.composite(avatar, 60, 60)
# Save as WebP
card.format = 'webp'
card.compression_quality = 95

View file

@ -10,4 +10,3 @@
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
Wand = "^0.6.11" # ImageMagick Python bindings for modern image processing