import io,unicodedata,urllib.request from pathlib import Path import discord from discord.ext import commands from PIL import Image,ImageDraw,ImageFilter,ImageFont,ImageOps from pilmoji import Pilmoji from core.threads import run_blocking W,H,AV,PAD,MAXC=1200,480,268,42,420 BG0,BG1,CARD,STROKE,ACC,TXT,SUB,MARK=(7,9,17),(15,20,36),(18,24,44),(46,64,108),(82,116,214),(236,242,255),(128,150,192),(52,80,160) FONT=Path(__file__).resolve().parents[1]/"font"/"NotoSans-Regular.ttf" def norm(t):return unicodedata.normalize("NFKC",t) def font(n): try:return ImageFont.truetype(str(FONT),n) except Exception:return ImageFont.load_default() def fetch(url): with urllib.request.urlopen(urllib.request.Request(url,headers={"User-Agent":"Mozilla/5.0"}),timeout=20) as r:return r.read() def names(t,users): for u in {u.id:u for u in users}.values(): n="@"+(getattr(u,"display_name",None) or u.name);t=t.replace(f"<@{u.id}>",n).replace(f"<@!{u.id}>",n) return t def gradient(): im=Image.new("RGB",(W,H));d=ImageDraw.Draw(im) for y in range(H): t=y/(H-1);d.line((0,y,W,y),fill=tuple(int(a+(b-a)*t) for a,b in zip(BG0,BG1))) return im.convert("RGBA") def glow(im): o=Image.new("RGBA",im.size,(0,0,0,0));d=ImageDraw.Draw(o);cx,cy,m=int(W*.74),int(H*.2),int(H*.7) for i in range(18): r=int(m*(1-i/18));d.ellipse((cx-r,cy-r,cx+r,cy+r),fill=(100,130,215,int(52*(1-i/18)))) o=o.filter(ImageFilter.GaussianBlur(12));im.paste(o,(0,0),o) def tw(d,t,f): l,_,r,_=d.textbbox((0,0),t,font=f);return r-l def chop(d,w,f,mw): out=[];cur="" for c in w: if cur and tw(d,cur+c,f)>mw:out.append(cur);cur=c else:cur+=c return out+([cur] if cur else []) def wrap(d,t,f,mw): lines=[] for p in t.splitlines() or [t]: ws=p.split() if not ws: if lines:lines.append("") continue cur="" for w in ws: for part in (chop(d,w,f,mw) if tw(d,w,f)>mw else [w]): x=part if not cur else cur+" "+part if tw(d,x,f)<=mw:cur=x else: if cur:lines.append(cur) cur=part if cur:lines.append(cur) return lines def lh(d,f): _,t,_,b=d.textbbox((0,0),"Ag",font=f);h=b-t;return max(h+6,int(h*1.26)) def fit(d,t,mw,mh): last=(["..."],font(15),lh(d,font(15))) for s in range(110,14,-2): f=font(s);l=wrap(d,t,f,mw);h=lh(d,f) if len(l)*h<=mh:return l or ["..."],f,h last=(l,f,h) l,f,h=last;return (l or ["..."])[:max(1,mh//max(1,h))],f,h def avatar(b): a=ImageOps.fit(Image.open(io.BytesIO(b)).convert("RGB"),(AV,AV));m=Image.new("L",(AV,AV),0) ImageDraw.Draw(m).ellipse((0,0,AV,AV),fill=255);r=Image.new("RGBA",(AV,AV),(0,0,0,0));r.paste(a,(0,0),m);return r def build(ab,text,sig): im=gradient();glow(im);d=ImageDraw.Draw(im) ax,ay=PAD,(H-AV)//2;cl,ct,cr,cb=ax+AV+18,24,W-24,H-24 sh=Image.new("RGBA",im.size,(0,0,0,0));ImageDraw.Draw(sh).rounded_rectangle((cl+7,ct+10,cr+7,cb+10),radius=28,fill=(0,0,0,105)) sh=sh.filter(ImageFilter.GaussianBlur(10));im.paste(sh,(0,0),sh) d.rounded_rectangle((cl,ct,cr,cb),radius=28,fill=CARD,outline=STROKE,width=2) gd=AV+28;g=Image.new("RGBA",(gd,gd),(0,0,0,0));gg=ImageDraw.Draw(g);c=gd//2 for i in range(10): r=c-i*4 if r<=0:break gg.ellipse((c-r,c-r,c+r,c+r),fill=(*ACC,int(55*(1-i/10)))) g=g.filter(ImageFilter.GaussianBlur(4));im.paste(g,(ax-14,ay-14),g) rd=AV+8;ring=Image.new("RGBA",(rd,rd),(0,0,0,0));ImageDraw.Draw(ring).ellipse((0,0,rd-1,rd-1),outline=(*ACC,175),width=3);im.paste(ring,(ax-4,ay-4),ring) a=avatar(ab);im.paste(a,(ax,ay),a) qx,zt,tr=cl+52,ct+44,cr-32;sf=font(27);sig=f"\u2014 {sig}";_,_,_,sh_=d.textbbox((0,0),sig,font=sf) sy=cb-sh_-30;dy=sy-15;zh=max(48,dy-zt-16) lines,qf,h=fit(d,text,tr-qx,zh) d.text((cl+10,ct+6),"\u201c",font=font(max(54,min(100,int(h*1.55)))),fill=MARK) cx=(qx+tr)//2;y=zt+max(0,(zh-len(lines)*h)//2) with Pilmoji(im) as p: for l in lines: p.text((cx-tw(d,l,qf)//2,y),l,font=qf,fill=TXT,emoji_scale_factor=1.08,emoji_position_offset=(0,-2));y+=h d.rectangle((qx,dy,tr,dy+1),fill=STROKE) p.text((qx,sy),sig,font=sf,fill=SUB,emoji_scale_factor=1.0,emoji_position_offset=(0,-1)) out=io.BytesIO();im.convert("RGB").save(out,format="PNG");out.seek(0);return out class q(commands.Cog): def __init__(s,bot):s.bot=bot @commands.command(name="q") async def q(s,ctx,*args): m=ctx.message;target=m.author;text="" ref=m.reference if not args and ref and ref.message_id: r=ref.resolved if isinstance(ref.resolved,discord.Message) else None if r is None: try:r=await m.channel.fetch_message(ref.message_id) except Exception:r=None if r: text=names((r.content or "").strip(),r.mentions) if text:target=r.author if not text: rest=list(args) if rest and rest[0].startswith("<@") and m.mentions: target=m.mentions[0];rest=[x for x in rest if x not in(f"<@{target.id}>",f"<@!{target.id}>")] text=names(" ".join(rest).strip(),m.mentions) if not text:return await ctx.send("usage: ;q text | ;q @user text | reply with ;q") text=norm(text[:MAXC]) url=str(target.display_avatar.replace(size=512,format="png").url) ab=await run_blocking(fetch,url) sig=norm(f"{getattr(target,'global_name',None) or target.display_name} (@{target.name})") img=await run_blocking(build,ab,text,sig) await ctx.send(file=discord.File(img,filename="quote.png")) async def setup(bot):await bot.add_cog(q(bot))