"""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)