def _extract_lp(self, name, tok): """ Extract "layer param" and reset image layer. """ if self.img_buff.not_empty(): self.layer_buff.images.append(self.img_buff) self.img_buff = Image('', self.img_buff.is_additive) self.trace_buff = TraceBuffer() self.status.update({ 'x': 0, 'y': 0, 'draw': 'OFF', 'interpolation': 'LINEAR', 'aperture': None, 'outline_fill': False, 'multi_quadrant': False, 'units': None, 'incremental_coords': None }) if name == 'LN': self.img_buff.name = tok elif name == 'LP': self.img_buff.is_additive = IMAGE_POLARITIES[tok] elif name == 'SR': tok, j = self._pop_val('J', tok, coerce_='float') tok, i = self._pop_val('I', tok, coerce_='float') tok, y = self._pop_val('Y', tok) tok, x = self._pop_val('X', tok) self.img_buff.x_repeats = x self.img_buff.x_step = i self.img_buff.y_repeats = y self.img_buff.y_step = j
def _extract_lp(self, name, tok): """ Extract "layer param" and reset image layer. """ if self.img_buff.not_empty(): self.layer_buff.images.append(self.img_buff) self.img_buff = Image('', self.img_buff.is_additive) self.trace_buff = TraceBuffer() self.status.update({'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None}) if name == 'LN': self.img_buff.name = tok elif name == 'LP': self.img_buff.is_additive = IMAGE_POLARITIES[tok] elif name == 'SR': tok, j = self._pop_val('J', tok, coerce_='float') tok, i = self._pop_val('I', tok, coerce_='float') tok, y = self._pop_val('Y', tok) tok, x = self._pop_val('X', tok) self.img_buff.x_repeats = x self.img_buff.x_step = i self.img_buff.y_repeats = y self.img_buff.y_step = j
def __init__(self, ignore_unknown=True): self.ignore_unknown = ignore_unknown self.layout = Layout() self.layer_buff = None self.macro_buff = None self.img_buff = Image() self.trace_buff = TraceBuffer() self.fill_buff = [] # establish gerber defaults self.params = {'AS':AxisDef('x', 'y'),# axis select 'FS':None, # format spec 'MI':AxisDef(0, 0), # mirror image 'MO':'IN', # mode: inches/mm 'OF':AxisDef(0, 0), # offset 'SF':AxisDef(1, 1), # scale factor 'IJ':AxisDef(('L', 0), # image justify ('L', 0)), 'IO':AxisDef(0, 0), # image offset 'IP':True, # image polarity 'IR':0} # image rotation # simulate a photo plotter self.status = {'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None}
def test_images(self): """ Capture images with no data. """ layer = Layer() layer.images.append(Image()) layout = Layout() layout.units = 'mm' layout.layers.append(layer) design = Design() design.layout = layout writer = Writer() writer.write(design)
def __init__(self, ignore_unknown=True): self.ignore_unknown = ignore_unknown self.layout = Layout() self.layer_buff = None self.macro_buff = None self.img_buff = Image() self.trace_buff = TraceBuffer() self.fill_buff = [] # establish gerber defaults self.params = { 'AS': AxisDef('x', 'y'), # axis select 'FS': None, # format spec 'MI': AxisDef(0, 0), # mirror image 'MO': 'IN', # mode: inches/mm 'OF': AxisDef(0, 0), # offset 'SF': AxisDef(1, 1), # scale factor 'IJ': AxisDef( ('L', 0), # image justify ('L', 0)), 'IO': AxisDef(0, 0), # image offset 'IP': True, # image polarity 'IR': 0 } # image rotation # simulate a photo plotter self.status = { 'x': 0, 'y': 0, 'draw': 'OFF', 'interpolation': 'LINEAR', 'aperture': None, 'outline_fill': False, 'multi_quadrant': False, 'units': None, 'incremental_coords': None }
class Gerber: """ The Gerber Format Parser """ def __init__(self, ignore_unknown=True): self.ignore_unknown = ignore_unknown self.layout = Layout() self.layer_buff = None self.macro_buff = None self.img_buff = Image() self.trace_buff = TraceBuffer() self.fill_buff = [] # establish gerber defaults self.params = { 'AS': AxisDef('x', 'y'), # axis select 'FS': None, # format spec 'MI': AxisDef(0, 0), # mirror image 'MO': 'IN', # mode: inches/mm 'OF': AxisDef(0, 0), # offset 'SF': AxisDef(1, 1), # scale factor 'IJ': AxisDef( ('L', 0), # image justify ('L', 0)), 'IO': AxisDef(0, 0), # image offset 'IP': True, # image polarity 'IR': 0 } # image rotation # simulate a photo plotter self.status = { 'x': 0, 'y': 0, 'draw': 'OFF', 'interpolation': 'LINEAR', 'aperture': None, 'outline_fill': False, 'multi_quadrant': False, 'units': None, 'incremental_coords': None } @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an gerber file """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0 if '%ADD' in data: confidence += 0.2 if 'D01*' in data: confidence += 0.2 if 'D02*' in data: confidence += 0.2 if 'D03*' in data: confidence += 0.2 if 'M02*' in data: confidence += 0.2 if filename.endswith('.ger'): confidence += 0.5 return confidence def parse(self, infile='.'): """ Parse tokens from gerber files into a design. """ is_zip = infile.endswith('.zip') openarchive = ZipFile if is_zip else TarFile.open archive = batch_member = None try: # define multiple layers from folder if LAYERS_CFG in infile: archive = None cfg_name = infile cfg = open(cfg_name, 'r') # define multiple layers from archivea else: archive = openarchive(infile) batch = archive.namelist if is_zip else archive.getnames batch_member = archive.open if is_zip else archive.extractfile cfg_name = [n for n in batch() if LAYERS_CFG in n][0] cfg = batch_member(cfg_name) # define single layer from single gerber file except ReadError: name, ext = path.split(infile)[1].rsplit('.', 1) layer_defs = [ LayerDef(ext.lower() == 'ger' and name or ext, 'unknown', infile) ] self._gen_layers(layer_defs, None, None) # tidy up batch specs else: layer_defs = [ LayerDef(rec[0], rec[1], path.join(path.split(cfg_name)[0], rec[2])) for rec in csv.reader(cfg, skipinitialspace=True) ] cfg.close() self._gen_layers(layer_defs, archive, batch_member) # tidy up archive finally: if archive: archive.close() # compile design if DEBUG: self._debug_stdout() self.layout.units = (self.params['MO'] == 'IN' and 'inch' or 'mm') design = Design() design.layout = self.layout return design # primary parser support methods def _gen_layers(self, layer_defs, archive, batch_member): """ Parse gerbers into a PCB layers. """ for layer_def in layer_defs: self.layer_buff = Layer(layer_def.name, layer_def.type) self.macro_buff = {} layer_file = (archive and batch_member(layer_def.filename) or open(layer_def.filename, 'r')) for block in self._tokenize(layer_file): if isinstance(block, MacroDef): self.macro_buff[block.name] = InternalMacro(block) effect = {} elif isinstance(block, Funct): effect = self._do_funct(block) else: effect = self._move(block) self.status.update(effect) self.layout.layers.append(self.layer_buff) def _do_funct(self, block): """ Set drawing modes, fill terminators. """ code = int(block.code) if 'D' in block.type_: if code < 10: effect = {'draw': D_MAP[code]} # flash current pos/aperture if 'X' in block.type_: apertures = self.layer_buff.apertures aperture = apertures[self.status['aperture']] pos = Point(self.status['x'], self.status['y']) shape_inst = ShapeInstance(pos, aperture) self.img_buff.shape_instances.append(shape_inst) # terminate fill mid mode if (self.status['outline_fill'] and code == 2 and self.fill_buff): self.img_buff.fills.append(self._check_fill()) else: effect = {'aperture': block.code} else: effect = G_MAP[code] if code == 37: # terminate fill if D02 was not specified if self.fill_buff: self.img_buff.fills.append(self._check_fill()) return effect def _move(self, block): """ Draw a shape, or a segment of a trace or fill. """ start = tuple([self.status[k] for k in ('x', 'y')]) end = self._target_pos(block) ends = (Point(start), Point(end)) apertures = self.layer_buff.apertures if self.status['draw'] == 'ON': # generate segment if self.status['interpolation'] == 'LINEAR': seg = Line(start, end) else: ctr_offset = block[2:] seg = self._draw_arc(ends, ctr_offset) # append segment to fill if self.status['outline_fill']: self.fill_buff.append((ends, seg)) else: aperture = apertures[self.status['aperture']] if isinstance(aperture.shape, Rectangle): # construct a smear self._check_smear(seg, aperture.shape) self.img_buff.smears.append(Smear(seg, aperture.shape)) else: wid = aperture.shape.radius * 2 trace = self.trace_buff.get_trace(wid, seg) if trace is None: trace = Trace(wid) self.img_buff.traces.append(trace) trace.segments.append(seg) self.trace_buff.add_segment(seg, trace) elif self.status['draw'] == 'FLASH': aperture = apertures[self.status['aperture']] shape_inst = ShapeInstance(ends[1], aperture) self.img_buff.shape_instances.append(shape_inst) return {'x': end[0], 'y': end[1]} # coordinate interpretation def _target_pos(self, block): """ Interpret coordinates in a data block. """ coord = {'x': block.x, 'y': block.y} for k in coord: if self.params['FS'].incremental_coords: if coord[k] is None: coord[k] = 0 coord[k] = self.status[k] + getattr(block, k) else: if coord[k] is None: coord[k] = self.status[k] return (coord['x'], coord['y']) # geometry def _draw_arc(self, end_pts, center_offset): """ Convert arc path into shape. """ start, end = end_pts offset = {'i': center_offset[0], 'j': center_offset[1]} for k in offset: if offset[k] is None: offset[k] = 0 center, radius = self._get_ctr_and_radius(end_pts, offset) start_angle = self._get_angle(center, start) end_angle = self._get_angle(center, end) clockwise = 'ANTI' not in self.status['interpolation'] self._check_mq(start_angle, end_angle, clockwise) return Arc(center.x, center.y, start_angle if clockwise else end_angle, end_angle if clockwise else start_angle, radius) def _get_ctr_and_radius(self, end_pts, offset): """ Apply gerber circular interpolation logic. """ start, end = end_pts radius = sqrt(offset['i']**2 + offset['j']**2) centers = [ Point(start.x + xsign * offset['i'], start.y + ysign * offset['j']) for xsign in (1, -1) for ysign in (1, -1) ] center = min(centers, key=lambda c: abs(c.dist(end) - radius)) return (center, radius) def _get_angle(self, arc_center, point): """ Convert 2 points to an angle in radians/pi. 0 radians = 3 o'clock, in accordance with the way arc angles are defined in shape.py """ adj = float(point.x - arc_center.x) opp = point.y - arc_center.y hyp = arc_center.dist(point) if hyp == 0.0: return 0.0 angle = acos(adj / hyp) / pi if opp > 0: angle = 2 - angle return angle # tokenizer def _tokenize(self, layer_file): """ Split gerber file into pythonic tokens. """ content = layer_file.read() layer_file.close() param_block = eof = False pos = 0 match = TOK_RE.match(content) while pos < len(content): if match is None: pos += 1 else: typ = match.lastgroup tok = match.group(typ)[:-1] try: if typ == 'MACRO': yield self._parse_macro(tok) elif typ == 'PARAM_DELIM': param_block = not param_block # params elif len(typ) == 2: self._check_pb(param_block, tok) self.params.update(self._parse_param(tok)) # data blocks elif typ not in IGNORE: if typ == 'EOF': self._check_eof(content[match.end():]) eof = True elif typ == 'UNKNOWN': if not self.ignore_unknown: raise UnintelligibleDataBlock(tok) else: self._check_pb(param_block, tok, False) # explode self-referential data blocks blocks = self._parse_data_block(tok) for block in blocks: yield block except Unparsable: raise pos = match.end() match = TOK_RE.match(content, pos) self._check_eof(eof=eof) self.layer_buff.images.append(self.img_buff) # tokenizer support - macros def _parse_macro(self, tok): """ Define a macro, with its component shapes. """ parts = [part.strip() for part in tok.split('*')] name = parts[0][3:] prims = [part.split(',') for part in parts[1:-1] if part] int_prims = [] for prim in prims: if prim[0].startswith('$'): shape_type = 'ignore' mods = prim else: shape_type = PRIMITIVES[int(prim[0])] mods = prim[1:] int_prims.append(InternalPrimitive(shape_type, mods)) return MacroDef(name, int_prims) # tokenizer support - params def _parse_param(self, tok): """ Convert a param specifier into pythonic data. """ name, tok = (tok[:2], tok[2:]) if name == 'FS': tup = self._extract_fs(tok) elif name == 'AD': self._extract_ad(tok) return {} elif name in AXIS_PARAMS: tup = self._extract_ap(name, tok) elif name in LAYER_PARAMS: self._extract_lp(name, tok) return {} else: tup = tok return {name: tup} def _extract_fs(self, tok): """ Extract format spec param parts into tuple. """ tok, m_max = self._pop_val('M', tok) tok, d_max = self._pop_val('D', tok) tok, y = self._pop_val('Y', tok, coerce_=False) y = CoordFmt(int(y[0]), int(y[1])) tok, x = self._pop_val('X', tok, coerce_=False) x = CoordFmt(int(x[0]), int(x[1])) tok, g_max = self._pop_val('G', tok) tok, n_max = self._pop_val('N', tok) inc_coords = ('I' in tok) z_omit = tok[0] return FormatSpec(z_omit, inc_coords, n_max, g_max, x, y, d_max, m_max) def _extract_ad(self, tok): """ Extract aperture definition into shapes dict. """ tok = tok if ',' in tok else tok + ',' code_end = 4 if tok[3].isdigit() else 3 code = tok[1:code_end] ap_type, mods = tok[code_end:].split(',') if mods: mods = [float(m) for m in mods.split('X') if m] # An aperture can use any of the 4 standard types, # (with or without a central hole), or a previously # defined macro. if ap_type == 'C': shape = Circle(0, 0, mods[0] / 2) hole_defs = len(mods) > 1 and mods[1:] elif ap_type == 'R': if len(mods) == 1: shape = Rectangle(-mods[0] / 2, mods[0] / 2, mods[0], mods[0]) else: shape = Rectangle(-mods[0] / 2, mods[1] / 2, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif ap_type == 'O': shape = Obround(0, 0, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif ap_type == 'P': if len(mods) < 3: mods.append(0) shape = RegularPolygon(0, 0, mods[0], mods[1], mods[2]) hole_defs = len(mods) > 3 and mods[3:] else: # macro shape = ap_type if shape in self.macro_buff: macro = self.macro_buff[shape].instantiate(mods) counter = 0 # pick a unique name for the macro while mods and macro.name in self.layer_buff.macros: macro.name = shape + str(counter) counter += 1 self.layer_buff.macros[macro.name] = macro hole_defs = None if hole_defs and (len(hole_defs) > 1): hole = Rectangle(-hole_defs[0] / 2, hole_defs[1] / 2, hole_defs[0], hole_defs[1]) elif hole_defs: hole = Circle(0, 0, hole_defs[0] / 2) else: hole = None self.layer_buff.apertures.update({code: Aperture(code, shape, hole)}) def _extract_ap(self, name, tok): """ Extract axis-defining param into tuple. """ if name in ('AS', 'IJ'): coerce_ = False else: coerce_ = name == 'MI' and 'int' or 'float' tok, b_val = self._pop_val('B', tok, coerce_=coerce_) tok, a_val = self._pop_val('A', tok, coerce_=coerce_) if name == 'IJ': tup = AxisDef(self._parse_justify(a_val), self._parse_justify(b_val)) else: tup = AxisDef(a_val, b_val) return tup def _extract_lp(self, name, tok): """ Extract "layer param" and reset image layer. """ if self.img_buff.not_empty(): self.layer_buff.images.append(self.img_buff) self.img_buff = Image('', self.img_buff.is_additive) self.trace_buff = TraceBuffer() self.status.update({ 'x': 0, 'y': 0, 'draw': 'OFF', 'interpolation': 'LINEAR', 'aperture': None, 'outline_fill': False, 'multi_quadrant': False, 'units': None, 'incremental_coords': None }) if name == 'LN': self.img_buff.name = tok elif name == 'LP': self.img_buff.is_additive = IMAGE_POLARITIES[tok] elif name == 'SR': tok, j = self._pop_val('J', tok, coerce_='float') tok, i = self._pop_val('I', tok, coerce_='float') tok, y = self._pop_val('Y', tok) tok, x = self._pop_val('X', tok) self.img_buff.x_repeats = x self.img_buff.x_step = i self.img_buff.y_repeats = y self.img_buff.y_step = j def _parse_justify(self, val): """ Make a tuple for each axis (special case). """ if len(val) > 1 or val not in 'LC': ab_tup = ('L', float(val.split('L')[-1])) else: ab_tup = (val, None) return ab_tup # tokenizer support - funct/coord def _parse_data_block(self, tok): """ Convert a non-param into pythonic data. """ if 'G' in tok: g_code = tok[1:3] tok = tok[3:] if int(g_code) in G_MAP: yield Funct('G', g_code) tok, d_code = self._pop_val('D', tok, coerce_=False) if d_code: # identify D03 without coord - flash at current pos type_ = (d_code == '03' and not tok) and 'XD' or 'D' yield Funct(type_, d_code) if tok: yield self._parse_coord(tok) def _parse_coord(self, tok): """ Convert a coordinate set into pythonic data. """ self._check_fs() tok, j = self._pop_val('J', tok, format_=True) tok, i = self._pop_val('I', tok, format_=True) tok, y = self._pop_val('Y', tok, format_=True) tok, x = self._pop_val('X', tok, format_=True) result = Coord(x, y, i, j) if tok: raise CoordMalformed('%s remainder=%s' % (result, tok)) return result def _format_dec(self, num_str, axis): """ Interpret a coordinate value using format spec. Params: num_str (the string representation of a number portended by format spec) axis (X or Y - not to be confused with A/B) Returns: float """ f_spec = self.params['FS'] sign_wid = num_str[0] in ('-', '+') and 1 or 0 int_wid = sign_wid + f_spec[axis].int wid = int_wid + f_spec[axis].dec # pad coordinate to specified width if f_spec.zero_omission == 'L': num_str = num_str.zfill(wid) elif f_spec.zero_omission == 'T': num_str = num_str.rjust(wid, '0') if len(num_str) != wid: raise CoordMalformed('num_str: %s wid: %s' % (num_str, wid)) # insert decimal point num_str = '%s.%s' % (num_str[:int_wid], num_str[int_wid:]) return float(num_str) # general support methods def _pop_val(self, key, tok, format_=False, coerce_='int'): """ Pop a labelled value from the end of a token. """ val = None if key in tok: tok, num_str = tok.split(key) if num_str: if format_: if key in ('X', 'I'): val = self._format_dec(num_str, 4) else: val = self._format_dec(num_str, 5) else: if coerce_ == 'float': if '.' in num_str: val = int(float(num_str)) else: val = int(num_str) elif coerce_: val = int(num_str) else: val = num_str return (tok, val) def _check_fill(self): """ Check that a fill is closed. """ ends = [pair[0] for pair in self.fill_buff] fill = [pair[1] for pair in self.fill_buff] self.fill_buff = [] if ends[0][0] == ends[-1][1]: return Fill(fill) else: raise OpenFillBoundary('%s != %s' % (ends[-1][1], ends[0][0])) def _check_smear(self, seg, shape): """ Enforce linear interpolation constraint. """ if not isinstance(seg, Line): raise IncompatibleAperture('%s cannot draw arc %s' % (shape, seg)) def _check_mq(self, start_angle, end_angle, clockwise): """ Enforce single quadrant arc length restriction. """ if not self.status['multi_quadrant']: if clockwise: arc = (2.0 if end_angle == 0.0 else end_angle) - \ (0.0 if start_angle == 2.0 else start_angle) else: arc = (2.0 if start_angle == 0.0 else start_angle) - \ (0.0 if end_angle == 2.0 else end_angle) if round(abs(arc), 4) > 0.5: raise QuadrantViolation('Arc(%s to %s) > 0.5 rad/pi' % (start_angle, end_angle)) def _check_pb(self, param_block, tok, should_be=True): """ Ensure we are parsing an appropriate block. """ if should_be and not param_block: raise DelimiterMissing if param_block and not should_be: raise ParamContainsBadData(tok) def _check_fs(self): """ Ensure coordinate is able to be interpreted. """ if not self.params['FS']: raise CoordPrecedesFormatSpec def _check_eof(self, trailing_fragment=None, eof=False): """ Ensure file is terminated correctly. """ if not (trailing_fragment or eof): raise FileNotTerminated elif (not eof) and trailing_fragment.strip(): raise DataAfterEOF(trailing_fragment) def _debug_stdout(self): """ Dump what we know. """ for layer in self.layout.layers: print '-- %s (%s) --' % (layer.name, layer.type) for j in layer.apertures: print '-- D%s --' % j print layer.apertures[j].json() for k in layer.macros: print '-- %s --' % k print layer.macros[k].json() for image in layer.images: print '-- %s (%s) --' % (image.name, image.is_additive and 'additive' or 'subtractive') print image.json() raise Unparsable('deliberate error')
def _define_images(self, design, layer_name): """ Define the images that make up the layer information. """ log.debug('creating images for layer "%s"', layer_name) # trace segments on this layer traces_image = Image(layer_name + '_traces', font_renderer=self.face) for segment in design.trace_segments: if segment.layer != layer_name: continue log.debug('Creating smear for trace: %s', segment) # Assumes segment is rounded, straignt trace_smear = Smear(Line(segment.p1, segment.p2), Circle(0, 0, segment.width / 2.0)) traces_image.smears.append(trace_smear) # Generated objects in the design (vias, PTHs) zero_pos = FootprintPos(0, 0, 0.0, False, 'top') for gen_obj in design.layout_objects: # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(zero_pos, {}): if body_attr.layer == layer_name: for shape in body.shapes: traces_image.add_shape(shape, design, zero_pos, body_attr) self.images.append(traces_image) # Pours on this layer for pour in design.pours: log.debug('adding body for pour: %s points, %s', len(pour.points), pour.layer) if layer_name == pour.layer: log.debug('adding body for pour: %s points, %s subtractive shapes', len(pour.points), len(pour.subtractive_shapes)) fill_image = Image('pour fill', font_renderer=self.face) fill_image.fills.append(Fill(pour.points)) self.images.append(fill_image) subtractive_image = Image('pour subtractive shapes', font_renderer=self.face, is_additive=False) for shape in pour.subtractive_shapes: if shape.type == 'rounded_segment': trace_smear = Smear(Line(shape.p1, shape.p2), Circle(0, 0, shape.width / 2.0)) subtractive_image.smears.append(trace_smear) else: subtractive_image.add_shape(shape, None, FootprintPos(0, 0, 0.0, False, ''), FootprintPos(0, 0, 0.0, False, '')) self.images.append(subtractive_image) readded_image = Image('pour readded shapes', font_renderer=self.face, is_additive=True) for shape in pour.readded_shapes: if shape.type == 'rounded_segment': trace_smear = Smear(Line(shape.p1, shape.p2), Circle(0, 0, shape.width / 2.0)) readded_image.smears.append(trace_smear) else: readded_image.add_shape(shape, None, FootprintPos(0, 0, 0.0, False, ''), FootprintPos(0, 0, 0.0, False, '')) self.images.append(readded_image) # trace segments on this layer traces_image = Image(layer_name + '_traces', font_renderer=self.face) for segment in design.trace_segments: if segment.layer != layer_name: continue log.debug('Creating smear for trace: %s', segment) # Assumes segment is rounded, straignt trace_smear = Smear(Line(segment.p1, segment.p2), Circle(0, 0, segment.width / 2.0)) traces_image.smears.append(trace_smear) # Generated objects in the design (vias, PTHs) zero_pos = FootprintPos(0, 0, 0.0, False, 'top') for gen_obj in design.layout_objects: # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(zero_pos, {}): if body_attr.layer == layer_name: for shape in body.shapes: traces_image.add_shape(shape, design, zero_pos, body_attr) self.images.append(traces_image) # Component aspects on this layer # a separate image is used for each component for component_instance in design.component_instances: component = design.components.components[component_instance.library_id] component_image = Image(layer_name + ' component ' + component_instance.instance_id, font_renderer=self.face) footprint_pos = component_instance.footprint_pos if footprint_pos.side is None: continue for idx, footprint_attr in enumerate(component_instance.footprint_attributes): log.debug('footprint pos: %s, side %s, flip %s', footprint_attr.layer, footprint_pos.side, footprint_pos.flip_horizontal) fp_attr_cpy = copy.deepcopy(footprint_attr) if footprint_attr.layer: if footprint_pos.side == 'bottom': rev_sides = {'top': 'bottom', 'bottom': 'top'} fp_attr_cpy.layer = ' '.join([rev_sides.get(piece, piece) for piece in footprint_attr.layer.split(' ')]) if fp_attr_cpy.layer == layer_name: footprint_body = component.footprints[component_instance.footprint_index].bodies[idx] log.debug('adding footprint attribute: %s, %d shapes', fp_attr_cpy, len(footprint_body.shapes)) for shape in footprint_body.shapes: component_image.add_shape(shape, component_instance, footprint_pos, fp_attr_cpy) for idx, gen_obj_attr in enumerate(component_instance.gen_obj_attributes): gen_obj = component.footprints[component_instance.footprint_index].gen_objs[idx] # FIXME(shamer): check for unplaced generated objects. # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(footprint_pos, gen_obj_attr.attributes): if body_attr.layer == layer_name: log.debug('adding body for generated object: %s, %s', footprint_pos, gen_obj_attr) for shape in body.shapes: component_image.add_shape(shape, component_instance, footprint_pos, body_attr) if component_image.not_empty(): self.images.append(component_image) # paths on the layer for path in design.paths: if layer_name == path.layer: log.debug('adding body for path: %s points, %s, %s, is closed: %s', len(path.points), path.width, path.layer, path.is_closed) path_image = Image('path', font_renderer=self.face) start = path.points[0] for point in path.points[1:]: path_image.add_shape(Line(start, point), Circle(0, 0, path.width), zero_pos, zero_pos) start = point if path.is_closed: path_image.add_shape(Line(path.points[0], path.points[-1]), Circle(0, 0, path.width), zero_pos, zero_pos) self.images.append(path_image) # stand alone text on the layer text_image = Image('text', font_renderer=self.face) for text in design.pcb_text: if layer_name == text.layer: log.debug('adding body for text: "%s"', text.value) text_image.add_shape(text.label, design, text, zero_pos) if text_image.not_empty(): self.images.append(text_image)
class Gerber: """ The Gerber Format Parser """ def __init__(self, ignore_unknown=True): self.ignore_unknown = ignore_unknown self.layout = Layout() self.layer_buff = None self.macro_buff = None self.img_buff = Image() self.trace_buff = TraceBuffer() self.fill_buff = [] # establish gerber defaults self.params = {'AS':AxisDef('x', 'y'),# axis select 'FS':None, # format spec 'MI':AxisDef(0, 0), # mirror image 'MO':'IN', # mode: inches/mm 'OF':AxisDef(0, 0), # offset 'SF':AxisDef(1, 1), # scale factor 'IJ':AxisDef(('L', 0), # image justify ('L', 0)), 'IO':AxisDef(0, 0), # image offset 'IP':True, # image polarity 'IR':0} # image rotation # simulate a photo plotter self.status = {'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None} @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an gerber file """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0 if '%ADD' in data: confidence += 0.2 if 'D01*' in data: confidence += 0.2 if 'D02*' in data: confidence += 0.2 if 'D03*' in data: confidence += 0.2 if 'M02*' in data: confidence += 0.2 if filename.endswith('.ger'): confidence += 0.5 return confidence def parse(self, infile='.'): """ Parse tokens from gerber files into a design. """ is_zip = infile.endswith('.zip') openarchive = ZipFile if is_zip else TarFile.open archive = batch_member = None try: # define multiple layers from folder if LAYERS_CFG in infile: archive = None cfg_name = infile cfg = open(cfg_name, 'r') # define multiple layers from archivea else: archive = openarchive(infile) batch = archive.namelist if is_zip else archive.getnames batch_member = archive.open if is_zip else archive.extractfile cfg_name = [n for n in batch() if LAYERS_CFG in n][0] cfg = batch_member(cfg_name) # define single layer from single gerber file except ReadError: name, ext = path.split(infile)[1].rsplit('.', 1) layer_defs = [LayerDef(ext.lower() == 'ger' and name or ext, 'unknown', infile)] self._gen_layers(layer_defs, None, None) # tidy up batch specs else: layer_defs = [LayerDef(rec[0], rec[1], path.join(path.split(cfg_name)[0], rec[2])) for rec in csv.reader(cfg, skipinitialspace=True)] cfg.close() self._gen_layers(layer_defs, archive, batch_member) # tidy up archive finally: if archive: archive.close() # compile design if DEBUG: self._debug_stdout() self.layout.units = (self.params['MO'] == 'IN' and 'inch' or 'mm') design = Design() design.layout = self.layout return design # primary parser support methods def _gen_layers(self, layer_defs, archive, batch_member): """ Parse gerbers into a PCB layers. """ for layer_def in layer_defs: self.layer_buff = Layer(layer_def.name, layer_def.type) self.macro_buff = {} layer_file = (archive and batch_member(layer_def.filename) or open(layer_def.filename, 'r')) for block in self._tokenize(layer_file): if isinstance(block, MacroDef): self.macro_buff[block.name] = InternalMacro(block) effect = {} elif isinstance(block, Funct): effect = self._do_funct(block) else: effect = self._move(block) self.status.update(effect) self.layout.layers.append(self.layer_buff) def _do_funct(self, block): """ Set drawing modes, fill terminators. """ code = int(block.code) if 'D' in block.type_: if code < 10: effect = {'draw':D_MAP[code]} # flash current pos/aperture if 'X' in block.type_: apertures = self.layer_buff.apertures aperture = apertures[self.status['aperture']] pos = Point(self.status['x'], self.status['y']) shape_inst = ShapeInstance(pos, aperture) self.img_buff.shape_instances.append(shape_inst) # terminate fill mid mode if (self.status['outline_fill'] and code == 2 and self.fill_buff): self.img_buff.fills.append(self._check_fill()) else: effect = {'aperture':block.code} else: effect = G_MAP[code] if code == 37: # terminate fill if D02 was not specified if self.fill_buff: self.img_buff.fills.append(self._check_fill()) return effect def _move(self, block): """ Draw a shape, or a segment of a trace or fill. """ start = tuple([self.status[k] for k in ('x', 'y')]) end = self._target_pos(block) ends = (Point(start), Point(end)) apertures = self.layer_buff.apertures if self.status['draw'] == 'ON': # generate segment if self.status['interpolation'] == 'LINEAR': seg = Line(start, end) else: ctr_offset = block[2:] seg = self._draw_arc(ends, ctr_offset) # append segment to fill if self.status['outline_fill']: self.fill_buff.append((ends, seg)) else: aperture = apertures[self.status['aperture']] if isinstance(aperture.shape, Rectangle): # construct a smear self._check_smear(seg, aperture.shape) self.img_buff.smears.append(Smear(seg, aperture.shape)) else: wid = aperture.shape.radius * 2 trace = self.trace_buff.get_trace(wid, seg) if trace is None: trace = Trace(wid) self.img_buff.traces.append(trace) trace.segments.append(seg) self.trace_buff.add_segment(seg, trace) elif self.status['draw'] == 'FLASH': aperture = apertures[self.status['aperture']] shape_inst = ShapeInstance(ends[1], aperture) self.img_buff.shape_instances.append(shape_inst) return {'x':end[0], 'y':end[1]} # coordinate interpretation def _target_pos(self, block): """ Interpret coordinates in a data block. """ coord = {'x':block.x, 'y':block.y} for k in coord: if self.params['FS'].incremental_coords: if coord[k] is None: coord[k] = 0 coord[k] = self.status[k] + getattr(block, k) else: if coord[k] is None: coord[k] = self.status[k] return (coord['x'], coord['y']) # geometry def _draw_arc(self, end_pts, center_offset): """ Convert arc path into shape. """ start, end = end_pts offset = {'i':center_offset[0], 'j':center_offset[1]} for k in offset: if offset[k] is None: offset[k] = 0 center, radius = self._get_ctr_and_radius(end_pts, offset) start_angle = self._get_angle(center, start) end_angle = self._get_angle(center, end) clockwise = 'ANTI' not in self.status['interpolation'] self._check_mq(start_angle, end_angle, clockwise) return Arc(center.x, center.y, start_angle if clockwise else end_angle, end_angle if clockwise else start_angle, radius) def _get_ctr_and_radius(self, end_pts, offset): """ Apply gerber circular interpolation logic. """ start, end = end_pts radius = sqrt(offset['i']**2 + offset['j']**2) centers = [Point(start.x + xsign * offset['i'], start.y + ysign * offset['j']) for xsign in (1, -1) for ysign in (1, -1)] center = min(centers, key=lambda c: abs(c.dist(end) - radius)) return (center, radius) def _get_angle(self, arc_center, point): """ Convert 2 points to an angle in radians/pi. 0 radians = 3 o'clock, in accordance with the way arc angles are defined in shape.py """ adj = float(point.x - arc_center.x) opp = point.y - arc_center.y hyp = arc_center.dist(point) if hyp == 0.0: return 0.0 angle = acos(adj/hyp)/pi if opp > 0: angle = 2 - angle return angle # tokenizer def _tokenize(self, layer_file): """ Split gerber file into pythonic tokens. """ content = layer_file.read() layer_file.close() param_block = eof = False pos = 0 match = TOK_RE.match(content) while pos < len(content): if match is None: pos += 1 else: typ = match.lastgroup tok = match.group(typ)[:-1] try: if typ == 'MACRO': yield self._parse_macro(tok) elif typ == 'PARAM_DELIM': param_block = not param_block # params elif len(typ) == 2: self._check_pb(param_block, tok) self.params.update(self._parse_param(tok)) # data blocks elif typ not in IGNORE: if typ == 'EOF': self._check_eof(content[match.end():]) eof = True elif typ == 'UNKNOWN': if not self.ignore_unknown: raise UnintelligibleDataBlock(tok) else: self._check_pb(param_block, tok, False) # explode self-referential data blocks blocks = self._parse_data_block(tok) for block in blocks: yield block except Unparsable: raise pos = match.end() match = TOK_RE.match(content, pos) self._check_eof(eof=eof) self.layer_buff.images.append(self.img_buff) # tokenizer support - macros def _parse_macro(self, tok): """ Define a macro, with its component shapes. """ parts = [part.strip() for part in tok.split('*')] name = parts[0][3:] prims = [part.split(',') for part in parts[1:-1] if part] return MacroDef(name, [InternalPrimitive(PRIMITIVES[int(m[0])], m[1:]) for m in prims]) # tokenizer support - params def _parse_param(self, tok): """ Convert a param specifier into pythonic data. """ name, tok = (tok[:2], tok[2:]) if name == 'FS': tup = self._extract_fs(tok) elif name == 'AD': self._extract_ad(tok) return {} elif name in AXIS_PARAMS: tup = self._extract_ap(name, tok) elif name in LAYER_PARAMS: self._extract_lp(name, tok) return {} else: tup = tok return {name:tup} def _extract_fs(self, tok): """ Extract format spec param parts into tuple. """ tok, m_max = self._pop_val('M', tok) tok, d_max = self._pop_val('D', tok) tok, y = self._pop_val('Y', tok, coerce_=False) y = CoordFmt(int(y[0]), int(y[1])) tok, x = self._pop_val('X', tok, coerce_=False) x = CoordFmt(int(x[0]), int(x[1])) tok, g_max = self._pop_val('G', tok) tok, n_max = self._pop_val('N', tok) inc_coords = ('I' in tok) z_omit = tok[0] return FormatSpec(z_omit, inc_coords, n_max, g_max, x, y, d_max, m_max) def _extract_ad(self, tok): """ Extract aperture definition into shapes dict. """ tok = tok if ',' in tok else tok + ',' code_end = 4 if tok[3].isdigit() else 3 code = tok[1:code_end] ap_type, mods = tok[code_end:].split(',') if mods: mods = [float(m) for m in mods.split('X') if m] # An aperture can use any of the 4 standard types, # (with or without a central hole), or a previously # defined macro. if ap_type == 'C': shape = Circle(0, 0, mods[0]/2) hole_defs = len(mods) > 1 and mods[1:] elif ap_type == 'R': if len(mods) == 1: shape = Rectangle(-mods[0]/2, mods[0]/2, mods[0], mods[0]) else: shape = Rectangle(-mods[0]/2, mods[1]/2, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif ap_type == 'O': shape = Obround(0, 0, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif ap_type == 'P': if len(mods) < 3: mods.append(0) shape = RegularPolygon(0, 0, mods[0], mods[1], mods[2]) hole_defs = len(mods) > 3 and mods[3:] else: # macro shape = ap_type if shape in self.macro_buff: macro = self.macro_buff[shape].instantiate(mods) counter = 0 # pick a unique name for the macro while mods and macro.name in self.layer_buff.macros: macro.name = shape + str(counter) counter += 1 self.layer_buff.macros[macro.name] = macro hole_defs = None if hole_defs and (len(hole_defs) > 1): hole = Rectangle(-hole_defs[0]/2, hole_defs[1]/2, hole_defs[0], hole_defs[1]) elif hole_defs: hole = Circle(0, 0, hole_defs[0]/2) else: hole = None self.layer_buff.apertures.update({code:Aperture(code, shape, hole)}) def _extract_ap(self, name, tok): """ Extract axis-defining param into tuple. """ if name in ('AS', 'IJ'): coerce_ = False else: coerce_ = name == 'MI' and 'int' or 'float' tok, b_val = self._pop_val('B', tok, coerce_=coerce_) tok, a_val = self._pop_val('A', tok, coerce_=coerce_) if name == 'IJ': tup = AxisDef(self._parse_justify(a_val), self._parse_justify(b_val)) else: tup = AxisDef(a_val, b_val) return tup def _extract_lp(self, name, tok): """ Extract "layer param" and reset image layer. """ if self.img_buff.not_empty(): self.layer_buff.images.append(self.img_buff) self.img_buff = Image('', self.img_buff.is_additive) self.trace_buff = TraceBuffer() self.status.update({'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None}) if name == 'LN': self.img_buff.name = tok elif name == 'LP': self.img_buff.is_additive = IMAGE_POLARITIES[tok] elif name == 'SR': tok, j = self._pop_val('J', tok, coerce_='float') tok, i = self._pop_val('I', tok, coerce_='float') tok, y = self._pop_val('Y', tok) tok, x = self._pop_val('X', tok) self.img_buff.x_repeats = x self.img_buff.x_step = i self.img_buff.y_repeats = y self.img_buff.y_step = j def _parse_justify(self, val): """ Make a tuple for each axis (special case). """ if len(val) > 1 or val not in 'LC': ab_tup = ('L', float(val.split('L')[-1])) else: ab_tup = (val, None) return ab_tup # tokenizer support - funct/coord def _parse_data_block(self, tok): """ Convert a non-param into pythonic data. """ if 'G' in tok: g_code = tok[1:3] tok = tok[3:] if int(g_code) in G_MAP: yield Funct('G', g_code) tok, d_code = self._pop_val('D', tok, coerce_=False) if d_code: # identify D03 without coord - flash at current pos type_ = (d_code == '03' and not tok) and 'XD' or 'D' yield Funct(type_, d_code) if tok: yield self._parse_coord(tok) def _parse_coord(self, tok): """ Convert a coordinate set into pythonic data. """ self._check_fs() tok, j = self._pop_val('J', tok, format_=True) tok, i = self._pop_val('I', tok, format_=True) tok, y = self._pop_val('Y', tok, format_=True) tok, x = self._pop_val('X', tok, format_=True) result = Coord(x, y, i, j) if tok: raise CoordMalformed('%s remainder=%s' % (result, tok)) return result def _format_dec(self, num_str, axis): """ Interpret a coordinate value using format spec. Params: num_str (the string representation of a number portended by format spec) axis (X or Y - not to be confused with A/B) Returns: float """ f_spec = self.params['FS'] sign_wid = num_str[0] in ('-', '+') and 1 or 0 int_wid = sign_wid + f_spec[axis].int wid = int_wid + f_spec[axis].dec # pad coordinate to specified width if f_spec.zero_omission == 'L': num_str = num_str.zfill(wid) elif f_spec.zero_omission == 'T': num_str = num_str.rjust(wid, '0') if len(num_str) != wid: raise CoordMalformed('num_str: %s wid: %s' % (num_str, wid)) # insert decimal point num_str = '%s.%s' % (num_str[:int_wid], num_str[int_wid:]) return float(num_str) # general support methods def _pop_val(self, key, tok, format_=False, coerce_='int'): """ Pop a labelled value from the end of a token. """ val = None if key in tok: tok, num_str = tok.split(key) if num_str: if format_: if key in ('X', 'I'): val = self._format_dec(num_str, 4) else: val = self._format_dec(num_str, 5) else: if coerce_ == 'float': if '.' in num_str: val = int(float(num_str)) else: val = int(num_str) elif coerce_: val = int(num_str) else: val = num_str return (tok, val) def _check_fill(self): """ Check that a fill is closed. """ ends = [pair[0] for pair in self.fill_buff] fill = [pair[1] for pair in self.fill_buff] self.fill_buff = [] if ends[0][0] == ends[-1][1]: return Fill(fill) else: raise OpenFillBoundary('%s != %s' % (ends[-1][1], ends[0][0])) def _check_smear(self, seg, shape): """ Enforce linear interpolation constraint. """ if not isinstance(seg, Line): raise IncompatibleAperture('%s cannot draw arc %s' % (shape, seg)) def _check_mq(self, start_angle, end_angle, clockwise): """ Enforce single quadrant arc length restriction. """ if not self.status['multi_quadrant']: if clockwise: arc = (2.0 if end_angle == 0.0 else end_angle) - \ (0.0 if start_angle == 2.0 else start_angle) else: arc = (2.0 if start_angle == 0.0 else start_angle) - \ (0.0 if end_angle == 2.0 else end_angle) if round(abs(arc), 4) > 0.5: raise QuadrantViolation('Arc(%s to %s) > 0.5 rad/pi' % (start_angle, end_angle)) def _check_pb(self, param_block, tok, should_be=True): """ Ensure we are parsing an appropriate block. """ if should_be and not param_block: raise DelimiterMissing if param_block and not should_be: raise ParamContainsBadData(tok) def _check_fs(self): """ Ensure coordinate is able to be interpreted. """ if not self.params['FS']: raise CoordPrecedesFormatSpec def _check_eof(self, trailing_fragment=None, eof=False): """ Ensure file is terminated correctly. """ if not (trailing_fragment or eof): raise FileNotTerminated elif (not eof) and trailing_fragment.strip(): raise DataAfterEOF(trailing_fragment) def _debug_stdout(self): """ Dump what we know. """ for layer in self.layout.layers: print '-- %s (%s) --' % (layer.name, layer.type) for j in layer.apertures: print '-- D%s --' % j print layer.apertures[j].json() for k in layer.macros: print '-- %s --' % k print layer.macros[k].json() for image in layer.images: print '-- %s (%s) --' % (image.name, image.is_additive and 'additive' or 'subtractive') print image.json() raise Unparsable('deliberate error')
class Gerber: """ The Gerber Format Parser """ def __init__(self): self.layout = Layout() self.layer_buff = None self.img_buff = Image() self.fill_buff = [] # establish gerber defaults self.params = {'AS':AxisDef('x', 'y'),# axis select 'FS':None, # format spec 'MI':AxisDef(0, 0), # mirror image 'MO':'IN', # mode: inches/mm 'OF':AxisDef(0, 0), # offset 'SF':AxisDef(1, 1), # scale factor 'IJ':AxisDef(('L', 0), # image justify ('L', 0)), 'IO':AxisDef(0, 0), # image offset 'IP':True, # image polarity 'IR':0} # image rotation # simulate a photo plotter self.status = {'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None} @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an gerber file """ with open(filename, 'r') as f: data = f.read() confidence = 0 if 'D01*' in data: confidence += 0.2 if 'D02*' in data: confidence += 0.2 if 'D03*' in data: confidence += 0.2 if 'M02*' in data: confidence += 0.4 return confidence def parse(self, infile='.'): """ Parse tokens from gerber files into a design. """ zip_ = infile.endswith('zip') openarchive = zip_ and ZipFile or TarFile.open archive = batch_member = None try: # define multiple layers from folder if LAYERS_CFG in infile: archive = None cfg_name = infile cfg = open(cfg_name, 'r') # define multiple layers from archive else: archive = openarchive(infile) batch = zip_ and archive.namelist or archive.getnames batch_member = zip_ and archive.open or archive.extractfile cfg_name = [n for n in batch() if LAYERS_CFG in n][0] cfg = batch_member(cfg_name) # define single layer from single gerber file except ReadError: name, ext = path.split(infile)[1].rsplit('.', 1) layer_defs = [LayerDef(ext.lower() == 'ger' and name or ext, 'unknown', infile)] self._gen_layers(layer_defs, None, None) # tidy up batch specs else: layer_defs = [LayerDef(rec[0], rec[1], path.join(path.split(cfg_name)[0], rec[2])) for rec in csv.reader(cfg, skipinitialspace=True)] cfg.close() self._gen_layers(layer_defs, archive, batch_member) # tidy up archive finally: if archive: archive.close() # compile design if DEBUG: self._debug_stdout() self.layout.units = (self.params['MO'] == 'IN' and 'inch' or 'mm') design = Design() design.layout = self.layout return design # primary parser support methods def _gen_layers(self, layer_defs, archive, batch_member): """ Parse gerbers into a PCB layers. """ for layer_def in layer_defs: self.layer_buff = Layer(layer_def.name, layer_def.type) layer_file = (archive and batch_member(layer_def.filename) or open(layer_def.filename, 'r')) for block in self._tokenize(layer_file): if isinstance(block, MacroDef): effect = self._build_macro(block) elif isinstance(block, Funct): effect = self._do_funct(block) else: effect = self._move(block) self.status.update(effect) self.layout.layers.append(self.layer_buff) def _build_macro(self, block): """ Build a macro out of component shape defs. """ primitives = [] for p_def in block.primitive_defs: type_, mods = (p_def[0], [float(i) for i in p_def[1:]]) is_additive = type_ in ('moire', 'thermal') or int(mods[0]) rotation = type_ not in ('circle', 'moire', 'thermal') and mods[-1]/180 shape, rotation = self._gen_shape(type_, mods, rotation) primitives.append(Primitive(is_additive, rotation, shape)) # generate and stow the macro self.layer_buff.macros[block.name] = Macro(block.name, primitives) return {} def _do_funct(self, block): """ Set drawing modes, fill terminators. """ code = int(block.code) if 'D' in block.type_: if code < 10: effect = {'draw':D_MAP[code]} # flash current pos/aperture if 'X' in block.type_: apertures = self.layer_buff.apertures aperture = apertures[self.status['aperture']] pos = Point(self.status['x'], self.status['y']) shape_inst = ShapeInstance(pos, aperture) self.img_buff.shape_instances.append(shape_inst) # terminate fill mid mode if (self.status['outline_fill'] and code == 2 and self.fill_buff): self.img_buff.fills.append(self._check_fill()) else: effect = {'aperture':block.code} else: effect = G_MAP[code] if code == 37: # terminate fill if D02 was not specified if self.fill_buff: self.img_buff.fills.append(self._check_fill()) return effect def _move(self, block): """ Draw a shape, or a segment of a trace or fill. """ start = tuple([self.status[k] for k in ('x', 'y')]) end = self._target_pos(block) ends = (Point(start), Point(end)) apertures = self.layer_buff.apertures if self.status['draw'] == 'ON': # generate segment if self.status['interpolation'] == 'LINEAR': seg = Line(start, end) else: ctr_offset = block[2:] seg = self._draw_arc(ends, ctr_offset) # append segment to fill if self.status['outline_fill']: self.fill_buff.append(seg) else: aperture = apertures[self.status['aperture']] if isinstance(aperture.shape, Rectangle): # construct a smear self._check_smear(seg, aperture.shape) self.img_buff.smears.append(Smear(seg, aperture.shape)) else: wid = aperture.shape.radius * 2 tr_ind = self.img_buff.get_trace(wid, ends) if tr_ind is None: # construct a trace self.img_buff.traces.append(Trace(wid, [seg])) else: # append segment to existing trace self.img_buff.traces[tr_ind].segments.append(seg) elif self.status['draw'] == 'FLASH': aperture = apertures[self.status['aperture']] shape_inst = ShapeInstance(ends[1], aperture) self.img_buff.shape_instances.append(shape_inst) return {'x':end[0], 'y':end[1]} # coordinate interpretation def _target_pos(self, block): """ Interpret coordinates in a data block. """ coord = {'x':block.x, 'y':block.y} for k in coord: if self.params['FS'].incremental_coords: if coord[k] is None: coord[k] = 0 coord[k] = self.status[k] + getattr(block, k) else: if coord[k] is None: coord[k] = self.status[k] return (coord['x'], coord['y']) # geometry def _gen_shape(self, type_, mods, rotation): """ Create a primitive shape component for a macro. """ if type_ == 'circle': shape = Circle(x=mods[2], y=mods[3], radius=mods[1]/2) elif type_ == 'vector': shape, rotation = self._vector_to_rect(mods, rotation) elif type_ == 'line': shape = Rectangle(x=mods[3] - mods[1]/2, y=mods[4] + mods[2]/2, width=mods[1], height=mods[2]) elif type_ == 'rectangle': shape = Rectangle(x=mods[3], y=mods[4] + mods[2], width=mods[1], height=mods[2]) elif type_ == 'outline': points = [Point(mods[i], mods[i + 1]) for i in range(2, len(mods[:-1]), 2)] shape = Polygon(points) elif type_ == 'polygon': shape = RegularPolygon(x=mods[2], y=mods[3], outer=mods[4], vertices=mods[1]) elif type_ == 'moire': mods[8] = 2 - mods[8]/180 shape = Moire(*mods[0:9]) elif type_ == 'thermal': mods[5] = 2 - mods[5]/180 shape = Thermal(*mods[0:6]) return (shape, rotation) def _vector_to_rect(self, mods, rotation): """ Convert a vector into a Rectangle. Strategy ======== If vect is not horizontal: - rotate about the origin until horizontal - define it as a normal rectangle - incorporate rotated angle into explicit rotation """ start, end = (mods[2:4], mods[4:6]) start_radius = sqrt(start[0]**2 + start[1]**2) end_radius = sqrt(end[0]**2 + end[1]**2) # Reverse the vector if its endpoint is closer # to the origin than its start point (avoids # mucking about with signage later). if start_radius > end_radius: end, start = (mods[2:4], mods[4:6]) radius = end_radius else: radius = start_radius # Calc the angle of the vector with respect to # the x axis. x, y = start adj = end[0] - x opp = end[1] - y hyp = sqrt(adj**2 + opp**2) theta = acos(adj/hyp)/pi if opp > 0: theta = 2 - theta # Represent vector angle as a delta. d_theta = 2 - theta # Calc the angle of the start point of the # flattened vector. theta = acos(x/radius)/pi if y > 0: theta = 2 - theta theta += d_theta # Redefine x and y at center of the rect's left # side. y = sin((2 - theta) * pi) * radius x = cos((2 - theta) * pi) * radius # Calc the composite rotation angle. rotation = (rotation + theta) % 2 return (Rectangle(x=x, y=y + mods[1]/2, width=hyp, height=mods[1]), rotation) def _draw_arc(self, end_pts, center_offset): """ Convert arc path into shape. """ start, end = end_pts offset = {'i':center_offset[0], 'j':center_offset[1]} for k in offset: if offset[k] is None: offset[k] = 0 center, radius = self._get_ctr_and_radius(end_pts, offset) start_angle = self._get_angle(center, start) end_angle = self._get_angle(center, end) self._check_mq(start_angle, end_angle) clockwise = 'ANTI' not in self.status['interpolation'] return Arc(center.x, center.y, clockwise and start_angle or end_angle, clockwise and end_angle or start_angle, radius) def _get_ctr_and_radius(self, end_pts, offset): """ Apply gerber circular interpolation logic. """ start, end = end_pts radius = sqrt(offset['i']**2 + offset['j']**2) center = Point(x=start.x + offset['i'], y=start.y + offset['j']) # In single-quadrant mode, gerber requires implicit # determination of offset direction, so we find the # center through trial and error. if not self.status['multi_quadrant']: if not snap(center.dist(end), radius): center = Point(x=start.x - offset['i'], y=start.y - offset['j']) if not snap(center.dist(end), radius): center = Point(x=start.x + offset['i'], y=start.y - offset['j']) if not snap(center.dist(end), radius): center = Point(x=start.x - offset['i'], y=start.y + offset['j']) if not snap(center.dist(end), radius): raise ImpossibleGeometry return (center, radius) def _get_angle(self, arc_center, point): """ Convert 2 points to an angle in radians/pi. 0 radians = 3 o'clock, in accordance with the way arc angles are defined in shape.py """ adj = float(point.x - arc_center.x) opp = point.y - arc_center.y hyp = arc_center.dist(point) angle = acos(adj/hyp)/pi if opp > 0: angle = 2 - angle return angle # tokenizer def _tokenize(self, layer_file): """ Split gerber file into pythonic tokens. """ content = layer_file.read() layer_file.close() param_block = eof = False pos = 0 match = TOK_RE.match(content) while pos < len(content): if match is None: pos += 1 else: typ = match.lastgroup tok = match.group(typ)[:-1] try: if typ == 'MACRO': yield self._parse_macro(tok) elif typ == 'PARAM_DELIM': param_block = not param_block # params elif len(typ) == 2: self._check_pb(param_block, tok) self.params.update(self._parse_param(tok)) # data blocks elif typ not in IGNORE: self._check_pb(param_block, tok, False) if typ == 'EOF': self._check_eof(content[match.end():]) eof = True else: self._check_typ(typ, tok) # explode self-referential data blocks blocks = self._parse_data_block(tok) for block in blocks: yield block except Unparsable: raise pos = match.end() match = TOK_RE.match(content, pos) self._check_eof(eof=eof) self.layer_buff.images.append(self.img_buff) # tokenizer support - macros def _parse_macro(self, tok): """ Define a macro, with its component shapes. """ parts = tok.split('*') name = parts[0][3:] prims = [part.strip().split(',') for part in parts[1:-1]] prim_defs = tuple([(PRIMITIVES[int(m[0])],) + tuple(m[1:]) for m in prims]) return MacroDef(name, prim_defs) # tokenizer support - params def _parse_param(self, tok): """ Convert a param specifier into pythonic data. """ name, tok = (tok[:2], tok[2:]) if name == 'FS': tup = self._extract_fs(tok) elif name == 'AD': self._extract_ad(tok) return {} elif name in AXIS_PARAMS: tup = self._extract_ap(name, tok) elif name in LAYER_PARAMS: self._extract_lp(name, tok) return {} else: tup = tok return {name:tup} def _extract_fs(self, tok): """ Extract format spec param parts into tuple. """ tok, m_max = self._pop_val('M', tok) tok, d_max = self._pop_val('D', tok) tok, y = self._pop_val('Y', tok, coerce_=False) y = CoordFmt(int(y[0]), int(y[1])) tok, x = self._pop_val('X', tok, coerce_=False) x = CoordFmt(int(x[0]), int(x[1])) tok, g_max = self._pop_val('G', tok) tok, n_max = self._pop_val('N', tok) inc_coords = (tok[1] == 'I') z_omit = tok[0] return FormatSpec(z_omit, inc_coords, n_max, g_max, x, y, d_max, m_max) def _extract_ad(self, tok): """ Extract aperture definition into shapes dict. """ tok = ',' in tok and tok or tok + ',' code_end = tok[3] in DIGITS and 4 or 3 code = tok[1:code_end] type_, mods = tok[code_end:].split(',') if mods: mods = [float(m) for m in mods.split('X')] # An aperture can use any of the 4 standard types, # (with or without a central hole), or a previously # defined macro. if type_ == 'C': shape = Circle(0, 0, mods[0]/2) hole_defs = len(mods) > 1 and mods[1:] elif type_ == 'R': shape = Rectangle(-mods[0]/2, mods[1]/2, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif type_ == 'O': shape = Obround(0, 0, mods[0], mods[1]) hole_defs = len(mods) > 2 and mods[2:] elif type_ == 'P': if len(mods) < 3: mods.append(0) shape = RegularPolygon(0, 0, mods[0], mods[1], mods[2]) hole_defs = len(mods) > 3 and mods[3:] else: shape = type_ hole_defs = None hole = hole_defs and (len(hole_defs) > 1 and Rectangle(-hole_defs[0]/2, hole_defs[1]/2, hole_defs[0], hole_defs[1]) or Circle(0, 0, hole_defs[0]/2)) self.layer_buff.apertures.update({code:Aperture(code, shape, hole)}) def _extract_ap(self, name, tok): """ Extract axis-defining param into tuple. """ if name in ('AS', 'IJ'): coerce_ = False else: coerce_ = name == 'MI' and 'int' or 'float' tok, b_val = self._pop_val('B', tok, coerce_=coerce_) tok, a_val = self._pop_val('A', tok, coerce_=coerce_) if name == 'IJ': tup = AxisDef(self._parse_justify(a_val), self._parse_justify(b_val)) else: tup = AxisDef(a_val, b_val) return tup def _extract_lp(self, name, tok): """ Extract "layer param" and reset image layer. """ if self.img_buff.not_empty(): self.layer_buff.images.append(self.img_buff) self.img_buff = Image('', self.img_buff.is_additive) self.status.update({'x':0, 'y':0, 'draw':'OFF', 'interpolation':'LINEAR', 'aperture':None, 'outline_fill':False, 'multi_quadrant':False, 'units':None, 'incremental_coords':None}) if name == 'LN': self.img_buff.name = tok elif name == 'LP': self.img_buff.is_additive = IMAGE_POLARITIES[tok] elif name == 'SR': tok, j = self._pop_val('J', tok, coerce_='float') tok, i = self._pop_val('I', tok, coerce_='float') tok, y = self._pop_val('Y', tok) tok, x = self._pop_val('X', tok) self.img_buff.x_repeats = x self.img_buff.x_step = i self.img_buff.y_repeats = y self.img_buff.y_step = j def _parse_justify(self, val): """ Make a tuple for each axis (special case). """ if len(val) > 1 or val not in 'LC': ab_tup = ('L', float(val.split('L')[-1])) else: ab_tup = (val, None) return ab_tup # tokenizer support - funct/coord def _parse_data_block(self, tok): """ Convert a non-param into pythonic data. """ if 'G' in tok: g_code = tok[1:3] tok = tok[3:] if int(g_code) in G_MAP: yield Funct('G', g_code) tok, d_code = self._pop_val('D', tok, coerce_=False) if d_code: # identify D03 without coord - flash at current pos type_ = (d_code == '03' and not tok) and 'XD' or 'D' yield Funct(type_, d_code) if tok: yield self._parse_coord(tok) def _parse_coord(self, tok): """ Convert a coordinate set into pythonic data. """ self._check_fs() tok, j = self._pop_val('J', tok, format_=True) tok, i = self._pop_val('I', tok, format_=True) tok, y = self._pop_val('Y', tok, format_=True) tok, x = self._pop_val('X', tok, format_=True) result = Coord(x, y, i, j) if tok: raise CoordMalformed('%s remainder=%s' % (result, tok)) return result def _format_dec(self, num_str, axis): """ Interpret a coordinate value using format spec. Params: num_str (the string representation of a number portended by format spec) axis (X or Y - not to be confused with A/B) Returns: float """ f_spec = self.params['FS'] sign_wid = num_str[0] in ('-', '+') and 1 or 0 int_wid = sign_wid + f_spec[axis].int wid = int_wid + f_spec[axis].dec # pad coordinate to specified width if f_spec.zero_omission == 'L': num_str = num_str.zfill(wid) elif f_spec.zero_omission == 'T': num_str = num_str.rjust(wid, '0') if len(num_str) != wid: raise CoordMalformed('num_str: %s wid: %s' % (num_str, wid)) # insert decimal point num_str = '%s.%s' % (num_str[:int_wid], num_str[int_wid:]) return float(num_str) # general support methods def _pop_val(self, key, tok, format_=False, coerce_='int'): """ Pop a labelled value from the end of a token. """ val = None if key in tok: tok, num_str = tok.split(key) if num_str: if format_: if key in ('X', 'I'): val = self._format_dec(num_str, 4) else: val = self._format_dec(num_str, 5) else: val = coerce_ and (coerce_ == 'float' and float(num_str) or int(num_str)) or num_str return (tok, val) def _check_fill(self): """ Check that a fill is closed. """ fill = self.fill_buff if len(fill) >= 2: segs = (fill[0], fill[1], fill[-2], fill[-1]) ends = [isinstance(s, Arc) and list(s.ends()) or [s.p1, s.p2] for s in segs] if len(fill) > 2: # If first or last seg is an arc that was defined # in anticlockwise mode, we need to reverse it. if ends[0][0] in ends[1]: ends[0].reverse() if ends[-1][1] in ends[-2]: ends[-1].reverse() if not ends[-1][1] == ends[0][0]: raise OpenFillBoundary('%s != %s' % (ends[-1][1], ends[0][0])) else: for end in ends[-1]: if end not in end[0]: raise OpenFillBoundary('%s != %s' % (ends[-1][1], ends[0][0])) else: start, end = fill[0].ends() if not start == end: raise OpenFillBoundary('%s != %s' % (start, end)) self.fill_buff = [] return Fill(fill) def _check_smear(self, seg, shape): """ Enforce linear interpolation constraint. """ if not isinstance(seg, Line): raise IncompatibleAperture('%s cannot draw arc %s' % (shape, seg)) def _check_mq(self, start_angle, end_angle): """ Enforce single quadrant arc length restriction. """ if not self.status['multi_quadrant']: if abs(end_angle - start_angle) > 0.5: raise QuadrantViolation('Arc(%s to %s) > 0.5 rad/pi' % (start_angle, end_angle)) def _check_pb(self, param_block, tok, should_be=True): """ Ensure we are parsing an appropriate block. """ if should_be and not param_block: raise DelimiterMissing if param_block and not should_be: raise ParamContainsBadData(tok) def _check_fs(self): """ Ensure coordinate is able to be interpreted. """ if not self.params['FS']: raise CoordPrecedesFormatSpec def _check_eof(self, trailing_fragment=None, eof=False): """ Ensure file is terminated correctly. """ if not (trailing_fragment or eof): raise FileNotTerminated elif (not eof) and trailing_fragment.strip(): raise DataAfterEOF(trailing_fragment) def _check_typ(self, typ, tok): """ Ensure data block is understood by the parser. """ if typ == 'UNKNOWN': raise UnintelligibleDataBlock(tok) def _debug_stdout(self): """ Dump what we know. """ for layer in self.layout.layers: print '-- %s (%s) --' % (layer.name, layer.type) for j in layer.apertures: print '-- D%s --' % j print layer.apertures[j].json() for k in layer.macros: print '-- %s --' % k print layer.macros[k].json() for image in layer.images: print '-- %s (%s) --' % (image.name, image.is_additive and 'additive' or 'subtractive') print image.json() raise Unparsable('deliberate error')