# 第133話 英語版：吹き出しの日本語を消して英語をフォントで組む（2026-10-31 試作）
import json, numpy as np
from collections import deque
from PIL import Image, ImageDraw, ImageFont
SRC="../仕上げ/第133話_P{}_1080x1350.png"; OUT="第133話_P{}_EN_1080x1350.png"
import os
FONTS=os.path.expanduser("~/mnt/Fonts")  # Mac の ~/Library/Fonts（Comic Neue。今までの英語版マンガに合わせる）
F_HUMAN=FONTS+"/ComicNeue-Bold.ttf"
F_ROBOT=F_HUMAN
F_BOARD=FONTS+"/ComicNeue-Bold.ttf"
# 吹き出し：(コマ, 吹き出しの中の1点[x,y], 英語, フォント)
BUBBLES=[
 (1,(550,166),"The robot is all set and ready to go!",F_HUMAN),
 (3,(540,213),"I am ready to roll.\nAny time.",F_ROBOT),
 (4,(310,177),"So before we moved\nthe robot, we had to\nmove everyone.",F_HUMAN),
 (4,(809,177),"For the next ward,\nall six sign-offs go out\non day one.",F_HUMAN),
 (4,(545,411),"Heading out.",F_ROBOT),
]
LABELS=["Pharmacy","Nursing","Facilities","IT","Fire\nSafety","Patient\nSafety"]
# ホワイトボード：縦線のx（左の枠〜右の枠）、札の帯のy（上・下）
BOARDS={1:dict(xs=[101,221,324,427,530,633,771],y=(330,389)),
        2:dict(xs=[77,238,393,548,704,858,1017],y=(266,333))}

def fill_region(g, seed, thr=230):
    H,W=g.shape; m=np.zeros((H,W),bool); q=deque([seed]); m[seed[1],seed[0]]=True
    while q:
        x,y=q.popleft()
        for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
            xx,yy=x+dx,y+dy
            if 0<=xx<W and 0<=yy<H and not m[yy,xx] and g[yy,xx]>thr:
                m[yy,xx]=True; q.append((xx,yy))
    return m
def interior(comp):
    ys,xs=np.nonzero(comp); y0,y1,x0,x1=ys.min(),ys.max(),xs.min(),xs.max()
    sub=comp[y0:y1+1,x0:x1+1]; h,w=sub.shape
    out=np.zeros((h+2,w+2),bool); pad=np.zeros((h+2,w+2),bool); pad[1:-1,1:-1]=sub
    q=deque([(0,0)]); out[0,0]=True
    while q:
        x,y=q.popleft()
        for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
            xx,yy=x+dx,y+dy
            if 0<=xx<w+2 and 0<=yy<h+2 and not out[yy,xx] and not pad[yy,xx]:
                out[yy,xx]=True; q.append((xx,yy))
    filled=~out[1:-1,1:-1]
    full=np.zeros_like(comp); full[y0:y1+1,x0:x1+1]=filled
    return full
def wrap(draw,text,font,maxw):
    if "\n" in text: return text.split("\n")  # 改行を指定したときはその通りに
    words=text.split(); lines=[]; cur=""
    for w in words:
        t=(cur+" "+w).strip()
        if draw.textlength(t,font=font)<=maxw or not cur: cur=t
        else: lines.append(cur); cur=w
    lines.append(cur); return lines
def fit(draw,text,fpath,inside,smax,pad=22):
    ys,xs=np.nonzero(inside)
    spans={y:(xs[ys==y].min(),xs[ys==y].max()) for y in np.unique(ys)}
    widths=np.array([spans[y][1]-spans[y][0] for y in sorted(spans)])
    core=[y for y in sorted(spans) if spans[y][1]-spans[y][0]>=0.6*widths.max()]
    cy=(core[0]+core[-1])/2; cx=np.mean([ (spans[y][0]+spans[y][1])/2 for y in core])
    for size in range(smax,15,-1):
        font=ImageFont.truetype(fpath,size); lh=int(size*1.18)
        for maxw in range(int(widths.max()),60,-10):
            lines=wrap(draw,text,font,maxw); H=lh*len(lines); top=cy-H/2
            ok=True; place=[]
            for i,l in enumerate(lines):
                lw=draw.textlength(l,font=font); a=int(top+i*lh); b=int(top+i*lh+lh)
                x=cx-lw/2
                for y in range(a-pad//2,b+pad//2):
                    if y not in spans or spans[y][0]+pad>x or spans[y][1]-pad<x+lw: ok=False;break
                if not ok: break
                place.append((x,a,l))
            if ok: return font,place,lh
    raise SystemExit(f"収まらない: {text}")

report={}
for p in (1,2,3,4):
    im=Image.open(SRC.format(p)).convert('RGB'); a=np.asarray(im).copy(); g=np.asarray(im.convert('L')).astype(int)
    changed=np.zeros(g.shape,bool); allowed=np.zeros(g.shape,bool)
    jobs=[b for b in BUBBLES if b[0]==p]; texts=[]
    for _,seed,text,fp in jobs:
        comp=None
        for r in range(0,60,3):
            for dx,dy in ((r,0),(-r,0),(0,r),(0,-r)):
                x,y=seed[0]+dx,seed[1]+dy
                if g[y,x]>245:
                    c=fill_region(g,(x,y))
                    if c.sum()>5000: comp=c;break
            if comp is not None: break
        print(p,seed,int(comp.sum()))
        assert not (comp[0].any() or comp[-1].any() or comp[:,0].any() or comp[:,-1].any()), "吹き出しが閉じていない"
        ins=interior(comp)
        # 文字を消す範囲：内側から縁へ6px離す（縁の線を削らない）
        er=ins.copy()
        for _ in range(6):
            e=er.copy(); e[1:]&=er[:-1]; e[:-1]&=er[1:]; e[:,1:]&=er[:,:-1]; e[:,:-1]&=er[:,1:]; er=e
        left=(g<200)&ins&~er
        white=np.median(a[comp],axis=0).astype(np.uint8)
        a[er]=white; allowed|=ins
        texts.append((text,fp,ins,int(left.sum())))
    if p in BOARDS:
        bd=BOARDS[p]; y0,y1=bd["y"]; boxes=[]
        for i,lab in enumerate(LABELS):
            xa,xb=bd["xs"][i]+5,bd["xs"][i+1]-4
            dk=g[y0:y1,xa:xb]<200; cols=np.nonzero(dk.sum(0))[0]
            cl=[];st=cols[0];pv=cols[0]
            for c in cols[1:]:
                if c-pv>10: cl.append((st,pv)); st=c
                pv=c
            cl.append((st,pv))
            # セルの端に触れているかたまりは札ではない（人物・枠）。消さない
            keep=[c for c in cl if c[0]>3 and c[1]<(xb-xa)-4] or cl
            lx0,lx1=keep[0][0],keep[-1][1]
            rows=np.nonzero(dk[:,lx0:lx1+1].sum(1))[0]
            bx0=max(xa,xa+lx0-4); bx1=min(xb,xa+lx1+5); by0=max(y0,y0+rows[0]-4); by1=min(y1,y0+rows[-1]+5)
            cell=np.zeros(g.shape,bool); cell[by0:by1,bx0:bx1]=True
            a[cell]=(254,254,254); allowed|=cell
            # 英語を置ける幅：消さなかったかたまりの手前まで
            right=xb if keep[-1]==cl[-1] else xa+cl[cl.index(keep[-1])+1][0]-4
            left=xa if keep[0]==cl[0] else xa+cl[cl.index(keep[0])-1][1]+4
            area=np.zeros(g.shape,bool); area[y0+3:y1-3,left:right]=True
            stray=int(((g<200)&area&~cell).sum())
            print("P",p,lab,"札の外の暗い画素",stray)
            allowed|=area; boxes.append((lab,area))
        # 6つの札は同じ大きさにそろえる
        d0=ImageDraw.Draw(Image.new("RGB",(10,10)))
        def best(lab,area):
            ys,xs=np.nonzero(area); w=xs.max()-xs.min(); h=ys.max()-ys.min()
            for size in range(44 if p==2 else 30,8,-1):
                f=ImageFont.truetype(F_BOARD,size); lines=wrap(d0,lab,f,w-8); lh=int(size*1.08)
                if max(d0.textlength(l,font=f) for l in lines)<=w-8 and lh*len(lines)<=h: return size
        S=min(best(l,ar) for l,ar in boxes)
        for lab,area in boxes: texts.append((lab,F_BOARD,area,-S))
    img=Image.fromarray(a); d=ImageDraw.Draw(img); info=[]
    for text,fp,ins,left in texts:
        if left<0:
            size=-left; ys,xs=np.nonzero(ins); w=xs.max()-xs.min()
            f=ImageFont.truetype(fp,size); lines=wrap(d,text,f,w-8); lh=int(size*1.08)
            top=(ys.min()+ys.max())/2-lh*len(lines)/2-size*0.08
            for k,l in enumerate(lines):
                d.text(((xs.min()+xs.max())/2-d.textlength(l,font=f)/2,top+k*lh),l,font=f,fill=(25,25,25))
            info.append(dict(text=text,size=size,lines=lines))
        else:
            smax=44
            f,place,lh=fit(d,text,fp,ins,smax)
            for x,y,l in place: d.text((x,y),l,font=f,fill=(15,15,15))
            info.append(dict(text=text,size=f.size,lines=[l for _,_,l in place],縁に残った暗い画素=left))
    out=np.asarray(img); changed=(np.abs(out.astype(int)-np.asarray(im).astype(int)).sum(2)>0)
    report[p]=dict(items=info,吹き出しの外で変わった画素=int((changed&~allowed).sum()))
    img.save(OUT.format(p))
json.dump(report,open("確認_数値.json","w"),ensure_ascii=False,indent=1)
print(json.dumps(report,ensure_ascii=False,indent=1))
