62 خطوط
3.4 KiB
Python
Executable File
62 خطوط
3.4 KiB
Python
Executable File
#!/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())
|