Ruby-Cogs/levelup/generator/skia_generator.py
Valerie 914e7a725b Add Skia support for image generation and update project metadata
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.
2025-05-26 09:06:08 -04:00

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)