From 7ee1866b607f5a7227c0fb949fd909f49c1b6334 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Wed, 5 Feb 2014 00:34:57 +0100 Subject: N w modular organization. * parser and render separeted on its own modules. * svn made optional. * PEP-8 compiant. --- gerber/__init__.py | 16 ++ gerber/__main__.py | 31 +++ gerber/gerber.py | 567 --------------------------------------------------- gerber/parser.py | 355 ++++++++++++++++++++++++++++++++ gerber/render.py | 140 +++++++++++++ gerber/render_svg.py | 106 ++++++++++ 6 files changed, 648 insertions(+), 567 deletions(-) create mode 100644 gerber/__main__.py delete mode 100644 gerber/gerber.py create mode 100644 gerber/parser.py create mode 100644 gerber/render.py create mode 100644 gerber/render_svg.py (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index e69de29..0bf7c24 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -0,0 +1,16 @@ +#! /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. diff --git a/gerber/__main__.py b/gerber/__main__.py new file mode 100644 index 0000000..6f861cf --- /dev/null +++ b/gerber/__main__.py @@ -0,0 +1,31 @@ +#! /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. + +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) diff --git a/gerber/gerber.py b/gerber/gerber.py deleted file mode 100644 index ee7fcbc..0000000 --- a/gerber/gerber.py +++ /dev/null @@ -1,567 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import re -import json -import traceback - -import svgwrite - - -def red(s): - return '\033[1;31m{0}\033[0;m'.format(s) - - -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 - - -class MOParamStmt(ParamStmt): - def __init__(self, param, mo): - ParamStmt.__init__(self, param) - self.mo = mo - - -class IPParamStmt(ParamStmt): - def __init__(self, param, ip): - ParamStmt.__init__(self, param) - self.ip = ip - - -class OFParamStmt(ParamStmt): - def __init__(self, param, a, b): - ParamStmt.__init__(self, param) - self.a = a - self.b = b - - -class LPParamStmt(ParamStmt): - def __init__(self, param, lp): - ParamStmt.__init__(self, param) - self.lp = 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(",")] - - -class AMParamStmt(ParamStmt): - def __init__(self, param, name, macro): - ParamStmt.__init__(self, param) - self.name = name - self.macro = macro - - -class INParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = name - - -class LNParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = 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 - - -class ApertureStmt(Statement): - def __init__(self, d): - Statement.__init__(self, "APERTURE") - self.d = int(d) - - -class CommentStmt(Statement): - def __init__(self, comment): - Statement.__init__(self, "COMMENT") - self.comment = comment - - -class EofStmt(Statement): - def __init__(self): - Statement.__init__(self, "EOF") - - -class UnknownStmt(Statement): - def __init__(self, line): - Statement.__init__(self, "UNKNOWN") - self.line = line - - -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 - - -class Shape(object): - pass - - -class Circle(Shape): - def __init__(self, diameter=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): - 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 SvgContext(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(SvgContext, 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(SvgContext, 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(SvgContext, self).arc(x, y) - - def flash(self, x, y): - super(SvgContext, 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") - - -class Gerber(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)) - - EOF_STMT = re.compile(r"(?PM02)\*") - - def __init__(self): - self.statements = [] - self.ctx = SvgContext() - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - self._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): - multiline = None - - for i, line in enumerate(data): - # remove EOL - if multiline: - line = multiline + line.strip() - else: - line = line.strip() - - # skip empty lines - if not len(line): - continue - - # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): - multiline = line - continue - else: - multiline = None - - # coord - coords = self._match_many(self.COORD_STMT, line) - if coords: - for coord in coords: - yield CoordStmt(**coord) - continue - - # aperture selection - aperture = self._match_one(self.APERTURE_STMT, line) - if aperture: - yield ApertureStmt(**aperture) - continue - - # comment - comment = self._match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - continue - - # parameter - param = 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) - - continue - - # eof - eof = self._match_one(self.EOF_STMT, line) - if eof: - yield EofStmt() - 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 - - yield UnknownStmt(line) - - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return {} - else: - return match.groupdict() - - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return match.groupdict() - - return {} - - def _match_many(self, expr, data): - result = [] - pos = 0 - while True: - match = expr.match(data, pos) - if match: - result.append(match.groupdict()) - pos = match.endpos - else: - break - - return result - - 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.ctx.set_coord_format(stmt.zero, stmt.x, stmt.y) - self.ctx.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": - self.ctx.set_coord_unit(stmt.mo) - elif stmt.param == "IP:": - self.ctx.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": - self.ctx.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.ctx.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - - if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): - self.ctx.set_interpolation(stmt.function) - - if stmt.op == "D01": - self.ctx.stroke(stmt.x, stmt.y) - elif stmt.op == "D02": - self.ctx.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.ctx.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.ctx.set_aperture(stmt.d) - - -if __name__ == "__main__": - import sys - - for f in sys.argv[1:]: - print "parsing: %s" % f - try: - g = Gerber() - g.parse(f) - except Exception, e: - traceback.print_exc() - diff --git a/gerber/parser.py b/gerber/parser.py new file mode 100644 index 0000000..cf755d4 --- /dev/null +++ b/gerber/parser.py @@ -0,0 +1,355 @@ +#! /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 + + +class MOParamStmt(ParamStmt): + def __init__(self, param, mo): + ParamStmt.__init__(self, param) + self.mo = mo + + +class IPParamStmt(ParamStmt): + def __init__(self, param, ip): + ParamStmt.__init__(self, param) + self.ip = ip + + +class OFParamStmt(ParamStmt): + def __init__(self, param, a, b): + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + +class LPParamStmt(ParamStmt): + def __init__(self, param, lp): + ParamStmt.__init__(self, param) + self.lp = 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(",")] + + +class AMParamStmt(ParamStmt): + def __init__(self, param, name, macro): + ParamStmt.__init__(self, param) + self.name = name + self.macro = macro + + +class INParamStmt(ParamStmt): + def __init__(self, param, name): + ParamStmt.__init__(self, param) + self.name = name + + +class LNParamStmt(ParamStmt): + def __init__(self, param, name): + ParamStmt.__init__(self, param) + self.name = 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 + + +class ApertureStmt(Statement): + def __init__(self, d): + Statement.__init__(self, "APERTURE") + self.d = int(d) + + +class CommentStmt(Statement): + def __init__(self, comment): + Statement.__init__(self, "COMMENT") + self.comment = comment + + +class EofStmt(Statement): + def __init__(self): + Statement.__init__(self, "EOF") + + +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)) + + EOF_STMT = re.compile(r"(?PM02)\*") + + def __init__(self, ctx): + 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) + self._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): + multiline = None + + for i, line in enumerate(data): + # remove EOL + if multiline: + line = multiline + line.strip() + else: + line = line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + multiline = line + continue + else: + multiline = None + + # coord + coords = self._match_many(self.COORD_STMT, line) + if coords: + for coord in coords: + yield CoordStmt(**coord) + continue + + # aperture selection + aperture = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + continue + + # comment + comment = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + continue + + # parameter + param = 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) + + continue + + # eof + eof = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + 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 + + yield UnknownStmt(line) + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return {} + else: + return match.groupdict() + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return match.groupdict() + + return {} + + def _match_many(self, expr, data): + result = [] + pos = 0 + while True: + match = expr.match(data, pos) + if match: + result.append(match.groupdict()) + pos = match.endpos + else: + break + + return result + + 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.ctx.set_coord_format(stmt.zero, stmt.x, stmt.y) + self.ctx.set_coord_notation(stmt.notation) + elif stmt.param == "MO:": + self.ctx.set_coord_unit(stmt.mo) + elif stmt.param == "IP:": + self.ctx.set_image_polarity(stmt.ip) + elif stmt.param == "LP:": + self.ctx.set_level_polarity(stmt.lp) + elif stmt.param == "AD": + self.ctx.define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + + if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): + self.ctx.set_interpolation(stmt.function) + + if stmt.op == "D01": + self.ctx.stroke(stmt.x, stmt.y) + elif stmt.op == "D02": + self.ctx.move(stmt.x, stmt.y) + elif stmt.op == "D03": + self.ctx.flash(stmt.x, stmt.y) + + def _evaluate_aperture(self, stmt): + self.ctx.set_aperture(stmt.d) diff --git a/gerber/render.py b/gerber/render.py new file mode 100644 index 0000000..dd30ae0 --- /dev/null +++ b/gerber/render.py @@ -0,0 +1,140 @@ +#! /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. + + +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 diff --git a/gerber/render_svg.py b/gerber/render_svg.py new file mode 100644 index 0000000..bfe6859 --- /dev/null +++ b/gerber/render_svg.py @@ -0,0 +1,106 @@ +#! /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") -- cgit