from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
from pathlib import Path

SRC=Path('/home/spazio/.hermes/cache/images')
OUT=Path('/home/spazio/.hermes/media/cosmos_luminosity_editorial_v2')
OUT.mkdir(parents=True,exist_ok=True)
W,H=1080,1350
SERIF='/usr/share/fonts/truetype/freefont/FreeSerif.ttf'
SERIF_B='/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf'
SANS='/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
SANS_B='/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
WHITE=(250,248,243,255); VIOLET=(218,199,255,255); GOLD=(240,203,133,255)

# Every original photo is kept exactly once.
slides=[
 {'img':'img_3cc4464ceb2c.jpg','top':105,'num':'01','label':'COSMOS LUMINOSITY','title':'ЛЁГКОСТЬ —\nЭТО СПОСОБ\nУВИДЕТЬ БОЛЬШЕ','sub':'5 вопросов, которые меняют направление внимания','pos':'left_top','cover':True,'x':62,'y':126,'maxw':650,'ts':58},
 {'img':'img_c8d51015a0b1.jpg','top':35,'num':'02','label':'ПЕРВЫЙ ВОПРОС','title':'Что здесь\nфакт, а что —\nмой старый\nвывод?','sub':'Отдели происходящее сейчас от истории, которую ты уже знаешь наизусть.','pos':'left_top','x':62,'y':126,'maxw':330,'ts':43},
 {'img':'img_8e6f69e42a10.jpg','top':145,'num':'03','label':'ВТОРОЙ ВОПРОС','title':'Я сейчас\nрасслаблена\nили напряжена?','sub':'А каким образом я могу расслабиться ещё на 1%?','pos':'left_top','x':62,'y':126,'maxw':330,'ts':45},
 {'img':'img_6fea458148b1.jpg','top':205,'num':'04','label':'ТРЕТИЙ ВОПРОС','title':'Какой вариант\nя пока даже\nне рассматриваю?','sub':'Вопрос открывает внимание туда, куда старый вывод уже перестал смотреть.','pos':'left_top','x':62,'y':126,'maxw':380,'ts':46},
 {'img':'img_7b02ae89a282.jpg','top':80,'num':'05','label':'ЧЕТВЁРТЫЙ ВОПРОС','title':'Где здесь\nуже есть\nлёгкость?','sub':'Возможно, она уже появилась — в помощи, паузе, честном разговоре или другом ритме.','pos':'left_top','x':62,'y':126,'maxw':315,'ts':47},
 {'img':'img_3649c3a605f4.jpg','top':5,'num':'06','label':'ПЯТЫЙ ВОПРОС','title':'Какой\nследующий\nживой ход\nоткрывается\nсейчас?','sub':'Один доступный шаг может изменить направление движения.','pos':'left_top','x':62,'y':140,'maxw':270,'ts':38},
 {'img':'img_377dc32aec1e.jpg','top':0,'num':'07','label':'COSMOS LUMINOSITY','title':'ВОПРОС МЕНЯЕТ\nНАПРАВЛЕНИЕ\nВНИМАНИЯ','sub':'А замеченное постепенно становится новым выбором, действием и доступной реальностью.\n\nСОХРАНИ ЭТИ ВОПРОСЫ.','pos':'left_top','x':62,'y':140,'maxw':440,'ts':45,'final':True},
]

def f(path,size): return ImageFont.truetype(path,size)

def crop(im,top):
    # Exact 4:5 crop from original 721×1280; preserve full width.
    ch=round(im.width*5/4)
    top=max(0,min(im.height-ch,top))
    return im.crop((0,top,im.width,top+ch)).resize((W,H),Image.Resampling.LANCZOS)

def wrap(draw,text,font,maxw):
    out=[]
    for para in text.split('\n'):
        if para=='': out.append(''); continue
        words=para.split(); line=''
        for word in words:
            trial=word if not line else line+' '+word
            if draw.textlength(trial,font=font)<=maxw: line=trial
            else:
                if line: out.append(line)
                line=word
        if line: out.append(line)
    return '\n'.join(out)

def local_gradient(im,pos):
    layer=Image.new('RGBA',(W,H),(0,0,0,0)); px=layer.load()
    # Soft editorial veil only behind the typography, no box.
    if pos=='left_top':
        for y in range(0,760):
            vy=max(0,1-y/760)
            for x in range(W):
                vx=max(0,1-x/820)
                a=int(185*vx*(0.55+0.45*vy))
                px[x,y]=(4,5,11,a)
    else:
        for y in range(500,H):
            vy=(y-500)/(H-500)
            for x in range(W):
                vx=max(.12,1-x/900)
                a=int((45+175*vy)*vx)
                px[x,y]=(4,5,11,a)
    return Image.alpha_composite(im.convert('RGBA'),layer)

def shadow_text(draw,xy,text,font,fill,spacing=4,stroke=0):
    x,y=xy
    draw.multiline_text((x+2,y+3),text,font=font,fill=(0,0,0,125),spacing=spacing,stroke_width=stroke)
    draw.multiline_text((x,y),text,font=font,fill=fill,spacing=spacing,stroke_width=stroke)

def render(s,i):
    im=crop(Image.open(SRC/s['img']).convert('RGB'),s['top'])
    im=ImageEnhance.Contrast(im).enhance(1.05)
    im=ImageEnhance.Color(im).enhance(.94)
    im=local_gradient(im,s['pos'])
    d=ImageDraw.Draw(im)

    # Hairline frame and small editorial folio.
    d.rectangle((28,28,W-28,H-28),outline=(255,255,255,45),width=1)
    d.text((60,52),s['label'],font=f(SANS_B,17),fill=VIOLET)
    d.text((1020,52),s['num']+' / 07',font=f(SANS,17),fill=(255,255,255,175),anchor='ra')

    x=s.get('x',62); y=s.get('y',126)

    title_font=f(SERIF_B,s.get('ts',55 if not s.get('cover') else 58))
    title=wrap(d,s['title'],title_font,s.get('maxw',650))
    shadow_text(d,(x,y),title,title_font,WHITE,spacing=-1)
    box=d.multiline_textbbox((x,y),title,font=title_font,spacing=-1)
    yy=box[3]+26
    d.line((x,yy,x+78,yy),fill=GOLD,width=3)
    yy+=25
    sub_font=f(SANS,20 if not s.get('final') else 19)
    sub=wrap(d,s['sub'],sub_font,s.get('maxw',650))
    shadow_text(d,(x,yy),sub,sub_font,(255,255,255,225),spacing=8)

    # Tiny signature kept away from UI edges.
    d.text((1020,1290),'a living interface of attention',font=f(SERIF,17),fill=(255,255,255,145),anchor='ra')
    p=OUT/f'{i:02d}_editorial.jpg'
    im.convert('RGB').save(p,quality=96,subsampling=0)
    return p

outs=[render(s,i) for i,s in enumerate(slides,1)]
# contact sheet 2 rows, large enough for visual QA
thumbs=[]
for p in outs:
    t=Image.open(p); t.thumbnail((360,450)); thumbs.append(t)
sheet=Image.new('RGB',(1440,900),(13,13,16))
for i,t in enumerate(thumbs): sheet.paste(t,((i%4)*360,(i//4)*450))
sheet.save(OUT/'00_preview.jpg',quality=94)
print('\n'.join(map(str,outs)))
print(OUT/'00_preview.jpg')
