From 695e3d9220be8773f6630bb5c512d122b8576742 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 18:07:15 -0400 Subject: Added excellon support and refactored project --- gerber/gerber.py | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 gerber/gerber.py (limited to 'gerber/gerber.py') diff --git a/gerber/gerber.py b/gerber/gerber.py new file mode 100644 index 0000000..31d9b82 --- /dev/null +++ b/gerber/gerber.py @@ -0,0 +1,305 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from parser.py by Paulo Henrique Silva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import json +from .statements import * + + + + +class GerberFile(object): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + filename : string + Parameter. + + zero_suppression : string + Zero-suppression mode. May be either 'leading' or 'trailing' + + notation : string + Notation mode. May be either 'absolute' or 'incremental' + + format : tuple (int, int) + Gerber precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + units : string + either 'inch' or 'metric'. + + size : tuple, (, ) + Size in [self.units] of the layer described by the gerber file. + + bounds: tuple, ((, ), (, )) + boundaries of the layer described by the gerber file. + `bounds` is stored as ((min x, max x), (min y, max y)) + + """ + + @classmethod + def read(cls, filename): + """ Read data from filename and return a GerberFile + """ + return GerberParser().parse(filename) + + def __init__(self, statements, settings, filename=None): + self.filename = filename + self.statements = statements + self.settings = settings + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @property + def units(self): + return self.settings['units'] + + @property + def size(self): + xbounds, ybounds = self.bounds + return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) + + @property + def bounds(self): + xbounds = [0.0, 0.0] + ybounds = [0.0, 0.0] + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: + if stmt.x is not None and stmt.x < xbounds[0]: + xbounds[0] = stmt.x + if stmt.x is not None and stmt.x > xbounds[1]: + xbounds[1] = stmt.x + if stmt.i is not None and stmt.i < xbounds[0]: + xbounds[0] = stmt.i + if stmt.i is not None and stmt.i > xbounds[1]: + xbounds[1] = stmt.i + if stmt.y is not None and stmt.y < ybounds[0]: + ybounds[0] = stmt.y + if stmt.y is not None and stmt.y > ybounds[1]: + ybounds[1] = stmt.y + if stmt.j is not None and stmt.j < ybounds[0]: + ybounds[0] = stmt.j + if stmt.j is not None and stmt.j > ybounds[1]: + ybounds[1] = stmt.j + + return (xbounds, ybounds) + + def write(self, filename): + """ Write data out to a gerber file + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber()) + + def render(self, filename, ctx): + """ Generate image of layer. + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + ctx.dump(filename) + + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + FUNCTION = r"G\d{2}" + + COORD_OP = r"D[0]?[123]" + + FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + MO = r"(?PMO)(?P(MM|IN))" + IP = r"(?PIP)(?P(POS|NEG))" + LP = r"(?PLP)(?P(D|C))" + AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" + AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" + AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) + + # begin deprecated + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + IN = r"(?PIN)(?P.*)" + LN = r"(?PLN)(?P.*)" + # end deprecated + + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM02)\*") + + def __init__(self): + self.settings = {} + self.statements = [] + + def parse(self, filename): + fp = open(filename, "r") + data = fp.readlines() + + for stmt in self._parse(data): + self.statements.append(stmt) + + return GerberFile(self.statements, self.settings, filename) + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + s = "" + for stmt in self.statements: + s += str(stmt) + "\n" + return s + + def _parse(self, data): + oldline = '' + + for i, line in enumerate(data): + line = oldline + line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + oldline = line + continue + + did_something = True # make sure we do at least one loop + while did_something and len(line) > 0: + did_something = False + + # coord + (coord, r) = self._match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + + did_something = True + line = r + continue + + # comment + (comment, r) = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # parameter + (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + if param: + if param["param"] == "FS": + stmt = FSParamStmt.from_dict(param) + self.settings = {'zero_suppression': stmt.zero_suppression, + 'format': stmt.format, + 'notation': stmt.notation} + yield stmt + elif param["param"] == "MO": + stmt = MOParamStmt.from_dict(param) + self.settings['units'] = stmt.mode + yield stmt + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "LP": + yield LPParamStmt.from_dict(param) + elif param["param"] == "AD": + yield ADParamStmt.from_dict(param) + elif param["param"] == "AM": + yield AMParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmtfrom_dict(param) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + line = r + continue + + if False: + print self.COORD_STMT.pattern + print self.APERTURE_STMT.pattern + print self.COMMENT_STMT.pattern + print self.EOF_STMT.pattern + for i in self.PARAM_STMT: + print i.pattern + + if line.find('*') > 0: + yield UnknownStmt(line) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit