from pathlib import Path
from pptx import Presentation
from PIL import Image,ImageDraw
import json,subprocess,re,hashlib,zipfile,xml.etree.ElementTree as ET
R=Path(__file__).parent;D=R/'deliverables';Q=D/'qa';P=D/'政府机关智慧食堂一体化解决方案.pptx';PDF=P.with_suffix('.pdf')
content=json.loads((R/'slide-content.json').read_text());prs=Presentation(P)
assert len(prs.slides)==35
text=subprocess.check_output(['pdftotext',str(PDF),'-']).decode();pages=text.split('\f')[:35]
assert len(pages)==35
checks=[]
for r,sl,t in zip(content,prs.slides,pages):
 norm=lambda x:re.sub(r'\s+','',x)
 assert norm(r['title']) in norm(t),(r['page'],'title missing')
 if r['message']:assert norm(r['message']) in norm(t),(r['page'],'conclusion missing')
 assert sl.has_notes_slide
 assert '来源' in sl.notes_slide.notes_text_frame.text
 if r['status']=='reserved':
  assert len([x for x in sl.shapes if x.has_text_frame and x.text.strip()])==3
 for x in r['text']:
  assert x['x']>=0 and x['y']>=0 and x['x']+x['w']<13.335 and x['y']+x['h']<=7.501,(r['page'],'text outside',x)
 editable=sum(x.has_text_frame and bool(x.text.strip()) for x in sl.shapes)
 checks.append({'page':r['page'],'title':r['title'],'editable_text_shapes':editable,'images':len(r['images']),'notes':True,'reserved':r['status']=='reserved'})
for forbidden in ['餐余减少80%','↓30%','↓70%','↓90%','三级等保已取得','保证上线']:
 assert forbidden not in text,forbidden
fonts=subprocess.check_output(['pdffonts',str(PDF)]).decode();assert 'MicrosoftYaHei' in fonts;assert 'Arial' in fonts
assert 'FrankRuhl' not in fonts
# PDF actual glyph positions must remain within the slide.
xml=subprocess.check_output(['pdftotext','-bbox',str(PDF),'-']);root=ET.fromstring(xml)
ns={'x':'http://www.w3.org/1999/xhtml'}
for n,p in enumerate(root.findall('.//x:page',ns),1):
 for w in p.findall('x:word',ns):
  assert float(w.attrib['xMin'])>=-1 and float(w.attrib['yMin'])>=-1
  assert float(w.attrib['xMax'])<=float(p.attrib['width'])+1 and float(w.attrib['yMax'])<=float(p.attrib['height'])+1,(n,w.text)
files=sorted(Q.glob('page-*.jpg'));assert len(files)==35
for off in range(0,len(files),12):
 subset=files[off:off+12];sheet=Image.new('RGB',(1600,((len(subset)+2)//3)*320),'#e2e2e2');draw=ImageDraw.Draw(sheet)
 for j,p in enumerate(subset):
  im=Image.open(p);im.thumbnail((520,293));x=(j%3)*533+6;y=(j//3)*320;sheet.paste(im,(x,y));draw.text((x+8,y+298),p.stem,fill='black')
 sheet.save(Q/f'contact-{off//12+1}.jpg')
# Selected preview for user, no rasterization of editable deck.
selected=[1,7,9,10,13,15,18,21,25]
sheet=Image.new('RGB',(1800,1056),'#e8e8e8');dr=ImageDraw.Draw(sheet)
for j,n in enumerate(selected):
 im=Image.open(Q/f'page-{n:02d}.jpg');im.thumbnail((588,331));x=j%3*600+6;y=j//3*352;sheet.paste(im,(x,y));dr.text((x+8,y+334),f'{n:02d}',fill='black')
sheet.save(D/'方案预览.jpg',quality=92)
result={'pptx_pages':35,'pdf_pages':35,'content_pages':30,'reserved_pages':[2,3,4,34,35],'editable_text_shapes':sum(x['editable_text_shapes'] for x in checks),'page_checks':checks,'font_check':'Microsoft YaHei and Arial embedded','boundary_check':'pass','title_and_conclusion_readback':'pass','notes':'35/35','prohibited_claims':'absent','visual_review':'pending'}
(R/'validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2));(Q/'pdf-text.txt').write_text(text)
assets=[]
for r in content:
 for n in r['images']:
  p=D/'assets'/n
  assets.append({'page':r['page'],'asset':n,'source':r['source'],'status':r['status'],'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'boundary':r['boundary']})
(R/'asset-ledger.json').write_text(json.dumps(assets,ensure_ascii=False,indent=2))
print(json.dumps({k:v for k,v in result.items() if k!='page_checks'},ensure_ascii=False))
