Introduce a new command to toggle Skia-based image generation in the LevelUp cog, enhancing image quality and performance. Update the pyproject.toml file to include project metadata and dependencies, including skia-python. Modify the levelalert generator to support Skia, with fallback to the original implementation if necessary. Additionally, update the author list in info.json to include a new contributor.
185 lines
No EOL
5.9 KiB
Python
185 lines
No EOL
5.9 KiB
Python
"""Modern image generation using Skia for better quality and performance."""
|
|
|
|
import logging
|
|
import math
|
|
import typing as t
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
import skia
|
|
|
|
try:
|
|
from . import imgtools
|
|
except ImportError:
|
|
import imgtools
|
|
|
|
log = logging.getLogger("red.vrt.levelup.generator.skia_generator")
|
|
|
|
def create_surface(width: int, height: int) -> t.Tuple[skia.Surface, skia.Canvas]:
|
|
"""Create a new Skia surface and canvas."""
|
|
surface = skia.Surface(width, height)
|
|
canvas = surface.getCanvas()
|
|
return surface, canvas
|
|
|
|
def create_rounded_rect_path(rect: skia.Rect, radius: float) -> skia.Path:
|
|
"""Create a rounded rectangle path."""
|
|
path = skia.Path()
|
|
path.addRoundRect(rect, radius, radius)
|
|
return path
|
|
|
|
def create_glass_effect(canvas: skia.Canvas, rect: skia.Rect, radius: float, alpha: int = 128):
|
|
"""Create a modern glass effect with blur."""
|
|
# Create semi-transparent background
|
|
paint = skia.Paint(
|
|
Color=skia.Color4f(0, 0, 0, alpha/255),
|
|
AntiAlias=True,
|
|
)
|
|
|
|
# Create blur effect
|
|
blur_mask = skia.Surface(int(rect.width()), int(rect.height()))
|
|
blur_canvas = blur_mask.getCanvas()
|
|
blur_paint = skia.Paint(
|
|
Color=skia.ColorWHITE,
|
|
AntiAlias=True,
|
|
)
|
|
blur_path = create_rounded_rect_path(
|
|
skia.Rect(0, 0, rect.width(), rect.height()),
|
|
radius
|
|
)
|
|
blur_canvas.drawPath(blur_path, blur_paint)
|
|
|
|
# Apply gaussian blur
|
|
blur_filter = skia.ImageFilters.Blur(10, 10)
|
|
paint.setImageFilter(blur_filter)
|
|
|
|
# Draw the glass effect
|
|
canvas.save()
|
|
canvas.translate(rect.left(), rect.top())
|
|
canvas.drawPath(create_rounded_rect_path(rect, radius), paint)
|
|
canvas.restore()
|
|
|
|
def generate_level_img(
|
|
background_bytes: t.Optional[bytes] = None,
|
|
avatar_bytes: t.Optional[bytes] = None,
|
|
level: int = 1,
|
|
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,
|
|
) -> t.Tuple[bytes, bool]:
|
|
"""Generate a modern level-up alert image using Skia."""
|
|
|
|
# Default color if none provided
|
|
if color is None:
|
|
color = (155, 17, 30) # Ruby red
|
|
|
|
# Card dimensions
|
|
width, height = 200, 70
|
|
|
|
# Create surface and canvas
|
|
surface, canvas = create_surface(width, height)
|
|
|
|
# Load and draw background
|
|
if background_bytes:
|
|
try:
|
|
background_data = skia.Data.makeWithCopy(background_bytes)
|
|
background_image = skia.Image.MakeFromEncoded(background_data)
|
|
if background_image:
|
|
# Scale background to fit
|
|
scale = max(width / background_image.width(), height / background_image.height())
|
|
scaled_width = int(background_image.width() * scale)
|
|
scaled_height = int(background_image.height() * scale)
|
|
|
|
# Center the background
|
|
x = (width - scaled_width) // 2
|
|
y = (height - scaled_height) // 2
|
|
|
|
canvas.drawImageRect(
|
|
background_image,
|
|
skia.Rect(x, y, x + scaled_width, y + scaled_height),
|
|
skia.Paint(AntiAlias=True)
|
|
)
|
|
except Exception as e:
|
|
log.error("Failed to load background image", exc_info=e)
|
|
|
|
# Create glass effect background
|
|
glass_rect = skia.Rect(0, 0, width, height)
|
|
create_glass_effect(canvas, glass_rect, 20)
|
|
|
|
# Load and draw avatar
|
|
if avatar_bytes:
|
|
try:
|
|
avatar_data = skia.Data.makeWithCopy(avatar_bytes)
|
|
avatar_image = skia.Image.MakeFromEncoded(avatar_data)
|
|
if avatar_image:
|
|
# Create circular clip for avatar
|
|
avatar_size = height
|
|
avatar_path = skia.Path()
|
|
avatar_path.addCircle(avatar_size/2, height/2, avatar_size/2)
|
|
|
|
canvas.save()
|
|
canvas.clipPath(avatar_path, antiAlias=True)
|
|
|
|
# Draw avatar
|
|
canvas.drawImageRect(
|
|
avatar_image,
|
|
skia.Rect(0, 0, avatar_size, avatar_size),
|
|
skia.Paint(AntiAlias=True)
|
|
)
|
|
canvas.restore()
|
|
except Exception as e:
|
|
log.error("Failed to load avatar image", exc_info=e)
|
|
|
|
# Load font
|
|
font_size = 30
|
|
if font_path:
|
|
typeface = skia.Typeface.MakeFromFile(str(font_path))
|
|
else:
|
|
typeface = skia.Typeface.MakeDefault()
|
|
font = skia.Font(typeface, font_size)
|
|
|
|
# Draw level text with modern styling
|
|
text = f"Level {level}"
|
|
text_paint = skia.Paint(
|
|
AntiAlias=True,
|
|
Color=skia.Color(*color),
|
|
)
|
|
|
|
# Add text shadow
|
|
shadow_paint = skia.Paint(
|
|
AntiAlias=True,
|
|
Color=skia.Color(0, 0, 0, 100),
|
|
)
|
|
|
|
# Calculate text position
|
|
text_blob = skia.TextBlob(text, font)
|
|
text_x = height + ((width - height) / 2) - (text_blob.bounds().width() / 2)
|
|
text_y = height/2 + font_size/3
|
|
|
|
# Draw shadow
|
|
canvas.drawTextBlob(text_blob, text_x + 2, text_y + 2, shadow_paint)
|
|
# Draw text
|
|
canvas.drawTextBlob(text_blob, text_x, text_y, text_paint)
|
|
|
|
# Convert to bytes
|
|
image = surface.makeImageSnapshot()
|
|
data = image.encodeToData(skia.kPNG)
|
|
|
|
if debug:
|
|
# Save debug image
|
|
with open("debug_level.png", "wb") as f:
|
|
f.write(data.bytes())
|
|
|
|
return data.bytes(), False # False since we don't support GIF yet
|
|
|
|
if __name__ == "__main__":
|
|
# Test the generator
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
test_avatar = (imgtools.ASSETS / "tests" / "tree.gif").read_bytes()
|
|
res, animated = generate_level_img(
|
|
avatar_bytes=test_avatar,
|
|
level=10,
|
|
debug=True,
|
|
)
|
|
result_path = imgtools.ASSETS / "tests" / "level_skia.png"
|
|
result_path.write_bytes(res) |