File size: 5,987 Bytes
732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 78a11e2 732ca45 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | # utils/generators/custom.py
import io
import logging
from PIL import Image, ImageDraw, ImageOps, ImageFilter, ImageStat
from .base import BaseGenerator
logger = logging.getLogger(__name__)
class CustomGenerator(BaseGenerator):
def build_template(self, image_data: bytes, frame_data: bytes, char_name: str) -> Image.Image:
cleanup = []
try:
canvas = Image.new("RGBA", (self.CANVAS_W, self.CANVAS_H), (0, 0, 0, 0))
# 1. Load the frame and IMMEDIATELY resize it to the exact card bounds (580x820)
# This guarantees we never double-resample the character later.
frame_raw = Image.open(io.BytesIO(frame_data)).convert("RGBA")
cleanup.append(frame_raw)
frame_base = frame_raw.resize((self.CARD_W, self.CARD_H), Image.Resampling.LANCZOS)
cleanup.append(frame_base)
f_w, f_h = self.CARD_W, self.CARD_H
alpha = frame_base.split()[3]
# 2. Extract dynamic theme color
try:
solid_pixels = alpha.point(lambda p: 255 if p > 50 else 0, mode="1")
stat = ImageStat.Stat(frame_base.convert("RGB"), mask=solid_pixels)
avg_r, avg_g, avg_b = [int(x) for x in stat.mean]
if max(avg_r, avg_g, avg_b) < 80:
avg_r, avg_g, avg_b = 160, 160, 160
self.frame_color = (avg_r, avg_g, avg_b)
except Exception as e:
logger.error(f"Color extraction failed: {e}")
self.frame_color = None
# 3. Create a STRICT binary mask for the floodfill
# This prevents the "diagonal slice" by removing soft, semi-transparent pixels
binary_alpha = alpha.point(lambda p: 255 if p > 20 else 0, mode="L")
cleanup.append(binary_alpha)
ImageDraw.floodfill(binary_alpha, (f_w // 2, f_h // 2), 128)
hole_mask = binary_alpha.point(lambda p: 255 if p == 128 else 0, mode="L")
cleanup.append(hole_mask)
# Anti-alias the final cutout so edges are smooth
bleed_mask = hole_mask.filter(ImageFilter.MaxFilter(5)).filter(ImageFilter.GaussianBlur(1.5))
cleanup.append(bleed_mask)
# 4. Load Character
char_raw = Image.open(io.BytesIO(image_data)).convert("RGBA")
cleanup.append(char_raw)
bbox = hole_mask.getbbox()
if bbox:
hx1, hy1, hx2, hy2 = bbox
hw, hh = hx2 - hx1, hy2 - hy1
else:
hx1, hy1, hw, hh = 0, 0, f_w, f_h
# 5. Crop character EXACTLY ONCE to the final 1:1 pixel dimensions (LANCZOS for max quality)
char_fitted = ImageOps.fit(char_raw, (hw, hh), method=Image.Resampling.LANCZOS, centering=(0.5, 0.25))
cleanup.append(char_fitted)
# 6. Composite
char_full = Image.new("RGBA", (f_w, f_h), (0, 0, 0, 0))
char_full.paste(char_fitted, (hx1, hy1))
cleanup.append(char_full)
char_full.putalpha(bleed_mask)
composite = Image.new("RGBA", (f_w, f_h), (0, 0, 0, 0))
composite.paste(char_full, (0, 0), char_full)
composite.paste(frame_base, (0, 0), frame_base)
cleanup.append(composite)
# 7. Final Paste (Card fits perfectly on the 660x900 canvas with no extra resizing)
canvas.paste(composite, (self.OFFSET_X, self.OFFSET_Y), composite)
return canvas
except Exception as e:
logger.error(f"Cookie Cutter Frame Error: {e}", exc_info=True)
return Image.new("RGBA", (self.CANVAS_W, self.CANVAS_H), (0,0,0,0))
finally:
for img in cleanup:
try: img.close()
except: pass
def apply_text(self, canvas: Image.Image, char_name: str, print_num: int, element: str, rarity: str) -> io.BytesIO:
uid = ImageDraw.Draw(canvas, "RGBA")
card_left = self.OFFSET_X
card_right = self.OFFSET_X + self.CARD_W
card_bottom = self.OFFSET_Y + self.CARD_H
card_w = self.CARD_W
name_sz = max(24, int(card_w * 0.080))
min_sz = 20
name_font = self.get_font(name_sz)
num_font = self.get_font(int(name_sz * 0.95))
r_color = self.ELEMENT_COLORS.get(element, (168, 85, 247))
glow_solid = getattr(self, 'frame_color', r_color[:3])
final_char_name = char_name.title()
# Keep text strictly inside the borders
max_name_w = int(card_w * 0.85)
# Dynamic Text Scaler Loop
while True:
try:
n_bbox = uid.textbbox((0, 0), final_char_name, font=name_font)
except:
n_bbox = (0, 0, 20, name_sz)
n_w = n_bbox[2] - n_bbox[0]
if n_w <= max_name_w or name_sz <= min_sz:
break
name_sz -= 2
name_font = self.get_font(name_sz)
name_x = card_left + (card_w - n_w) // 2
name_y = card_bottom - 120
num_text = f"{print_num}"
try: num_bbox = uid.textbbox((0, 0), num_text, font=num_font)
except: num_bbox = (0, 0, 40, 30)
num_w = num_bbox[2] - num_bbox[0]
num_h = num_bbox[3] - num_bbox[1]
right_margin = max(40, int(card_w * 0.18))
num_x = card_right - right_margin - num_w
num_x = max(card_left + right_margin, num_x)
num_y = name_y - num_h - 10
self.draw_text_with_glow(uid, (name_x, name_y), final_char_name, name_font, core_color="white", glow_color=glow_solid)
self.draw_text_with_glow(uid, (num_x, num_y), num_text, num_font, core_color="white", glow_color=glow_solid)
buf = io.BytesIO()
canvas.save(buf, format="WEBP", quality=95)
buf.seek(0)
return buf
|