290 lines
No EOL
9.4 KiB
Python
290 lines
No EOL
9.4 KiB
Python
import typing as t
|
|
from pathlib import Path
|
|
from io import BytesIO, StringIO
|
|
import logging
|
|
import svgwrite
|
|
from wand.image import Image
|
|
from wand.drawing import Drawing
|
|
from wand.color import Color
|
|
from redbot.core.utils.chat_formatting import humanize_number
|
|
|
|
try:
|
|
from .. import imgtools
|
|
except ImportError:
|
|
import imgtools
|
|
|
|
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,
|
|
username: str = "Spartan117",
|
|
status: str = "online",
|
|
level: int = 3,
|
|
messages: int = 420,
|
|
voicetime: int = 3600,
|
|
stars: int = 69,
|
|
prestige: int = 0,
|
|
prestige_emoji: t.Optional[t.Union[bytes, str]] = None,
|
|
balance: int = 0,
|
|
currency_name: str = "Credits",
|
|
previous_xp: int = 100,
|
|
current_xp: int = 125,
|
|
next_xp: int = 200,
|
|
position: int = 3,
|
|
role_icon: t.Optional[t.Union[bytes, str]] = None,
|
|
blur: bool = False,
|
|
base_color: t.Tuple[int, int, int] = (255, 255, 255),
|
|
user_color: t.Optional[t.Tuple[int, int, int]] = None,
|
|
stat_color: t.Optional[t.Tuple[int, int, int]] = None,
|
|
level_bar_color: t.Optional[t.Tuple[int, int, int]] = None,
|
|
font_path: t.Optional[t.Union[str, Path]] = None,
|
|
render_gif: bool = False,
|
|
debug: bool = False,
|
|
**kwargs,
|
|
) -> t.Tuple[bytes, bool]:
|
|
"""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
|
|
|
|
# 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 if provided
|
|
if background_bytes:
|
|
with Image(blob=background_bytes) as bg:
|
|
bg.transform(resize='1050x320^')
|
|
bg.crop(width=1050, height=320, gravity='center')
|
|
if blur:
|
|
bg.gaussian_blur(sigma=15)
|
|
card.composite(bg, 0, 0)
|
|
else:
|
|
# Use gradient background
|
|
card.gradient('linear-gradient', 'navy-darkblue')
|
|
|
|
# Add SVG elements
|
|
with Image(blob=svg_data.encode(), format='svg') as svg:
|
|
card.composite(svg, 0, 0)
|
|
|
|
# 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
|
|
|
|
buffer = BytesIO()
|
|
card.save(buffer)
|
|
return buffer.getvalue(), False
|
|
|
|
if __name__ == "__main__":
|
|
# Test code
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
test_avatar = (imgtools.ASSETS / "tests" / "default.png").read_bytes()
|
|
res, _ = generate_modern_profile(
|
|
avatar_bytes=test_avatar,
|
|
debug=True
|
|
)
|
|
(imgtools.ASSETS / "tests" / "modern_result.webp").write_bytes(res) |