#!/usr/bin/env python3 """Render actual SchDoc and compare to a trusted Altium page PNG, without alignment. Images must represent the same sheet viewport. No registration, recoloring or cropping hides coordinate differences. Altium PDF exports can be rasterized to PNG with pdftoppm first. This tool does not manufacture reference images. """ import argparse import hashlib import json import sys from pathlib import Path sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'backend')) import cairosvg import numpy as np from PIL import Image from app.parser.schdoc_parser import parse_schdoc from app.render import svg_document def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('schematic',type=Path) p.add_argument('--reference',type=Path) p.add_argument('--output',type=Path,default=Path('validation-output')) p.add_argument('--width',type=int,default=2000) p.add_argument('--max-mae',type=float,default=2.0,help='Mean absolute RGB error threshold, 0..255') p.add_argument('--max-changed-fraction',type=float,default=.01) p.add_argument('--pixel-tolerance',type=int,default=12) args=p.parse_args(); args.output.mkdir(parents=True,exist_ok=True) raw=args.schematic.read_bytes(); doc=parse_schdoc(raw,args.schematic.name,'validation') svg=svg_document(doc); (args.output/'drawing.svg').write_bytes(svg) width=args.width; height=max(1,round(width*doc.sheet.bounds.height/doc.sheet.bounds.width)) if args.reference: with Image.open(args.reference) as ref: width,height=ref.size expected_ratio=doc.sheet.bounds.width/doc.sheet.bounds.height if abs(width/height/expected_ratio-1)>.005: p.error('Reference aspect ratio differs from original sheet; export the same viewport') png=cairosvg.svg2png(bytestring=svg,output_width=width,output_height=height) (args.output/'rendered.png').write_bytes(png) report={'source_sha256':hashlib.sha256(raw).hexdigest(),'record_count':doc.metadata['record_count'], 'component_count':len(doc.components),'primitive_count':len(doc.primitives), 'diagnostics':[d.model_dump() for d in doc.diagnostics], 'reference_compared':bool(args.reference),'fidelity_verified':False} passed=True if args.reference: reference=Image.open(args.reference).convert('RGB'); actual=Image.open(args.output/'rendered.png').convert('RGB') a=np.array(actual,dtype=np.int16); b=np.array(reference,dtype=np.int16); delta=np.abs(a-b) mae=float(delta.mean()); changed=float((delta.max(axis=2)>args.pixel_tolerance).mean()) heat=np.zeros_like(a,dtype=np.uint8); heat[:,:,0]=np.clip(delta.max(axis=2)*4,0,255).astype(np.uint8) Image.fromarray(heat).save(args.output/'difference.png') Image.blend(reference,actual,.5).save(args.output/'overlay.png') passed=mae<=args.max_mae and changed<=args.max_changed_fraction report.update(reference_sha256=hashlib.sha256(args.reference.read_bytes()).hexdigest(),mae=mae, changed_pixel_fraction=changed,thresholds={'max_mae':args.max_mae,'max_changed_fraction':args.max_changed_fraction,'pixel_tolerance':args.pixel_tolerance}, regression_passed=passed) (args.output/'report.json').write_text(json.dumps(report,indent=2)) print(json.dumps({k:v for k,v in report.items() if k!='diagnostics'},indent=2)) return 0 if passed else 1 if __name__=='__main__': sys.exit(main())