main
John Lauer Publish 0.2.15 bb17f84
#!/usr/bin/env python3
"""Independent polygon preflight for text, pointer strokes and enclosure strokes.
Inputs must include native mask polygons, fitted bodies, holes and existing artwork.
Fail closed for missing required inventory or invalid association geometry.
"""
import json,argparse
from pathlib import Path
from shapely.geometry import shape,box,LineString
from shapely.strtree import STRtree

def check(data):
 errors=[];placements=data['placements'];obstacles=data['obstacles'];required=set(data.get('requiredIndices',[]));present=[p['index'] for p in placements]
 if not required:errors.append(dict(kind='missing-coverage-contract'))
 for i in sorted(required-set(present)):errors.append(dict(kind='missing-label',index=i))
 if len(present)!=len(set(present)):errors.append(dict(kind='duplicate-label'))
 for side in ['F.SilkS','B.SilkS']:
  obs=[o for o in obstacles if o['layer']==side];geoms=[shape(o['polygon']) for o in obs];tree=STRtree(geoms)
  marks=[(p['index'],'text',box(*p['box'])) for p in placements if p['layer']==side]
  for g in data.get('graphics',[]):
   if g['layer']==side:marks.append((g['index'],g['kind'],LineString(g['samples']).buffer(g.get('width',.06)/2)))
  for index,kind,g in marks:
   if not g.is_valid:errors.append(dict(kind='invalid-mark',index=index));continue
   for k in tree.query(g):
    o=obs[int(k)];other=geoms[int(k)]
    if g.intersection(other).area>1e-7:errors.append(dict(kind='obstacle-collision',index=index,mark=kind,obstacle=o['id'] if 'id' in o else o.get('reference'),obstacleKind=o['kind'],layer=side))
  # Text rectangles may not overlap. Association lines also reserve their stroke width.
  tree=STRtree([g for _,_,g in marks])
  for k,(index,kind,g) in enumerate(marks):
   for j in tree.query(g):
    j=int(j)
    if j>=k:continue
    otherindex,otherkind,other=marks[j]
    if kind!='text' and otherkind!='text':continue # joined segments are allowed
    if g.intersection(other).area>1e-7:errors.append(dict(kind='mark-collision',index=index,otherIndex=otherindex,mark=kind,otherMark=otherkind,layer=side))
 groups={}
 for p in placements:
  if p.get('reference') and p.get('role') in ['reference','value']:groups.setdefault((p['layer'],p['reference']),{})[p['role']]=p
  if p.get('lineSizes') and any(z>=p['lineSizes'][0] for z in p['lineSizes'][1:]):errors.append(dict(kind='multiline-hierarchy',index=p['index']))
 for (side,ref),g in groups.items():
  if 'value' in g and ('reference' not in g or g['value']['size']>g['reference']['size']*.8):errors.append(dict(kind='pair-hierarchy',reference=ref,layer=side))
 return dict(passed=not errors,errors=errors,labels=len(placements),graphics=len(data.get('graphics',[])),obstacles=len(obstacles),nativeVerified=False)
if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=check(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(r,indent=2));print(('OK' if r['passed'] else 'ERROR')+': '+str(len(r['errors']))+' preflight findings');raise SystemExit(0 if r['passed'] else 2)