513 lines
22 KiB
Python
513 lines
22 KiB
Python
import importlib.util
|
|
import logging
|
|
import math
|
|
import sys
|
|
import typing as t
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFont, ImageSequence, UnidentifiedImageError
|
|
from redbot.core.i18n import Translator
|
|
from redbot.core.utils.chat_formatting import humanize_number
|
|
|
|
try:
|
|
# Loaded from cog
|
|
from .. import imgtools
|
|
from ..pilmojisrc.core import Pilmoji
|
|
except ImportError:
|
|
# Running in vscode "Run Python File in Terminal"
|
|
# Add parent directory to sys.path to enable imports
|
|
parent_dir = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(parent_dir))
|
|
|
|
# Import imgtools directly
|
|
imgtools_path = parent_dir / "imgtools.py"
|
|
if imgtools_path.exists():
|
|
spec = importlib.util.spec_from_file_location("imgtools", imgtools_path)
|
|
imgtools = importlib.util.module_from_spec(spec)
|
|
sys.modules["imgtools"] = imgtools
|
|
spec.loader.exec_module(imgtools)
|
|
else:
|
|
raise ImportError(f"Could not find imgtools at {imgtools_path}")
|
|
|
|
# Set up pilmojisrc as a package
|
|
pilmoji_dir = parent_dir / "pilmojisrc"
|
|
if not pilmoji_dir.exists():
|
|
raise ImportError(f"Could not find pilmojisrc directory at {pilmoji_dir}")
|
|
|
|
# Create and register the pilmojisrc package
|
|
pilmojisrc_init = pilmoji_dir / "__init__.py"
|
|
if pilmojisrc_init.exists():
|
|
spec = importlib.util.spec_from_file_location("pilmojisrc", pilmojisrc_init)
|
|
pilmojisrc = importlib.util.module_from_spec(spec)
|
|
sys.modules["pilmojisrc"] = pilmojisrc
|
|
spec.loader.exec_module(pilmojisrc)
|
|
else:
|
|
# Create an empty module if __init__.py doesn't exist
|
|
pilmojisrc = type(sys)("pilmojisrc")
|
|
sys.modules["pilmojisrc"] = pilmojisrc
|
|
|
|
# Import helpers module first (since core depends on it)
|
|
helpers_path = pilmoji_dir / "helpers.py"
|
|
if helpers_path.exists():
|
|
spec = importlib.util.spec_from_file_location("pilmojisrc.helpers", helpers_path)
|
|
helpers = importlib.util.module_from_spec(spec)
|
|
pilmojisrc.helpers = helpers
|
|
sys.modules["pilmojisrc.helpers"] = helpers
|
|
spec.loader.exec_module(helpers)
|
|
else:
|
|
raise ImportError(f"Could not find helpers module at {helpers_path}")
|
|
|
|
# Now import core module
|
|
core_path = pilmoji_dir / "core.py"
|
|
if core_path.exists():
|
|
spec = importlib.util.spec_from_file_location("pilmojisrc.core", core_path)
|
|
core = importlib.util.module_from_spec(spec)
|
|
pilmojisrc.core = core
|
|
sys.modules["pilmojisrc.core"] = core
|
|
spec.loader.exec_module(core)
|
|
Pilmoji = core.Pilmoji
|
|
else:
|
|
raise ImportError(f"Could not find core module at {core_path}")
|
|
|
|
|
|
log = logging.getLogger("red.vrt.levelup.generator.styles.default")
|
|
_ = Translator("LevelUp", __file__)
|
|
|
|
|
|
def generate_default_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,
|
|
reraise: bool = False,
|
|
square: bool = False,
|
|
**kwargs,
|
|
) -> t.Tuple[bytes, bool]:
|
|
"""
|
|
Generate a full profile image with customizable parameters.
|
|
If the avatar is animated and not the background, the avatar will be rendered as a gif.
|
|
If the background is animated and not the avatar, the background will be rendered as a gif.
|
|
If both are animated, the avatar will be rendered as a gif and the background will be rendered as a static image.
|
|
To optimize performance, the profile will be generated in 3 layers, the background, the avatar, and the stats.
|
|
The stats layer will be generated as a separate image and then pasted onto the background.
|
|
|
|
Args:
|
|
background (t.Optional[bytes], optional): The background image as bytes. Defaults to None.
|
|
avatar (t.Optional[bytes], optional): The avatar image as bytes. Defaults to None.
|
|
username (t.Optional[str], optional): The username. Defaults to "Spartan117".
|
|
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.
|
|
stars (t.Optional[int], optional): The number of stars. Defaults to 0.
|
|
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.
|
|
currency_name (t.Optional[str], optional): The name of the currency. Defaults to "Credits".
|
|
previous_xp (t.Optional[int], optional): The previous XP. Defaults to 0.
|
|
current_xp (t.Optional[int], optional): The current XP. Defaults to 0.
|
|
next_xp (t.Optional[int], optional): The next XP. Defaults to 0.
|
|
position (t.Optional[int], optional): The position. Defaults to 0.
|
|
role_icon (t.Optional[bytes, str], optional): The role icon as bytes or url. Defaults to None.
|
|
blur (t.Optional[bool], optional): Whether to blur the box behind the stats. Defaults to False.
|
|
user_color (t.Optional[t.Tuple[int, int, int]], optional): The color for the user. Defaults to None.
|
|
base_color (t.Optional[t.Tuple[int, int, int]], optional): The base color. Defaults to None.
|
|
stat_color (t.Optional[t.Tuple[int, int, int]], optional): The color for the stats. Defaults to None.
|
|
level_bar_color (t.Optional[t.Tuple[int, int, int]], optional): The color for the level bar. Defaults to None.
|
|
font_path (t.Optional[t.Union[str, Path], optional): The path to the font file. Defaults to None.
|
|
render_gif (t.Optional[bool], optional): Whether to render as gif if profile or background is one. Defaults to False.
|
|
debug (t.Optional[bool], optional): Whether to raise any errors rather than suppressing. Defaults to False.
|
|
reraise (t.Optional[bool], optional): Whether to raise any errors rather than suppressing. Defaults to False.
|
|
square (t.Optional[bool], optional): Whether to render the profile as a square. Defaults to False.
|
|
**kwargs: Additional keyword arguments.
|
|
|
|
Returns:
|
|
t.Tuple[bytes, bool]: The generated full profile image as bytes, and whether the image is animated.
|
|
"""
|
|
user_color = user_color or (155, 17, 30) # Default to ruby red
|
|
stat_color = stat_color or (155, 17, 30) # Default to ruby red
|
|
level_bar_color = level_bar_color or (155, 17, 30) # Default to ruby red
|
|
|
|
if isinstance(background_bytes, str) and background_bytes.startswith("http"):
|
|
log.debug("Background image is a URL, attempting to download")
|
|
background_bytes = imgtools.download_image(background_bytes)
|
|
|
|
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 isinstance(prestige_emoji, str) and prestige_emoji.startswith("http"):
|
|
log.debug("Prestige emoji is a URL, attempting to download")
|
|
prestige_emoji = imgtools.download_image(prestige_emoji)
|
|
|
|
if isinstance(role_icon, str) and role_icon.startswith("http"):
|
|
log.debug("Role icon is a URL, attempting to download")
|
|
role_icon_bytes = imgtools.download_image(role_icon)
|
|
else:
|
|
role_icon_bytes = role_icon
|
|
|
|
if background_bytes:
|
|
try:
|
|
card = Image.open(BytesIO(background_bytes))
|
|
except UnidentifiedImageError as e:
|
|
if reraise:
|
|
raise e
|
|
log.error(
|
|
f"Failed to open background image ({type(background_bytes)} - {len(background_bytes)})", exc_info=e
|
|
)
|
|
card = imgtools.get_random_background()
|
|
else:
|
|
card = imgtools.get_random_background()
|
|
if avatar_bytes:
|
|
pfp = Image.open(BytesIO(avatar_bytes))
|
|
else:
|
|
pfp = imgtools.DEFAULT_PFP
|
|
|
|
pfp_animated = getattr(pfp, "is_animated", False)
|
|
bg_animated = getattr(card, "is_animated", False)
|
|
log.debug(f"PFP animated: {pfp_animated}, BG animated: {bg_animated}")
|
|
|
|
# Setup
|
|
default_fill = (255, 255, 255) # Default fill color for text
|
|
stroke_width = 1 # Reduced stroke width for cleaner look
|
|
|
|
# Card dimensions and layout
|
|
if square:
|
|
desired_card_size = (450, 450)
|
|
else:
|
|
desired_card_size = (1050, 320)
|
|
|
|
# Define profile picture size and positions
|
|
pfp_size = (270, 270)
|
|
pfp_x = 15 # Slight padding from left
|
|
pfp_y = (desired_card_size[1] - pfp_size[1]) // 2
|
|
circle_x = pfp_x
|
|
circle_y = pfp_y
|
|
|
|
# Create the dark background layer that covers everything
|
|
dark_bg = Image.new("RGBA", desired_card_size, (0, 0, 0, 0))
|
|
dark_bg_draw = ImageDraw.Draw(dark_bg)
|
|
dark_bg_draw.rectangle((0, 0, desired_card_size[0], desired_card_size[1]), fill=(0, 0, 0, 180))
|
|
|
|
# Create the stats layer
|
|
stats_layer = Image.new("RGBA", desired_card_size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(stats_layer)
|
|
|
|
# Stats positioning - progress bar starts after profile, stats on right
|
|
left_edge = pfp_x + pfp_size[0] + 30 # Start progress bar after profile picture
|
|
stats_x = desired_card_size[0] - 400 # Right side stats position
|
|
<<<<<<< HEAD
|
|
top_padding = 20 # Reduced top padding for higher text placement
|
|
=======
|
|
top_padding = 40 # Consistent top padding for all text
|
|
>>>>>>> 3ecf002 (Refactor default.py to standardize top padding for text elements in profile card rendering, ensuring consistent alignment of balance and level text. Move level text positioning to improve layout aesthetics and readability.)
|
|
|
|
# Draw stats with consistent spacing
|
|
title_font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), 32)
|
|
value_font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), 32)
|
|
|
|
# Balance and Rank stats - back on the right side
|
|
balance_y = top_padding
|
|
draw.text((stats_x, balance_y), "BALANCE:", font=title_font, fill=(200, 200, 200))
|
|
draw.text((stats_x + 150, balance_y), f"{humanize_number(balance)} CREDITS", font=value_font, fill=stat_color or (255, 255, 255))
|
|
|
|
rank_y = balance_y + 50
|
|
draw.text((stats_x, rank_y), "RANK:", font=title_font, fill=(200, 200, 200))
|
|
draw.text((stats_x + 150, rank_y), f"#{humanize_number(position)}", font=value_font, fill=stat_color or (255, 255, 255))
|
|
|
|
# XP Text - back on the right
|
|
xp_y = rank_y + 50
|
|
xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}"
|
|
draw.text((stats_x, xp_y), xp_text, font=value_font, fill=(255, 255, 255))
|
|
|
|
# Level text - aligned with balance text
|
|
level_font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), 48)
|
|
level_text = f"LEVEL {level}"
|
|
if prestige > 0:
|
|
level_text = f"P{prestige} • {level_text}"
|
|
draw.text((left_edge, top_padding), level_text, font=level_font, fill=user_color or (255, 255, 255))
|
|
|
|
# Progress bar - starts after profile picture
|
|
progress = (current_xp - previous_xp) / (next_xp - previous_xp) if next_xp > previous_xp else 0
|
|
progress = max(0, min(1, progress)) # Ensure progress is between 0 and 1
|
|
|
|
bar_width = desired_card_size[0] - left_edge - 15 # Width from after profile to right edge
|
|
bar_height = 40
|
|
bar_x = left_edge # Start after profile picture
|
|
bar_y = desired_card_size[1] - bar_height - 15 # Bottom padding
|
|
|
|
# Progress bar background
|
|
draw.rectangle((bar_x, bar_y, bar_x + bar_width, bar_y + bar_height), fill=(50, 50, 50))
|
|
|
|
# Progress bar fill
|
|
if progress > 0:
|
|
progress_width = int(bar_width * progress)
|
|
draw.rectangle((bar_x, bar_y, bar_x + progress_width, bar_y + bar_height), fill=level_bar_color or user_color)
|
|
|
|
# Progress percentage
|
|
percent_text = f"{int(progress * 100)}%"
|
|
percent_font = ImageFont.truetype(str(font_path or imgtools.DEFAULT_FONT), 24)
|
|
percent_w = draw.textlength(percent_text, font=percent_font)
|
|
percent_x = bar_x + 10 # Offset from start of progress bar
|
|
percent_y = bar_y + (bar_height - 24) // 2 # Center in bar
|
|
|
|
# Draw percentage
|
|
draw.text((percent_x, percent_y), percent_text, font=percent_font, fill=(255, 255, 255))
|
|
|
|
# Composite the layers
|
|
if not render_gif or (not pfp_animated and not bg_animated):
|
|
if card.mode != "RGBA":
|
|
card = card.convert("RGBA")
|
|
if pfp.mode != "RGBA":
|
|
pfp = pfp.convert("RGBA")
|
|
|
|
# Fit the background to the desired size
|
|
card = imgtools.fit_aspect_ratio(card, desired_card_size)
|
|
|
|
# Create circular profile picture
|
|
pfp = pfp.resize(pfp_size, Image.Resampling.LANCZOS)
|
|
pfp = imgtools.make_profile_circle(pfp)
|
|
|
|
# Create final image
|
|
final_image = Image.new("RGBA", desired_card_size, (0, 0, 0, 0))
|
|
|
|
# Layer order: card -> dark background -> stats -> profile picture
|
|
final_image.paste(card, (0, 0), card)
|
|
final_image = Image.alpha_composite(final_image, dark_bg)
|
|
final_image = Image.alpha_composite(final_image, stats_layer)
|
|
final_image.paste(pfp, (pfp_x, pfp_y), pfp)
|
|
|
|
if debug:
|
|
final_image.show()
|
|
buffer = BytesIO()
|
|
final_image.save(buffer, format="WEBP", quality=95)
|
|
final_image.close()
|
|
return buffer.getvalue(), False
|
|
|
|
if pfp_animated and not bg_animated:
|
|
if card.mode != "RGBA":
|
|
log.debug(f"Converting card mode '{card.mode}' to RGBA")
|
|
card = card.convert("RGBA")
|
|
card = imgtools.fit_aspect_ratio(card, desired_card_size)
|
|
if blur:
|
|
blur_section = imgtools.blur_section(card, (stats_area[0], 0, card.width, card.height))
|
|
# Paste onto the stats
|
|
card.paste(blur_section, (stats_area[0], 0), blur_section)
|
|
|
|
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")
|
|
frames: t.List[Image.Image] = []
|
|
for frame in range(pfp.n_frames):
|
|
pfp.seek(frame)
|
|
# Prepare copies of the card, stats, and pfp
|
|
card_frame = card.copy()
|
|
pfp_frame = pfp.copy()
|
|
if pfp_frame.mode != "RGBA":
|
|
pfp_frame = pfp_frame.convert("RGBA")
|
|
# Resize the profile image for each frame
|
|
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
|
|
card_frame.paste(pfp_frame, (circle_x, circle_y), pfp_frame)
|
|
frames.append(card_frame)
|
|
|
|
buffer = BytesIO()
|
|
frames[0].save(
|
|
buffer,
|
|
format="GIF",
|
|
save_all=True,
|
|
append_images=frames[1:],
|
|
duration=avg_duration,
|
|
loop=0,
|
|
quality=75,
|
|
optimize=True,
|
|
)
|
|
buffer.seek(0)
|
|
if debug:
|
|
Image.open(buffer).show()
|
|
return buffer.getvalue(), True
|
|
elif bg_animated and not pfp_animated:
|
|
avg_duration = imgtools.get_avg_duration(card)
|
|
log.debug(f"Rendering card as gif with avg duration of {avg_duration}ms")
|
|
frames: t.List[Image.Image] = []
|
|
|
|
if pfp.mode != "RGBA":
|
|
log.debug(f"Converting pfp mode '{pfp.mode}' to RGBA")
|
|
pfp = pfp.convert("RGBA")
|
|
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):
|
|
card.seek(frame)
|
|
# Prepare copies of the card and stats
|
|
card_frame = card.copy()
|
|
card_frame = imgtools.fit_aspect_ratio(card_frame, desired_card_size)
|
|
if card_frame.mode != "RGBA":
|
|
card_frame = card_frame.convert("RGBA")
|
|
|
|
# Paste items onto the card
|
|
if blur:
|
|
blur_section = imgtools.blur_section(card_frame, (stats_area[0], 0, card_frame.width, card_frame.height))
|
|
card_frame.paste(blur_section, (stats_area[0], 0), blur_section)
|
|
|
|
card_frame.paste(pfp, (circle_x, circle_y), pfp)
|
|
card_frame.paste(stats_layer, (0, 0), stats_layer)
|
|
|
|
frames.append(card_frame)
|
|
|
|
buffer = BytesIO()
|
|
frames[0].save(
|
|
buffer,
|
|
format="GIF",
|
|
save_all=True,
|
|
append_images=frames[1:],
|
|
duration=avg_duration,
|
|
loop=0,
|
|
quality=75,
|
|
optimize=True,
|
|
)
|
|
buffer.seek(0)
|
|
if debug:
|
|
Image.open(buffer).show()
|
|
return buffer.getvalue(), True
|
|
|
|
# If we're here, both the avatar and background are gifs
|
|
# Figure out how to merge the two frame counts and durations together
|
|
# Calculate frame durations based on the LCM
|
|
pfp_duration = imgtools.get_avg_duration(pfp) # example: 50ms
|
|
card_duration = imgtools.get_avg_duration(card) # example: 100ms
|
|
log.debug(f"PFP duration: {pfp_duration}ms, Card duration: {card_duration}ms")
|
|
# Figure out how to round the durations
|
|
# Favor the card's duration time over the pfp
|
|
# Round both durations to the nearest X ms based on what will get the closest to the LCM
|
|
pfp_duration = round(card_duration, -1) # Round to the nearest 10ms
|
|
card_duration = round(card_duration, -1) # Round to the nearest 10ms
|
|
|
|
log.debug(f"Modified PFP duration: {pfp_duration}ms, Card duration: {card_duration}ms")
|
|
combined_duration = math.lcm(pfp_duration, card_duration) # example: 100ms would be the LCM of 50 and 100
|
|
log.debug(f"Combined duration: {combined_duration}ms")
|
|
# The combined duration should be no more than 20% offset from the image with the highest duration
|
|
max_duration = max(pfp_duration, card_duration)
|
|
if combined_duration > max_duration * 1.2:
|
|
log.debug(f"Combined duration is more than 20% offset from the max duration ({max_duration}ms)")
|
|
combined_duration = max_duration
|
|
|
|
total_pfp_duration = pfp.n_frames * pfp_duration # example: 2250ms
|
|
total_card_duration = card.n_frames * card_duration # example: 3300ms
|
|
# Total duration for the combined animation cycle (LCM of 2250 and 3300)
|
|
total_duration = math.lcm(total_pfp_duration, total_card_duration) # example: 9900ms
|
|
num_combined_frames = total_duration // combined_duration
|
|
|
|
# The maximum frame count should be no more than 20% offset from the image with the highest frame count to avoid filesize bloat
|
|
max_frame_count = max(pfp.n_frames, card.n_frames) * 1.2
|
|
max_frame_count = min(round(max_frame_count), num_combined_frames)
|
|
log.debug(f"Max frame count: {max_frame_count}")
|
|
# Create a list to store the combined frames
|
|
combined_frames = []
|
|
for frame_num in range(max_frame_count):
|
|
time = frame_num * combined_duration
|
|
|
|
# Calculate the frame index for both the card and pfp
|
|
card_frame_index = (time // card_duration) % card.n_frames
|
|
pfp_frame_index = (time // pfp_duration) % pfp.n_frames
|
|
|
|
# Get the frames for the card and pfp
|
|
card_frame = ImageSequence.Iterator(card)[card_frame_index]
|
|
pfp_frame = ImageSequence.Iterator(pfp)[pfp_frame_index]
|
|
|
|
card_frame = imgtools.fit_aspect_ratio(card_frame, desired_card_size)
|
|
if card_frame.mode != "RGBA":
|
|
card_frame = card_frame.convert("RGBA")
|
|
|
|
if blur:
|
|
blur_section = imgtools.blur_section(card_frame, (stats_area[0], 0, card_frame.width, card_frame.height))
|
|
# Paste onto the stats
|
|
card_frame.paste(blur_section, (stats_area[0], 0), blur_section)
|
|
if pfp_frame.mode != "RGBA":
|
|
pfp_frame = pfp_frame.convert("RGBA")
|
|
|
|
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_layer, (0, 0), stats_layer)
|
|
|
|
combined_frames.append(card_frame)
|
|
|
|
buffer = BytesIO()
|
|
combined_frames[0].save(
|
|
buffer,
|
|
format="GIF",
|
|
save_all=True,
|
|
append_images=combined_frames[1:],
|
|
loop=0,
|
|
duration=combined_duration,
|
|
quality=75,
|
|
optimize=True,
|
|
)
|
|
buffer.seek(0)
|
|
|
|
if debug:
|
|
Image.open(buffer).show()
|
|
|
|
return buffer.getvalue(), True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Setup console logging
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
logging.getLogger("PIL").setLevel(logging.INFO)
|
|
|
|
test_banner = (imgtools.ASSETS / "tests" / "banner3.gif").read_bytes()
|
|
test_avatar = (imgtools.ASSETS / "tests" / "tree.gif").read_bytes()
|
|
test_icon = (imgtools.ASSETS / "tests" / "icon.png").read_bytes()
|
|
font_path = imgtools.ASSETS / "fonts" / "BebasNeue.ttf"
|
|
res, animated = generate_default_profile(
|
|
background_bytes=test_banner,
|
|
avatar_bytes=test_avatar,
|
|
username="Vertyco",
|
|
status="online",
|
|
level=999,
|
|
messages=420,
|
|
voicetime=399815,
|
|
stars=693333,
|
|
prestige=2,
|
|
prestige_emoji=test_icon,
|
|
balance=1000000,
|
|
currency_name="Coinz 💰",
|
|
previous_xp=1000,
|
|
current_xp=1258,
|
|
next_xp=5000,
|
|
role_icon=test_icon,
|
|
blur=True,
|
|
font_path=font_path,
|
|
render_gif=True,
|
|
debug=True,
|
|
)
|
|
result_path = imgtools.ASSETS / "tests" / "result.gif"
|
|
result_path.write_bytes(res)
|