KiBot/kiplot/config_reader.py

501 lines
14 KiB
Python

"""
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