#!/usr/bin/env python3 """Reproduce single-font NFC/NFD shaping observations. Requires fonttools[brotli] and uharfbuzz. No fonts are redistributed: --download fetches each public preview URL and verifies its SHA-256. python reproduce.py --root . --output ./recomputed --download """ import argparse,concurrent.futures,csv,gzip,hashlib,io,json,pathlib,platform,subprocess,sys import fontTools from fontTools.ttLib import TTFont import uharfbuzz as hb import unicodedata def digest(data):return hashlib.sha256(data).hexdigest() def write(path,value):path.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n') def shape(font,text): b=hb.Buffer();b.add_str(text);b.direction='ltr';b.script='Latn';b.language='en';b.cluster_level=hb.BufferClusterLevel.MONOTONE_GRAPHEMES;b.flags=hb.BufferFlags.DEFAULT hb.shape(font,b,{},shapers=['ot']) return [{'gid':g.codepoint,'cluster':g.cluster,'flags':int(g.flags),'xAdvance':p.x_advance,'yAdvance':p.y_advance,'xOffset':p.x_offset,'yOffset':p.y_offset} for g,p in zip(b.glyph_infos,b.glyph_positions)] def main(): p=argparse.ArgumentParser();p.add_argument('--root',type=pathlib.Path,default=pathlib.Path(__file__).parent);p.add_argument('--output',type=pathlib.Path,required=True);p.add_argument('--download',action='store_true');p.add_argument('--slugs',default='');a=p.parse_args();a.output.mkdir(parents=True,exist_ok=True);cache=a.root/'font-cache';cache.mkdir(exist_ok=True) manifest=json.loads((a.root/'manifest.json').read_text());cases=json.loads((a.root/'cases.json').read_text())['cases'];select=set(a.slugs.split(',')) if a.slugs else None;fonts=[f for f in manifest['fonts'] if not select or f['slug'] in select] unknown=(select or set())-{f['slug'] for f in manifest['fonts']} if unknown:raise SystemExit('Unknown slugs: '+', '.join(sorted(unknown))) if not fonts:raise SystemExit('No fonts selected from manifest') def fetch(f): path=cache/(f['slug']+'.woff2') if not path.exists(): if not a.download:raise RuntimeError('Missing cached font; use --download: '+f['slug']) result=subprocess.run(['curl','--fail','--silent','--show-error','--location','--retry','2','--max-time','120',f['url']],check=True,capture_output=True);path.write_bytes(result.stdout) data=path.read_bytes();sha=digest(data) if f.get('sha256') and f['sha256']!=sha:raise ValueError('Font SHA256 mismatch: '+f['slug']) return f,path,data,sha with concurrent.futures.ThreadPoolExecutor(5) as ex:sources=list(ex.map(fetch,fonts)) rawdir=a.output/'raw';rawdir.mkdir(exist_ok=True);rows=[];summaries=[] for f,path,data,sha in sources: tt=TTFont(io.BytesIO(data),recalcTimestamp=False);cmap=tt.getBestCmap();tt.flavor=None;sfnt=io.BytesIO();tt.save(sfnt);font=hb.Font(hb.Face(sfnt.getvalue()));upm=font.face.upem;font.scale=(upm,upm) raw=[];summary={'slug':f['slug'],'manifestVersion':f['manifestVersion'],'fontSha256':sha,'fontBytes':len(data),'upm':upm,'cmapCodePointCount':len(cmap),'combiningMarkCmapCount':sum(0x300<=cp<=0x36f for cp in cmap),'tables':sorted(tt.keys()),'canonicalCases':0,'nfcDirectCmapCases':0,'nfcNoNotdefCases':0,'nfdDirectCmapCases':0,'nfdNoNotdefCases':0,'equalOutputCases':0,'equalNoNotdefCases':0,'differentNoNotdefCases':0,'nfcWithoutDirectCmapButNoNotdefCases':0,'rescuedNfdCases':0,'controlCases':0,'controlsWithNotdef':0} for c in cases: nfc=c['nfc'];nfd=c['nfd'];out1=shape(font,nfc);out2=shape(font,nfd);direct1=all(ord(x) in cmap and tt.getGlyphID(cmap[ord(x)])!=0 for x in nfc);direct2=all(ord(x) in cmap and tt.getGlyphID(cmap[ord(x)])!=0 for x in nfd);ok1=all(x['gid']!=0 for x in out1) and bool(out1);ok2=all(x['gid']!=0 for x in out2) and bool(out2);equal=[{k:v for k,v in x.items() if k not in ('cluster','flags')} for x in out1]==[{k:v for k,v in x.items() if k not in ('cluster','flags')} for x in out2];rescue=not direct2 and ok1 and ok2 and equal r={'slug':f['slug'],'caseId':c['id'],'kind':c['kind'],'nfc':nfc,'nfd':nfd,'nfcCodePoints':' '.join(f'U+{ord(x):04X}' for x in nfc),'nfdCodePoints':' '.join(f'U+{ord(x):04X}' for x in nfd),'nfcDirectCmap':direct1,'nfdDirectCmap':direct2,'nfcNoNotdef':ok1,'nfdNoNotdef':ok2,'equalGlyphOutput':equal,'rescuedNfd':rescue};rows.append(r);raw.append({**r,'nfcCmap':[{'codePoint':f'U+{ord(x):04X}','glyphName':cmap.get(ord(x))} for x in nfc],'nfdCmap':[{'codePoint':f'U+{ord(x):04X}','glyphName':cmap.get(ord(x))} for x in nfd],'nfcOutput':out1,'nfdOutput':out2}) if c['kind']=='canonical-latin': for k,v in [('canonicalCases',True),('nfcDirectCmapCases',direct1),('nfcNoNotdefCases',ok1),('nfdDirectCmapCases',direct2),('nfdNoNotdefCases',ok2),('equalOutputCases',equal),('equalNoNotdefCases',equal and ok1 and ok2),('differentNoNotdefCases',not equal and ok1 and ok2),('nfcWithoutDirectCmapButNoNotdefCases',not direct1 and ok1),('rescuedNfdCases',rescue)]:summary[k]+=int(v) else:summary['controlCases']+=1;summary['controlsWithNotdef']+=int(not ok2) write(rawdir/(f['slug']+'.json'),{'font':{**f,'sha256':sha},'summary':summary,'cases':raw});summaries.append(summary) with (a.output/'results.csv').open('w',newline='') as stream: w=csv.DictWriter(stream,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows) write(a.output/'results.json',{'fonts':summaries,'totals':{'fonts':len(summaries),'canonicalCasesPerFont':sum(c['kind']=='canonical-latin' for c in cases),'controlsPerFont':sum(c['kind']=='noncomposable-control' for c in cases),'fontCasePairs':len(rows),'rescuedNfdPairs':sum(s['rescuedNfdCases'] for s in summaries),'fontsWithRescuedNfd':sum(s['rescuedNfdCases']>0 for s in summaries),'controlPairsWithNotdef':sum(s['controlsWithNotdef'] for s in summaries)}}) with (a.output/'font-summary.csv').open('w',newline='') as stream: w=csv.DictWriter(stream,fieldnames=list(summaries[0]));w.writeheader();w.writerows({**s,'tables':' '.join(s['tables'])} for s in summaries) with (a.output/'raw-shapes.jsonl.gz').open('wb') as rawstream: with gzip.GzipFile(filename='',mode='wb',fileobj=rawstream,mtime=0) as compressed: for f in fonts: raw=json.loads((rawdir/(f['slug']+'.json')).read_text()) compressed.write((json.dumps({'type':'font','font':raw['font']},ensure_ascii=False,separators=(',',':'))+'\n').encode()) for c in raw['cases']: compressed.write((json.dumps({'type':'shape','slug':f['slug'],'caseId':c['caseId'],'nfcCmap':c['nfcCmap'],'nfdCmap':c['nfdCmap'],'nfcOutput':c['nfcOutput'],'nfdOutput':c['nfdOutput']},ensure_ascii=False,separators=(',',':'))+'\n').encode()) write(a.output/'runtime.json',{'python':platform.python_version(),'unicodedata':unicodedata.unidata_version,'fontTools':fontTools.__version__,'uharfbuzz':hb.__version__,'harfbuzz':hb.version_string(),'direction':'ltr','script':'Latn','language':'en','clusterLevel':'MONOTONE_GRAPHEMES (0)','bufferFlags':'DEFAULT (0)','featureOverrides':{},'shapers':['ot'],'fontScale':'unitsPerEm on both axes','fontVariations':{},'comparisonFields':['gid','xAdvance','yAdvance','xOffset','yOffset'],'recordedButExcludedFromEquality':['cluster','flags'],'fallback':'none; one HarfBuzz font per call','inputs':{n:digest((a.root/n).read_bytes()) for n in ['manifest.json','cases.json']}}) print(json.dumps({'fonts':len(summaries),'output':str(a.output),'summary':summaries},ensure_ascii=False)) if __name__=='__main__':main()