119 lines
5.7 KiB
Python
119 lines
5.7 KiB
Python
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))
|