@@ -0,0 +1,183 @@
|
||||
import hashlib
|
||||
import re
|
||||
from collections import Counter
|
||||
from app.models.schematic import (Document, Sheet, Component, Symbol, Pin, SchematicObject, Diagnostic, Point)
|
||||
from .ole_reader import read_document
|
||||
from .images import read_images, asset_name
|
||||
from app.models.schematic import Primitive
|
||||
from .record_decoder import ParseError
|
||||
from .geometry import point, flag, integer, coordinate, number, bounds, union, transform_point, color
|
||||
from .primitives import decode, KINDS, SUPPORTED
|
||||
|
||||
ELECTRICAL=['Input','Bidirectional','Output','Open collector','Passive','Tri-state','Open emitter','Power']
|
||||
|
||||
def parse_schdoc(data: bytes, filename: str, document_id: str) -> Document:
|
||||
fmt,streams,main,extra=read_document(data)
|
||||
if not main or 'HEADER' not in main[0].properties:
|
||||
raise ParseError('Missing schematic version header')
|
||||
header=main[0].properties
|
||||
version=header['HEADER']
|
||||
if not re.fullmatch(r'Protel for Windows - Schematic Capture (?:Binary|Ascii|ASCII) File Version 5\.0',version):
|
||||
raise ParseError(f'Unsupported schematic version: {version[:150]}')
|
||||
records=[r for r in main+extra if r.index>=0]
|
||||
if len(records)>100000: raise ParseError('Combined record count limit exceeded')
|
||||
lookup={(r.stream,r.index):r for r in records}
|
||||
diagnostics=[]
|
||||
grouped={}
|
||||
def warn(code,message,r=None,severity='warning'):
|
||||
key=(code,r.properties.get('RECORD') if r else None,message if code not in ('unsupported_record','binary_record') else '')
|
||||
if key in grouped:
|
||||
grouped[key].count+=1
|
||||
else:
|
||||
try: record_type=integer(r.properties,'RECORD') if r and 'RECORD' in r.properties else None
|
||||
except ValueError: record_type=None
|
||||
d=Diagnostic(severity=severity,code=code,message=message,
|
||||
record_id=r.id if r else None,record_type=record_type)
|
||||
grouped[key]=d;diagnostics.append(d)
|
||||
images, image_errors = read_images(data)
|
||||
for message in image_errors: warn('embedded_storage', message)
|
||||
sheet_records=[r for r in records if r.properties.get('RECORD')=='31']
|
||||
if not sheet_records: raise ParseError('No schematic sheet record found')
|
||||
sheet_props=sheet_records[0].properties
|
||||
if len(sheet_records)>1: warn('multiple_sheets','Multiple sheet records found; only the first defines page properties')
|
||||
for s in streams:
|
||||
if s['name'] not in ('FileHeader','Additional','Storage'):
|
||||
warn('unhandled_stream',f"Uninterpreted OLE stream: {s['name']} ({s['size']} bytes)")
|
||||
if extra: warn('additional_indexing','Additional records use stream-local owner indexes; cross-stream ownership is not inferred',severity='info')
|
||||
components={}
|
||||
for r in records:
|
||||
if r.properties.get('RECORD')=='1':
|
||||
p=r.properties
|
||||
try:
|
||||
components[r.id]=Component(id=r.id,symbol_id=f'{r.id}:symbol',name=p.get('DESIGNITEMID',p.get('LIBREFERENCE','')),
|
||||
description=p.get('COMPONENTDESCRIPTION',''),library_reference=p.get('LIBREFERENCE',''),source_library=p.get('SOURCELIBRARYNAME',''),
|
||||
position=point(p),rotation=90*integer(p,'ORIENTATION'),mirrored=flag(p,'ISMIRRORED'),current_part=integer(p,'CURRENTPARTID',1),bounds=bounds([point(p)],5))
|
||||
except ValueError as exc: raise ParseError(f'Invalid component {r.id}: {exc}') from exc
|
||||
def owner_chain(r):
|
||||
chain=[];seen={r.id};current=r
|
||||
while 'OWNERINDEX' in current.properties:
|
||||
index=integer(current.properties,'OWNERINDEX',-1)
|
||||
parent=lookup.get((current.stream,index))
|
||||
if not parent: break
|
||||
if parent.id in seen: raise ValueError('Cyclic ownership')
|
||||
seen.add(parent.id);chain.append(parent);current=parent
|
||||
if len(chain)>64: raise ValueError('Excessive ownership depth')
|
||||
return chain
|
||||
# Collect component/sheet parameter values before resolving text expressions.
|
||||
parameter_maps={}
|
||||
for r in records:
|
||||
if r.properties.get('RECORD') in ('34','41'):
|
||||
p=r.properties
|
||||
try: chain=owner_chain(r)
|
||||
except ValueError as exc: warn('ownership',str(exc),r);continue
|
||||
c=next((components[q.id] for q in chain if q.id in components),None)
|
||||
key=p.get('NAME','Designator' if p.get('RECORD')=='34' else '')
|
||||
value=p.get('TEXT','')
|
||||
owner=c.id if c else 'sheet-0'
|
||||
parameter_maps.setdefault(owner,{})[key.upper()]=value
|
||||
if c:
|
||||
c.parameters[key]=value
|
||||
if p.get('RECORD')=='34': c.designator=value
|
||||
if key.upper()=='COMMENT': c.comment=value
|
||||
if key.upper()=='VALUE': c.value=value
|
||||
for c in components.values():
|
||||
c.value=c.value or c.comment
|
||||
objects=[];primitives=[]
|
||||
counts=Counter()
|
||||
for r in records:
|
||||
p=r.properties
|
||||
try:
|
||||
t=integer(p,'RECORD',-1);counts[str(t)]+=1
|
||||
chain=owner_chain(r)
|
||||
c=next((components[q.id] for q in [r]+chain if q.id in components),None)
|
||||
owner=lookup.get((r.stream,integer(p,'OWNERINDEX',-1)))
|
||||
active=True
|
||||
if c and r.id!=c.id:
|
||||
cp=lookup[(r.stream,int(c.id.split(':')[-1]))].properties
|
||||
for q in [r]+chain:
|
||||
if q.id==c.id: break
|
||||
part=integer(q.properties,'OWNERPARTID',-1)
|
||||
mode=integer(q.properties,'OWNERPARTDISPLAYMODE',0)
|
||||
if part not in (-1,0,c.current_part) or mode!=integer(cp,'DISPLAYMODE',0): active=False
|
||||
visible=active and not flag(p,'ISHIDDEN')
|
||||
if t==41 and any(q.properties.get('RECORD')=='48' for q in chain): visible=False
|
||||
if t==2 and integer(p,'PINCONGLOMERATE')&4: visible=False
|
||||
if p.get('TEXT','').startswith('='):
|
||||
key=p['TEXT'][1:].upper()
|
||||
value=parameter_maps.get(c.id if c else 'sheet-0',{}).get(key,parameter_maps.get('sheet-0',{}).get(key))
|
||||
if value is not None: p['_RESOLVED_TEXT']=value
|
||||
obj=SchematicObject(id=r.id,kind=KINDS.get(t,'unsupported'),record_type=t,source_stream=r.stream,source_index=r.index,
|
||||
owner_id=owner.id if owner else None,component_id=c.id if c else None,visible=visible,position=point(p),bounds=bounds([point(p)]),properties=p)
|
||||
if r.binary:
|
||||
warn('binary_record','Binary schematic record retained by index but payload is unsupported',r)
|
||||
elif t == 30 and visible:
|
||||
image = images.get(asset_name(p.get('FILENAME', '')))
|
||||
if image:
|
||||
box = bounds([point(p), point(p, 'CORNER')])
|
||||
shape = Primitive(id=r.id+':image', kind='image', bounds=box, object_id=r.id, component_id=c.id if c else None, image_data=image, keep_aspect=flag(p, 'KEEPASPECT'))
|
||||
primitives.append(shape); obj.primitive_ids=[shape.id]; obj.bounds=box
|
||||
else: warn('missing_image', 'Embedded raster unavailable; external files are never fetched', r)
|
||||
elif t not in SUPPORTED:
|
||||
warn('unsupported_record',f'Unsupported schematic record type {t}; properties preserved',r)
|
||||
elif visible:
|
||||
shapes=decode(r,c.id if c else None,sheet_props,warn,lookup)
|
||||
primitives.extend(shapes);obj.primitive_ids=[s.id for s in shapes]
|
||||
if shapes: obj.bounds=union([s.bounds for s in shapes])
|
||||
if t==2:
|
||||
bits=integer(p,'PINCONGLOMERATE');length=coordinate(p,'PINLENGTH')
|
||||
end=transform_point(Point(x=length,y=0),obj.position,(bits&3)*90)
|
||||
obj.connection_points=[end]
|
||||
pin=Pin(id=r.id,name=p.get('NAME',''),number=p.get('DESIGNATOR',''),electrical_type=ELECTRICAL[max(0,min(7,integer(p,'ELECTRICAL')))],
|
||||
position=obj.position,connection=end,length=length,rotation=(bits&3)*90,visible=visible,active=active)
|
||||
if c: c.pins.append(pin)
|
||||
else: warn('orphan_pin','Pin has no component owner',r)
|
||||
elif t in (26,27):
|
||||
n=integer(p,'LOCATIONCOUNT')
|
||||
if not 0<=n<=20000: raise ValueError('Invalid electrical point count')
|
||||
obj.connection_points=[Point(x=coordinate(p,f'X{i}'),y=-coordinate(p,f'Y{i}')) for i in range(1,n+1)]
|
||||
elif t in (17,18,25,29,37): obj.connection_points=[obj.position]
|
||||
if c:
|
||||
c.object_ids.append(obj.id);c.primitive_ids.extend(obj.primitive_ids)
|
||||
objects.append(obj)
|
||||
except (ValueError,OverflowError,KeyError) as exc:
|
||||
warn('malformed_record',f'Record not rendered: {str(exc)[:180]}',r,severity='error')
|
||||
objects.append(SchematicObject(id=r.id,kind='malformed',record_type=-1,source_stream=r.stream,source_index=r.index,
|
||||
visible=False,position=Point(x=0,y=0),bounds=bounds([]),properties=p))
|
||||
if len(primitives)>250000: raise ParseError('Drawable count limit exceeded')
|
||||
if not primitives: raise ParseError('No supported visible geometry found in this schematic')
|
||||
by_component={}
|
||||
for shape in primitives:
|
||||
if shape.component_id: by_component.setdefault(shape.component_id,[]).append(shape.bounds)
|
||||
for c in components.values():
|
||||
if c.id in by_component: c.bounds=union(by_component[c.id])
|
||||
else: warn('empty_symbol',f'Component {c.designator or c.id} has no visible supported symbol geometry')
|
||||
# Standard sheet presets in 10-mil units. Custom page dimensions win only when enabled.
|
||||
sizes={0:(1150,760),1:(1550,1110),2:(2230,1570),3:(3150,2230),4:(4460,3150),5:(950,750),6:(1500,950),7:(2000,1500),8:(3200,2000),9:(4200,3200),10:(1100,850),11:(1400,850),12:(1700,1100),13:(990,790),14:(1540,990),15:(2060,1560),16:(3260,2060),17:(4280,3280)}
|
||||
style=integer(sheet_props,'SHEETSTYLE')
|
||||
if style not in sizes and not flag(sheet_props,'USECUSTOMSHEET'): raise ParseError('Unsupported sheet dimensions')
|
||||
w,h=sizes.get(style,(1150,760))
|
||||
if flag(sheet_props,'USECUSTOMSHEET'):
|
||||
w,h=coordinate(sheet_props,'CUSTOMX'),coordinate(sheet_props,'CUSTOMY')
|
||||
if integer(sheet_props,'WORKSPACEORIENTATION') and w>h: w,h=h,w
|
||||
if w <= 0 or h <= 0: raise ParseError('Sheet dimensions must be positive')
|
||||
page=bounds([Point(x=0,y=-h),Point(x=w,y=0)])
|
||||
content=union([s.bounds for s in primitives])
|
||||
if flag(sheet_props,'BORDERON') or flag(sheet_props,'TITLEBLOCKON'):
|
||||
warn('page_decoration','Automatic Altium page border, zones and title block are not reconstructed. Explicit template geometry is rendered.')
|
||||
try:
|
||||
if 'WEIGHT' in header and int(header['WEIGHT'])!=len(main)-1:
|
||||
warn('record_count_mismatch',f"Header declares {header['WEIGHT']} records; found {len(main)-1} in FileHeader")
|
||||
except ValueError: warn('record_count_mismatch','Header record count is not numeric')
|
||||
warn('connectivity_scope','Connection anchors and wire vertices are extracted. Net names are not a compiled netlist; bus expansion, harnesses and cross-sheet connectivity are not resolved.',severity='info')
|
||||
warn('font_metrics','Exact original fonts are required on the client. Font size conversion and glyph metrics have not been validated against Altium.',severity='warning')
|
||||
warn('pin_text_metrics','Default pin text offsets and port text anchors require validation against a trusted Altium export.',severity='warning')
|
||||
return Document(id=document_id,filename=filename,
|
||||
metadata={'format':fmt,'header':version,'minor_version':header.get('MINORVERSION'),
|
||||
'sha256':hashlib.sha256(data).hexdigest(),'input_bytes':len(data),'streams':streams,
|
||||
'record_counts':dict(counts),'record_count':len(records),'component_count':len(components),
|
||||
'primitive_count':len(primitives),'units':'10mil','coordinates':'X right, Y down; Y inverted once from Altium',
|
||||
'compatibility':'partial' if any(d.severity in ('warning','error') for d in diagnostics) else 'supported_subset',
|
||||
'header_properties':header},
|
||||
sheet=Sheet(name=filename,background=color(sheet_props.get('AREACOLOR','16317695')),properties=sheet_props,bounds=page),bounds=content,
|
||||
components=list(components.values()),symbols=[Symbol(id=c.symbol_id,component_id=c.id,primitive_ids=c.primitive_ids) for c in components.values()],
|
||||
objects=objects,primitives=primitives,diagnostics=diagnostics)
|
||||
مرجع در شماره جدید
مسدود کردن کاربر