# users/signals.py import io import random from PIL import Image, ImageDraw, ImageFont from django.conf import settings from django.core.files.base import ContentFile from django.db.models.signals import post_save from django.dispatch import receiver from .models import User PALETTE = ["#34344A", "#845A6D", "#D47386", "#C89B7B", "#32746D"] def initials_for(user: User) -> str: if getattr(user, "initials", None): return user.initials[:2].upper() first = (user.first_name or "").strip() last = (user.last_name or "").strip() if first and last: return (first[0] + last[0]).upper() if first: return first[:2].upper() # fallback: email return (user.email or "U")[:2].upper() def generate_avatar_png(text: str, bg_hex: str, size: int = 256) -> bytes: img = Image.new("RGBA", (size, size), bg_hex) draw = ImageDraw.Draw(img) font_path = getattr(settings, "AVATAR_FONT_PATH", None) if font_path: font = ImageFont.truetype(font_path, int(size * 0.42)) else: font = ImageFont.load_default() bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] x = (size - tw) / 2 y = (size - th) / 2 - (size * 0.05) draw.text((x, y), text, font=font, fill="white") out = io.BytesIO() img.save(out, format="PNG", optimize=True) return out.getvalue() @receiver(post_save, sender=User) def set_default_avatar(sender, instance: User, created: bool, **kwargs): if not created: return if instance.image: return text = initials_for(instance) bg = random.choice(PALETTE) png_bytes = generate_avatar_png(text=text, bg_hex=bg, size=256) filename = f"user_{instance.pk}_avatar.png" instance.image.save(filename, ContentFile(png_bytes), save=False) User.objects.filter(pk=instance.pk).update( image=instance.image.name, )