""" Class to read KiPlot config files """ import logging import yaml import os import re import pcbnew from . import plot_config as PC from . import error class CfgReader(object): def __init__(self): pass class YamlError(error.KiPlotError): pass class CfgYamlReader(CfgReader): def __init__(self): super(CfgYamlReader, self).__init__() def _check_version(self, data): try: version = data['kiplot']['version'] except KeyError: raise YamlError("YAML config needs kiplot.version.") return None if version != 1: raise YamlError("Unknown KiPlot config version: {}" .format(version)) return None return version def _get_required(self, data, key): try: val = data[key] except KeyError: raise YamlError("Value is needed for {}".format(key)) return val def _parse_drill_map(self, map_opts): mo = PC.DrillMapOptions() TYPES = { 'hpgl': pcbnew.PLOT_FORMAT_HPGL, 'ps': pcbnew.PLOT_FORMAT_POST, 'gerber': pcbnew.PLOT_FORMAT_GERBER, 'dxf': pcbnew.PLOT_FORMAT_DXF, 'svg': pcbnew.PLOT_FORMAT_SVG, 'pdf': pcbnew.PLOT_FORMAT_PDF } type_s = self._get_required(map_opts, 'type') try: mo.type = TYPES[type_s] except KeyError: raise YamlError("Unknown drill map type: {}".format(type_s)) return mo def _parse_drill_report(self, report_opts): opts = PC.DrillReportOptions() opts.filename = self._get_required(report_opts, 'filename') return opts def _perform_config_mapping(self, otype, cfg_options, mapping_list, target): """ Map a config dict onto a target object given a mapping list """ for mapping in mapping_list: # if this output type matches the mapping specification: if otype in mapping['types']: key = mapping['key'] # set the internal option as needed if mapping['required'](cfg_options): cfg_val = self._get_required(cfg_options, key) elif key in cfg_options: # not required but given anyway cfg_val = cfg_options[key] else: continue # transform the value if needed if 'transform' in mapping: cfg_val = mapping['transform'](cfg_val) setattr(target, mapping['to'], cfg_val) def _parse_out_opts(self, otype, options): # note - type IDs are strings form the _config_, not the internal # strings used as enums (in plot_config) ANY_LAYER = ['gerber', 'ps', 'svg', 'hpgl', 'pdf', 'dxf'] ANY_DRILL = ['excellon', 'gerb_drill'] # mappings from YAML keys to type_option keys MAPPINGS = [ { 'key': 'use_aux_axis_as_origin', 'types': ['gerber', 'dxf'], 'to': 'use_aux_axis_as_origin', 'required': lambda opts: True, }, { 'key': 'exclude_edge_layer', 'types': ANY_LAYER, 'to': 'exclude_edge_layer', 'required': lambda opts: True, }, { 'key': 'exclude_pads_from_silkscreen', 'types': ANY_LAYER, 'to': 'exclude_pads_from_silkscreen', 'required': lambda opts: True, }, { 'key': 'plot_sheet_reference', 'types': ANY_LAYER, 'to': 'plot_sheet_reference', 'required': lambda opts: True, }, { 'key': 'plot_footprint_refs', 'types': ANY_LAYER, 'to': 'plot_footprint_refs', 'required': lambda opts: True, }, { 'key': 'plot_footprint_values', 'types': ANY_LAYER, 'to': 'plot_footprint_values', 'required': lambda opts: True, }, { 'key': 'force_plot_invisible_refs_vals', 'types': ANY_LAYER, 'to': 'force_plot_invisible_refs_vals', 'required': lambda opts: True, }, { 'key': 'tent_vias', 'types': ANY_LAYER, 'to': 'tent_vias', 'required': lambda opts: True, }, { 'key': 'check_zone_fills', 'types': ANY_LAYER, 'to': 'check_zone_fills', 'required': lambda opts: True, }, { 'key': 'line_width', 'types': ['gerber', 'ps', 'svg', 'pdf'], 'to': 'line_width', 'required': lambda opts: True, }, { 'key': 'subtract_mask_from_silk', 'types': ['gerber'], 'to': 'subtract_mask_from_silk', 'required': lambda opts: True, }, { 'key': 'mirror_plot', 'types': ['ps', 'svg', 'hpgl', 'pdf'], 'to': 'mirror_plot', 'required': lambda opts: True, }, { 'key': 'negative_plot', 'types': ['ps', 'svg', 'pdf'], 'to': 'negative_plot', 'required': lambda opts: True, }, { 'key': 'sketch_plot', 'types': ['ps', 'hpgl'], 'to': 'sketch_plot', 'required': lambda opts: True, }, { 'key': 'scaling', 'types': ['ps', 'hpgl'], 'to': 'scaling', 'required': lambda opts: True, }, { 'key': 'drill_marks', 'types': ['ps', 'svg', 'dxf', 'hpgl', 'pdf'], 'to': 'drill_marks', 'required': lambda opts: True, }, { 'key': 'use_protel_extensions', 'types': ['gerber'], 'to': 'use_protel_extensions', 'required': lambda opts: True, }, { 'key': 'gerber_precision', 'types': ['gerber'], 'to': 'gerber_precision', 'required': lambda opts: True, }, { 'key': 'create_gerber_job_file', 'types': ['gerber'], 'to': 'create_gerber_job_file', 'required': lambda opts: True, }, { 'key': 'use_gerber_x2_attributes', 'types': ['gerber'], 'to': 'use_gerber_x2_attributes', 'required': lambda opts: True, }, { 'key': 'use_gerber_net_attributes', 'types': ['gerber'], 'to': 'use_gerber_net_attributes', 'required': lambda opts: True, }, { 'key': 'scale_adjust_x', 'types': ['ps'], 'to': 'scale_adjust_x', 'required': lambda opts: True, }, { 'key': 'scale_adjust_y', 'types': ['ps'], 'to': 'scale_adjust_y', 'required': lambda opts: True, }, { 'key': 'width_adjust', 'types': ['ps'], 'to': 'width_adjust', 'required': lambda opts: True, }, { 'key': 'a4_output', 'types': ['ps'], 'to': 'a4_output', 'required': lambda opts: True, }, { 'key': 'pen_width', 'types': ['hpgl'], 'to': 'pen_width', 'required': lambda opts: True, }, { 'key': 'polygon_mode', 'types': ['dxf'], 'to': 'polygon_mode', 'required': lambda opts: True, }, { 'key': 'use_aux_axis_as_origin', 'types': ANY_DRILL, 'to': 'use_aux_axis_as_origin', 'required': lambda opts: True, }, { 'key': 'map', 'types': ANY_DRILL, 'to': 'map_options', 'required': lambda opts: False, 'transform': self._parse_drill_map }, { 'key': 'report', 'types': ANY_DRILL, 'to': 'report_options', 'required': lambda opts: False, 'transform': self._parse_drill_report }, { 'key': 'metric_units', 'types': ['excellon'], 'to': 'metric_units', 'required': lambda opts: True, }, { 'key': 'pth_and_npth_single_file', 'types': ['excellon'], 'to': 'pth_and_npth_single_file', 'required': lambda opts: True, }, { 'key': 'minimal_header', 'types': ['excellon'], 'to': 'minimal_header', 'required': lambda opts: True, }, { 'key': 'mirror_y_axis', 'types': ['excellon'], 'to': 'mirror_y_axis', 'required': lambda opts: True, }, { 'key': 'format', 'types': ['position'], 'to': 'format', 'required': lambda opts: True, }, { 'key': 'units', 'types': ['position'], 'to': 'units', 'required': lambda opts: True, }, { 'key': 'separate_files_for_front_and_back', 'types': ['position'], 'to': 'separate_files_for_front_and_back', 'required': lambda opts: True, }, { 'key': 'only_smd', 'types': ['position'], 'to': 'only_smd', 'required': lambda opts: True, } ] po = PC.OutputOptions(otype) # options that apply to the specific output type to = po.type_options self._perform_config_mapping(otype, options, MAPPINGS, to) return po def _get_layer_from_str(self, s): """ Get the pcbnew layer from a string in the config """ D = { 'F.Cu': pcbnew.F_Cu, 'B.Cu': pcbnew.B_Cu, 'F.Adhes': pcbnew.F_Adhes, 'B.Adhes': pcbnew.B_Adhes, 'F.Paste': pcbnew.F_Paste, 'B.Paste': pcbnew.B_Paste, 'F.SilkS': pcbnew.F_SilkS, 'B.SilkS': pcbnew.B_SilkS, 'F.Mask': pcbnew.F_Mask, 'B.Mask': pcbnew.B_Mask, 'Dwgs.User': pcbnew.Dwgs_User, 'Cmts.User': pcbnew.Cmts_User, 'Eco1.User': pcbnew.Eco1_User, 'Eco2.User': pcbnew.Eco2_User, 'Edge.Cuts': pcbnew.Edge_Cuts, 'Margin': pcbnew.Margin, 'F.CrtYd': pcbnew.F_CrtYd, 'B.CrtYd': pcbnew.B_CrtYd, 'F.Fab': pcbnew.F_Fab, 'B.Fab': pcbnew.B_Fab, } layer = None if s in D: layer = PC.LayerInfo(D[s], False) elif s.startswith("Inner"): m = re.match(r"^Inner\.([0-9]+)$", s) if not m: raise YamlError("Malformed inner layer name: {}" .format(s)) layer = PC.LayerInfo(int(m.group(1)), True) else: raise YamlError("Unknown layer name: {}".format(s)) return layer def _parse_layer(self, l_obj): l_str = self._get_required(l_obj, 'layer') layer_id = self._get_layer_from_str(l_str) layer = PC.LayerConfig(layer_id) layer.desc = l_obj['description'] if 'description' in l_obj else None layer.suffix = l_obj['suffix'] if 'suffix' in l_obj else "" return layer def _parse_output(self, o_obj): try: name = o_obj['name'] except KeyError: raise self.YamlError("Output needs a name") try: desc = o_obj['comment'] except KeyError: desc = None try: otype = o_obj['type'] except KeyError: raise YamlError("Output needs a type") if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg', 'gerb_drill', 'excellon', 'position']: raise YamlError("Unknown output type: {}".format(otype)) try: options = o_obj['options'] except KeyError: raise YamlError("Output need to have options specified") logging.debug("Parsing output options for {} ({})".format(name, otype)) outdir = self._get_required(o_obj, 'dir') output_opts = self._parse_out_opts(otype, options) o_cfg = PC.PlotOutput(name, desc, otype, output_opts) o_cfg.outdir = outdir try: layers = o_obj['layers'] except KeyError: layers = [] for l in layers: o_cfg.layers.append(self._parse_layer(l)) return o_cfg def _parse_preflight(self, pf, cfg): logging.debug("Parsing preflight options: {}".format(pf)) if 'check_zone_fills' in pf: cfg.check_zone_fills = pf['check_zone_fills'] if 'run_drc' in pf: cfg.run_drc = pf['run_drc'] def read(self, fstream): """ Read a file object into a config object :param fstream: file stream of a config YAML file """ try: data = yaml.load(fstream) except yaml.YAMLError as e: raise YamlError("Error loading YAML") return None self._check_version(data) cfg = PC.PlotConfig() if 'preflight' in data: self._parse_preflight(data['preflight'], cfg) for o in data['outputs']: op_cfg = self._parse_output(o) cfg.add_output(op_cfg) return cfg