Ruby-Cogs/levelup/generator/styles/runescape.py
Valerie 477974d53c
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
Upload 2 Cogs & Update README
2025-05-23 01:30:53 -04:00

243 lines
8.1 KiB
Python

"""
Generate a full profile image with customizable parameters.
Args:
avatar_bytes (t.Optional[bytes], optional): The avatar image as bytes. Defaults to None.
status (t.Optional[str], optional): The status. Defaults to "online".
level (t.Optional[int], optional): The level. Defaults to 1.
messages (t.Optional[int], optional): The number of messages. Defaults to 0.
voicetime (t.Optional[int], optional): The voicetime. Defaults to 3600.
prestige (t.Optional[int], optional): The prestige level. Defaults to 0.
prestige_emoji (t.Optional[bytes], optional): The prestige emoji as bytes. Defaults to None.
balance (t.Optional[int], optional): The balance. Defaults to 0.
previous_xp (t.Optional[int], optional): The previous XP. Defaults to 0.
current_xp (t.Optional[int], optional): The current XP. Defaults to 4.
next_xp (t.Optional[int], optional): The next XP. Defaults to 10.
position (t.Optional[int], optional): The position. Defaults to 1.
stat_color (t.Optional[t.Tuple[int, int, int]], optional): The color for the stats. Defaults to (0, 255, 68).
render_gif (t.Optional[bool], optional): Whether to render as gif. Defaults to False.
debug (t.Optional[bool], optional): Whether to show the generated image. Defaults to False.
Returns:
bytes: The generated full profile image as bytes.
"""
import logging
import typing as t
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
try:
from .. import imgtools
except ImportError:
import imgtools
log = logging.getLogger("red.vrt.levelup.generator.styles.default")
def generate_runescape_profile(
avatar_bytes: t.Optional[bytes] = None,
status: str = "online",
level: int = 1,
messages: int = 0,
voicetime: int = 3600,
prestige: int = 0,
balance: int = 0,
previous_xp: int = 0,
current_xp: int = 4,
next_xp: int = 10,
position: int = 1,
stat_color: t.Tuple[int, int, int] = (0, 255, 68), # Green
render_gif: bool = False,
debug: bool = False,
**kwargs,
):
if isinstance(avatar_bytes, str) and avatar_bytes.startswith("http"):
log.debug("Avatar image is a URL, attempting to download")
avatar_bytes = imgtools.download_image(avatar_bytes)
if avatar_bytes:
pfp = Image.open(BytesIO(avatar_bytes))
else:
pfp = imgtools.DEFAULT_PFP
pfp_animated = getattr(pfp, "is_animated", False)
log.debug(f"PFP animated: {pfp_animated}")
profile_size = (219, 192)
# Create blank transparent image at 219 x 192 to put everything on
card = Image.new("RGBA", profile_size, (0, 0, 0, 0))
# Template also at 219 x 192
template = imgtools.RS_TEMPLATE_BALANCE.copy() if balance else imgtools.RS_TEMPLATE.copy()
# Place status icon
status_icon = imgtools.STATUS[status].resize((25, 25), Image.Resampling.LANCZOS)
card.paste(status_icon, (197, -2), status_icon)
draw = ImageDraw.Draw(template)
# Draw stats
font_path = imgtools.ASSETS / "fonts" / "Runescape.ttf"
# Draw balance
if balance:
balance_text = f"{imgtools.abbreviate_number(balance)}"
balance_size = 20
balance_font = ImageFont.truetype(str(font_path), balance_size)
draw.text(
xy=(44, 23),
text=balance_text,
font=balance_font,
fill=stat_color,
anchor="mm",
)
# Draw prestige
if prestige:
prestige_text = f"{imgtools.abbreviate_number(prestige)}"
prestige_size = 35
prestige_font = ImageFont.truetype(str(font_path), prestige_size)
draw.text(
xy=(197, 149),
text=prestige_text,
font=prestige_font,
fill=stat_color,
anchor="mm",
stroke_width=1,
)
# Draw level
level_text = f"{imgtools.abbreviate_number(level)}"
level_size = 20
level_font = ImageFont.truetype(str(font_path), level_size)
draw.text(
xy=(20, 58),
text=level_text,
font=level_font,
fill=stat_color,
align="center",
anchor="mm",
)
# Draw rank
rank_text = f"#{imgtools.abbreviate_number(position)}"
rank_size = 20
rank_font = ImageFont.truetype(str(font_path), rank_size)
lb, rb = 2, 32
while rank_font.getlength(rank_text) > rb - lb:
rank_size -= 1
rank_font = ImageFont.truetype(str(font_path), rank_size)
draw.text(
xy=(17, 93),
text=rank_text,
font=rank_font,
fill=stat_color,
anchor="mm",
)
# Draw messages
messages_text = f"{imgtools.abbreviate_number(messages)}"
messages_size = 20
messages_font = ImageFont.truetype(str(font_path), messages_size)
draw.text(
xy=(27, 127),
text=messages_text,
font=messages_font,
fill=stat_color,
align="center",
anchor="mm",
)
# Draw voicetime
voicetime_text = f"{imgtools.abbreviate_time(voicetime, short=True)}"
voicetime_size = 20
voicetime_font = ImageFont.truetype(str(font_path), voicetime_size)
lb, rb = 30, 65
while voicetime_font.getlength(voicetime_text) > rb - lb:
voicetime_size -= 1
voicetime_font = ImageFont.truetype(str(font_path), voicetime_size)
draw.text(
xy=(46, 155),
text=voicetime_text,
font=voicetime_font,
fill=stat_color,
align="center",
anchor="mm",
)
# Draw xp
current = imgtools.abbreviate_number(current_xp - previous_xp)
goal = imgtools.abbreviate_number(next_xp - previous_xp)
percent = round((current_xp - previous_xp) / (next_xp - previous_xp) * 100)
xp_text = f"{current}/{goal} ({percent}%)"
xp_size = 20
xp_font = ImageFont.truetype(str(font_path), xp_size)
draw.text(
xy=(105, 182),
text=xp_text,
font=xp_font,
fill=stat_color,
align="center",
anchor="mm",
)
# ---------------- Start finalizing the image ----------------
if pfp_animated and render_gif:
avg_duration = imgtools.get_avg_duration(pfp)
frames: t.List[Image.Image] = []
for frame in range(pfp.n_frames):
pfp.seek(frame)
# Prep each frame
card_frame = card.copy()
pfp_frame = pfp.copy()
if pfp_frame.mode != "RGBA":
pfp_frame = pfp_frame.convert("RGBA")
pfp_frame = pfp_frame.resize((145, 145), Image.Resampling.NEAREST)
pfp_frame = imgtools.make_profile_circle(pfp_frame, Image.Resampling.NEAREST)
# Place the pfp
card_frame.paste(pfp_frame, (65, 9), pfp_frame)
# Place the template
card_frame.paste(template, (0, 0), template)
frames.append(card_frame)
buffer = BytesIO()
frames[0].save(
buffer,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=avg_duration,
loop=0,
)
buffer.seek(0)
if debug:
Image.open(buffer).show()
return buffer.getvalue(), True
# Place the pfp
if pfp.mode != "RGBA":
pfp = pfp.convert("RGBA")
pfp = pfp.resize((145, 145), Image.Resampling.LANCZOS)
pfp = imgtools.make_profile_circle(pfp)
card.paste(pfp, (65, 9), pfp)
# Place the template
card.paste(template, (0, 0), template)
if debug:
card.show()
buffer = BytesIO()
card.save(buffer, format="WEBP")
return buffer.getvalue(), False
if __name__ == "__main__":
# Setup console logging
logging.basicConfig(level=logging.DEBUG)
test_avatar = (imgtools.ASSETS / "tests" / "tree.gif").read_bytes()
test_icon = (imgtools.ASSETS / "tests" / "icon.png").read_bytes()
font_path = imgtools.ASSETS / "fonts" / "Runescape.ttf"
res, animated = generate_runescape_profile(
avatar_bytes=test_avatar,
prestige=2,
username="vertyco",
level=0,
debug=True,
balance=1000000,
status="dnd",
position=100000,
render_gif=True,
role_icon=test_icon,
)
result_path = imgtools.ASSETS / "tests" / "result.gif"
result_path.write_bytes(res)