Ejemplo n.º 1
0
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 """
        f = open(filename, 'r')
        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')