Spaces:
Sleeping
Sleeping
| # utils/generators/base.py | |
| import io | |
| import os | |
| import urllib.request | |
| from PIL import Image, ImageDraw, ImageFont | |
| class BaseGenerator: | |
| CANVAS_W, CANVAS_H = (660, 900) | |
| CARD_W, CARD_H = (580, 820) | |
| CORNER_RAD = 16 | |
| BANNER_H = 160 | |
| OFFSET_X, OFFSET_Y = (40, 40) | |
| ELEMENT_COLORS = { | |
| "Solstice": (239, 68, 68), "Equinox": (59, 130, 246), "Eclipse": (147, 197, 253), | |
| "Zenith": (168, 85, 247), "Aether": (16, 185, 129), "Nadir": (250, 204, 21), | |
| "Genesis": (34, 197, 94), "Light": (255, 255, 224), "Dark": (30, 27, 75), "None": (156, 163, 175), | |
| "Pyro": (239, 68, 68), "Fire": (239, 68, 68), "Hydro": (59, 130, 246), "Water": (59, 130, 246), | |
| "Cryo": (147, 197, 253), "Ice": (147, 197, 253), "Electro": (168, 85, 247), | |
| "Anemo": (16, 185, 129), "Wind": (16, 185, 129), "Geo": (250, 204, 21), "Earth": (250, 204, 21), | |
| "Dendro": (34, 197, 94) | |
| } | |
| def get_font(size: int): | |
| # 🛡️ THE BULLETPROOF FONT FIX | |
| base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| assets_dir = os.path.join(base_dir, "assets") | |
| os.makedirs(assets_dir, exist_ok=True) | |
| font_path = os.path.join(assets_dir, "font.ttf") | |
| # If font is missing OR corrupted (< 10KB), download a fresh one automatically | |
| if not os.path.exists(font_path) or os.path.getsize(font_path) < 10000: | |
| try: | |
| print("Font missing or corrupted! Downloading fresh font...") | |
| url = "https://github.com/google/fonts/raw/main/ofl/roboto/Roboto-Bold.ttf" | |
| urllib.request.urlretrieve(url, font_path) | |
| except Exception as e: | |
| print(f"Download failed: {e}") | |
| try: | |
| return ImageFont.truetype(font_path, size) | |
| except Exception as e: | |
| print(f"Pillow failed to load font: {e}") | |
| return ImageFont.load_default() | |
| def create_rounded_mask(size, radius): | |
| scale = 3 | |
| mask = Image.new('L', (size[0]*scale, size[1]*scale), 0) | |
| draw = ImageDraw.Draw(mask) | |
| draw.rounded_rectangle((0, 0, size[0]*scale, size[1]*scale), radius=radius*scale, fill=255) | |
| return mask.resize(size, Image.Resampling.LANCZOS) | |
| def create_bottom_rounded_mask(size, radius): | |
| scale = 3 | |
| mask = Image.new('L', (size[0]*scale, size[1]*scale), 0) | |
| draw = ImageDraw.Draw(mask) | |
| draw.rounded_rectangle((0, 0, size[0]*scale, size[1]*scale), radius=radius*scale, fill=255) | |
| draw.rectangle((0, 0, size[0]*scale, radius*scale), fill=255) | |
| return mask.resize(size, Image.Resampling.LANCZOS) | |
| def draw_text_with_glow(uid, position, text, font, core_color="white", glow_color=(0,0,0)): | |
| uid.text(position, text, font=font, fill=core_color, stroke_width=3, stroke_fill=glow_color) | |
| def apply_text(self, blank_template: Image.Image, char_name: str, print_num: int, element: str, rarity: str) -> io.BytesIO: | |
| canvas = blank_template.copy() | |
| uid = ImageDraw.Draw(canvas) | |
| r_color = self.ELEMENT_COLORS.get(element, (250, 204, 21) if rarity in ["Legendary", "Mythic", "SSR", "UR"] else (156, 163, 175)) | |
| light_r = tuple(min(255, c + 150) for c in r_color) | |
| glow_solid = r_color[:3] | |
| base_sz = int(self.CARD_W * 0.085) | |
| num_font = self.get_font(base_sz) | |
| num_text = str(print_num) | |
| # ========================================== | |
| # 1. Standard Number Box (100% UNTOUCHED LOGIC) | |
| # ========================================== | |
| try: | |
| num_w = num_font.getlength(num_text) | |
| except: | |
| num_w = 20 | |
| box_w = num_w + 24 | |
| box_h = 46 | |
| box_x2 = self.OFFSET_X + self.CARD_W - 30 | |
| box_x1 = box_x2 - box_w | |
| box_y1 = self.OFFSET_Y + self.CARD_H - self.BANNER_H + (self.BANNER_H - box_h) // 2 | |
| box_y2 = box_y1 + box_h | |
| uid.rounded_rectangle((box_x1, box_y1, box_x2, box_y2), radius=10, fill=(10, 10, 10, 180), outline=light_r, width=2) | |
| num_x = box_x1 + (box_w - num_w) / 2 | |
| num_y = box_y1 - 6 | |
| self.draw_text_with_glow(uid, (num_x, num_y), num_text, num_font, core_color="white", glow_color=glow_solid) | |
| # ========================================== | |
| # 2. Dynamic Centered Character Name | |
| # ========================================== | |
| final_char_name = char_name.title() | |
| available_name_w = (box_x1 - self.OFFSET_X) - 20 | |
| name_sz = base_sz | |
| min_sz = 20 | |
| name_font = self.get_font(name_sz) | |
| # ⚡ THE FIX: Dynamic Scaler Loop. Shrink font until it fits the available width. | |
| while True: | |
| try: | |
| n_w = name_font.getlength(final_char_name) | |
| except AttributeError: | |
| n_w = 20 | |
| if n_w <= available_name_w or name_sz <= min_sz: | |
| break | |
| name_sz -= 2 | |
| name_font = self.get_font(name_sz) | |
| try: | |
| n_bbox = uid.textbbox((0, 0), final_char_name, font=name_font, stroke_width=3) | |
| except: | |
| n_bbox = (0, 0, n_w, name_sz) | |
| n_w_final = n_bbox[2] - n_bbox[0] | |
| n_h_final = n_bbox[3] - n_bbox[1] | |
| # Center horizontally in the available space, and perfectly vertically in the banner | |
| name_x = self.OFFSET_X + 10 + (available_name_w - n_w_final) // 2 - n_bbox[0] | |
| name_y = self.OFFSET_Y + self.CARD_H - self.BANNER_H + (self.BANNER_H - n_h_final) // 2 - n_bbox[1] | |
| self.draw_text_with_glow(uid, (name_x, name_y), final_char_name, name_font, core_color="white", glow_color=glow_solid) | |
| buf = io.BytesIO() | |
| canvas.save(buf, format="WEBP", quality=85, method=0) | |
| buf.seek(0) | |
| return buf | |