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 --- .gitignore | 7 + Makefile | 17 ++ gerber/__main__.py | 33 ++- gerber/excellon.py | 165 +++++++++++++ gerber/gerber.py | 305 +++++++++++++++++++++++ gerber/parser.py | 363 --------------------------- gerber/render.py | 185 -------------- gerber/render/__init__.py | 28 +++ gerber/render/apertures.py | 58 +++++ gerber/render/render.py | 133 ++++++++++ gerber/render/svg.py | 171 +++++++++++++ gerber/render_svg.py | 106 -------- gerber/statements.py | 605 +++++++++++++++++++++++++++++++++++++++++++++ gerber/tests/__init__.py | 0 gerber/tests/test_utils.py | 68 +++++ gerber/utils.py | 40 ++- 16 files changed, 1615 insertions(+), 669 deletions(-) create mode 100644 Makefile create mode 100755 gerber/excellon.py create mode 100644 gerber/gerber.py delete mode 100644 gerber/parser.py delete mode 100644 gerber/render.py create mode 100644 gerber/render/__init__.py create mode 100644 gerber/render/apertures.py create mode 100644 gerber/render/render.py create mode 100644 gerber/render/svg.py delete mode 100644 gerber/render_svg.py create mode 100644 gerber/statements.py create mode 100644 gerber/tests/__init__.py create mode 100644 gerber/tests/test_utils.py diff --git a/.gitignore b/.gitignore index 4c6b1be..abfd2fa 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,10 @@ nosetests.xml .idea/workspace.xml .idea/misc.xml .idea + +# Komodo +*.komodoproject + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a5e411 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ + +PYTHON ?= python +NOSETESTS ?= nosetests + + +clean: + #$(PYTHON) setup.py clean + find . -name '*.pyc' -delete + rm -rf coverage .coverage + rm -rf *.egg-info +test: + $(NOSETESTS) -s -v gerber + +test-coverage: + rm -rf coverage .coverage + $(NOSETESTS) -s -v --with-coverage gerber + diff --git a/gerber/__main__.py b/gerber/__main__.py index 6f861cf..d32fa01 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,16 +16,23 @@ # limitations under the License. if __name__ == '__main__': - from .parser import GerberParser - from .render import GerberContext - - import sys - - if len(sys.argv) < 2: - print >> sys.stderr, "Usage: python -m gerber ..." - sys.exit(1) - - for filename in sys.argv[1:]: - print "parsing %s" % filename - g = GerberParser(GerberContext()) - g.parse(filename) + from .gerber import GerberFile + from .excellon import ExcellonParser + from .render import GerberSvgContext + + #import sys + # + #if len(sys.argv) < 2: + # print >> sys.stderr, "Usage: python -m gerber ..." + # sys.exit(1) + # + ##for filename in sys.argv[1]: + ## print "parsing %s" % filename + ctx = GerberSvgContext() + g = GerberFile.read('SCB.GTL') + g.render('test.svg', ctx) + p = ExcellonParser(ctx) + p.parse('ncdrill.txt') + p.dump('testwithdrill.svg') + + diff --git a/gerber/excellon.py b/gerber/excellon.py new file mode 100755 index 0000000..fef5844 --- /dev/null +++ b/gerber/excellon.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +import re +from itertools import tee, izip +from .utils import parse_gerber_value + + +INCH = 0 +METRIC = 1 + +ABSOLUTE = 0 +INCREMENTAL = 1 + +LZ = 0 +TZ = 1 + +class Tool(object): + + @classmethod + def from_line(cls, line, settings): + commands = re.split('([BCFHSTZ])', line)[1:] + commands = [(command, value) for command, value in pairwise(commands)] + args = {} + format = settings['format'] + zero_suppression = settings['zero_suppression'] + for cmd, val in commands: + if cmd == 'B': + args['retract_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'C': + args['diameter'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'F': + args['feed_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'H': + args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'S': + args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression) + elif cmd == 'T': + args['number'] = int(val) + elif cmd == 'Z': + args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) + return cls(settings, **args) + + def __init__(self, settings, **kwargs): + self.number = kwargs.get('number') + self.feed_rate = kwargs.get('feed_rate') + self.retract_rate = kwargs.get('retract_rate') + self.rpm = kwargs.get('rpm') + self.diameter = kwargs.get('diameter') + self.max_hit_count = kwargs.get('max_hit_count') + self.depth_offset = kwargs.get('depth_offset') + self.units = settings.get('units', INCH) + + def __repr__(self): + unit = 'in.' if self.units == INCH else 'mm' + return '' % (self.number, self.diameter, unit) + + + +class ExcellonParser(object): + def __init__(self, ctx=None): + self.ctx=ctx + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2,5) + self.state = 'INIT' + self.tools = {} + self.hits = [] + self.active_tool = None + self.pos = [0., 0.] + if ctx is not None: + self.ctx.set_coord_format(zero_suppression='trailing', format=[2,5], notation='absolute') + def parse(self, filename): + with open(filename, 'r') as f: + for line in f: + self._parse(line) + + def dump(self, filename): + self.ctx.dump(filename) + + def _parse(self, line): + if 'M48' in line: + self.state = 'HEADER' + + if 'G00' in line: + self.state = 'ROUT' + + if 'G05' in line: + self.state = 'DRILL' + + elif line[0] == '%' and self.state == 'HEADER': + self.state = 'DRILL' + + if 'INCH' in line or line.strip() == 'M72': + self.units = 'INCH' + + elif 'METRIC' in line or line.strip() == 'M71': + self.units = 'METRIC' + + if 'LZ' in line: + self.zeros = 'L' + + elif 'TZ' in line: + self.zeros = 'T' + + if 'ICI' in line and 'ON' in line or line.strip() == 'G91': + self.notation = 'incremental' + + if 'ICI' in line and 'OFF' in line or line.strip() == 'G90': + self.notation = 'incremental' + + zs = self._settings()['zero_suppression'] + fmt = self._settings()['format'] + + # tool definition + if line[0] == 'T' and self.state == 'HEADER': + tool = Tool.from_line(line,self._settings()) + self.tools[tool.number] = tool + + elif line[0] == 'T' and self.state != 'HEADER': + self.active_tool = self.tools[int(line.strip().split('T')[1])] + + + if line[0] in ['X', 'Y']: + x = None + y = None + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x = parse_gerber_value(splitline[0].strip(), fmt, zs) + if len(splitline) == 2: + y = parse_gerber_value(splitline[1].strip(), fmt,zs) + else: + y = parse_gerber_value(line.strip(' Y'), fmt,zs) + + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + if self.state == 'DRILL': + self.hits.append((self.active_tool, self.pos)) + if self.ctx is not None: + self.ctx.drill(self.pos[0], self.pos[1], + self.active_tool.diameter) + + def _settings(self): + return {'units':self.units, 'zero_suppression':self.zero_suppression, + 'format': self.format} + +def pairwise(iterator): + itr = iter(iterator) + while True: + yield tuple([itr.next() for i in range(2)]) + +if __name__ == '__main__': + tools = [] + settings = {'units':INCH, 'zeros':LZ} + p = parser() + p.parse('examples/ncdrill.txt') + + \ No newline at end of file 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) diff --git a/gerber/parser.py b/gerber/parser.py deleted file mode 100644 index 8f89211..0000000 --- a/gerber/parser.py +++ /dev/null @@ -1,363 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 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 - - -class Statement(object): - def __init__(self, type): - self.type = type - - def __str__(self): - s = "<{0} ".format(self.__class__.__name__) - - for key, value in self.__dict__.items(): - s += "{0}={1} ".format(key, value) - - s = s.rstrip() + ">" - return s - - -class ParamStmt(Statement): - def __init__(self, param): - Statement.__init__(self, "PARAM") - self.param = param - - - -class FSParamStmt(ParamStmt): - def __init__(self, param, zero="L", notation="A", x="24", y="24"): - ParamStmt.__init__(self, param) - self.zero = zero - self.notation = notation - self.x = x - self.y = y - - def to_gerber(self): - return '%FS{0}{1}X{2}Y{3}*%'.format(self.zero, self.notation, - self.x, self.y) - -class MOParamStmt(ParamStmt): - def __init__(self, param, mo): - ParamStmt.__init__(self, param) - self.mo = mo - - def to_gerber(self): - return '%MO{0}*%'.format(self.mo) - -class IPParamStmt(ParamStmt): - def __init__(self, param, ip): - ParamStmt.__init__(self, param) - self.ip = ip - - def to_gerber(self): - return '%IP{0}*%'.format(self.ip) - - -class OFParamStmt(ParamStmt): - def __init__(self, param, a, b): - ParamStmt.__init__(self, param) - self.a = a - self.b = b - - def to_gerber(self): - ret = '%OF' - if self.a: - ret += 'A' + self.a - if self.b: - ret += 'B' + self.b - return ret + '*%' - - -class LPParamStmt(ParamStmt): - def __init__(self, param, lp): - ParamStmt.__init__(self, param) - self.lp = lp - - def to_gerber(self): - return '%LP{0}*%'.format(self.lp) - - -class ADParamStmt(ParamStmt): - def __init__(self, param, d, shape, modifiers): - ParamStmt.__init__(self, param) - self.d = d - self.shape = shape - self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")] - - def to_gerber(self): - return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, - ','.join(['X'.join(e) for e in self.modifiers])) - -class AMParamStmt(ParamStmt): - def __init__(self, param, name, macro): - ParamStmt.__init__(self, param) - self.name = name - self.macro = macro - - def to_gerber(self): - #think this is right... - return '%AM{0}*{1}*%'.format(self.name, self.macro) - -class INParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%IN{0}*%'.format(self.name) - - -class LNParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%LN{0}*%'.format(self.name) - -class CoordStmt(Statement): - def __init__(self, function, x, y, i, j, op): - Statement.__init__(self, "COORD") - self.function = function - self.x = x - self.y = y - self.i = i - self.j = j - self.op = op - - def to_gerber(self): - ret = '' - if self.function: - ret += self.function - if self.x: - ret += 'X{0}'.format(self.x) - if self.y: - ret += 'Y{0}'.format(self.y) - if self.i: - ret += 'I{0}'.format(self.i) - if self.j: - ret += 'J{0}'.format(self.j) - if self.op: - ret += self.op - return ret + '*' - - -class ApertureStmt(Statement): - def __init__(self, d): - Statement.__init__(self, "APERTURE") - self.d = int(d) - - def to_gerber(self): - return 'G54D{0}*'.format(self.d) - -class CommentStmt(Statement): - def __init__(self, comment): - Statement.__init__(self, "COMMENT") - self.comment = comment - - def to_gerber(self): - return 'G04{0}*'.format(self.comment) - -class EofStmt(Statement): - def __init__(self): - Statement.__init__(self, "EOF") - - def to_gerber(self): - return 'M02*' - -class UnknownStmt(Statement): - def __init__(self, line): - Statement.__init__(self, "UNKNOWN") - self.line = line - - -class GerberParser(object): - 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{string})(\*)?".format(string=STRING)) - #spec is unclear on whether all chars allowed in comment string - - #seems reasonable to be more permissive. - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") - - EOF_STMT = re.compile(r"(?PM02)\*") - - def __init__(self, ctx=None): - self.statements = [] - self.ctx = ctx - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - if self.ctx: - self.ctx.evaluate(stmt) - - 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 dump(self): - self.ctx.dump() - - 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(**coord) - 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": - yield FSParamStmt(**param) - elif param["param"] == "MO": - yield MOParamStmt(**param) - elif param["param"] == "IP": - yield IPParamStmt(**param) - elif param["param"] == "LP": - yield LPParamStmt(**param) - elif param["param"] == "AD": - yield ADParamStmt(**param) - elif param["param"] == "AM": - yield AMParamStmt(**param) - elif param["param"] == "OF": - yield OFParamStmt(**param) - elif param["param"] == "IN": - yield INParamStmt(**param) - elif param["param"] == "LN": - yield LNParamStmt(**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) diff --git a/gerber/render.py b/gerber/render.py deleted file mode 100644 index 201f793..0000000 --- a/gerber/render.py +++ /dev/null @@ -1,185 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 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. - -from .parser import CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt - -IMAGE_POLARITY_POSITIVE = 1 -IMAGE_POLARITY_NEGATIVE = 2 - -LEVEL_POLARITY_DARK = 1 -LEVEL_POLARITY_CLEAR = 2 - -NOTATION_ABSOLUTE = 1 -NOTATION_INCREMENTAL = 2 - -UNIT_INCH = 1 -UNIT_MM = 2 - -INTERPOLATION_LINEAR = 1 -INTERPOLATION_ARC = 2 - - -class GerberCoordFormat(object): - def __init__(self, zeroes, x, y): - self.omit_leading_zeroes = True if zeroes == "L" else False - self.omit_trailing_zeroes = True if zeroes == "T" else False - self.x_int_digits, self.x_dec_digits = [int(d) for d in x] - self.y_int_digits, self.y_dec_digits = [int(d) for d in y] - - def resolve(self, x, y): - new_x = x - new_y = y - - if new_x is not None: - negative = "-" in new_x - new_x = new_x.replace("-", "") - - missing_zeroes = (self.x_int_digits + self.x_dec_digits) - len(new_x) - - if missing_zeroes and self.omit_leading_zeroes: - new_x = (missing_zeroes * "0") + new_x - elif missing_zeroes and self.omit_trailing_zeroes: - new_x += missing_zeroes * "0" - - new_x = float("{0}{1}.{2}".format("-" if negative else "", - new_x[:self.x_int_digits], - new_x[self.x_int_digits:])) - - if new_y is not None: - negative = "-" in new_y - new_y = new_y.replace("-", "") - - missing_zeroes = (self.y_int_digits + self.y_dec_digits) - len(new_y) - - if missing_zeroes and self.omit_leading_zeroes: - new_y = (missing_zeroes * "0") + new_y - elif missing_zeroes and self.omit_trailing_zeroes: - new_y += missing_zeroes * "0" - - new_y = float("{0}{1}.{2}".format("-" if negative else "", - new_y[:self.y_int_digits], - new_y[self.y_int_digits:])) - - return new_x, new_y - - -class GerberContext(object): - coord_format = None - coord_notation = NOTATION_ABSOLUTE - coord_unit = None - - x = 0 - y = 0 - - aperture = 0 - interpolation = INTERPOLATION_LINEAR - - image_polarity = IMAGE_POLARITY_POSITIVE - level_polarity = LEVEL_POLARITY_DARK - - def __init__(self): - pass - - def set_coord_format(self, zeroes, x, y): - self.coord_format = GerberCoordFormat(zeroes, x, y) - - def set_coord_notation(self, notation): - self.coord_notation = NOTATION_ABSOLUTE if notation == "A" else NOTATION_INCREMENTAL - - def set_coord_unit(self, unit): - self.coord_unit = UNIT_INCH if unit == "IN" else UNIT_MM - - def set_image_polarity(self, polarity): - self.image_polarity = IMAGE_POLARITY_POSITIVE if polarity == "POS" else IMAGE_POLARITY_NEGATIVE - - def set_level_polarity(self, polarity): - self.level_polarity = LEVEL_POLARITY_DARK if polarity == "D" else LEVEL_POLARITY_CLEAR - - def set_interpolation(self, interpolation): - self.interpolation = INTERPOLATION_LINEAR if interpolation in ("G01", "G1") else INTERPOLATION_ARC - - def set_aperture(self, d): - self.aperture = d - - def resolve(self, x, y): - x, y = self.coord_format.resolve(x, y) - return x or self.x, y or self.y - - def define_aperture(self, d, shape, modifiers): - pass - - def move(self, x, y, resolve=True): - if resolve: - self.x, self.y = self.resolve(x, y) - else: - self.x, self.y = x, y - - def stroke(self, x, y): - pass - - def line(self, x, y): - pass - - def arc(self, x, y): - pass - - def flash(self, x, y): - pass - - def evaluate(self, stmt): - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return - - elif isinstance(stmt, ParamStmt): - self._evaluate_param(stmt) - - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - - elif isinstance(stmt, ApertureStmt): - self._evaluate_aperture(stmt) - - else: - raise Exception("Invalid statement to evaluate") - - def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.set_coord_format(stmt.zero, stmt.x, stmt.y) - self.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": - self.set_coord_unit(stmt.mo) - elif stmt.param == "IP:": - self.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": - self.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - - if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): - self.set_interpolation(stmt.function) - - if stmt.op == "D01": - self.stroke(stmt.x, stmt.y) - elif stmt.op == "D02": - self.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.set_aperture(stmt.d) diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py new file mode 100644 index 0000000..cc87ee0 --- /dev/null +++ b/gerber/render/__init__.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe + +# 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. +""" +gerber.render +============ +**Gerber Renderers** + +This module provides contexts for rendering images of gerber layers. Currently +SVG is the only supported format. +""" + + +from svg import GerberSvgContext + diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py new file mode 100644 index 0000000..55e6a30 --- /dev/null +++ b/gerber/render/apertures.py @@ -0,0 +1,58 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. +""" +gerber.render.apertures +============ +**Gerber Aperture base classes** + +This module provides base classes for gerber apertures. These are used by +the rendering engine to draw the gerber file. +""" + + +class Aperture(object): + """ Gerber Aperture base class + """ + def draw(self, ctx, x, y): + raise NotImplementedError('The draw method must be implemented in an Aperture subclass.') + + def flash(self, ctx, x, y): + raise NotImplementedError('The flash method must be implemented in an Aperture subclass.') + + +class Circle(Aperture): + """ Circular Aperture base class + """ + def __init__(self, diameter=0.0): + self.diameter = diameter + +class Rect(Aperture): + """ Rectangular Aperture base class + """ + def __init__(self, size=(0, 0)): + self.size = size + +class Obround(Aperture): + """ Obround Aperture base class + """ + def __init__(self, size=(0, 0)): + self.size = size + +class Polygon(Aperture): + """ Polygon Aperture base class + """ + pass \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py new file mode 100644 index 0000000..e15a36f --- /dev/null +++ b/gerber/render/render.py @@ -0,0 +1,133 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from code 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. + +from ..statements import ( + CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt +) + + +class GerberContext(object): + settings = {} + + x = 0 + y = 0 + + aperture = 0 + interpolation = 'linear' + + image_polarity = 'positive' + level_polarity = 'dark' + + def __init__(self): + pass + + def set_format(self, settings): + self.settings = settings + + def set_coord_format(self, zero_suppression, format, notation): + self.settings['zero_suppression'] = zero_suppression + self.settings['format'] = format + self.settings['notation'] = notation + + def set_coord_notation(self, notation): + self.settings['notation'] = notation + + def set_coord_unit(self, unit): + self.settings['units'] = unit + + def set_image_polarity(self, polarity): + self.image_polarity = polarity + + def set_level_polarity(self, polarity): + self.level_polarity = polarity + + def set_interpolation(self, interpolation): + self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' + + def set_aperture(self, d): + self.aperture = d + + def resolve(self, x, y): + return x or self.x, y or self.y + + def define_aperture(self, d, shape, modifiers): + pass + + def move(self, x, y, resolve=True): + if resolve: + self.x, self.y = self.resolve(x, y) + else: + self.x, self.y = x, y + + def stroke(self, x, y): + pass + + def line(self, x, y): + pass + + def arc(self, x, y): + pass + + def flash(self, x, y): + pass + + def drill(self, x, y, diameter): + pass + + def evaluate(self, stmt): + if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + else: + raise Exception("Invalid statement to evaluate") + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) + self.set_coord_notation(stmt.notation) + elif stmt.param == "MO:": + self.set_coord_unit(stmt.mode) + elif stmt.param == "IP:": + self.set_image_polarity(stmt.ip) + elif stmt.param == "LP:": + self.set_level_polarity(stmt.lp) + elif stmt.param == "AD": + self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): + self.set_interpolation(stmt.function) + + if stmt.op == "D01": + self.stroke(stmt.x, stmt.y) + elif stmt.op == "D02": + self.move(stmt.x, stmt.y) + elif stmt.op == "D03": + self.flash(stmt.x, stmt.y) + + def _evaluate_aperture(self, stmt): + self.set_aperture(stmt.d) diff --git a/gerber/render/svg.py b/gerber/render/svg.py new file mode 100644 index 0000000..b16e534 --- /dev/null +++ b/gerber/render/svg.py @@ -0,0 +1,171 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.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. + +from .render import GerberContext +from .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'),] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"),] + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'),] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect,] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect,] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + #self.arc(x, y) + self.line(x,y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) + + diff --git a/gerber/render_svg.py b/gerber/render_svg.py deleted file mode 100644 index bfe6859..0000000 --- a/gerber/render_svg.py +++ /dev/null @@ -1,106 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 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. - -from .render import GerberContext, INTERPOLATION_LINEAR, INTERPOLATION_ARC -import svgwrite - - -class Shape(object): - pass - - -class Circle(Shape): - def __init__(self, diameter=0.0): - self.diameter = diameter - - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)", - stroke_width=2, stroke_linecap="round") - - def flash(self, ctx, x, y): - return ctx.dwg.circle(center=(x*300, y*300), r=300*(self.diameter/2.0), fill="rgb(184, 115, 51)") - - -class Rect(Shape): - def __init__(self, size=(0, 0)): - self.size = size - - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)", - stroke_width=2, stroke_linecap="butt") - - def flash(self, ctx, x, y): - return ctx.dwg.rect(insert=(300*x, 300*y), size=(300*float(self.size[0]), 300*float(self.size[1])), - fill="rgb(184, 115, 51)") - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - - self.apertures = {} - self.dwg = svgwrite.Drawing() - self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) - - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == "C": - aperture = Circle(diameter=float(modifiers[0][0])) - elif shape == "R": - aperture = Rect(size=modifiers[0][0:2]) - - self.apertures[d] = aperture - - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) - - if self.interpolation == INTERPOLATION_LINEAR: - self.line(x, y) - elif self.interpolation == INTERPOLATION_ARC: - self.arc(x, y) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - - x, y = self.resolve(x, y) - - ap = self.apertures.get(str(self.aperture), None) - if ap is None: - return - - self.dwg.add(ap.draw(self, x, y)) - - self.move(x, y, resolve=False) - - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - - x, y = self.resolve(x, y) - - ap = self.apertures.get(str(self.aperture), None) - if ap is None: - return - - self.dwg.add(ap.flash(self, x, y)) - - self.move(x, y, resolve=False) - - def dump(self): - self.dwg.saveas("teste.svg") diff --git a/gerber/statements.py b/gerber/statements.py new file mode 100644 index 0000000..53f7f78 --- /dev/null +++ b/gerber/statements.py @@ -0,0 +1,605 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +gerber.statements +================= +**Gerber file statement classes ** + +""" +from .utils import parse_gerber_value, write_gerber_value, decimal_string + + + +__all__ = ['FSParamStmt', 'MOParamStmt','IPParamStmt', 'OFParamStmt', + 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', + 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', + 'EofStmt', 'UnknownStmt'] + +class Statement(object): + def __init__(self, type): + self.type = type + + def __str__(self): + s = "<{0} ".format(self.__class__.__name__) + + for key, value in self.__dict__.items(): + s += "{0}={1} ".format(key, value) + + s = s.rstrip() + ">" + return s + + +class ParamStmt(Statement): + def __init__(self, param): + Statement.__init__(self, "PARAM") + self.param = param + + +class FSParamStmt(ParamStmt): + """ FS - Gerber Format Specification Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + """ + """ + param = stmt_dict.get('param').strip() + zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' + notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' + x = map(int,stmt_dict.get('x').strip()) + format = (x[0], x[1]) + if notation == 'incremental': + print('This file uses incremental notation. To quote the gerber \ + file specification:\nIncremental notation is a source of \ + endless confusion. Always use absolute notation.\n\nYou \ + have been warned') + return cls(param, zeros, notation, format) + + def __init__(self, param, zero_suppression='leading', + notation='absolute', format=(2,4)): + """ Initialize FSParamStmt class + + .. note:: + The FS command specifies the format of the coordinate data. It + must only be used once at the beginning of a file. It must be + specified before the first use of coordinate data. + + Parameters + ---------- + param : 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) + + Returns + ------- + ParamStmt : FSParamStmt + Initialized FSParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.zero_suppression = zero_suppression + self.notation = notation + self.format = format + + def to_gerber(self): + zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' + notation = 'A' if self.notation == 'absolute' else 'I' + format = ''.join(map(str,self.format)) + return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, + format, format) + + def __str__(self): + return ('' % + (self.format[0], self.format[1], self.zero_suppression, + self.notation)) + + +class MOParamStmt(ParamStmt): + """ MO - Gerber Mode (measurement units) Statement. + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' + return cls(param, mo) + + def __init__(self, param, mo): + """ Initialize MOParamStmt class + + Parameters + ---------- + param : string + Parameter. + + mo : string + Measurement units. May be either 'inch' or 'metric' + + Returns + ------- + ParamStmt : MOParamStmt + Initialized MOParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.mode = mo + + def to_gerber(self): + mode = 'MM' if self.mode == 'metric' else 'IN' + return '%MO{0}*%'.format(mode) + + def __str__(self): + mode_str = 'millimeters' if self.mode == 'metric' else 'inches' + return ('' % mode_str) + + +class IPParamStmt(ParamStmt): + """ IP - Gerber Image Polarity Statement. (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative' + return cls(param, ip) + + def __init__(self, param, ip): + """ Initialize IPParamStmt class + + Parameters + ---------- + param : string + Parameter string. + + ip : string + Image polarity. May be either'positive' or 'negative' + + Returns + ------- + ParamStmt : IPParamStmt + Initialized IPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.ip = ip + + + def to_gerber(self): + ip = 'POS' if self.ip == 'positive' else 'negative' + return '%IP{0}*%'.format(ip) + + def __str__(self): + return ('' % self.ip) + + +class OFParamStmt(ParamStmt): + """ OF - Gerber Offset statement (Deprecated) + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = float(stmt_dict.get('a')) + b = float(stmt_dict.get('b')) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize OFParamStmt class + + Parameters + ---------- + param : string + Parameter + + a : float + Offset along the output device A axis + + b : float + Offset along the output device B axis + + Returns + ------- + ParamStmt : OFParamStmt + Initialized OFParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings): + stmt = '%OF' + if self.a: + ret += 'A' + decimal_string(self.a, precision=6) + if self.b: + ret += 'B' + decimal_string(self.b, precision=6) + return ret + '*%' + + def __str__(self): + offset_str = '' + if self.a: + offset_str += ('X: %f' % self.a) + if self.b: + offset_str += ('Y: %f' % self.b) + return ('' % offset_str) + +class LPParamStmt(ParamStmt): + """ LP - Gerber Level Polarity statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('lp') + lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' + return cls(param, lp) + + def __init__(self, param, lp): + """ Initialize LPParamStmt class + + Parameters + ---------- + param : string + Parameter + + lp : string + Level polarity. May be either 'clear' or 'dark' + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.lp = lp + + + def to_gerber(self, settings): + lp = 'C' if self.lp == 'clear' else 'dark' + return '%LP{0}*%'.format(self.lp) + + def __str__(self): + return '' % self.lp + + +class ADParamStmt(ParamStmt): + """ AD - Gerber Aperture Definition Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + d = int(stmt_dict.get('d')) + shape = stmt_dict.get('shape') + modifiers = stmt_dict.get('modifiers') + if modifiers is not None: + modifiers = [[float(x) for x in m.split('X')] + for m in modifiers.split(',')] + return cls(param, d, shape, modifiers) + + + def __init__(self, param, d, shape, modifiers): + """ Initialize ADParamStmt class + + Parameters + ---------- + param : string + Parameter code + + d : int + Aperture D-code + + shape : string + aperture name + + modifiers : list of lists of floats + Shape modifiers + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.d = d + self.shape = shape + self.modifiers = modifiers + + + def to_gerber(self, settings): + return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, + ','.join(['X'.join(e) for e in self.modifiers])) + + def __str__(self): + if self.shape == 'C': + shape = 'circle' + elif self.shape == 'R': + shape = 'rectangle' + elif self.shape == 'O': + shape = 'oblong' + else: + shape = self.shape + + return '' % (self.d, shape) + + +class AMParamStmt(ParamStmt): + """ AM - Aperture Macro Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name, macro): + """ Initialize AMParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Aperture macro name + + macro : string + Aperture macro string + + Returns + ------- + ParamStmt : AMParamStmt + Initialized AMParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + self.macro = macro + + def to_gerber(self): + return '%AM{0}*{1}*%'.format(self.name, self.macro) + + def __str__(self): + return '' % (self.name, macro) + + +class INParamStmt(ParamStmt): + """ IN - Image Name Statement + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize INParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Image name + + Returns + ------- + ParamStmt : INParamStmt + Initialized INParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%IN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + +class LNParamStmt(ParamStmt): + """ LN - Level Name Statement (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize LNParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Level name + + Returns + ------- + ParamStmt : LNParamStmt + Initialized LNParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%LN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + +class CoordStmt(Statement): + """ Coordinate Data Block + """ + + @classmethod + def from_dict(cls, stmt_dict, settings): + zeros = settings['zero_suppression'] + format = settings['format'] + function = stmt_dict.get('function') + x = stmt_dict.get('x') + y = stmt_dict.get('y') + i = stmt_dict.get('i') + j = stmt_dict.get('j') + op = stmt_dict.get('op') + + if x is not None: + x = parse_gerber_value(stmt_dict.get('x'), + format, zeros) + if y is not None: + y = parse_gerber_value(stmt_dict.get('y'), + format, zeros) + if i is not None: + i = parse_gerber_value(stmt_dict.get('i'), + format, zeros) + if j is not None: + j = parse_gerber_value(stmt_dict.get('j'), + format, zeros) + return cls(function, x, y, i, j, op, settings) + + + def __init__(self, function, x, y, i, j, op, settings): + """ Initialize CoordStmt class + + Parameters + ---------- + function : string + function + + x : float + X coordinate + + y : float + Y coordinate + + i : float + Coordinate offset in the X direction + + j : float + Coordinate offset in the Y direction + + op : string + Operation code + + settings : dict {'zero_suppression', 'format'} + Gerber file coordinate format + + Returns + ------- + Statement : CoordStmt + Initialized CoordStmt class. + + """ + Statement.__init__(self, "COORD") + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + self.function = function + self.x = x + self.y = y + self.i = i + self.j = j + self.op = op + + + def to_gerber(self): + ret = '' + if self.function: + ret += self.function + if self.x: + ret += 'X{0}'.format(write_gerber_value(self.x, self.zeros, + self.format)) + if self.y: + ret += 'Y{0}'.format(write_gerber_value(self.y,self. zeros, + self.format)) + if self.i: + ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros, + self.format)) + if self.j: + ret += 'J{0}'.format(write_gerber_value(self.j, self.zeros, + self.format)) + if self.op: + ret += self.op + return ret + '*' + + def __str__(self): + coord_str = '' + if self.function: + coord_str += 'Fn: %s ' % self.function + if self.x: + coord_str += 'X: %f ' % self.x + if self.y: + coord_str += 'Y: %f ' % self.y + if self.i: + coord_str += 'I: %f ' % self.i + if self.j: + coord_str += 'J: %f ' % self.j + if self.op: + if self.op == 'D01': + op = 'Lights On' + elif self.op == 'D02': + op = 'Lights Off' + elif self.op == 'D03': + op = 'Flash' + else: + op = self.op + coord_str += 'Op: %s' % op + + return '' % coord_str + + +class ApertureStmt(Statement): + """ Aperture Statement + """ + def __init__(self, d): + Statement.__init__(self, "APERTURE") + self.d = int(d) + + def to_gerber(self): + return 'G54D{0}*'.format(self.d) + + def __str__(self): + return '' % self.d + +class CommentStmt(Statement): + """ Comment Statment + """ + def __init__(self, comment): + Statement.__init__(self, "COMMENT") + self.comment = comment + + def to_gerber(self): + return 'G04{0}*'.format(self.comment) + + def __str__(self): + return '' % self.comment + + +class EofStmt(Statement): + """ EOF Statement + """ + def __init__(self): + Statement.__init__(self, "EOF") + + def to_gerber(self): + return 'M02*' + + def __str__(self): + return '' + + +class UnknownStmt(Statement): + """ Unknown Statement + """ + def __init__(self, line): + Statement.__init__(self, "UNKNOWN") + self.line = line + \ No newline at end of file diff --git a/gerber/tests/__init__.py b/gerber/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py new file mode 100644 index 0000000..50e2403 --- /dev/null +++ b/gerber/tests/test_utils.py @@ -0,0 +1,68 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..utils import decimal_string, parse_gerber_value, write_gerber_value + + +def test_zero_suppression(): + """ Test gerber value parser and writer handle zero suppression correctly. + """ + # Default format + format = (2, 5) + + # Test leading zero suppression + zero_suppression = 'leading' + test_cases = [('1', 0.00001), ('10', 0.0001), ('100', 0.001), + ('1000', 0.01), ('10000', 0.1), ('100000', 1.0),('1000000', 10.0), + ('-1', -0.00001), ('-10', -0.0001), ('-100', -0.001), + ('-1000', -0.01), ('-10000', -0.1), ('-100000', -1.0),('-1000000', -10.0),] + for string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + # Test trailing zero suppression + zero_suppression = 'trailing' + test_cases = [('1', 10.0), ('01', 1.0), ('001', 0.1), ('0001', 0.01), + ('00001', 0.001), ('000001', 0.0001), ('0000001', 0.00001), + ('-1', -10.0), ('-01', -1.0), ('-001', -0.1), ('-0001', -0.01), + ('-00001', -0.001), ('-000001', -0.0001), ('-0000001', -0.00001)] + for string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + + +def test_format(): + """ Test gerber value parser and writer handle format correctly + """ + zero_suppression = 'leading' + test_cases = [((2,7),'1',0.0000001), ((2,6),'1',0.000001), + ((2,5),'1',0.00001), ((2,4),'1',0.0001), ((2,3),'1',0.001), + ((2,2),'1',0.01), ((2,1),'1',0.1), ((2,7),'-1',-0.0000001), + ((2,6),'-1',-0.000001), ((2,5),'-1',-0.00001), ((2,4),'-1',-0.0001), + ((2,3),'-1',-0.001), ((2,2),'-1',-0.01), ((2,1),'-1',-0.1),] + for format, string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + zero_suppression = 'trailing' + test_cases = [((6, 5), '1' , 100000.0), ((5, 5), '1', 10000.0), + ((4, 5), '1', 1000.0), ((3, 5), '1', 100.0),((2, 5), '1', 10.0), + ((1, 5), '1', 1.0), ((6, 5), '-1' , -100000.0), + ((5, 5), '-1', -10000.0), ((4, 5), '-1', -1000.0), + ((3, 5), '-1', -100.0),((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0),] + for format, string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + +def test_decimal_truncation(): + """ Test decimal string truncates value to the correct precision + """ + value = 1.123456789 + for x in range(10): + result = decimal_string(value, precision=x) + calculated = '1.' + ''.join(str(y) for y in range(1,x+1)) + assert(result == calculated) \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 02a8a14..35b4fd0 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -10,7 +10,7 @@ files. """ # Author: Hamilton Kibbe -# License: MIT +# License: def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -54,6 +54,7 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): raise ValueError('Parser only supports precision up to 6:7 format') # Remove extraneous information + value = value.strip() value = value.strip(' +') negative = '-' in value if negative: @@ -67,7 +68,8 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) for i, digit in enumerate(value): digits[i + offset] = digit - + + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -1.0 * result if negative else result @@ -128,3 +130,37 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): return ''.join(digits) if not negative else ''.join(['-'] + digits) + + +def decimal_string(value, precision=6): + """ Convert float to string with limited precision + + Parameters + ---------- + value : float + A floating point value. + + precision : + Maximum number of decimal places to print + + Returns + ------- + value : string + The specified value as a string. + + """ + floatstr = '%0.20g' % value + integer = None + decimal = None + if '.' in floatstr: + integer, decimal = floatstr.split('.') + elif ',' in floatstr: + integer, decimal = floatstr.split(',') + if len(decimal) > precision: + decimal = decimal[:precision] + if integer or decimal: + return ''.join([integer, '.', decimal]) + else: + return int(floatstr) + + -- cgit