168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
import io
|
|
import json
|
|
import os.path
|
|
from jsonschema import validate, ValidationError
|
|
|
|
from .common import EcadParser, Component, BoundingBox, ExtraFieldData
|
|
from ..core.fontparser import FontParser
|
|
from ..errors import ParsingException
|
|
|
|
|
|
class GenericJsonParser(EcadParser):
|
|
COMPATIBLE_SPEC_VERSIONS = [1]
|
|
|
|
def extra_data_file_filter(self):
|
|
return "Json file ({f})|{f}".format(f=os.path.basename(self.file_name))
|
|
|
|
def latest_extra_data(self, extra_dirs=None):
|
|
return self.file_name
|
|
|
|
def get_extra_field_data(self, file_name):
|
|
if os.path.abspath(file_name) != os.path.abspath(self.file_name):
|
|
return None
|
|
|
|
_, components = self._parse()
|
|
field_set = set()
|
|
comp_dict = {}
|
|
|
|
for c in components:
|
|
ref_fields = comp_dict.setdefault(c.ref, {})
|
|
|
|
for k, v in c.extra_fields.items():
|
|
field_set.add(k)
|
|
ref_fields[k] = v
|
|
|
|
by_index = {
|
|
i: components[i].extra_fields for i in range(len(components))
|
|
}
|
|
|
|
return ExtraFieldData(list(field_set), comp_dict, by_index)
|
|
|
|
def get_generic_json_pcb(self):
|
|
with io.open(self.file_name, 'r', encoding='utf-8') as f:
|
|
pcb = json.load(f)
|
|
|
|
if 'spec_version' not in pcb:
|
|
raise ValidationError("'spec_version' is a required property")
|
|
|
|
if pcb['spec_version'] not in self.COMPATIBLE_SPEC_VERSIONS:
|
|
raise ValidationError("Unsupported spec_version ({})"
|
|
.format(pcb['spec_version']))
|
|
|
|
schema_dir = os.path.join(os.path.dirname(__file__), 'schema')
|
|
schema_file_name = os.path.join(schema_dir,
|
|
'genericjsonpcbdata_v{}.schema'.format(
|
|
pcb['spec_version']))
|
|
|
|
with io.open(schema_file_name, 'r', encoding='utf-8') as f:
|
|
schema = json.load(f)
|
|
|
|
validate(instance=pcb, schema=schema)
|
|
|
|
return pcb
|
|
|
|
def _verify(self, pcb):
|
|
"""Spot check the pcb object."""
|
|
|
|
if len(pcb['pcbdata']['footprints']) != len(pcb['components']):
|
|
self.logger.error("Length of components list doesn't match"
|
|
" length of footprints list.")
|
|
return False
|
|
|
|
return True
|
|
|
|
@staticmethod
|
|
def _texts(pcbdata):
|
|
for layer in pcbdata['drawings'].values():
|
|
for side in layer.values():
|
|
for dwg in side:
|
|
if 'text' in dwg:
|
|
yield dwg
|
|
|
|
@staticmethod
|
|
def _remove_control_codes(s):
|
|
import unicodedata
|
|
return ''.join(c for c in s if unicodedata.category(c)[0] != "C")
|
|
|
|
def _parse_font_data(self, pcbdata):
|
|
font_parser = FontParser()
|
|
for dwg in self._texts(pcbdata):
|
|
if 'svgpath' not in dwg:
|
|
dwg['text'] = self._remove_control_codes(dwg['text'])
|
|
font_parser.parse_font_for_string(dwg['text'])
|
|
|
|
if font_parser.get_parsed_font():
|
|
pcbdata['font_data'] = font_parser.get_parsed_font()
|
|
|
|
def _check_font_data(self, pcbdata):
|
|
mc = set()
|
|
for dwg in self._texts(pcbdata):
|
|
dwg['text'] = self._remove_control_codes(dwg['text'])
|
|
mc.update({c for c in dwg['text'] if 'svgpath' not in dwg and
|
|
c not in pcbdata['font_data']})
|
|
|
|
if mc:
|
|
s = ''.join(mc)
|
|
self.logger.error('Provided font_data is missing character(s)'
|
|
f' "{s}" that are present in text drawing'
|
|
' objects')
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def _parse(self):
|
|
try:
|
|
pcb = self.get_generic_json_pcb()
|
|
except ValidationError as e:
|
|
self.logger.error('File {f} does not comply with json schema. {m}'
|
|
.format(f=self.file_name, m=e.message))
|
|
return None, None
|
|
|
|
if not self._verify(pcb):
|
|
self.logger.error('File {} does not appear to be valid generic'
|
|
' InteractiveHtmlBom json file.'
|
|
.format(self.file_name))
|
|
return None, None
|
|
|
|
pcbdata = pcb['pcbdata']
|
|
components = [Component(**c) for c in pcb['components']]
|
|
|
|
if 'font_data' in pcbdata:
|
|
if not self._check_font_data(pcbdata):
|
|
raise ParsingException(f'Failed parsing {self.file_name}')
|
|
else:
|
|
self._parse_font_data(pcbdata)
|
|
if 'font_data' in pcbdata:
|
|
self.logger.info('No font_data provided in JSON, using '
|
|
'newstroke font')
|
|
|
|
self.logger.info('Successfully parsed {}'.format(self.file_name))
|
|
|
|
return pcbdata, components
|
|
|
|
def parse(self):
|
|
pcbdata, components = self._parse()
|
|
|
|
# override board bounding box based on edges
|
|
board_outline_bbox = BoundingBox()
|
|
for drawing in pcbdata['edges']:
|
|
self.add_drawing_bounding_box(drawing, board_outline_bbox)
|
|
if board_outline_bbox.initialized():
|
|
pcbdata['edges_bbox'] = board_outline_bbox.to_dict()
|
|
|
|
extra_fields = set(self.config.show_fields)
|
|
extra_fields.discard("Footprint")
|
|
extra_fields.discard("Value")
|
|
if self.config.dnp_field:
|
|
extra_fields.add(self.config.dnp_field)
|
|
if self.config.board_variant_field:
|
|
extra_fields.add(self.config.board_variant_field)
|
|
if extra_fields:
|
|
for c in components:
|
|
c.extra_fields = {
|
|
f: c.extra_fields.get(f, "") for f in extra_fields}
|
|
|
|
self.config.kicad_text_formatting = False
|
|
|
|
return pcbdata, components
|