cat > /workspace/scratch/layout.py <<'PY'
import json,numpy as np,cv2
from PIL import ImageFont,Image
from scipy import ndimage as ndi
plan=json.load(open('/workspace/input/page_plan.json'));meta=json.load(open('/workspace/scratch/assetmeta.json'));copy={p['sku']:p for p in json.load(open('/workspace/input/catalogue_copy.json'))['skus']}
copy['HH-3360']['price']='£94';copy['HH-3315']['price']='£185';copy['HH-7186']['name']='Rosewell bowl chandelier'
copy['HH-8256']['description']=copy['HH-8256']['description'].replace('The chimney lifts off for cleaning.','The chimney is fixed to the burner; clean it in place with a soft brush.')
copy['HH-9395']['description']=copy['HH-9395']['description'].replace('Finish with a clear oil to keep the grain bright.','It arrives with a clear hard-wax oil finish, so it needs no further treatment.')
reg=ImageFont.truetype('/workspace/input/NotoSans-Regular.ttf',1000);bold=ImageFont.truetype('/workspace/input/NotoSans-Bold.ttf',1000)
mmpt=25.4/72;pxmm=300/25.4;fs=10.0;leading=13.6*mmpt
width=lambda txt,font,size:font.getlength(txt)/1000*size*mmpt
layout=[]
for page in plan['pages']:
 p={'number':page['page'],'background':page['background'],'color':'White' if page['page'] in [1,3,5,7,8,9,11] else 'Black','products':[]}
 for r,sku in enumerate(page['skus']):
  c=copy[sku];w,h=meta[sku]['mm'];row=30+r*128;right=(r==0)
  imx=(198-w if w>=70 else 183-w) if right else (12 if w>=70 else 27)
  imy=row+21
  a=np.array(Image.open('/workspace/output/assets/'+sku+'.png'))[:,:,3]
  # Work on the external silhouette only. Padding is represented explicitly.
  full=ndi.binary_fill_holes(a>8)
  pad=45;full=np.pad(full,pad)
  dil=ndi.distance_transform_edt(~full)<=3.25*pxmm
  words=c['description'].split();idx=0;lines=[]
  start=imy+(h*.13 if w>=70 else h*.20)
  if sku=='HH-9374':start=imy+2
  for n in range(60):
   if idx>=len(words):break
   yy=start+n*leading
   y0=int((yy-imy)*pxmm)+pad;y1=int((yy+3.2-imy)*pxmm)+pad
   block=dil[max(0,y0):min(dil.shape[0],y1)]
   coords=np.where(block)[1]
   if len(coords):edge=(coords.min()-pad if right else coords.max()-pad)/pxmm+imx
   else:edge=(imx-3.25 if right else imx+w+3.25)
   if right:
    x1=edge; x0=max(12,x1-100)
   else:x0=edge;x1=min(198,x0+100)
   avail=x1-x0-.6
   if avail<22:
    print('NARROW',sku,n,avail)
   end=idx+1
   while end<=len(words) and width(' '.join(words[idx:end]),reg,fs)<=avail:end+=1
   end=max(idx+1,end-1);text=' '.join(words[idx:end]);idx=end
   lines.append({'text':text,'x':x0,'y':yy,'w':x1-x0,'h':5.1,'align':2 if right else 0,'font':'Noto Sans Regular','size':fs})
  prod={'sku':sku,'name':c['name'],'price':c['price'],'description':c['description'],'row':row,'image':[imx,imy,w,h],'right':right,'lines':lines}
  p['products'].append(prod)
  print(sku,'desc lines',len(lines),'end',round(lines[-1]['y']+4,1),'image',(round(imx,1),round(imy,1),round(w,1),round(h,1)))
 layout.append(p)
json.dump(layout,open('/workspace/scratch/layout.json','w'),indent=2)
json.dump(copy,open('/workspace/scratch/approved.json','w'),indent=2)
PY
OPENBLAS_NUM_THREADS=1 python /workspace/scratch/layout.py
cat > /workspace/scratch/build.py <<'PY'
import scribus,os,json,traceback
try:
 L=json.load(open('/workspace/scratch/layout.json'))
 scribus.newDocument((210,297),(8,8,8,8),0,1,scribus.UNIT_MILLIMETERS,0,0,12)
 scribus.setInfo('Harbour & Hearth','Autumn / Winter catalogue','Twelve seasonal colour stories, twenty-four products')
 # Direct float process definitions; no spots or RGB brand substitutes.
 for p in L:scribus.defineColorCMYKFloat(p['background']['name'],*[v/100 for v in p['background']['cmyk']])
 over=[]
 def text(x,y,w,h,txt,font,size,color,name,align=0):
  o=scribus.createText(x,y,w,h,name);scribus.setText(txt,o);scribus.setFont(font,o);scribus.setFontSize(size,o);scribus.setTextColor(color,o);scribus.setTextDistances(0,0,0,0,o);scribus.setFirstLineOffset(scribus.FLOP_REALGLYPHHEIGHT,o);scribus.setLineSpacing(size*1.36,o);scribus.setTextAlignment(align,o);scribus.setFillColor('None',o);scribus.setLineColor('None',o)
  if scribus.textOverflows(o):over.append([name,txt])
  return o
 for p in L:
  pg=p['number'];scribus.gotoPage(pg);col=p['color'];b=p['background']['name']
  bg=scribus.createRect(-3,-3,216,303,'Page_%02d_Bleed'%pg);scribus.setFillColor(b,bg);scribus.setLineColor('None',bg)
  text(12,11,100,5,'HARBOUR & HEARTH','Noto Sans Bold',9,col,'Brand_%02d'%pg)
  text(128,11,70,5,'AUTUMN / WINTER','Noto Sans Regular',8.5,col,'Season_%02d'%pg,2)
  text(12,284,140,5,b.upper()+'  /  THE SEASONAL COLLECTION','Noto Sans Regular',8,col,'Footer_%02d'%pg)
  text(183,283.6,15,5,'%02d'%pg,'Noto Sans Bold',9,col,'Folio_%02d'%pg,2)
  for pr in p['products']:
   s=pr['sku'];row=pr['row'];x,y,w,h=pr['image'];im=scribus.createImage(x,y,w,h,'Product_'+s);scribus.loadImage('/workspace/output/assets/'+s+'.png',im);scribus.setImageScale(1,1,im);scribus.setImageOffset(0,0,im);scribus.setFillColor('None',im);scribus.setLineColor('None',im)
   text(12,row,158,7.2,pr['name'],'Noto Sans Bold',15.5,col,'Name_'+s)
   text(172,row+.4,26,7.2,pr['price'],'Noto Sans Bold',13,col,'Price_'+s,2)
   text(12,row+8,130,5,s,'Noto Sans Regular',9,col,'Code_'+s)
   for n,line in enumerate(pr['lines']):
    text(line['x'],line['y'],line['w'],line['h'],line['text'],line['font'],line['size'],col,'Description_'+s+'_%02d'%n,line['align'])
 scribus.saveDocAs('/workspace/output/catalogue.sla')
 open('/workspace/scratch/build_overflows.json','w').write(json.dumps(over,indent=2))
except Exception:open('/workspace/scratch/build_error.txt','w').write(traceback.format_exc())
os._exit(0)
PY
DISPLAY=:99 scribus -g -ns -py /workspace/scratch/build.py > /workspace/scratch/build.log 2>&1
cat /workspace/scratch/build_error.txt 2>/dev/null; cat /workspace/scratch/build_overflows.json